diff --git a/boards/tdeck/src/tdeck_board.cpp b/boards/tdeck/src/tdeck_board.cpp index eddc8f31..7d6bf303 100644 --- a/boards/tdeck/src/tdeck_board.cpp +++ b/boards/tdeck/src/tdeck_board.cpp @@ -3,7 +3,6 @@ #include "board/sd_utils.h" #include "display/drivers/ST7789TDeck.h" #include "platform/esp/arduino_common/power/battery_adc.h" -#include "platform/esp/arduino_common/storage/persistence_bus_gate.h" #include "platform/esp/arduino_common/storage/sd_card_runtime.h" #include "platform/esp/common/shared_spi_coordinator.h" #include "sys/bus_access_scope.h" @@ -764,22 +763,8 @@ bool TDeckBoard::ensureSDReady() void TDeckBoard::uninstallSD() { - ::platform::esp::arduino_common::storage::PersistenceBusGate bus_gate( - ::platform::esp::common::shared_spi_coordinator(), - sys::runtime::BusAccessPolicy::RecoveryExclusive, - 500, - kSharedSpiBusResource, - kSharedSpiBusOwnerId + 2, - kSharedSpiBusOwnerId); - if (bus_gate.locked()) - { - ::platform::esp::arduino_common::storage::unmount_sd_card(); - Serial.println("[TDeckBoard] SD unmounted"); - } - else - { - Serial.println("[TDeckBoard] SD unmount: SPI lock failed"); - } + ::platform::esp::arduino_common::storage::unmount_sd_card(); + Serial.println("[TDeckBoard] SD unmounted"); } bool TDeckBoard::isRTCReady() const diff --git a/boards/tdeck_pro/src/tdeck_pro_board.cpp b/boards/tdeck_pro/src/tdeck_pro_board.cpp index 0a16517e..88ede3ef 100644 --- a/boards/tdeck_pro/src/tdeck_pro_board.cpp +++ b/boards/tdeck_pro/src/tdeck_pro_board.cpp @@ -14,7 +14,10 @@ #include #include "platform/esp/arduino_common/storage/sd_card_runtime.h" +#include "platform/esp/common/shared_spi_coordinator.h" #include "platform/ui/audio/pager_notification_tone.h" +#include "sys/bus_access_scope.h" +#include "sys/clock.h" namespace boards::tdeck_pro { @@ -29,7 +32,9 @@ constexpr uint32_t kEpdSpiHz = 2000000; constexpr uint32_t kSdSpiHz = 4000000; constexpr uint8_t kFlushLogLimit = 8; constexpr uint32_t kRadioTxMaxTimeoutMs = 120000; -SemaphoreHandle_t g_shared_spi_mutex = nullptr; +sys::runtime::BusAccessToken g_shared_spi_token{}; +TaskHandle_t g_shared_spi_task = nullptr; +uint32_t g_shared_spi_depth = 0; uint32_t radioTxTimeoutMs(SX1262Access& radio, size_t len) { @@ -55,37 +60,59 @@ void sharedSpiReleaseAllCs() void sharedSpiBusInit() { - if (g_shared_spi_mutex == nullptr) - { - g_shared_spi_mutex = xSemaphoreCreateRecursiveMutex(); - if (g_shared_spi_mutex == nullptr) - { - Serial.printf("[%s] shared SPI mutex create failed\n", kTag); - return; - } - } sharedSpiReleaseAllCs(); } -void sharedSpiLock() +bool sharedSpiLock( + sys::runtime::BusAccessPolicy policy = + sys::runtime::BusAccessPolicy::InteractiveWorkerBounded, + const char* owner = "tdeck_pro_spi", + uint32_t wait_ms = 200U) { - if (g_shared_spi_mutex == nullptr) + sys::runtime::BusAcquireRequest request{}; + request.resource = + ::platform::esp::common::SharedSpiCoordinator::kSharedBusResource; + request.policy = policy; + request.command_id = 0x54445053U; + request.origin = request.command_id; + request.deadline_ms = sys::millis_now() + wait_ms; + request.owner_label = owner; + const sys::runtime::BusAcquireResult result = + ::platform::esp::common::shared_spi_coordinator().acquire(request); + if (result.status != sys::runtime::BusAcquireStatus::Acquired || + !result.token.valid) { - sharedSpiBusInit(); - } - if (g_shared_spi_mutex != nullptr) - { - xSemaphoreTakeRecursive(g_shared_spi_mutex, portMAX_DELAY); + return false; } + g_shared_spi_token = result.token; + g_shared_spi_task = xTaskGetCurrentTaskHandle(); + g_shared_spi_depth = result.token.depth; + return true; } void sharedSpiUnlock() { - if (g_shared_spi_mutex != nullptr) + const TaskHandle_t current = xTaskGetCurrentTaskHandle(); + if (!g_shared_spi_token.valid || g_shared_spi_task != current) { - sharedSpiReleaseAllCs(); - xSemaphoreGiveRecursive(g_shared_spi_mutex); + return; } + sys::runtime::BusAccessToken token = g_shared_spi_token; + token.depth = g_shared_spi_depth; + const bool final_release = g_shared_spi_depth <= 1U; + if (final_release) + { + g_shared_spi_token = {}; + g_shared_spi_task = nullptr; + g_shared_spi_depth = 0; + } + else + { + --g_shared_spi_depth; + g_shared_spi_token.depth = g_shared_spi_depth; + } + sharedSpiReleaseAllCs(); + ::platform::esp::common::shared_spi_coordinator().release(token); } void sharedSpiPrepareDevice(int cs_pin) @@ -272,7 +299,12 @@ bool TDeckProBoard::initPower() bool TDeckProBoard::initDisplay() { SPI.begin(profile().spi.sck, profile().spi.miso, profile().spi.mosi, profile().epd.cs); - sharedSpiLock(); + if (!sharedSpiLock(sys::runtime::BusAccessPolicy::DisplayFrameCritical, + "tdeck_pro_epd_init", + 45U)) + { + return false; + } sharedSpiPrepareDevice(profile().epd.cs); epd_.epd2.selectSPI(SPI, SPISettings(kEpdSpiHz, MSBFIRST, SPI_MODE0)); epd_.init(0, true, 2, false); @@ -327,7 +359,12 @@ bool TDeckProBoard::initMotion() bool TDeckProBoard::initRadio() { SPI.begin(profile().spi.sck, profile().spi.miso, profile().spi.mosi, profile().lora.cs); - sharedSpiLock(); + if (!sharedSpiLock(sys::runtime::BusAccessPolicy::InteractiveWorkerBounded, + "tdeck_pro_radio_init", + 200U)) + { + return false; + } sharedSpiPrepareDevice(profile().lora.cs); radio_.reset(); radio_ready_ = (radio_.begin() == RADIOLIB_ERR_NONE); @@ -356,15 +393,12 @@ bool TDeckProBoard::installSD() pinMode(pin, OUTPUT); digitalWrite(pin, HIGH); } - sharedSpiLock(); - sharedSpiPrepareDevice(profile().sd.cs); const bool ok = ::platform::esp::arduino_common::storage::mount_sd_card( profile().sd.cs, SPI, kSdSpiHz, "/sd", 8); - sharedSpiUnlock(); return ok; } @@ -612,7 +646,12 @@ void TDeckProBoard::renderEpd() return; } - sharedSpiLock(); + if (!sharedSpiLock(sys::runtime::BusAccessPolicy::DisplayFrameCritical, + "tdeck_pro_epd_frame", + 45U)) + { + return; + } sharedSpiPrepareDevice(profile().epd.cs); epd_.setRotation(rotation_); epd_.setFullWindow(); @@ -738,7 +777,12 @@ int TDeckProBoard::getKeyChar(char* c) int TDeckProBoard::transmitRadio(const uint8_t* data, size_t len) { const uint32_t timeout_ms = radioTxTimeoutMs(radio_, len); - sharedSpiLock(); + if (!sharedSpiLock(sys::runtime::BusAccessPolicy::InteractiveWorkerBounded, + "tdeck_pro_radio_tx", + 200U)) + { + return RADIOLIB_ERR_SPI_CMD_TIMEOUT; + } sharedSpiPrepareDevice(profile().lora.cs); const int rc = radio_.startTransmit(data, len); sharedSpiUnlock(); @@ -752,7 +796,12 @@ int TDeckProBoard::transmitRadio(const uint8_t* data, size_t len) { if (static_cast(millis() - started_ms) > timeout_ms) { - sharedSpiLock(); + if (!sharedSpiLock(sys::runtime::BusAccessPolicy::InteractiveWorkerBounded, + "tdeck_pro_radio_tx_timeout", + 200U)) + { + return RADIOLIB_ERR_TX_TIMEOUT; + } sharedSpiPrepareDevice(profile().lora.cs); (void)radio_.finishTransmit(); sharedSpiUnlock(); @@ -761,7 +810,12 @@ int TDeckProBoard::transmitRadio(const uint8_t* data, size_t len) vTaskDelay(pdMS_TO_TICKS(1)); } - sharedSpiLock(); + if (!sharedSpiLock(sys::runtime::BusAccessPolicy::InteractiveWorkerBounded, + "tdeck_pro_radio_tx_finish", + 200U)) + { + return RADIOLIB_ERR_SPI_CMD_TIMEOUT; + } sharedSpiPrepareDevice(profile().lora.cs); const int finish_rc = radio_.finishTransmit(); sharedSpiUnlock(); @@ -770,7 +824,12 @@ int TDeckProBoard::transmitRadio(const uint8_t* data, size_t len) int TDeckProBoard::startRadioReceive() { - sharedSpiLock(); + if (!sharedSpiLock(sys::runtime::BusAccessPolicy::InteractiveWorkerBounded, + "tdeck_pro_radio_rx_start", + 200U)) + { + return RADIOLIB_ERR_SPI_CMD_TIMEOUT; + } sharedSpiPrepareDevice(profile().lora.cs); const int rc = radio_.startReceive(); sharedSpiUnlock(); @@ -779,7 +838,12 @@ int TDeckProBoard::startRadioReceive() uint32_t TDeckProBoard::getRadioIrqFlags() { - sharedSpiLock(); + if (!sharedSpiLock(sys::runtime::BusAccessPolicy::InteractiveWorkerBounded, + "tdeck_pro_radio_irq", + 200U)) + { + return 0; + } sharedSpiPrepareDevice(profile().lora.cs); const uint32_t flags = radio_.getIrqFlags(); sharedSpiUnlock(); @@ -788,7 +852,12 @@ uint32_t TDeckProBoard::getRadioIrqFlags() int TDeckProBoard::getRadioPacketLength(bool update) { - sharedSpiLock(); + if (!sharedSpiLock(sys::runtime::BusAccessPolicy::InteractiveWorkerBounded, + "tdeck_pro_radio_length", + 200U)) + { + return -1; + } sharedSpiPrepareDevice(profile().lora.cs); const int len = static_cast(radio_.getPacketLength(update)); sharedSpiUnlock(); @@ -797,7 +866,12 @@ int TDeckProBoard::getRadioPacketLength(bool update) int TDeckProBoard::readRadioData(uint8_t* buf, size_t len) { - sharedSpiLock(); + if (!sharedSpiLock(sys::runtime::BusAccessPolicy::InteractiveWorkerBounded, + "tdeck_pro_radio_read", + 200U)) + { + return RADIOLIB_ERR_SPI_CMD_TIMEOUT; + } sharedSpiPrepareDevice(profile().lora.cs); const int rc = radio_.readData(buf, len); sharedSpiUnlock(); @@ -806,7 +880,12 @@ int TDeckProBoard::readRadioData(uint8_t* buf, size_t len) void TDeckProBoard::clearRadioIrqFlags(uint32_t flags) { - sharedSpiLock(); + if (!sharedSpiLock(sys::runtime::BusAccessPolicy::InteractiveWorkerBounded, + "tdeck_pro_radio_clear_irq", + 200U)) + { + return; + } sharedSpiPrepareDevice(profile().lora.cs); radio_.clearIrqFlags(flags); sharedSpiUnlock(); @@ -814,7 +893,12 @@ void TDeckProBoard::clearRadioIrqFlags(uint32_t flags) float TDeckProBoard::getRadioRSSI() { - sharedSpiLock(); + if (!sharedSpiLock(sys::runtime::BusAccessPolicy::InteractiveWorkerBounded, + "tdeck_pro_radio_rssi", + 200U)) + { + return std::numeric_limits::quiet_NaN(); + } sharedSpiPrepareDevice(profile().lora.cs); const float rssi = radio_.getRSSI(); sharedSpiUnlock(); @@ -823,7 +907,12 @@ float TDeckProBoard::getRadioRSSI() float TDeckProBoard::getRadioInstantRSSI() { - sharedSpiLock(); + if (!sharedSpiLock(sys::runtime::BusAccessPolicy::InteractiveWorkerBounded, + "tdeck_pro_radio_instant_rssi", + 200U)) + { + return std::numeric_limits::quiet_NaN(); + } sharedSpiPrepareDevice(profile().lora.cs); const float rssi = radio_.getRSSI(false); sharedSpiUnlock(); @@ -832,7 +921,12 @@ float TDeckProBoard::getRadioInstantRSSI() float TDeckProBoard::getRadioSNR() { - sharedSpiLock(); + if (!sharedSpiLock(sys::runtime::BusAccessPolicy::InteractiveWorkerBounded, + "tdeck_pro_radio_snr", + 200U)) + { + return std::numeric_limits::quiet_NaN(); + } sharedSpiPrepareDevice(profile().lora.cs); const float snr = radio_.getSNR(); sharedSpiUnlock(); @@ -843,7 +937,12 @@ void TDeckProBoard::configureLoraRadio(float freq_mhz, float bw_khz, uint8_t sf, int8_t tx_power, uint16_t preamble_len, uint8_t sync_word, uint8_t crc_len) { - sharedSpiLock(); + if (!sharedSpiLock(sys::runtime::BusAccessPolicy::InteractiveWorkerBounded, + "tdeck_pro_radio_config", + 200U)) + { + return; + } sharedSpiPrepareDevice(profile().lora.cs); radio_.setFrequency(freq_mhz); radio_.setBandwidth(bw_khz); diff --git a/boards/tlora_pager/src/tlora_pager_board.cpp b/boards/tlora_pager/src/tlora_pager_board.cpp index e7007c5e..9363bfd5 100644 --- a/boards/tlora_pager/src/tlora_pager_board.cpp +++ b/boards/tlora_pager/src/tlora_pager_board.cpp @@ -19,7 +19,6 @@ #include "display/drivers/ST7796.h" #include "pins_arduino.h" #include "platform/esp/arduino_common/power/battery_adc.h" -#include "platform/esp/arduino_common/storage/persistence_bus_gate.h" #include "platform/esp/arduino_common/storage/sd_card_runtime.h" #include "platform/esp/common/shared_spi_coordinator.h" #include "platform/ui/audio/call_notification_tone.h" @@ -567,6 +566,17 @@ uint32_t TLoRaPagerBoard::begin(uint32_t disable_hw_init) Serial.printf("[TLoRaPagerBoard::begin] ===== DISPLAY HARDWARE READY =====\n"); if (display_only_boot_) { + // LVGL creates its input devices immediately after the display + // hardware phase. The keyboard must therefore be online before + // returning from this phase, otherwise beginLvglHelper() sees + // hasKeyboard() == false and never creates keypad_read(). + if (!(disable_hw_init & NO_HW_KEYBOARD)) + { + Serial.printf("[TLoRaPagerBoard::begin] keyboard init for display phase begin\n"); + const bool keyboard_ready = initKeyboard(); + Serial.printf("[TLoRaPagerBoard::begin] keyboard init for display phase end ok=%d\n", + keyboard_ready ? 1 : 0); + } return devices_probe; } } @@ -899,6 +909,11 @@ bool TLoRaPagerBoard::initDrv() bool TLoRaPagerBoard::initKeyboard() { #ifdef USING_INPUT_DEV_KEYBOARD + if (devices_probe & HW_KEYBOARD_ONLINE) + { + return true; + } + if (devices_probe & HW_EXPAND_ONLINE) { powerControl(POWER_KEYBOARD, true); @@ -934,9 +949,21 @@ bool TLoRaPagerBoard::initKeyboard() bool TLoRaPagerBoard::initLoRa() { - radio_.reset(); + int state = RADIOLIB_ERR_NONE; + const bool bus_acquired = withSharedSpiRadioAccess( + "radio_init", + pdMS_TO_TICKS(200), + [&]() + { + radio_.reset(); + state = radio_.begin(); + }); - int state = radio_.begin(); + if (!bus_acquired) + { + devices_probe &= ~HW_RADIO_ONLINE; + return false; + } if (state != RADIOLIB_ERR_NONE) { @@ -966,8 +993,18 @@ bool TLoRaPagerBoard::initLoRa() }; radio_.setRfSwitchTable(rfswitch_dio_pins, rfswitch_table); - state = radio_.setTCXO(3.0f); - if (state != RADIOLIB_ERR_NONE) + const bool tcxo_bus_acquired = withSharedSpiRadioAccess( + "radio_tcxo", + pdMS_TO_TICKS(200), + [&]() + { + state = radio_.setTCXO(3.0f); + }); + if (!tcxo_bus_acquired) + { + log_w("LR1121 TCXO configuration skipped: shared SPI unavailable"); + } + else if (state != RADIOLIB_ERR_NONE) { log_w("LR1121 TCXO config returned code: %d", state); } @@ -1052,22 +1089,8 @@ bool TLoRaPagerBoard::ensureSDReady() void TLoRaPagerBoard::uninstallSD() { - ::platform::esp::arduino_common::storage::PersistenceBusGate bus_gate( - ::platform::esp::common::shared_spi_coordinator(), - sys::runtime::BusAccessPolicy::RecoveryExclusive, - 500, - kSharedSpiBusResource, - kSharedSpiBusOwnerId + 2, - kSharedSpiBusOwnerId); - if (bus_gate.locked()) - { - ::platform::esp::arduino_common::storage::unmount_sd_card(); - log_d("SD card unmounted"); - } - else - { - log_w("Failed to acquire SPI lock for SD card unmount"); - } + ::platform::esp::arduino_common::storage::unmount_sd_card(); + log_d("SD card unmounted"); } bool TLoRaPagerBoard::isCardReady() @@ -1627,6 +1650,7 @@ int TLoRaPagerBoard::getKey(char* c) #ifdef USING_INPUT_DEV_KEYBOARD if (devices_probe & HW_KEYBOARD_ONLINE) { + I2CGuard i2c; return kb.getKey(c); } #endif diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 38e19942..490bd435 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -18,9 +18,11 @@ This document defines: - how the project should evolve to support `PIO + IDF + Linux` without maintaining multiple full copies of the same source tree The shared-SPI ownership and scheduling contract is documented separately in -[spi_bus_architecture.md](./spi_bus_architecture.md). Any ESP board that shares -display, SD, radio, font, track, team, or USB SPI access must follow that -contract; a feature-local mutex or arbiter is not a valid replacement. +[spi_bus_architecture.md](./spi_bus_architecture.md). It is the only document +that defines shared-SPI locking and transaction rules. Any ESP board that +shares display, SD, radio, font, track, team, or USB SPI access must follow +that technical boundary; feature specifications describe only semantic +behavior and must not copy the locking model. This is a direction document first, and an implementation plan second. diff --git a/docs/Best Practices/ESP_SHARED_SPI_BUS.md b/docs/Best Practices/ESP_SHARED_SPI_BUS.md deleted file mode 100644 index 6cedf932..00000000 --- a/docs/Best Practices/ESP_SHARED_SPI_BUS.md +++ /dev/null @@ -1,62 +0,0 @@ -# ESP Shared SPI Bus Rule - -## Scope - -本文档约束 ESP 平台上“多个外设共享同一条 SPI 总线”的运行时访问规则。 - -当前直接受此规则约束的典型设备包括: - -- `T-Deck` -- `T-LoRa Pager` - -在这些设备上,显示、SD、LoRa、NFC 等外设并不是各自拥有独立 SPI 控制器,而是在板级上共享同一条 SPI 总线。 - ---- - -## Rule - -`shared_spi_lock` 表达的是 **共享 SPI 总线所有权**,不是“显示锁”。 - -它的职责是: - -- 串行化共享 SPI 总线上的访问 -- 防止显示刷新与 SD / LoRa / NFC 等访问并发打总线 -- 作为运行时层统一的总线仲裁入口 - -推荐用法: - -- 直接调用 `shared_spi_lock` / `shared_spi_unlock` -- 优先使用 `SharedSpiLockGuard` - ---- - -## Naming Contract - -以下命名语义已经固定: - -- `shared_spi_lock` - - 含义是“申请共享 SPI 总线所有权” -- `SharedSpiLockGuard` - - 含义是“一个有作用域的共享 SPI 总线占用会话” - -`display_spi_lock` 只允许作为 **历史兼容别名** 存在,不能再作为新代码的主命名。 - -原因是: - -- 真实被保护的对象不是 display -- 而是 board-level shared SPI bus - -如果后续新代码继续使用“display lock”语义命名,等同于重新把总线仲裁误导回显示私有概念。 - ---- - -## Boundary - -本规则约束的是: - -- 平台运行时 -- UI 运行时 -- 地图瓦片 / 轨迹 / USB MSC / SSTV 等共享 SPI 访问路径 - -本规则不强制板级显示驱动内部必须如何组织其私有 mutex 实现; -但只要代码已经站在“共享 SPI 访问者”位置,而不是“显示驱动私有实现”位置,就应通过共享 SPI 语义入口表达自己。 diff --git a/docs/design/PROTOCOL_PARTITIONED_STORAGE_V2_DETAILED_DESIGN.md b/docs/design/PROTOCOL_PARTITIONED_STORAGE_V2_DETAILED_DESIGN.md index 4d7f6011..72285d76 100644 --- a/docs/design/PROTOCOL_PARTITIONED_STORAGE_V2_DETAILED_DESIGN.md +++ b/docs/design/PROTOCOL_PARTITIONED_STORAGE_V2_DETAILED_DESIGN.md @@ -203,8 +203,8 @@ delta thresholds are exceeded: Normal chat `flush()` does not inspect and compact all healthy journals. It only retries a dirty protocol projection, at most once per five seconds. Each -filesystem operation uses the bounded SD runtime guard; compaction never wraps -the complete operation sequence in a physical SPI lease. +filesystem operation goes through the storage service; compaction never wraps +the complete operation sequence in one device transaction. Snapshot replacement sequence: @@ -244,18 +244,14 @@ invariants, and the logical transaction that connects them. Recursive locking is required because public query/update methods may call another method on the same owner. This mutex is a state lock, not a bus lock. -`sd_card_runtime` is the only normal owner of physical SD/display/radio SPI -arbitration. `open`, `read`, `write`, `flush`, `exists`, `rename`, and `remove` -take bounded per-operation guards. Store/repository/page-cache code must not -acquire `PersistenceBusGate` or `SharedSpiBusAdapter` around a sequence of those -operations. Doing so stretches physical ownership across CPU work and creates -radio/display starvation even when every nested file call is individually -bounded. +The storage service is the only normal owner of physical SD I/O. Store, +repository, and page-cache code calls semantic storage operations and must not +open a physical device session around a sequence of those operations. The +shared-SPI mechanism is defined only in `docs/spi_bus_architecture.md`. Explicit hardware sessions are separate: SD unmount/recovery, USB mass storage, -and user-visible external font loading may own an exclusive bus/session token -under their dedicated lifecycle specification. They must not be copied into a -normal message, peer, or page-cache repository. +and user-visible external font loading are owned by their device services. +They must not be copied into a normal message, peer, or page-cache repository. No renderer waits for radio TX, MQTT forwarding, LoRa airtime, or projection compaction. SPI contention can delay or fail a bounded filesystem operation; diff --git a/docs/design/PROTOCOL_PARTITIONED_STORAGE_V2_OVERVIEW.md b/docs/design/PROTOCOL_PARTITIONED_STORAGE_V2_OVERVIEW.md index a90d5408..5b430e01 100644 --- a/docs/design/PROTOCOL_PARTITIONED_STORAGE_V2_OVERVIEW.md +++ b/docs/design/PROTOCOL_PARTITIONED_STORAGE_V2_OVERVIEW.md @@ -44,10 +44,10 @@ projections, not authoritative mutable blobs. message and peer writes do not wait for expensive UI-oriented reconstruction. **Two-level concurrency ownership:** Store/repository recursive mutexes protect -aggregate state and sequence invariants. They never represent physical SPI -ownership. `SdRuntimeFile`/`SdRuntimeDir` alone acquire the shared SPI bus for -each bounded filesystem operation. A repository transaction must not hold a -shared-SPI token around a sequence of SD runtime calls. +aggregate state and sequence invariants. They never represent device I/O +ownership. The storage service owns file transactions and the repository must +not hold a device transaction around a sequence of storage calls. The physical +shared-SPI mechanism is defined only in `docs/spi_bus_architecture.md`. ## High-Level Commit Flows diff --git a/docs/models/track-recording/model.md b/docs/models/track-recording/model.md index 1e319b0a..7f8d45a4 100644 --- a/docs/models/track-recording/model.md +++ b/docs/models/track-recording/model.md @@ -46,7 +46,7 @@ stateDiagram-v2 - `DefaultTrackFlushPolicy` 在 buffer size ≥ 8 时 flush。 - `StopTrack` 与 `Flush` 被视为 critical command。 - `TrackStorageWorker` 同时只持有一个 `pending_command_`;忙时 `submit` 返回 false。 -- 文件写入通过 `ITrackFileAdapter`,总线争用通过 `IBusArbiter` 表达。 +- 文件写入通过 `ITrackFileAdapter`,设备可用性由文件适配器返回语义结果。 这是 Trail Mate ESP stack hygiene 在领域运行时中的直接体现,不是一般性的“性能建议”。 diff --git a/docs/protocol_runtime_budget_policy.md b/docs/protocol_runtime_budget_policy.md index b0a3fdf2..fe3e024b 100644 --- a/docs/protocol_runtime_budget_policy.md +++ b/docs/protocol_runtime_budget_policy.md @@ -190,11 +190,11 @@ Forbidden scheduler shapes: - Separate local drain counters that allow protocol actions, app sends, ACK retry, and MQTT downlink each to consume a full TX slot in the same tick. -Shared-SPI busy is a deferred radio condition, not successful airtime. The -radio task retains the front TX packet and retries it with bounded exponential -backoff when the board reports SPI access busy. IRQ polling uses a zero-wait -probe and must never block a frame-critical display operation. Queue buffers and -RX scratch use PSRAM on PSRAM-capable targets. +Device I/O deferral is not successful airtime. The radio task retains the front +TX packet and retries it with bounded exponential backoff when the radio device +service reports `Deferred`. IRQ polling remains bounded and must never block a +frame-critical display operation. Queue buffers and RX scratch use PSRAM on +PSRAM-capable targets. ## Protocol Switch Lifecycle @@ -206,7 +206,7 @@ install the backend, and switch Chat/Contacts active protocol projections. If quiescing or installation fails, the old protocol remains active and its configuration is reapplied. A task must never be force-suspended while it may -hold a shared-SPI lock. Settings reports success/failure from this transition; +be inside a radio device transaction. Settings reports success/failure from this transition; it must not issue an unconditional software reset that makes a successful switch look like a crash. diff --git a/docs/specification/CHAT_DELIVERY_RUNTIME_SPEC.md b/docs/specification/CHAT_DELIVERY_RUNTIME_SPEC.md index a58cfebd..21c4c139 100644 --- a/docs/specification/CHAT_DELIVERY_RUNTIME_SPEC.md +++ b/docs/specification/CHAT_DELIVERY_RUNTIME_SPEC.md @@ -111,10 +111,10 @@ authoritative; catalog/read/status are projections. Once an inbound message and its required RT LXMF dedup identity are durable, catalog failure must not block observer, UI, or notification publication. -On ESP shared-SPI targets, chat append and status transactions use a -non-blocking outer SPI lease. Failure to obtain the lease produces `Deferred`; -the hot path must not multiply the storage runtime's per-operation wait or run -a synchronous full index rebuild. +On ESP targets, chat append and status transactions call the storage service +and may produce `Deferred` when the device cannot complete the operation yet. +The chat runtime does not own or inspect the storage service's physical +transaction mechanism. ## Boundaries diff --git a/docs/specification/LOCALIZATION_SPEC.md b/docs/specification/LOCALIZATION_SPEC.md index cd8d5db9..b91565ae 100644 --- a/docs/specification/LOCALIZATION_SPEC.md +++ b/docs/specification/LOCALIZATION_SPEC.md @@ -504,8 +504,8 @@ English、基础特殊字符输入与精选 emoji 输入必须在没有任何外 11. 显式切换 locale 时加载 UI 字体属于 locale 激活流程;内容文本缺字路径可以请求内容字体 owner,但不能在页面/widget 内私自复用 SD 读。 12. 外部 `source=binfont` 字体 pack 必须在 catalog 阶段验证 `font.bin` 路径可规范化且可打开;缺少 payload 的 locale 不能进入可选 locale 列表。 13. “显示 busy modal” 的代码语义不是只创建 LVGL 对象,而是必须在进入 `lv_binfont_create()` / 外部 `font.bin` 读取之前,强制把 modal flush 到屏幕。当前绑定点是 `resource_pack_registry.cpp` 的 `ScopedFontLoadOverlay`,它是 `load_font_pack()` 的唯一同步字体加载 UI 边界。 -14. ESP 上所有通过 LVGL FS 读取外部 `font.bin` 的同步加载,都必须进入完整的 shared-SPI bus transaction。`lv_begin_external_font_load_fs_scope()` 不能只是 depth flag 或“让每次 FS callback 多等一点”的旁路;它必须成功取得 runtime bus token 后,才允许进入 `lv_binfont_create()`。 -15. 外部字体加载事务取得 bus token 失败属于瞬时 `bus_busy`,只能进入短退避并保留后续重试机会;只有已经取得 bus token 但 `lv_binfont_create()` 返回空,才按字体文件/格式失败进入长 backoff。 +14. ESP 上所有通过 LVGL FS 读取外部 `font.bin` 的同步加载,都必须经过平台字体设备服务;页面和 registry 不直接执行存储事务。 +15. 字体设备服务暂时无法完成读取时,加载进入可重试的 `pending` 状态;只有字体文件或格式本身确认失败,才进入长 backoff。 这条规则的目标是同时保护 UI 实时域和内容可读性:联系人页、聊天页、地图 overlay、节点详情页等内容页面不得因为遇到中文/日文/韩文/阿拉伯文本而静默拖入无主 SD 阻塞 IO,也不得为了避免阻塞而让可用字体永远不加载。 @@ -514,16 +514,16 @@ English、基础特殊字符输入与精选 emoji 输入必须在没有任何外 外部字体加载事务的边界如下: 1. `ScopedFontLoadOverlay` 通过 foreground operation `I18nFontLoad` slot 先发布阻塞式 busy modal,并保留字体加载要求的强制刷新帧数。 -2. `ScopedExternalFontLoadFs` 调用平台 scope begin,申请 owner 为 `lvgl_font_sd` 的 shared-SPI runtime bus token。 -3. 只有 begin 返回成功,`load_font_pack()` 才能调用 `lv_binfont_create()`。 -4. `lv_binfont_create()` 内部触发的 LVGL SD FS `open/read/seek/tell/close` 回调仍然通过 `SharedSpiLockGuard`,但由于底层 physical lock 是同任务可重入的,这些回调会复用外层事务,而不是每个小读片段重新竞争总线。 -5. scope end 释放 runtime bus token;busy modal 随后关闭并刷新。 +2. `ScopedExternalFontLoadFs` 调用平台字体设备服务,提交一次受控的外部字体加载请求。 +3. 只有设备服务确认可以开始加载,`load_font_pack()` 才能调用 `lv_binfont_create()`。 +4. `lv_binfont_create()` 内部触发的 LVGL FS 回调属于平台适配器;页面和 registry 不参与其文件访问细节。 +5. 设备服务报告完成或失败后,busy modal 随后关闭并刷新。 这个事务是同步外部字体加载的唯一平台入口。禁止重新引入以下旧实现: -- 只维护 `external_font_load_depth`,不持有 shared-SPI token。 -- 在 LVGL FS callback 中把 timeout 调大来掩盖外层没有事务的问题。 -- 总线忙时把失败计入 5 分钟字体文件 backoff。 +- 只维护 `external_font_load_depth` 并绕过字体设备服务。 +- 在页面或 LVGL FS callback 中把等待时间调大来掩盖外层没有设备事务的问题。 +- 设备暂时不可用时把失败计入 5 分钟字体文件 backoff。 - 页面/widget 直接绕过 registry 读取 `font.bin`。 #### Registry-time preferred content supplement preload diff --git a/docs/specification/MAP_TILE_RENDER_QUEUE_CACHE_SPEC.md b/docs/specification/MAP_TILE_RENDER_QUEUE_CACHE_SPEC.md index 8ec16597..eeb46037 100644 --- a/docs/specification/MAP_TILE_RENDER_QUEUE_CACHE_SPEC.md +++ b/docs/specification/MAP_TILE_RENDER_QUEUE_CACHE_SPEC.md @@ -11,10 +11,14 @@ Render queue owns the current visible tile plan. Renderer draws widgets from the visible plan. It must not own tile path policy. The render queue is a UI-facing projection of tile state. It is not the owner of -slow tile work. File lookup, SD access, shared-SPI arbitration, image decode, -and cache fill must run through the command/worker/event design described in +slow tile work. File lookup, SD access, device I/O, image decode, and cache fill +must run through the command/worker/event design described in `UI_STORAGE_EVENT_RUNTIME_DESIGN_SPEC.md`. +The shared-SPI arbitration itself is specified only in +`docs/spi_bus_architecture.md`; this render specification does not expose or +restate its lock details. + ## Objects | Object | Pattern | Responsibility | Forbidden | diff --git a/docs/specification/MAP_TILE_SOURCE_CACHE_RUNTIME_SPEC.md b/docs/specification/MAP_TILE_SOURCE_CACHE_RUNTIME_SPEC.md index 68023755..dacf9ab9 100644 --- a/docs/specification/MAP_TILE_SOURCE_CACHE_RUNTIME_SPEC.md +++ b/docs/specification/MAP_TILE_SOURCE_CACHE_RUNTIME_SPEC.md @@ -13,9 +13,13 @@ contour data-source credentials. Tile source/cache work must also conform to `UI_STORAGE_EVENT_RUNTIME_DESIGN_SPEC.md`: UI owner code submits tile intents, workers perform filesystem/storage work, and tile results return as events. A -renderer must not open tile files, wait for shared-SPI, or decode tile payloads +renderer must not open tile files, wait for device I/O, or decode tile payloads from an input callback, LVGL timer, GTK callback, or page render callback. +The physical shared-device mechanism is a platform technical concern defined +only by `docs/spi_bus_architecture.md`. The map runtime calls a tile/storage +service and consumes semantic results. + ## Objects | Object | Pattern | Responsibility | Forbidden | @@ -111,7 +115,7 @@ The distinction for this phase is: | Concern | Owner | Must Not | | --- | --- | --- | -| Visible tile math | UI owner / renderer | Open files, wait for shared SPI | +| Visible tile math | UI owner / renderer | Open files, wait for device I/O | | Tile availability | Tile source worker | Drive LVGL objects | | Tile payload bytes | Tile source worker -> event payload | Leak filesystem paths into render code | | Decoded LVGL image descriptors | Renderer-owned decoded cache | Perform SD reads during decode from a page/timer/input callback | @@ -123,17 +127,16 @@ Mandatory behavior: visible tile refs, move/hide LVGL objects, submit async tile commands, and apply already-delivered tile events. - `tile_loader_step()` and functions it calls synchronously must not call - `lv_fs_open`, Arduino `SD.open`, `SdRuntimeFile`, `SharedSpiLockGuard`, or - any other display-shared SPI acquisition. + `lv_fs_open`, Arduino `SD.open`, `SdRuntimeFile`, or any device I/O + transaction directly. - Missing tile detection is a worker result, not a UI probe. A missing result is cached/backed off so dragging over a sparse map area cannot repeatedly open the same missing files from the UI cadence. -- A transient worker read failure is not a missing tile. Only an explicit - not-found classification may populate missing-tile memory. If the platform - storage API cannot expose the open/read failure reason directly, the worker - may perform a one-time failure-path lookup after a failed read to classify - the tile as missing or retryable. The successful read path must remain a - single source read. +- A transient worker read failure is not a missing tile. The storage adapter + returns a typed read result (`Ready`, `Missing`, `RetryLater`, `Error`, or + `Invalid`) so the worker never performs a second `exists()` probe to guess + the reason. Only the explicit `Missing` result may populate missing-tile + memory. The successful read path remains a single source read. - Base and contour tiles use the same async source/event mechanism. Contour overlays must not keep a second synchronous `lv_fs_open` path. - ESP UI helpers such as `base_tile_available()` are not authoritative storage @@ -146,12 +149,11 @@ Mandatory behavior: nonexistent SD paths and starve the display SPI bus. - The ESP worker must read a tile payload in one source operation on the success path. It must not perform a separate existence lookup followed by a - read for every tile in the active map path. A failure-path classification - lookup is allowed only after read failure and only to decide whether the - failure is confirmed missing or retryable pressure/transient failure. -- The ESP worker may return `ResourceBusy` when display-shared SPI is busy or - cooling down. `ResourceBusy` is not a tile-missing result; the renderer keeps - the tile requestable after a short backoff. + read for every tile in the active map path. The device storage adapter owns + the complete sequence of bounded storage transactions. +- The ESP worker may return `RetryLater` when the tile/storage service cannot + complete the request yet. `RetryLater` is not a tile-missing result; the + renderer keeps the tile requestable after a short backoff. - Display pressure is IO backpressure, not a viewport/layout veto. The UI owner may continue visible tile math, anchor updates, loaded-tile layout, render queue rebuilds, and bounded event draining while display pressure is recent. diff --git a/docs/specification/PROTOCOL_PARTITIONED_STORAGE_V2_SPEC.md b/docs/specification/PROTOCOL_PARTITIONED_STORAGE_V2_SPEC.md index 90f76c64..5a689bb3 100644 --- a/docs/specification/PROTOCOL_PARTITIONED_STORAGE_V2_SPEC.md +++ b/docs/specification/PROTOCOL_PARTITIONED_STORAGE_V2_SPEC.md @@ -201,15 +201,16 @@ internal memory. They must not become an additional fact source. ## Scheduling Rule -Message append uses a non-blocking durable SPI lease. Contention defers the -message through the ledger rather than blocking the UI. +Message append uses the storage service's non-blocking durable operation. +Contention or temporary device unavailability defers the message through the +ledger rather than blocking the UI. Peer observation deltas use a non-blocking append. Failed appends enter a PSRAM-backed ordered queue. The runtime drains at most four peer deltas per service tick and does not drain during call realtime resource preemption. -Contact edits are explicit user transactions and use a bounded durable lease. -Memory is updated only after the contact delta is durable. +Contact edits are explicit user transactions and use a bounded durable storage +operation. Memory is updated only after the contact delta is durable. Large snapshot compaction runs during startup. Normal runtime flush only retries a dirty chat projection at a throttled interval; it does not compact diff --git a/docs/specification/RUNTIME_CONCURRENCY_SPEC.md b/docs/specification/RUNTIME_CONCURRENCY_SPEC.md index 6cfb49f0..422d6391 100644 --- a/docs/specification/RUNTIME_CONCURRENCY_SPEC.md +++ b/docs/specification/RUNTIME_CONCURRENCY_SPEC.md @@ -55,8 +55,8 @@ Storage backends must declare their concurrency model. Mutable app-service state must have a single owner context. -The UI owner context must not wait for blocking storage, shared-SPI, -filesystem, decode, or persistence work. UI paths may submit commands, consume +The UI owner context must not wait for blocking storage, device I/O, filesystem, +decode, or persistence work. UI paths may submit commands, consume ready events, or attempt explicitly non-blocking work that can be abandoned within the frame budget. @@ -97,7 +97,7 @@ radio_irq -> MeshSession gps_task -> lvgl gtk_worker -> GtkWidget ui_thread -> blocking storage write -ui_thread -> blocking shared-SPI wait +ui_thread -> blocking device I/O wait ui_thread -> filesystem open/read/write/list ui_thread -> image decode from storage ui_thread -> track file create/flush/list @@ -202,7 +202,7 @@ Other contexts may send commands, publish events, or consume snapshots. Slow work must have an explicit owner. Page widgets, LVGL timers, GTK callbacks, and input handlers are not valid owners for durable storage, filesystem walking, -tile decode, shared-SPI waits, protocol retries, or persistence flushes. +tile decode, device I/O waits, protocol retries, or persistence flushes. Valid slow-work owners include: @@ -237,5 +237,5 @@ UI event drain ``` The simulator must assert that UI owner code does not execute blocking -storage/shared-SPI/filesystem calls and that background code does not execute +storage/device-I/O/filesystem calls and that background code does not execute concrete renderer calls. diff --git a/docs/specification/RUNTIME_OWNERSHIP_BOUNDARY_FREEZE.md b/docs/specification/RUNTIME_OWNERSHIP_BOUNDARY_FREEZE.md index 1dd32b0f..7350ee10 100644 --- a/docs/specification/RUNTIME_OWNERSHIP_BOUNDARY_FREEZE.md +++ b/docs/specification/RUNTIME_OWNERSHIP_BOUNDARY_FREEZE.md @@ -298,8 +298,8 @@ outgoing protocol acceptance 3. outgoing record 或后续 status 写入失败时,由 `MessageLedger` 保留 bounded pending write; message page、conversation page 和 lookup 必须合并该 pending state,不能谎报 `stored`, 也不能让 UI 另造临时消息。 -4. pending write 每个 runtime tick 只允许执行有限预算。ESP chat store 必须先以 non-blocking - 方式取得共享 SPI transaction lease;拿不到立即 deferred,不能在同一 tick 连续等待多个 +4. pending write 每个 runtime tick 只允许执行有限预算。ESP chat store 必须调用 storage + service 的非阻塞语义接口;设备暂时不可用时立即 deferred,不能在同一 tick 连续等待多个 250ms SD 操作。 5. conversation index、header mirror 和 UI cache 仍然只是 projection。projection 写失败可以让 ledger operation 保持 pending,但不得触发收发热路径中的同步全盘 `rebuildIndex()`。 @@ -479,7 +479,7 @@ Nomad page cache 读取属于后台存储工作,Network 页面只提交请求 是可观察的终止状态,不能把 mutex/queue 已创建误认为 worker 可用。 4. `request_cached_page_load()` 和 `poll_cached_page_load()` 在 worker unavailable 时必须快速返回 明确状态,不能继续排队、等待 SD,或在每帧重新创建 task。 -5. cache read/write 必须经过 PageCache bus owner;UI 不等待 SD/shared-SPI transaction 完成。 +5. cache read/write 必须经过 PageCache storage service;UI 不等待设备存储事务完成。 禁止: @@ -500,7 +500,7 @@ Nomad page cache 读取属于后台存储工作,Network 页面只提交请求 3. 缺字检测可以发生在内容路径,但加载决策必须交给 `FontRuntimeCoordinator` / `ResourcePackRegistry`。 4. 同步外部字体加载是允许的,但只能作为用户可见的 foreground operation: - 显示 loading/progress/busy 页面或 modal,flush 到屏幕,拿到 SD/shared-SPI lease,然后加载。 + 显示 loading/progress/busy 页面或 modal,flush 到屏幕,然后交给字体设备服务加载。 5. 普通 render/list/timer 路径不得无主静默阻塞 SD IO。 6. 总线忙、内存不足、文件损坏必须形成可解释诊断和重试/失败状态,不能被永久 hard skip。 7. 页面不得因为 `ui_hot_path`、`active_locale`、或 `content_supplement` 标签直接否决字体加载。 diff --git a/docs/specification/UI_FEEDBACK_RUNTIME_SPEC.md b/docs/specification/UI_FEEDBACK_RUNTIME_SPEC.md index 249c4f97..5e9f0f86 100644 --- a/docs/specification/UI_FEEDBACK_RUNTIME_SPEC.md +++ b/docs/specification/UI_FEEDBACK_RUNTIME_SPEC.md @@ -113,7 +113,7 @@ EventBus dispatch This prevents a feedback prompt from re-entering LVGL while a page is being destroyed, while event dispatch is draining queued runtime events, or while the -display/shared-SPI presenter is in a sensitive refresh window. +display presenter is in a sensitive refresh window. If async scheduling fails, the runtime must release the request payload and return failure to the producer. Producers may ignore that return value for diff --git a/docs/specification/UI_STORAGE_EVENT_RUNTIME_DESIGN_SPEC.md b/docs/specification/UI_STORAGE_EVENT_RUNTIME_DESIGN_SPEC.md index 0b8b24e1..2ce9bb54 100644 --- a/docs/specification/UI_STORAGE_EVENT_RUNTIME_DESIGN_SPEC.md +++ b/docs/specification/UI_STORAGE_EVENT_RUNTIME_DESIGN_SPEC.md @@ -3,23 +3,27 @@ Status date: 2026-06-17 This document defines the design baseline for removing UI stalls caused by -blocking storage, shared-SPI contention, synchronous persistence, and page-owned -runtime side effects. +blocking storage, synchronous persistence, and page-owned runtime side +effects. It complements `RUNTIME_CONCURRENCY_SPEC.md`. The concurrency spec states the rules. This document explains the design that makes those rules implementable and testable. +The physical shared-device mechanism is a technical concern owned by +`docs/spi_bus_architecture.md`. This document defines only UI/runtime ownership +and semantic storage behavior. + ## Problem Statement Trail Mate currently has several paths where renderer-owned code, runtime -services, and platform storage operations can enter the same slow resource path: +services, and platform device operations can enter the same slow resource path: -- LVGL timers and page callbacks can trigger SD or shared-SPI access. +- LVGL timers and page callbacks can trigger device I/O. - Map tile loading can perform file lookup, image object setup, and decode work from the UI owner context. -- GPS track recording can hold recorder state and shared-SPI access while it - opens, writes, flushes, and closes files. +- GPS track recording can hold recorder state while it opens, writes, flushes, + and closes files. - Node and contact persistence can be reached from event dispatch paths. - Feedback notices, protocol completion, and UI mutation can be coupled to the page that happened to start an operation. @@ -32,17 +36,16 @@ handling appear frozen even though background logs continue. The design goal is therefore: ```text -UI owner context never waits for storage, shared-SPI, filesystem, decode, -protocol completion, or persistence. +UI owner context never waits for device I/O, filesystem, decode, protocol +completion, or persistence. ``` -After the ESP shared-SPI map freeze investigation, this goal is tightened: +After the map freeze investigation, this goal is tightened: ```text Moving work off the UI owner context is necessary but not sufficient. -No storage worker may hold, starve, or repeatedly reacquire a physical bus that -display flush depends on in a way that prevents frame progress, input-visible -feedback, wake rendering, or page navigation. +No device service may starve frame presentation or input-visible feedback in a +way that prevents wake rendering or page navigation. ``` Responsiveness is protected by ownership of time-critical resources, not only @@ -128,7 +131,7 @@ objects. A platform adapter wraps a technical API: - SD file open/read/write/flush -- SPI bus acquire/release +- semantic device I/O - LVGL image descriptor setup - nRF flash write - GTK idle invocation @@ -137,43 +140,6 @@ A platform adapter wraps a technical API: Adapters must not own business rules, retry policy, tile priority, feedback eligibility, chat delivery semantics, or tracker state transitions. -### Resource Topology - -Resource topology describes the physical resource domains that can block each -other on a target: - -- display flush bus / DMA path -- SD or flash storage bus -- radio SPI bus -- touch or NFC bus -- shared mutex or controller domain - -Two features are in different business contexts but the same physical topology -when they share a controller, chip-select bus, DMA path, or lock. On ESP targets -such as T-Deck and T-LoRa Pager, display, SD, radio, and optional NFC can share -the same SPI domain. Therefore a storage operation that holds the shared bus can -freeze display progress even when it runs outside the UI owner task. - -### Display Frame Critical Resource - -Display flush, wake rendering, and input-visible feedback are frame-critical. -They are not ordinary bus consumers. Any storage or protocol worker using a -display-shared resource must yield to frame progress. - -Four budgets are distinct: - -| Budget | Meaning | Failure mode when missing | -| --- | --- | --- | -| `wait_budget` | Maximum time a caller waits to acquire a resource | Caller blocks before work starts | -| `hold_budget` | Maximum expected time a holder keeps the resource | Display frames are starved after acquisition | -| `burst_budget` | Maximum repeated acquisitions in a time window | Many short operations still freeze UI | -| `frame_budget` | Maximum time display can be denied progress | Screen appears frozen while logs continue | - -Bounded waiting alone is not sufficient. A worker can acquire immediately and -still hold the display-shared bus for tens of milliseconds during SD open/read. -That violates this spec unless the operation is outside UI hot paths, explicitly -budgeted, and followed by cooldown/backpressure. - ## Pattern Decision The design uses a small set of patterns with explicit responsibilities. @@ -184,12 +150,12 @@ The design uses a small set of patterns with explicit responsibilities. | Command | Runtime intents queued to workers | Separates request creation from execution and enables cancellation, priority, diagnostics | Wire-format payloads or platform calls | | Observer / Event Bus | Completion, failure, state, feedback, UI effects | Reports asynchronous outcomes without coupling to the current page | Direct renderer mutation from background contexts | | State | Track recording, tile loading, storage health, chat send lifecycle | Makes cross-time behavior explicit instead of encoded in locks or call stacks | Page-local boolean flags | -| Strategy | Bus access, tile loading, flush, persistence, retry policies | Keeps environment and mode differences explicit and swappable | Scattered `if target` branches | +| Strategy | Device I/O, tile loading, flush, persistence, retry policies | Keeps environment and mode differences explicit and swappable | Scattered `if target` branches | | Bridge | Runtime rule side to platform execution side | Keeps shared business rules single-source while allowing ESP/nRF/Linux backends | Another adapter with business decisions | | Adapter | Wrap platform APIs | Contains technical integration details | A second business implementation | -| Mediator / Arbiter | Shared resource scheduling | Replaces ad-hoc lock competition with observable scheduling | A new god object for product behavior | -| Unit of Work | Track flush, node/contact save, config persistence | Batches durable writes and reduces SD/SPI transaction count | An unbounded memory buffer | -| Circuit Breaker / Backpressure | Slow or failing SD/SPI/storage | Degrades features without freezing UI | Silent data loss | +| Device service | Technical I/O boundary | Replaces ad-hoc hardware calls with observable semantic results | A new god object for product behavior | +| Unit of Work | Track flush, node/contact save, config persistence | Batches durable writes and reduces device I/O calls | An unbounded memory buffer | +| Circuit Breaker / Backpressure | Slow or failing device I/O/storage | Degrades features without freezing UI | Silent data loss | | Visitor / Effect Apply | Optional UI-effect application in owner context | Centralizes renderer-side effect application | Business decision engine | The primary axis is: @@ -212,7 +178,7 @@ The first code slice for a runtime area must add: - value objects for intents, commands, events, state, priority, generation, and diagnostics -- interfaces for queues, workers, bus/storage arbitration, platform adapters, +- interfaces for queues, workers, device services, platform adapters, event sinks, and UI-effect application - fake implementations for clock, command queue, event bus, storage backend, bus arbiter, worker completion, and feedback presenter @@ -309,12 +275,6 @@ classDiagram +publish(event) } - class IBusArbiter { - <> - +acquire(policy, command_id) - +release(token) - } - class IPlatformStorageAdapter { <> +read(request) @@ -331,7 +291,6 @@ classDiagram class RuntimePolicyStrategy { <> +selectPriority(intent) - +selectBusPolicy(command) +selectRetry(command, result) } @@ -343,7 +302,6 @@ classDiagram RuntimeFacade o-- RuntimePolicyStrategy IActiveWorker --> RuntimeCommand IActiveWorker --> RuntimeEvent - IActiveWorker o-- IBusArbiter IActiveWorker o-- IPlatformStorageAdapter IEventSink --> RuntimeEvent IUiEffectSink --> RuntimeEvent @@ -378,7 +336,7 @@ flowchart TB end subgraph Platform["Platform Adapters"] - Bus["Bus / Storage Arbiter"] + DeviceIo["Device I/O Services"] Storage["Storage Adapter"] Decode["Decode Adapter"] end @@ -390,10 +348,10 @@ flowchart TB Commands --> TileWorker Commands --> TrackWorker Commands --> PersistWorker - TileWorker --> Bus - TrackWorker --> Bus - PersistWorker --> Bus - Bus --> Storage + TileWorker --> DeviceIo + TrackWorker --> DeviceIo + PersistWorker --> DeviceIo + DeviceIo --> Storage TileWorker --> Decode TileWorker --> Events TrackWorker --> Events @@ -408,7 +366,7 @@ Forbidden reverse dependencies: - platform adapter to page/widget - worker to concrete renderer - page/widget to storage adapter -- page/widget to shared-SPI lock +- page/widget to device I/O implementation - adapter to product policy - UI event drain to blocking worker execution @@ -502,7 +460,7 @@ flowchart TB subgraph PlatformContext["Platform Adapter Context"] StorageAdapter["Storage adapter"] - BusAdapter["Bus / SPI adapter"] + DeviceIo["Device I/O adapter"] DecodeAdapter["Decode adapter"] ClockAdapter["Clock adapter"] RadioAdapter["Radio adapter"] @@ -511,7 +469,7 @@ flowchart TB subgraph TestContext["Simulation Context"] FakeClock["Fake clock"] FakeStorage["Fake storage"] - FakeBus["Fake bus arbiter"] + FakeDeviceIo["Fake device I/O"] FakeUi["Fake UI owner"] FakeEvents["Fake event bus"] end @@ -528,9 +486,6 @@ flowchart TB TileWorker --> StorageAdapter TrackWorker --> StorageAdapter PersistWorker --> StorageAdapter - TileWorker --> BusAdapter - TrackWorker --> BusAdapter - PersistWorker --> BusAdapter TileWorker --> DecodeAdapter ProtocolWorker --> RadioAdapter WorkerContext --> Events @@ -577,7 +532,7 @@ flowchart LR end subgraph ResourceOwner["Resource owner"] - Arbiter["Storage / bus arbiter"] + DeviceIo["Device I/O service"] Storage["SD / flash adapter"] Decode["Decode adapter"] end @@ -593,10 +548,10 @@ flowchart LR CommandPump --> TileWorker CommandPump --> TrackWorker CommandPump --> PersistWorker - TileWorker --> Arbiter - TrackWorker --> Arbiter - PersistWorker --> Arbiter - Arbiter --> Storage + TileWorker --> DeviceIo + TrackWorker --> DeviceIo + PersistWorker --> DeviceIo + DeviceIo --> Storage TileWorker --> Decode TileWorker --> UiDrain TrackWorker --> UiDrain @@ -608,264 +563,16 @@ one physical thread, the same ownership model still applies cooperatively: slow work must be incremental, budgeted, and represented as commands/events rather than blocking UI execution. -When storage and display share a physical SPI domain, the storage worker is also -not the resource owner. It owns command state. A bus/storage scheduler owns -permission to occupy the display-shared resource and must apply wait, hold, -burst, and frame budgets. +The storage worker is not the device owner. It owns command state and calls a +device storage service. The service owns the physical transaction mechanism +described in `docs/spi_bus_architecture.md`. -## UML Bus Arbitration Class Model +## Device I/O Boundary -```mermaid -classDiagram - class BusAccessPolicy { - <> - DisplayFrameCritical - UiNeverBlock - InteractiveWorkerBounded - BackgroundWorkerBounded - DurableCommit - RecoveryExclusive - } - - class BusAcquireRequest { - +resource - +policy - +command_id - +deadline_ms - +origin - } - - class BusAccessToken { - +resource - +owner - +acquired_ms - +valid - } - - class BusAcquireResult { - +status - +token - +diagnostics - } - - class BusDiagnostics { - +resource - +owner_task - +wait_ms - +hold_ms - +policy - +command_id - } - - class IBusArbiter { - <> - +acquire(request) BusAcquireResult - +release(token) - +health() StorageHealthState - } - - class IBusAdapter { - <> - +tryAcquire(timeout_ms) - +release() - } - - class BusPolicyStrategy { - <> - +select(command) BusAccessPolicy - +timeoutFor(policy) - } - - class StorageHealthState { - <> - +status - +last_error - +last_transition_ms - } - - class StorageBusArbiter { - +acquire(request) - +release(token) - +health() - } - - IBusArbiter <|.. StorageBusArbiter - StorageBusArbiter o-- IBusAdapter - StorageBusArbiter o-- BusPolicyStrategy - StorageBusArbiter o-- StorageHealthState - BusAcquireRequest --> BusAccessPolicy - BusAcquireResult --> BusAccessToken - BusAcquireResult --> BusDiagnostics -``` - -## UML ESP Shared-SPI Lock Mechanism - -This section is the normative specification for the ESP shared-SPI lock -mechanism that protects UI responsiveness when display refresh, SD-backed LVGL -FS, map tile IO, track persistence, radio, NFC, USB, and other peripherals share -one physical SPI controller. - -The mechanism is intentionally specified in `docs/specification`, not in -historical best-practice notes. Any future change to the lock behavior must -update this section and the code bindings below in the same commit. - -### Lock Mechanism Distinctions - -| Concept | Meaning | Current code binding | -| --- | --- | --- | -| Physical shared bus | The real contested resource: board-level SPI controller and chip-select domain | `platform/esp/common/include/platform/esp/common/shared_spi_lock.h` | -| Frame-critical display consumer | Display flush, wake rendering, and input-visible feedback | `platform/esp/boards/src/display/DisplayInterface.cpp` | -| Worker-domain storage consumer | Map tile SD reads, track persistence, node/team/config stores, pack IO | Platform/runtime adapters that use `SharedSpiLockGuard` or `IBusArbiter` | -| LVGL FS adapter | Technical adapter allowing LVGL to read SD/flash paths; not a scheduler and not a business owner | `platform/esp/arduino_common/src/LV_Helper_v9.cpp` | -| Runtime bus arbiter | Policy boundary that turns commands into bounded bus acquire/release attempts | `modules/core_sys/include/sys/runtime_async.h`, ESP map binding in `platform/esp/arduino_common/src/ui/widgets/map/map_tiles.cpp` | -| Display pressure signal | A recent display lock timeout that tells worker-domain storage to cool down | `note_display_spi_timeout()`, `display_spi_recently_timed_out()` | -| Legacy display-lock alias | Old naming that implied the lock belonged to display instead of the bus | Burned down; `display_spi_lock()` / `display_spi_unlock()` must not exist in active headers | - -The most important distinction is this: - -```text -The lock belongs to the physical shared SPI bus. -Display is the frame-critical client of that bus, not the owner of the concept. -``` - -Therefore code outside the display driver must not name the mechanism as a -display-private lock. New code must use runtime bus ports, `shared_spi_lock*`, -or `SharedSpiLockGuard` depending on layer. - -### Why LVGL FS Does Not Solve This - -LVGL can register file-system callbacks. That gives LVGL a path to call open, -read, write, seek, tell, and dir operations. It does not give LVGL knowledge of: - -- which board peripherals share one physical SPI controller -- which consumer is frame-critical -- whether a display flush just timed out -- how many storage commands may run in one burst -- whether a tile load is stale after a map drag -- whether a track flush is durable or background - -For that reason `init_sd_fs_driver()` is only a platform adapter registration. -It does not make SD and display contention safe by itself. The callbacks in -`LV_Helper_v9.cpp` must remain short, bounded, and allowed to fail with busy or -failed-open semantics. Product/UI code must not use LVGL FS as a synchronous -storage probe from renderer hot paths. - -### Static Class Binding - -```mermaid -classDiagram - class SharedSpiLockPort { - <> - +shared_spi_lock(wait_ticks) - +shared_spi_lock_with_owner(wait_ticks, owner) - +shared_spi_unlock() - +note_display_spi_timeout(now_ms) - +display_spi_recently_timed_out(now_ms, window_ms) - } - - class SharedSpiLockGuard { - <> - +SharedSpiLockGuard(wait_ticks, owner) - +locked() - } - - class SharedSpiBusAdapter { - <> - +tryAcquire(timeout_ms) - +release() - +nowMs() - +owner() - } - - class FixedSharedSpiBusPolicyStrategy { - <> - +select(command) - +timeoutFor(policy) - } - - class LilyGoDispArduinoSPI { - <> - +pushColors(area) - +writeCommand(cmd) - +lock(wait_ticks, owner) - +unlock() - +lockOwnerLabel() - +lastLockHeldMs() - } - - class LvglSdFsAdapter { - <> - +sd_fs_open(path, mode) - +sd_fs_read(file, buffer, bytes) - +sd_fs_write(file, buffer, bytes) - +sd_fs_dir_open(path) - } - - class RuntimeBusAbstractions { - <> - +BusAcquireRequest - +BusAccessToken - +BusAcquireResult - +IBusArbiter - +StorageHealthState - } - - class EspMapTileBusArbiter { - <> - +acquire(request) - +release(token) - +health() - } - - class LvglExternalFontLoadScope { - <> - +lv_begin_external_font_load_fs_scope() - +lv_end_external_font_load_fs_scope() - } - - class MapTileWorker { - <> - +execute(command, now_ms) - } - - class SdMapTileFileSystem { - <> - +exists(path) - +isDirectory(path) - +readFile(path, buffer, capacity) - } - - SharedSpiLockGuard --> SharedSpiLockPort - SharedSpiBusAdapter --> SharedSpiLockPort - FixedSharedSpiBusPolicyStrategy --> RuntimeBusAbstractions - LilyGoDispArduinoSPI --> SharedSpiLockPort : publishes pressure - LvglSdFsAdapter --> SharedSpiLockGuard - LvglExternalFontLoadScope --> RuntimeBusAbstractions - LvglExternalFontLoadScope --> SharedSpiBusAdapter - LvglExternalFontLoadScope --> LvglSdFsAdapter : bounds lv_binfont_create - RuntimeBusAbstractions <|.. EspMapTileBusArbiter - EspMapTileBusArbiter --> SharedSpiLockPort - MapTileWorker --> RuntimeBusAbstractions - MapTileWorker --> SdMapTileFileSystem - SdMapTileFileSystem --> SharedSpiLockPort : chunk yield -``` - -Static ownership rules: - -- `LilyGoDispArduinoSPI` is allowed to own the concrete mutex because it is the - platform object that initializes the shared display SPI implementation on the - Arduino ESP boards. -- The public platform name remains `shared_spi_*`; callers must not use or - reintroduce `display_spi_*` aliases. -- Runtime workers should depend on `IBusArbiter`, not directly on the physical - lock. A temporary ESP adapter may call `shared_spi_*` while it implements the - arbiter port. -- LVGL FS callbacks are adapters. They must not contain product policy, retry - strategy, tile priority, or page state. -- External font loading is the UI-visible exception that may synchronously hold - the display-shared bus, but only after a busy modal has been flushed and only - through an `IBusArbiter` token. The old depth-only font-load flag is not a bus - contract. +Workers call semantic device services for storage, display, and radio work. +Those services own all physical transactions. The runtime layer receives only +semantic results. The complete shared-device mechanism is specified only in +`docs/spi_bus_architecture.md`. ### Long-Running Progress Overlay Boundary @@ -887,10 +594,10 @@ Foreground operation snapshots are fixed-size UI projections. They describe: They are not task executors, storage transactions, JSON payloads, or business state owners. The underlying runtimes continue to own their real state. -The coordinator is used by: +The foreground operation runtime is used by: - external font loading, where the presenter flushes the modal before the - separate `lvgl_font_sd` shared-SPI transaction begins + device font service begins its foreground load - package/language-pack installation, where download status may publish `progress_percent` while SD writes stay in the storage runtime/file adapter - firmware update, where OTA status publishes `progress_percent` and the @@ -899,9 +606,9 @@ The coordinator is used by: a foreground operation while route preview and GPS map pages keep their own page-local route/image context widgets -The illegal shortcut is to treat every progress bar as a bus lock. Progress UI -describes user-visible operation state; shared-SPI tokens describe physical bus -ownership for bounded storage/display critical sections. +The illegal shortcut is to treat every progress bar as a device transaction. +Progress UI describes user-visible operation state; device services own +physical storage, display, and radio work. Route image tasks are explicitly split by presentation semantics: @@ -912,255 +619,16 @@ Route image tasks are explicitly split by presentation semantics: - `Hidden` route image tasks are status-only. Route image HTTP downloads use `wifi_access` with the `RouteStorage` client. -They must not hold a shared-SPI token for the whole HTTP transfer. SD file -open/read/write/flush/rename stays protected by the SD runtime's bounded -per-operation guards; cache/build stages may add bounded bus policy, but must -not become an unbounded batch-level SPI lock. +They must not make an HTTP transfer a storage transaction. Cache/build stages +call the storage service for bounded semantic operations; they must not own +device transaction state. -### Component Deployment +### Device I/O Delegation -```mermaid -flowchart TB - subgraph UiDomain["UI realtime domain"] - LvglTick["lv_timer_handler / input drain"] - DisplayFlush["Display flush callback"] - WakeRender["Wake/sleep visual transition"] - end - - subgraph RuntimeDomain["Runtime command domain"] - TileRuntime["MapTileAsyncRuntime"] - TrackRuntime["Track runtime"] - PersistRuntime["Persistence runtime"] - CommandQueue["Command queues"] - end - - subgraph WorkerDomain["Worker / adapter domain"] - TileWorker["MapTileWorker"] - TrackWorker["TrackStorageWorker"] - PersistWorker["PersistenceWorker"] - LvglFs["LVGL SD FS adapter"] - end - - subgraph BusDomain["Physical shared-SPI domain"] - DisplayLock["LilyGoDispArduinoSPI mutex"] - SharedPort["shared_spi_* port"] - Pressure["display pressure timestamp"] - end - - subgraph StorageDomain["Storage media"] - SdRuntime["SdRuntimeFile / SdRuntimeDir"] - SdCard["SD card"] - end - - LvglTick --> DisplayFlush - DisplayFlush --> DisplayLock - DisplayLock --> Pressure - TileRuntime --> CommandQueue - TrackRuntime --> CommandQueue - PersistRuntime --> CommandQueue - CommandQueue --> TileWorker - CommandQueue --> TrackWorker - CommandQueue --> PersistWorker - TileWorker --> SharedPort - TrackWorker --> SharedPort - PersistWorker --> SharedPort - LvglFs --> SharedPort - SharedPort --> DisplayLock - SharedPort --> SdRuntime - SdRuntime --> SdCard - Pressure --> TileWorker -``` - -The UI realtime domain may be denied a single frame, but it must never wait -indefinitely. Worker-domain storage may be delayed, cancelled, retried, or -marked busy when display pressure exists. - -### Display Flush Sequence - -```mermaid -sequenceDiagram - participant UI as UI owner / LVGL - participant Display as LilyGoDispArduinoSPI - participant Lock as Shared SPI mutex - participant Pressure as Display pressure signal - - UI->>Display: pushColorsArea() - Display->>Lock: lock(wait=frame budget, owner="display") - alt acquired - Lock-->>Display: token - Display->>Display: SPI transaction - Display->>Lock: unlock() - Display-->>UI: flush complete - else timed out - Display->>Pressure: note_display_spi_timeout(now_ms) - Display-->>UI: return without blocking - end -``` - -Display timeout is not a normal success path. It is a pressure signal that -storage workers must observe. Returning from the flush is still required because -blocking inside display refresh prevents input, wake rendering, and page -navigation from recovering. - -### LVGL SD FS Callback Sequence - -```mermaid -sequenceDiagram - participant LVGL as LVGL file consumer - participant Fs as LVGL SD FS adapter - participant Guard as SharedSpiLockGuard - participant SD as SdRuntimeFile - - LVGL->>Fs: sd_fs_open/read/write/dir() - Fs->>Guard: acquire(short bounded wait) - alt acquired - Fs->>SD: perform one storage operation - Fs->>Guard: release on scope exit - Fs-->>LVGL: ok/result - else busy - Fs-->>LVGL: LV_FS_RES_BUSY or failed open - end -``` - -The adapter must not retry in a loop. A busy result is valid and lets the caller -or runtime decide whether to defer, cancel, or show pending state. This keeps -legacy LVGL FS callers from monopolising the UI owner task. - -### Map Tile Worker Sequence With Display Pressure - -```mermaid -sequenceDiagram - participant UI as Map UI owner - participant Runtime as MapTileAsyncRuntime - participant Worker as MapTileWorker - participant Arbiter as EspMapTileBusArbiter - participant Bus as shared_spi_* port - participant TileStore as SdMapTileFileSystem - participant Events as MapTileEventSink - - UI->>Runtime: requestVisibleTiles(plan, generation) - Runtime->>Worker: enqueue LoadTileCommand - UI-->>UI: return to input/render loop - Worker->>Arbiter: acquire(command policy) - Arbiter->>Bus: display_spi_recently_timed_out() - alt recent display pressure or cooldown - Arbiter-->>Worker: Busy - Worker->>Events: ResourceBusy(generation, tile) - else acquired - Arbiter->>Bus: shared_spi_lock_with_owner("map_tile_sd") - Bus-->>Arbiter: token - Worker->>TileStore: readFile() - loop between tile chunks - TileStore->>Bus: shared_spi_unlock() - TileStore->>Bus: bounded reacquire - end - Worker->>Arbiter: release(token) - Arbiter->>Bus: shared_spi_unlock() - Arbiter->>Arbiter: cooldown based on hold/display pressure - Worker->>Events: Ready or Failed - end - Events-->>UI: drain at most one tile event per UI pass -``` - -This is the current ESP Arduino map binding. It is conforming only because: - -- the UI owner submits a command and returns -- the worker owns the SD read -- `EspMapTileBusArbiter` checks display pressure before acquiring -- tile reads release the bus between chunks -- completed events are drained with a UI budget - -Moving any of these operations back into a page callback, LVGL timer, input -handler, or renderer mutation path is a regression. - -Display pressure is a storage-worker backpressure signal, not permission to -stop UI-domain map state work. While pressure is recent, the UI owner may still -calculate visible tile refs, update anchors, move already-created LVGL tile -objects, rebuild render queues, and drain a bounded number of already-delivered -tile events. The pressure signal must reduce or pause new SD-backed worker -requests; it must not make viewport/layout calculation return early. - -### Lock Health State - -```mermaid -stateDiagram-v2 - [*] --> Healthy - Healthy --> DisplayPressure: display lock timeout - DisplayPressure --> WorkerCooldown: worker observes pressure - WorkerCooldown --> Healthy: cooldown elapsed without new timeout - DisplayPressure --> Slow: repeated busy/timed out acquire - Slow --> Degraded: consecutive worker acquire failures >= threshold - Degraded --> Recovering: recovery/backoff window starts - Recovering --> Healthy: acquired and released within budget - Recovering --> Degraded: pressure continues -``` - -The state is intentionally driven by observable resource events, not by page -state. A map page, contacts page, tracker page, chat page, or boot UI can all be -affected by the same physical contention. - -### Code Binding Table - -| Role | File | Required behavior | -| --- | --- | --- | -| Runtime lock contract | `modules/core_sys/include/sys/runtime_async.h` | Owns `BusAccessPolicy`, `BusAcquireRequest`, `BusAccessToken`, `BusAcquireResult`, `IBusArbiter`, `StorageHealthState`. Shared business/runtime code depends on these abstractions. | -| Shared SPI port | `platform/esp/common/include/platform/esp/common/shared_spi_lock.h` | Names the physical bus. Must not expose `display_spi_lock` aliases. | -| Arduino display mutex implementation | `platform/esp/boards/src/display/DisplayInterface.cpp` | Uses bounded display waits; logs and records pressure; never waits forever in `pushColors*`. | -| IDF no-contention implementation | `platform/esp/idf_common/src/shared_spi_lock.cpp` | Provides a no-op conforming implementation for ESP IDF targets that do not use the Arduino shared bus path. | -| LVGL SD FS adapter | `platform/esp/arduino_common/src/LV_Helper_v9.cpp` | Uses short bounded acquisitions and returns busy/failed-open instead of retrying. | -| Map runtime worker contract | `modules/ui_map_runtime/src/map_tiles/map_tile_async_runtime.cpp` | Executes tile commands through `IBusArbiter` and publishes ready/busy/failed events. | -| ESP map bus arbiter | `platform/esp/arduino_common/src/ui/widgets/map/map_tiles.cpp` | Checks display pressure, applies cooldown, maps runtime policy to short waits, and releases bus after each tile command. | -| ESP map tile SD adapter | `platform/esp/arduino_common/src/ui/widgets/map/map_tiles.cpp` | Reads tile payload in worker domain and yields the bus between chunks. Only confirmed not-found results may enter missing-tile memory; retryable read failures remain requestable after short backoff. | - -### Forbidden Bypasses - -The following are non-conforming in active UI-visible paths: - -- adding `display_spi_lock()` / `display_spi_unlock()` aliases back to public - headers -- page/widget code calling `shared_spi_lock*` -- page/widget code calling Arduino `SD.open`, `SdRuntimeFile`, or LVGL SD FS to - probe whether content exists -- LVGL timer callbacks performing synchronous SD open/read/list operations -- storage adapters spinning until the bus becomes available -- worker loops draining many SD operations without cooldown after display - pressure -- `lv_refr_now()` or forced flush used as feedback while storage owns the - display-shared bus - -Allowed exceptions must be explicit platform adapter code and must document -their wait, hold, burst, and frame budgets. - -### Legacy Burn-Down Status - -| Legacy path | Status | Deletion/containment rule | -| --- | --- | --- | -| `display_spi_lock` / `display_spi_unlock` public aliases | Burned down | No active declaration or inline alias may remain. New code must use `shared_spi_*` or runtime bus ports. | -| `display_spi_lock.cpp` source filename | Burned down | Platform implementations must be named after `shared_spi_lock`, not display-private terminology. | -| ESP map UI source synchronous storage behavior | Burned down | UI map source is path/planning only; worker source performs SD reads. | -| LVGL SD FS adapter reachable from legacy resource paths | Contained | Adapter remains but uses short bounded lock attempts and must not be used as a UI hot-path storage probe. | -| External font load depth-only LVGL FS scope | Burned down | Font loading must acquire a shared-SPI runtime bus token before `lv_binfont_create()`; callback wait stretching is not a valid transaction. | -| ESP map worker direct physical lock calls | Contained adapter | Allowed only inside `EspMapTileBusArbiter` and tile SD adapter until all ESP storage paths use a common `IBusArbiter` implementation. | -| Team UI store direct SD persistence | Remaining legacy | Must move behind team/storage runtime worker in a separate migration. | -| Route/track file load from GPS page | Remaining legacy | Must move behind route/track runtime worker in a separate migration. | -| Pack repository direct file operations | Remaining legacy | Must move behind pack repository commands/events. | - -### Simulation Requirements - -Every future change to this mechanism must have a hardware-free simulation or -host test that covers: - -- display acquire timeout while storage owns the bus -- worker receiving `Busy` because display pressure was recent -- worker cooldown ending and a later command succeeding -- stale map generation completion being ignored -- LVGL SD FS callback returning busy rather than blocking -- deletion guard proving no `display_spi_lock` alias or UI-page direct SD probe - was reintroduced - -The simulation may use fake `IBusArbiter`, fake clock, fake event sink, fake UI -drain, and fake storage backend. It must not require real SD, LVGL, display SPI, -or radio hardware. +UI/runtime specifications describe commands, worker ownership, semantic +results, and event delivery. The concrete display, radio, and storage +services perform physical I/O behind the device boundary. Their shared-device +transaction rules are defined only in `docs/spi_bus_architecture.md`. ## UML Map Tile Class Model @@ -1223,7 +691,6 @@ classDiagram MapTileRuntime --> LoadTileCommand MapTileRuntime o-- MapTileStateMachine MapTileWorker --> LoadTileCommand - MapTileWorker o-- IBusArbiter MapTileWorker o-- IMapTileSource MapTileWorker o-- IMapTileDecoder MapTileWorker --> MapTileEvent @@ -1295,7 +762,6 @@ classDiagram TrackRuntime o-- TrackFlushPolicy TrackStorageWorker --> TrackCommand TrackStorageWorker o-- ITrackFileAdapter - TrackStorageWorker o-- IBusArbiter TrackStorageWorker --> TrackEvent TrackRuntime --> TrackEvent ``` @@ -1360,7 +826,6 @@ classDiagram PersistenceWorker --> PersistenceCommand PersistenceWorker o-- IStoreSnapshotProvider PersistenceWorker o-- IStoreStorageAdapter - PersistenceWorker o-- IBusArbiter PersistenceWorker --> PersistenceEvent PersistenceRuntime --> PersistenceEvent ``` @@ -1538,7 +1003,7 @@ sequenceDiagram participant Track as TrackRuntime participant Queue as Command Queue participant Worker as Workers - participant Bus as Bus Arbiter + participant DeviceIo as Device I/O service participant Events as Event Bus UI->>Map: requestVisibleTiles(generation=42, interactive) @@ -1546,12 +1011,12 @@ sequenceDiagram GPS->>Track: appendPoint(point) Track->>Queue: enqueue/buffer AppendTrackPointCommand Queue->>Worker: dispatch tile before idle/background work - Worker->>Bus: acquire InteractiveWorkerBounded - Bus-->>Worker: acquired + Worker->>DeviceIo: execute tile read + DeviceIo-->>Worker: ready or retry later Worker->>Events: MapTileReady(generation=42) Queue->>Worker: dispatch batched track write - Worker->>Bus: acquire BackgroundWorkerBounded - Bus-->>Worker: delayed or acquired + Worker->>DeviceIo: execute track write + DeviceIo-->>Worker: completed or deferred Worker->>Events: TrackFlushSucceeded or Backpressure Events->>UI: drain UI-safe events ``` @@ -1610,16 +1075,16 @@ sequenceDiagram participant Power as Power Manager participant Runtime as Runtime Facade participant Worker as Storage Worker - participant Bus as Bus Arbiter + participant DeviceIo as Device I/O service UI->>Runtime: submit command Runtime->>Worker: enqueue slow storage work - Worker->>Bus: acquire BackgroundWorkerBounded + Worker->>DeviceIo: execute storage command Power->>UI: sleep timeout event UI->>UI: render sleep transition Power->>UI: wake input UI->>UI: process wake input - Bus-->>Worker: storage complete later + DeviceIo-->>Worker: storage complete later Worker->>UI: publish completion event ``` @@ -1656,9 +1121,9 @@ classDiagram +write(request) } - class FakeBusArbiter { - +scriptAcquire(result) - +acquire(request) + class FakeDeviceIo { + +scriptResult(result) + +execute(request) +diagnostics() } @@ -1677,7 +1142,7 @@ classDiagram RuntimeHarness o-- FakeCommandQueue RuntimeHarness o-- FakeEventBus RuntimeHarness o-- FakeStorageBackend - RuntimeHarness o-- FakeBusArbiter + RuntimeHarness o-- FakeDeviceIo RuntimeHarness o-- FakeUiOwner RuntimeHarness o-- FakeFeedbackPresenter ``` @@ -1694,8 +1159,8 @@ flowchart LR Intent --> Facade["Runtime Facade"] Facade --> CommandQueue["Command Queue"] CommandQueue --> Worker["Active Object Worker"] - Worker --> Arbiter["Storage / Bus Arbiter"] - Arbiter --> Adapter["Platform Adapter"] + Worker --> DeviceIo["Device I/O service"] + DeviceIo --> Adapter["Platform Adapter"] Adapter --> Completion["Completion Result"] Completion --> EventBus["Event Bus"] EventBus --> State["Runtime State Projection"] @@ -1709,42 +1174,12 @@ or app service. The rule is the same: submit intent or command, then return. The UI event drain is the only step in this flow that may touch concrete UI objects. -## Shared Resource Arbitration +## Device I/O Delegation -Direct shared-SPI locking from feature code is legacy. The target design is a -bus/storage arbiter with explicit policies: - -| Policy | Intended callers | Wait behavior | Failure behavior | -| --- | --- | --- | --- | -| `DisplayFrameCritical` | display flush, wake render, input-visible feedback | frame-budget bounded, highest priority | skip/defer non-critical storage | -| `UiNeverBlock` | UI event/timer/input paths | no wait or frame-budget-only try | defer, cancel, or render pending state | -| `InteractiveWorkerBounded` | map tile worker during drag | short bounded wait | cancel stale tile or retry later | -| `BackgroundWorkerBounded` | track append, node save, prefetch | bounded wait with backpressure | reschedule, batch, or enter degraded state | -| `DurableCommit` | explicit stop/close/final flush | bounded but higher priority | report durable failure event | -| `RecoveryExclusive` | storage remount or card recovery | exclusive, never from UI | publish degraded/unavailable state | - -Every acquisition must be diagnosable: - -```text -resource -owner_task_or_thread -command_id -wait_start_ms -acquired_ms -released_ms -hold_ms -wait_ms -policy -``` - -The arbiter expresses scheduling intent. A mutex only expresses exclusion. Code -that uses a bare blocking mutex to coordinate UI, SD, and display refresh is not -conformant. - -For display-shared SPI, storage work must use a try-lock or bounded acquisition -and must enter cooldown/backpressure after every slow hold. The scheduler may -drop, defer, or mark commands `ResourceBusy`; it must not spin on the lock or -drain a burst of SD operations while display is trying to render. +Workers submit semantic operations to device services. Device services own +physical arbitration and return semantic completion, retry, unavailable, or +failure results. The shared-device mechanism is specified only in +`docs/spi_bus_architecture.md`. ## Map Tile Runtime Design @@ -1757,14 +1192,14 @@ sequenceDiagram participant Runtime as MapTileRuntime participant Worker as MapTileWorker participant Store as Tile Storage Adapter - participant Bus as Storage/Bus Arbiter + participant DeviceIo as Device I/O service participant Events as Event Bus UI->>Runtime: requestVisibleTiles(viewport, generation) Runtime->>Worker: enqueue LoadTileCommand(ref, generation, priority) UI-->>UI: return to input/render loop - Worker->>Bus: acquire(policy) - Bus-->>Worker: acquired or retry later + Worker->>DeviceIo: execute tile read + DeviceIo-->>Worker: ready or retry later Worker->>Store: read/decode tile payload Worker->>Events: MapTileReady/Failed(ref, generation, image_ref) Events->>UI: drain event on UI owner context @@ -1774,7 +1209,7 @@ sequenceDiagram Mandatory behavior: - LVGL timers and input callbacks must not open tile files. -- LVGL timers and input callbacks must not wait for shared-SPI. +- LVGL timers and input callbacks must not wait for device I/O. - Tile file lookup/read/decode must not be performed from drag, timer, input, or page render callbacks. - Tile requests carry a viewport generation. @@ -1798,13 +1233,11 @@ ESP active loader rules: - `tile_loader_step()` may calculate visible tile refs, move existing renderer objects, apply completed in-memory tile events, and submit/cancel runtime events. -- `tile_loader_step()` must not call `lv_fs_open`, SD file APIs, or a - shared-SPI lock. -- The ESP worker adapter uses the SD runtime file adapter and acquires - display-shared SPI only through the bus/storage scheduler. For visible tiles - it must use try-lock or tightly bounded acquisition, publish `ResourceBusy` - when the bus is not immediately available, and apply cooldown after any slow - hold. +- `tile_loader_step()` must not call `lv_fs_open`, SD file APIs, or a device + I/O transaction. +- The ESP worker adapter uses the SD storage service. It may return + `RetryLater` when the device cannot complete the read yet; the service owns + all arbitration and transaction backpressure. - `MapTileAsyncEvent` carries a copied `MapTilePayload` to the UI owner drain. The payload must be released even when stale. - The ESP UI drain is non-blocking on command/event queues. A busy queue means @@ -1950,7 +1383,7 @@ The simulator must provide: | `FakeEventBus` | publishes and drains runtime events deterministically | | `FakeCommandQueue` | bounded queue, priorities, cancellation, dedupe | | `FakeStorageBackend` | scripted read/write/list/flush delay and failure | -| `FakeBusArbiter` | scripted acquisition delay, timeout, owner diagnostics | +| `FakeDeviceIo` | scripted device result, delay, and diagnostic outcome | | `FakeMapTileWorker` | completes tile commands in controlled order | | `FakeTrackStorageWorker` | batches points and emits track events | | `FakeFeedbackPresenter` | captures notices without concrete renderer objects | @@ -2091,8 +1524,7 @@ Assertions: All runtime tests must assert: -- UI owner context does not call blocking storage, blocking shared-SPI, or - filesystem APIs. +- UI owner context does not call blocking device I/O or filesystem APIs. - Background workers do not call concrete renderer APIs. - Every command completes, fails, is cancelled, or remains pending for an explained reason. @@ -2114,7 +1546,7 @@ The burn-down should proceed in slices that each leave the system shippable. 4. Move track start/stop/list/append/flush into an asynchronous track storage worker. 5. Move node/contact persistence to a debounced persistence worker. -6. Replace direct shared-SPI lock calls in UI-facing code with arbiter policies. +6. Replace direct hardware access in UI-facing code with device service calls. 7. Burn down adapter-owned business decisions and route them through shared runtimes/facades. 8. Turn remaining synchronous storage calls in page/widget code into compile or diff --git a/docs/spi_bus_architecture.md b/docs/spi_bus_architecture.md index 49ea4b74..b0be6a8b 100644 --- a/docs/spi_bus_architecture.md +++ b/docs/spi_bus_architecture.md @@ -2,16 +2,32 @@ ## Document status -This document is the design contract for the ESP shared-SPI refactor. It is -written before the implementation migration so that the implementation can be -reviewed against explicit invariants instead of being judged by individual -timeout values. +This is the single authoritative specification for the shared-SPI mechanism. +It describes a technical resource boundary, not any product feature. Map, +chat, contacts, localization, tracking, team, package, and protocol documents +must not repeat its lock, token, priority, or deadline rules. -The affected devices include boards where the display, SD card, LoRa radio, -external fonts, GPS/track persistence, USB mass-storage, or other peripherals -share one physical SPI controller. Boards without a shared SPI controller may -use the same API with a no-op backend, but they must not reintroduce a second -locking model. +The implementation is reviewed against the invariants in this document, not +against individual timeout values or feature-local conventions. + +The mechanism is selected from the board's physical topology. It applies to +boards where the display, SD card, LoRa radio, external fonts, GPS/track +persistence, USB mass-storage, or other peripherals share one physical SPI +controller. Boards without a shared SPI controller use their native independent +bus or SDMMC driver and do not enter this coordinator. A no-op backend is valid +only for an API-compatible device path that is physically independent; it must +not hide a second locking model. + +The current board profiles are intentionally different: + +- T-LoRa Pager, T-Deck, and T-Deck Pro place display, SD, and LoRa on the same + Arduino SPI bus. Their board configuration enables the SdFat shared-SPI + adapter. +- T-Display P4 uses SDMMC for the SD card and a separate SPI path for LoRa. + SDMMC must not acquire the shared-SPI coordinator. +- Future boards must declare one coordinator per actual physical bus. A board + may have no coordinator, one coordinator, or several independent + coordinators; the business layer remains unaware of that topology. ## Why the old model failed @@ -104,10 +120,66 @@ request(resource, class, deadline, owner) deadline timing background ``` -The coordinator is not a collection of independent `StorageBusArbiter` -instances. Feature code receives a typed `SharedSpiAccess` handle or uses the -coordinator's transaction helper. The handle contains no independent mutex and -cannot release another handle's acquisition. +The coordinator is not a collection of independent storage arbiters. The +coordinator API is an infrastructure API. It is callable only +from device and platform I/O owners, never from business modules or UI +feature/runtime code. + +Device owners translate semantic requests into private hardware transactions: + +```text +business/runtime request + -> device service + -> device transaction executor + -> SharedSpiCoordinator + -> peripheral driver +``` + +The device executor may carry policy, command identity, deadlines, owner +labels, and tokens internally. None of those details cross the device service +boundary. A map tile request is a tile request; it is not a bus request. A +message persistence request is a storage request; it is not a token request. + +### Memory and DMA boundary + +Shared-SPI correctness includes the memory used around a transaction. ESP +hardware has limited internal RAM, so large protocol, file, packet, decoded +payload, and runtime scratch objects must follow these rules: + +- Prefer PSRAM for long-lived device objects and reusable scratch storage. + `heap_caps_malloc_prefer` must request `MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT` + first and use internal 8-bit heap only as an explicit fallback. +- Do not create protocol-sized automatic locals in ESP hot paths. Use a member + scratch slot, a fixed-depth ring slot with clear ownership, or caller-owned + output storage. A scratch slot may be reused only after the previous + operation has completed. +- Keep DMA, cache, semaphore, and driver objects in internal memory when the + hardware or SDK requires it. PSRAM is not a universal replacement for + DMA-capable storage. +- A physical bus transaction must not retain a large application buffer or + protobuf object longer than necessary. Encode, decode, parse, and allocate + outside the physical coordinator ownership window. + +The memory placement decision belongs to the device/platform owner. Business +code does not select heap capabilities or carry SPI scratch buffers. + +### Visibility boundary + +The following concepts are implementation details of the device/platform +layer and must not appear in business or UI-facing headers: + +- `SharedSpiCoordinator` +- `BusAccessPolicy` +- `BusAcquireRequest`, `BusAcquireResult`, and `BusAcquireStatus` +- `BusAccessToken`, `ScopedBusAccessToken`, and direct release operations +- the private SD logical-filesystem session guard and SdFat bus hook +- `PersistenceBusGate` or any equivalent outer admission gate +- shared-SPI resource identifiers and owner labels + +Business code receives semantic operation results such as `Completed`, +`RetryLater`, `Missing`, `Unavailable`, or `Failed`. The device owner decides +whether a result came from arbitration, a peripheral timeout, media removal, +or an I/O error. ## Request classes and ordering @@ -123,6 +195,10 @@ Requests are ordered by class, deadline, and age: The ordering rules are: +- The device adapter selects the request class from the semantic operation. A + file read needed for content currently visible to the user is `Interactive`; + preload, hydration, compaction, and maintenance remain `Background`. The + business-facing API does not expose the bus policy used for that selection. - A waiting `DisplayFrame` blocks new `Background` acquisitions. - A waiting `RadioTiming` blocks new `Background` acquisitions unless the radio transaction has explicitly declared that it can be deferred. @@ -132,6 +208,11 @@ The ordering rules are: - The coordinator may finish the transaction that already owns the bus, but it must not grant the next transaction to a lower class while a higher class is waiting. +- A slow transaction updates coordinator health diagnostics and may emit a + warning, but it does not create a cross-business cooldown or reject a later + radio, display, or storage request. Fairness comes from request class, + deadline, and age ordering. The coordinator never treats map, radio, + display, and storage as one worker class. The coordinator does not interrupt an active hardware-safe transaction. This is why all callers must keep the transaction small and must release the bus before @@ -160,9 +241,10 @@ Any mismatch is a diagnostic failure and does not clear the coordinator's ownership. A double release must never be able to unlock another task's transaction. -Same-task nesting is allowed only through the coordinator and increments a -coordinator-owned depth counter. A legacy direct `unlock()` must not be able to -participate in nesting. +Same-task nesting is allowed only inside one device transaction executor and +increments a coordinator-owned depth counter. A business caller must never see +the nesting or participate in it. A legacy direct `unlock()` must not be able +to participate in nesting. ## Transaction boundary rules @@ -218,15 +300,30 @@ budget. ### SD and background storage -Every SD file or directory call is one transaction. Hydration and compaction: +The SD adapter has two distinct scopes: -- acquire for one filesystem operation; -- release; -- parse/copy/build application state outside the bus; -- re-check the foreground/display gate before the next operation. +- A logical filesystem session serializes SdFat object access. It belongs to + the SD device adapter and is invisible to business code. +- The physical shared-SPI ownership is acquired by the SdFat driver at its + `activate`/`deactivate` transaction boundary. On shared-SPI boards, payload + reads and writes are sliced to at most one 512-byte sector so the display or + radio can be granted between physical transactions. On SDMMC or independent + buses, this hook is not used. -No background worker may hold the bus while performing snapshot parsing, -repository updates, compression, or allocation. +The logical session may remain open while an interactive read-only `FsFile` +object is alive, but it must not hold the physical coordinator across sectors. +The session must not include parsing, UI work, repository updates, compression, +or application-object construction. Background hydration and compaction must: + +- perform one bounded adapter operation; +- release the adapter session and physical bus; +- parse/copy/build application state outside the device layer; +- re-check the foreground/display state before the next operation. + +Once a file has been opened, every return path must close it through the SD +device adapter before reporting `Ready`, `RetryLater`, or `Failed`. A +filesystem call itself remains non-interruptible; only the lower-level physical +SPI transaction boundaries are schedulable. ### Font, track, team, and USB @@ -355,10 +452,9 @@ can be declared operationally complete. The refactor is complete only when all of the following are true: 1. `git grep` finds no production use of `SharedSpiBusAdapter`, - `shared_spi_lock_with_owner`, or direct `shared_spi_unlock()` outside the - coordinator implementation and its compatibility tests. The generic - `sys::runtime::StorageBusArbiter` may remain for non-SPI host/runtime tests, - but it must not be instantiated as a shared-SPI implementation. + `shared_spi_lock_with_owner`, `StorageBusArbiter`, or direct + `shared_spi_unlock()` outside the coordinator implementation and its + compatibility tests. 2. Display flush has an explicit success/failure result and never reports a lock timeout as a successful transfer. 3. Unit tests cover priority ordering, FIFO within a class, strict release, diff --git a/docs/startup_architecture.md b/docs/startup_architecture.md index 1ee02b7a..60c70d2f 100644 --- a/docs/startup_architecture.md +++ b/docs/startup_architecture.md @@ -61,9 +61,9 @@ timeout or an extra retry could not guarantee that the first frame was ever visible. The new contract makes the dependency explicit: the boot UI is the barrier -between hardware visibility and service startup. It also keeps the existing -SPI coordinator semantics intact: the first frame must complete as a physical -display transaction before shared-SPI users are started. +between hardware visibility and service startup. The display device service +must report the first frame as complete before other hardware services are +started; the physical transaction policy remains inside that device service. ## Review invariants diff --git a/modules/core_chat/include/chat/infra/meshtastic/mt_codec_pb.h b/modules/core_chat/include/chat/infra/meshtastic/mt_codec_pb.h index b6513d87..d421704e 100644 --- a/modules/core_chat/include/chat/infra/meshtastic/mt_codec_pb.h +++ b/modules/core_chat/include/chat/infra/meshtastic/mt_codec_pb.h @@ -40,11 +40,13 @@ namespace meshtastic */ bool encodeTextMessage(ChannelId channel, const std::string& text, NodeId from_node, uint32_t packet_id, NodeId dest_node, - uint8_t* out_buffer, size_t* out_size); + uint8_t* out_buffer, size_t* out_size, + meshtastic_Data* data_scratch = nullptr); bool encodeTextMessageBytes(ChannelId channel, const char* text, size_t text_len, NodeId from_node, uint32_t packet_id, NodeId dest_node, - uint8_t* out_buffer, size_t* out_size); + uint8_t* out_buffer, size_t* out_size, + meshtastic_Data* data_scratch = nullptr); /** * @brief Decode an already-parsed Meshtastic Data payload into text @@ -74,7 +76,8 @@ bool decodeTextPayloadToBuffer(const meshtastic_Data& data, * @param out Output message * @return true if successful */ -bool decodeTextMessage(const uint8_t* buffer, size_t size, MeshIncomingText* out); +bool decodeTextMessage(const uint8_t* buffer, size_t size, MeshIncomingText* out, + meshtastic_Data* data_scratch = nullptr); /** * @brief Decode Meshtastic Data payload to key verification message @@ -83,7 +86,9 @@ bool decodeTextMessage(const uint8_t* buffer, size_t size, MeshIncomingText* out * @param out Output KeyVerification message * @return true if successful */ -bool decodeKeyVerificationMessage(const uint8_t* buffer, size_t size, meshtastic_KeyVerification* out); +bool decodeKeyVerificationMessage(const uint8_t* buffer, size_t size, + meshtastic_KeyVerification* out, + meshtastic_Data* data_scratch = nullptr); /** * @brief Encode node info (User) message to Meshtastic Data payload using protobuf @@ -99,7 +104,8 @@ bool decodeKeyVerificationMessage(const uint8_t* buffer, size_t size, meshtastic bool encodeNodeInfoMessage(const std::string& user_id, const std::string& long_name, const std::string& short_name, meshtastic_HardwareModel hw_model, const uint8_t macaddr[6], const uint8_t* public_key, size_t public_key_len, - bool want_response, uint8_t* out_buffer, size_t* out_size); + bool want_response, uint8_t* out_buffer, size_t* out_size, + meshtastic_Data* data_scratch = nullptr); /** * @brief Encode app payload to Meshtastic Data message @@ -112,12 +118,15 @@ bool encodeNodeInfoMessage(const std::string& user_id, const std::string& long_n * @return true if successful */ bool encodeAppData(uint32_t portnum, const uint8_t* payload, size_t payload_len, - bool want_response, uint8_t* out_buffer, size_t* out_size); + bool want_response, uint8_t* out_buffer, size_t* out_size, + meshtastic_Data* data_scratch = nullptr); bool encodeAppDataWithRequestId(uint32_t portnum, const uint8_t* payload, size_t payload_len, bool want_response, uint32_t request_id, - uint8_t* out_buffer, size_t* out_size); + uint8_t* out_buffer, size_t* out_size, + meshtastic_Data* data_scratch = nullptr); bool decodeAppPayload(const meshtastic_Data& data, MeshIncomingData* out); -bool decodeAppData(const uint8_t* buffer, size_t size, MeshIncomingData* out); +bool decodeAppData(const uint8_t* buffer, size_t size, MeshIncomingData* out, + meshtastic_Data* data_scratch = nullptr); /** * @brief Encode MeshPacket to buffer diff --git a/modules/core_chat/include/chat/runtime/meshtastic_self_announcement_core.h b/modules/core_chat/include/chat/runtime/meshtastic_self_announcement_core.h index cf8a718a..8ac1c9a4 100644 --- a/modules/core_chat/include/chat/runtime/meshtastic_self_announcement_core.h +++ b/modules/core_chat/include/chat/runtime/meshtastic_self_announcement_core.h @@ -32,6 +32,7 @@ struct MeshtasticAnnouncementPacket uint8_t wire[384] = {}; size_t wire_size = 0; uint8_t channel_hash = 0; + meshtastic_Data data_scratch = meshtastic_Data_init_default; }; class MeshtasticSelfAnnouncementCore final : public SelfAnnouncementCore diff --git a/modules/core_chat/src/infra/meshtastic/mt_codec_pb.cpp b/modules/core_chat/src/infra/meshtastic/mt_codec_pb.cpp index 6f205e5c..814f85e7 100644 --- a/modules/core_chat/src/infra/meshtastic/mt_codec_pb.cpp +++ b/modules/core_chat/src/infra/meshtastic/mt_codec_pb.cpp @@ -11,15 +11,35 @@ #include "chat/time_utils.h" #include #include +#include namespace chat { namespace meshtastic { +namespace +{ + +meshtastic_Data* acquireDataScratch(meshtastic_Data* supplied, + std::unique_ptr& owned) +{ + if (supplied != nullptr) + { + *supplied = meshtastic_Data_init_default; + return supplied; + } + owned = std::make_unique(); + *owned = meshtastic_Data_init_default; + return owned.get(); +} + +} // namespace + bool encodeTextMessage(ChannelId channel, const std::string& text, NodeId from_node, uint32_t packet_id, NodeId dest_node, - uint8_t* out_buffer, size_t* out_size) + uint8_t* out_buffer, size_t* out_size, + meshtastic_Data* data_scratch) { return encodeTextMessageBytes(channel, text.data(), @@ -28,12 +48,14 @@ bool encodeTextMessage(ChannelId channel, const std::string& text, packet_id, dest_node, out_buffer, - out_size); + out_size, + data_scratch); } bool encodeTextMessageBytes(ChannelId channel, const char* text, size_t text_len, NodeId from_node, uint32_t packet_id, NodeId dest_node, - uint8_t* out_buffer, size_t* out_size) + uint8_t* out_buffer, size_t* out_size, + meshtastic_Data* data_scratch) { if (!out_buffer || !out_size || !text || text_len == 0) { @@ -42,24 +64,25 @@ bool encodeTextMessageBytes(ChannelId channel, const char* text, size_t text_len (void)channel; // Create a Meshtastic Data message payload - meshtastic_Data data = meshtastic_Data_init_default; - data.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; - data.want_response = false; - data.has_bitfield = true; - data.bitfield = 0; // No special flags for now - data.dest = dest_node; - data.source = from_node; + std::unique_ptr owned_data; + meshtastic_Data* data = acquireDataScratch(data_scratch, owned_data); + data->portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; + data->want_response = false; + data->has_bitfield = true; + data->bitfield = 0; // No special flags for now + data->dest = dest_node; + data->source = from_node; // Set text payload - if (text_len > sizeof(data.payload.bytes)) + if (text_len > sizeof(data->payload.bytes)) { return false; // Text too long } - data.payload.size = text_len; - memcpy(data.payload.bytes, text, text_len); + data->payload.size = text_len; + memcpy(data->payload.bytes, text, text_len); pb_ostream_t data_stream = pb_ostream_from_buffer(out_buffer, *out_size); - if (!pb_encode(&data_stream, meshtastic_Data_fields, &data)) + if (!pb_encode(&data_stream, meshtastic_Data_fields, data)) { return false; } @@ -146,7 +169,8 @@ bool decodeTextPayload(const meshtastic_Data& data, MeshIncomingText* out) return true; } -bool decodeTextMessage(const uint8_t* buffer, size_t size, MeshIncomingText* out) +bool decodeTextMessage(const uint8_t* buffer, size_t size, MeshIncomingText* out, + meshtastic_Data* data_scratch) { if (!buffer || !out || size == 0) { @@ -154,49 +178,53 @@ bool decodeTextMessage(const uint8_t* buffer, size_t size, MeshIncomingText* out } // Decode a Meshtastic Data message payload - meshtastic_Data data = meshtastic_Data_init_default; + std::unique_ptr owned_data; + meshtastic_Data* data = acquireDataScratch(data_scratch, owned_data); pb_istream_t stream = pb_istream_from_buffer(buffer, size); - if (!pb_decode(&stream, meshtastic_Data_fields, &data)) + if (!pb_decode(&stream, meshtastic_Data_fields, data)) { return false; } - return decodeTextPayload(data, out); + return decodeTextPayload(*data, out); } bool decodeKeyVerificationMessage(const uint8_t* buffer, size_t size, - meshtastic_KeyVerification* out) + meshtastic_KeyVerification* out, + meshtastic_Data* data_scratch) { if (!buffer || !out || size == 0) { return false; } - meshtastic_Data data = meshtastic_Data_init_default; + std::unique_ptr owned_data; + meshtastic_Data* data = acquireDataScratch(data_scratch, owned_data); pb_istream_t stream = pb_istream_from_buffer(buffer, size); - if (!pb_decode(&stream, meshtastic_Data_fields, &data)) + if (!pb_decode(&stream, meshtastic_Data_fields, data)) { return false; } - if (data.portnum != meshtastic_PortNum_KEY_VERIFICATION_APP) + if (data->portnum != meshtastic_PortNum_KEY_VERIFICATION_APP) { return false; } - if (data.payload.size == 0 || data.payload.size > sizeof(data.payload.bytes)) + if (data->payload.size == 0 || data->payload.size > sizeof(data->payload.bytes)) { return false; } - pb_istream_t kv_stream = pb_istream_from_buffer(data.payload.bytes, data.payload.size); + pb_istream_t kv_stream = pb_istream_from_buffer(data->payload.bytes, data->payload.size); return pb_decode(&kv_stream, meshtastic_KeyVerification_fields, out); } bool encodeNodeInfoMessage(const std::string& user_id, const std::string& long_name, const std::string& short_name, meshtastic_HardwareModel hw_model, const uint8_t macaddr[6], const uint8_t* public_key, size_t public_key_len, - bool want_response, uint8_t* out_buffer, size_t* out_size) + bool want_response, uint8_t* out_buffer, size_t* out_size, + meshtastic_Data* data_scratch) { if (!out_buffer || !out_size) { @@ -232,21 +260,22 @@ bool encodeNodeInfoMessage(const std::string& user_id, const std::string& long_n } size_t user_len = user_stream.bytes_written; - meshtastic_Data data = meshtastic_Data_init_default; - data.portnum = meshtastic_PortNum_NODEINFO_APP; - data.want_response = want_response; - data.has_bitfield = true; - data.bitfield = 0; + std::unique_ptr owned_data; + meshtastic_Data* data = acquireDataScratch(data_scratch, owned_data); + data->portnum = meshtastic_PortNum_NODEINFO_APP; + data->want_response = want_response; + data->has_bitfield = true; + data->bitfield = 0; - if (user_len > sizeof(data.payload.bytes)) + if (user_len > sizeof(data->payload.bytes)) { return false; } - data.payload.size = user_len; - memcpy(data.payload.bytes, user_buf, user_len); + data->payload.size = user_len; + memcpy(data->payload.bytes, user_buf, user_len); pb_ostream_t data_stream = pb_ostream_from_buffer(out_buffer, *out_size); - if (!pb_encode(&data_stream, meshtastic_Data_fields, &data)) + if (!pb_encode(&data_stream, meshtastic_Data_fields, data)) { return false; } @@ -256,44 +285,54 @@ bool encodeNodeInfoMessage(const std::string& user_id, const std::string& long_n } bool encodeAppData(uint32_t portnum, const uint8_t* payload, size_t payload_len, - bool want_response, uint8_t* out_buffer, size_t* out_size) + bool want_response, uint8_t* out_buffer, size_t* out_size, + meshtastic_Data* data_scratch) { return encodeAppDataWithRequestId( - portnum, payload, payload_len, want_response, 0, out_buffer, out_size); + portnum, + payload, + payload_len, + want_response, + 0, + out_buffer, + out_size, + data_scratch); } bool encodeAppDataWithRequestId(uint32_t portnum, const uint8_t* payload, size_t payload_len, bool want_response, uint32_t request_id, - uint8_t* out_buffer, size_t* out_size) + uint8_t* out_buffer, size_t* out_size, + meshtastic_Data* data_scratch) { if (!out_buffer || !out_size) { return false; } - meshtastic_Data data = meshtastic_Data_init_default; - data.portnum = static_cast(portnum); - data.want_response = want_response; - data.has_bitfield = true; - data.bitfield = 0; - data.request_id = request_id; + std::unique_ptr owned_data; + meshtastic_Data* data = acquireDataScratch(data_scratch, owned_data); + data->portnum = static_cast(portnum); + data->want_response = want_response; + data->has_bitfield = true; + data->bitfield = 0; + data->request_id = request_id; - if (payload_len > sizeof(data.payload.bytes)) + if (payload_len > sizeof(data->payload.bytes)) { return false; } - data.payload.size = static_cast(payload_len); + data->payload.size = static_cast(payload_len); if (payload_len > 0) { if (!payload) { return false; } - memcpy(data.payload.bytes, payload, payload_len); + memcpy(data->payload.bytes, payload, payload_len); } pb_ostream_t data_stream = pb_ostream_from_buffer(out_buffer, *out_size); - if (!pb_encode(&data_stream, meshtastic_Data_fields, &data)) + if (!pb_encode(&data_stream, meshtastic_Data_fields, data)) { return false; } @@ -302,21 +341,23 @@ bool encodeAppDataWithRequestId(uint32_t portnum, const uint8_t* payload, size_t return true; } -bool decodeAppData(const uint8_t* buffer, size_t size, MeshIncomingData* out) +bool decodeAppData(const uint8_t* buffer, size_t size, MeshIncomingData* out, + meshtastic_Data* data_scratch) { if (!buffer || !out || size == 0) { return false; } - meshtastic_Data data = meshtastic_Data_init_default; + std::unique_ptr owned_data; + meshtastic_Data* data = acquireDataScratch(data_scratch, owned_data); pb_istream_t stream = pb_istream_from_buffer(buffer, size); - if (!pb_decode(&stream, meshtastic_Data_fields, &data)) + if (!pb_decode(&stream, meshtastic_Data_fields, data)) { return false; } - return decodeAppPayload(data, out); + return decodeAppPayload(*data, out); } bool decodeAppPayload(const meshtastic_Data& data, MeshIncomingData* out) diff --git a/modules/core_chat/src/infra/meshtastic/mt_packet_wire.cpp b/modules/core_chat/src/infra/meshtastic/mt_packet_wire.cpp index 647ec6cb..b7b7b65c 100644 --- a/modules/core_chat/src/infra/meshtastic/mt_packet_wire.cpp +++ b/modules/core_chat/src/infra/meshtastic/mt_packet_wire.cpp @@ -107,24 +107,6 @@ bool buildWirePacket(const uint8_t* data_payload, size_t data_len, return false; } - uint8_t payload[256]; - const size_t payload_len = data_len; - memcpy(payload, data_payload, data_len); - - if (psk && psk_len > 0) - { -#if TRAILMATE_MESHTASTIC_WIRE_HAS_CRYPTO - uint8_t nonce[16]; - memset(nonce, 0, sizeof(nonce)); - const uint64_t packet_id64 = static_cast(packet_id); - memcpy(nonce, &packet_id64, sizeof(uint64_t)); - memcpy(nonce + sizeof(uint64_t), &from_node, sizeof(uint32_t)); - aesCtrCrypt(psk, psk_len, nonce, payload, payload_len); -#else - return false; -#endif - } - PacketHeaderWire hdr{}; hdr.to = dest_node; hdr.from = from_node; @@ -142,15 +124,32 @@ bool buildWirePacket(const uint8_t* data_payload, size_t data_len, hdr.next_hop = 0; hdr.relay_node = static_cast(from_node & 0xFF); - const size_t required_size = sizeof(hdr) + payload_len; + const size_t required_size = sizeof(hdr) + data_len; if (*out_size < required_size) { *out_size = required_size; return false; } + // The caller may intentionally use the same buffer for the source and + // destination. Move the payload behind the header before encrypting it + // in place so no protocol-sized automatic scratch buffer is needed. + memmove(out_buffer + sizeof(hdr), data_payload, data_len); memcpy(out_buffer, &hdr, sizeof(hdr)); - memcpy(out_buffer + sizeof(hdr), payload, payload_len); + + if (psk && psk_len > 0) + { +#if TRAILMATE_MESHTASTIC_WIRE_HAS_CRYPTO + uint8_t nonce[16]; + memset(nonce, 0, sizeof(nonce)); + const uint64_t packet_id64 = static_cast(packet_id); + memcpy(nonce, &packet_id64, sizeof(uint64_t)); + memcpy(nonce + sizeof(uint64_t), &from_node, sizeof(uint32_t)); + aesCtrCrypt(psk, psk_len, nonce, out_buffer + sizeof(hdr), data_len); +#else + return false; +#endif + } *out_size = required_size; return true; diff --git a/modules/core_chat/src/infra/meshtastic/mt_protocol_helpers.cpp b/modules/core_chat/src/infra/meshtastic/mt_protocol_helpers.cpp index ff848534..86fdb1e1 100644 --- a/modules/core_chat/src/infra/meshtastic/mt_protocol_helpers.cpp +++ b/modules/core_chat/src/infra/meshtastic/mt_protocol_helpers.cpp @@ -372,30 +372,33 @@ bool makeEncryptedPacketFromWire(const uint8_t* wire_data, size_t wire_size, return false; } + *out_packet = meshtastic_MeshPacket_init_zero; PacketHeaderWire header{}; - uint8_t payload[256]; - size_t payload_size = sizeof(payload); - if (!parseWirePacket(wire_data, wire_size, &header, payload, &payload_size)) + size_t payload_size = sizeof(out_packet->encrypted.bytes); + if (!parseWirePacket(wire_data, + wire_size, + &header, + out_packet->encrypted.bytes, + &payload_size)) { return false; } - meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero; - packet.from = header.from; - packet.to = header.to; - packet.channel = header.channel; - packet.id = header.id; - packet.hop_limit = header.flags & PACKET_FLAGS_HOP_LIMIT_MASK; - packet.want_ack = (header.flags & PACKET_FLAGS_WANT_ACK_MASK) != 0; - packet.via_mqtt = (header.flags & PACKET_FLAGS_VIA_MQTT_MASK) != 0; - packet.hop_start = (header.flags & PACKET_FLAGS_HOP_START_MASK) >> PACKET_FLAGS_HOP_START_SHIFT; - packet.next_hop = header.next_hop; - packet.relay_node = header.relay_node; - packet.pki_encrypted = (header.channel == 0); - packet.which_payload_variant = meshtastic_MeshPacket_encrypted_tag; - packet.encrypted.size = static_cast(std::min(payload_size, sizeof(packet.encrypted.bytes))); - memcpy(packet.encrypted.bytes, payload, packet.encrypted.size); - *out_packet = packet; + out_packet->from = header.from; + out_packet->to = header.to; + out_packet->channel = header.channel; + out_packet->id = header.id; + out_packet->hop_limit = header.flags & PACKET_FLAGS_HOP_LIMIT_MASK; + out_packet->want_ack = (header.flags & PACKET_FLAGS_WANT_ACK_MASK) != 0; + out_packet->via_mqtt = (header.flags & PACKET_FLAGS_VIA_MQTT_MASK) != 0; + out_packet->hop_start = + (header.flags & PACKET_FLAGS_HOP_START_MASK) >> PACKET_FLAGS_HOP_START_SHIFT; + out_packet->next_hop = header.next_hop; + out_packet->relay_node = header.relay_node; + out_packet->pki_encrypted = (header.channel == 0); + out_packet->which_payload_variant = meshtastic_MeshPacket_encrypted_tag; + out_packet->encrypted.size = + static_cast(std::min(payload_size, sizeof(out_packet->encrypted.bytes))); return true; } diff --git a/modules/core_chat/src/runtime/meshtastic_self_announcement_core.cpp b/modules/core_chat/src/runtime/meshtastic_self_announcement_core.cpp index d45107e5..4d978e5d 100644 --- a/modules/core_chat/src/runtime/meshtastic_self_announcement_core.cpp +++ b/modules/core_chat/src/runtime/meshtastic_self_announcement_core.cpp @@ -86,8 +86,7 @@ bool MeshtasticSelfAnnouncementCore::buildNodeInfoPacket(const MeshtasticAnnounc (request.user_id_override && request.user_id_override[0] != '\0') ? std::string(request.user_id_override) : default_user_id; - uint8_t payload[192] = {}; - size_t payload_size = sizeof(payload); + size_t payload_size = sizeof(out_packet->wire); if (!chat::meshtastic::encodeNodeInfoMessage(user_id, request.identity.long_name, @@ -97,8 +96,9 @@ bool MeshtasticSelfAnnouncementCore::buildNodeInfoPacket(const MeshtasticAnnounc request.public_key, request.public_key_len, request.want_response, - payload, - &payload_size)) + out_packet->wire, + &payload_size, + &out_packet->data_scratch)) { return false; } @@ -111,7 +111,7 @@ bool MeshtasticSelfAnnouncementCore::buildNodeInfoPacket(const MeshtasticAnnounc key, key_len); out_packet->wire_size = sizeof(out_packet->wire); - if (!chat::meshtastic::buildWirePacket(payload, + if (!chat::meshtastic::buildWirePacket(out_packet->wire, payload_size, request.identity.node_id, request.packet_id, diff --git a/modules/core_gps/include/gps/track_runtime.h b/modules/core_gps/include/gps/track_runtime.h index 7432631e..7432553a 100644 --- a/modules/core_gps/include/gps/track_runtime.h +++ b/modules/core_gps/include/gps/track_runtime.h @@ -1,7 +1,5 @@ #pragma once -#include "sys/runtime_async.h" - #include #include @@ -249,10 +247,9 @@ class TrackStorageWorker { public: TrackStorageWorker(ITrackFileAdapter& files, - sys::runtime::IBusArbiter& bus, ITrackEventSink& events, TrackFlushPolicy& policy) - : files_(files), bus_(bus), events_(events), policy_(policy) + : files_(files), events_(events), policy_(policy) { } @@ -273,36 +270,6 @@ class TrackStorageWorker { return; } - if (static_cast(retry_not_before_ms_ - now_ms) > 0) - { - return; - } - - sys::runtime::BusAcquireRequest request{}; - request.command_id = pending_command_.command_id; - request.deadline_ms = pending_command_.deadline_ms; - request.policy = policy_.isCritical(pending_command_) - ? sys::runtime::BusAccessPolicy::DurableCommit - : sys::runtime::BusAccessPolicy::BackgroundWorkerBounded; - const sys::runtime::BusAcquireResult acquire = bus_.acquire(request); - if (acquire.status != sys::runtime::BusAcquireStatus::Acquired) - { - if (expired(now_ms)) - { - publish(acquireFailureKind(), now_ms, -10); - pending_ = false; - retry_not_before_ms_ = 0; - busy_event_published_ = false; - return; - } - publishResourceBusy(now_ms, acquire.status); - retry_not_before_ms_ = now_ms + kBusyRetryDelayMs; - return; - } - - retry_not_before_ms_ = 0; - busy_event_published_ = false; - bool ok = false; switch (pending_command_.kind) { @@ -340,7 +307,6 @@ class TrackStorageWorker } } - bus_.release(acquire.token); pending_ = false; } @@ -350,9 +316,6 @@ class TrackStorageWorker } private: - static constexpr uint32_t kBusyRetryDelayMs = 25; - static constexpr uint32_t kBusyEventIntervalMs = 250; - bool appendBatchIfAny() { if (pending_command_.point_batch.count == 0) @@ -364,36 +327,6 @@ class TrackStorageWorker pending_command_.point_batch.count); } - bool expired(uint32_t now_ms) const - { - return pending_command_.deadline_ms != 0 && - static_cast(pending_command_.deadline_ms - now_ms) <= 0; - } - - TrackEventKind acquireFailureKind() const - { - switch (pending_command_.kind) - { - case TrackCommandKind::AppendPoint: - case TrackCommandKind::Flush: - return TrackEventKind::FlushFailed; - default: - return TrackEventKind::Failed; - } - } - - void publishResourceBusy(uint32_t now_ms, sys::runtime::BusAcquireStatus status) - { - if (busy_event_published_ && - static_cast(now_ms - last_busy_event_ms_) < kBusyEventIntervalMs) - { - return; - } - publish(TrackEventKind::ResourceBusy, now_ms, static_cast(status)); - busy_event_published_ = true; - last_busy_event_ms_ = now_ms; - } - void publish(TrackEventKind kind, uint32_t now_ms, int32_t error) { TrackEvent event{}; @@ -407,14 +340,10 @@ class TrackStorageWorker } ITrackFileAdapter& files_; - sys::runtime::IBusArbiter& bus_; ITrackEventSink& events_; TrackFlushPolicy& policy_; TrackCommand pending_command_{}; bool pending_ = false; - uint32_t retry_not_before_ms_ = 0; - uint32_t last_busy_event_ms_ = 0; - bool busy_event_published_ = false; }; template diff --git a/modules/core_mesh/include/mesh/protocol/meshtastic/meshtastic_protocol_strategy.h b/modules/core_mesh/include/mesh/protocol/meshtastic/meshtastic_protocol_strategy.h index 77f1f2c3..a2ffd733 100644 --- a/modules/core_mesh/include/mesh/protocol/meshtastic/meshtastic_protocol_strategy.h +++ b/modules/core_mesh/include/mesh/protocol/meshtastic/meshtastic_protocol_strategy.h @@ -1,6 +1,7 @@ #pragma once #include "mesh/protocol/mesh_protocol_strategy.h" +#include "meshtastic/mesh.pb.h" namespace mesh { @@ -17,6 +18,9 @@ class MeshtasticProtocolStrategy final : public MeshProtocolStrategy EncodedPacket& out) override; ProtocolResult parseRadioPacket(const RadioRxPacket& packet, MeshProtocolEvent& out) override; + + private: + meshtastic_Data data_scratch_ = meshtastic_Data_init_default; }; } // namespace meshtastic diff --git a/modules/core_mesh/include/mesh/usecase/direct_message_service.h b/modules/core_mesh/include/mesh/usecase/direct_message_service.h index 6d5ec0b2..4d9ae063 100644 --- a/modules/core_mesh/include/mesh/usecase/direct_message_service.h +++ b/modules/core_mesh/include/mesh/usecase/direct_message_service.h @@ -27,6 +27,7 @@ class DirectMessageService IPacketRadio& radio_; IClock& clock_; IMeshEventSink& events_; + EncodedPacket packet_scratch_{}; }; } // namespace mesh diff --git a/modules/core_mesh/include/mesh/usecase/mesh_session.h b/modules/core_mesh/include/mesh/usecase/mesh_session.h index 46ad39fd..54304323 100644 --- a/modules/core_mesh/include/mesh/usecase/mesh_session.h +++ b/modules/core_mesh/include/mesh/usecase/mesh_session.h @@ -32,6 +32,7 @@ class MeshSession ReceivePacketService& receive_; IClock& clock_; MeshSessionState state_ = MeshSessionState::Stopped; + RadioRxPacket rx_packet_scratch_{}; }; } // namespace mesh diff --git a/modules/core_mesh/include/mesh/usecase/receive_packet_service.h b/modules/core_mesh/include/mesh/usecase/receive_packet_service.h index 3f2a5746..e4fb7ef2 100644 --- a/modules/core_mesh/include/mesh/usecase/receive_packet_service.h +++ b/modules/core_mesh/include/mesh/usecase/receive_packet_service.h @@ -28,6 +28,7 @@ class ReceivePacketService IMeshEventSink& events_; IClock& clock_; MeshDedupService* dedup_ = nullptr; + MeshProtocolEvent event_scratch_{}; }; } // namespace mesh diff --git a/modules/core_mesh/src/protocol/meshtastic/meshtastic_protocol_strategy.cpp b/modules/core_mesh/src/protocol/meshtastic/meshtastic_protocol_strategy.cpp index c89d0e7d..fb245a4c 100644 --- a/modules/core_mesh/src/protocol/meshtastic/meshtastic_protocol_strategy.cpp +++ b/modules/core_mesh/src/protocol/meshtastic/meshtastic_protocol_strategy.cpp @@ -53,26 +53,28 @@ uint32_t portFromCommand(const DirectMessageCommand& command) bool encodeAppData(const ProtocolBuildContext& context, const DirectMessageCommand& command, uint8_t* out, - size_t* out_size) + size_t* out_size, + meshtastic_Data* data) { - if (!out || !out_size || command.payload.size > sizeof(meshtastic_Data::payload.bytes)) + if (!out || !out_size || !data || + command.payload.size > sizeof(meshtastic_Data::payload.bytes)) { return false; } - meshtastic_Data data = meshtastic_Data_init_default; - data.portnum = static_cast(portFromCommand(command)); - data.want_response = command.request_ack; - data.has_bitfield = true; - data.bitfield = 0; + *data = meshtastic_Data_init_default; + data->portnum = static_cast(portFromCommand(command)); + data->want_response = command.request_ack; + data->has_bitfield = true; + data->bitfield = 0; if (context.include_payload_dest) { - data.dest = command.to.value; + data->dest = command.to.value; } - data.payload.size = static_cast(command.payload.size); - std::memcpy(data.payload.bytes, command.payload.data, command.payload.size); + data->payload.size = static_cast(command.payload.size); + std::memcpy(data->payload.bytes, command.payload.data, command.payload.size); pb_ostream_t stream = pb_ostream_from_buffer(out, *out_size); - if (!pb_encode(&stream, meshtastic_Data_fields, &data)) + if (!pb_encode(&stream, meshtastic_Data_fields, data)) { return false; } @@ -110,21 +112,21 @@ ProtocolResult MeshtasticProtocolStrategy::buildDirectMessage( return ProtocolResult::fail(ProtocolFailure::InvalidInput); } - uint8_t data[256]{}; - size_t data_size = sizeof(data); - if (!encodeAppData(context, command, data, &data_size)) + out.size = sizeof(out.bytes); + if (!encodeAppData(context, command, out.bytes, &out.size, &data_scratch_)) { return ProtocolResult::fail(ProtocolFailure::EncodeFailed); } - out.size = sizeof(out.bytes); const bool want_ack = context.has_air_want_ack ? context.air_want_ack : ::chat::meshtastic::shouldSetAirWantAck(command.to.value, command.request_ack); const uint8_t* psk = context.channel_key.data; const size_t psk_len = context.channel_key.size; - if (!::chat::meshtastic::buildWirePacket(data, + const size_t data_size = out.size; + out.size = sizeof(out.bytes); + if (!::chat::meshtastic::buildWirePacket(out.bytes, data_size, context.local_node.value, packetIdFromContext(context), @@ -150,30 +152,29 @@ ProtocolResult MeshtasticProtocolStrategy::parseRadioPacket(const RadioRxPacket& return ProtocolResult::fail(ProtocolFailure::InvalidInput); } + out = MeshProtocolEvent{}; ::chat::meshtastic::PacketHeaderWire header{}; - uint8_t payload[256]{}; - size_t payload_size = sizeof(payload); + size_t payload_size = sizeof(out.payload_bytes); if (!::chat::meshtastic::parseWirePacket(packet.bytes, packet.size, &header, - payload, + out.payload_bytes, &payload_size)) { return ProtocolResult::fail(ProtocolFailure::DecodeFailed); } - meshtastic_Data data = meshtastic_Data_init_default; - pb_istream_t stream = pb_istream_from_buffer(payload, payload_size); - if (!pb_decode(&stream, meshtastic_Data_fields, &data)) + data_scratch_ = meshtastic_Data_init_default; + pb_istream_t stream = pb_istream_from_buffer(out.payload_bytes, payload_size); + if (!pb_decode(&stream, meshtastic_Data_fields, &data_scratch_)) { return ProtocolResult::fail(ProtocolFailure::DecodeFailed); } - out = MeshProtocolEvent{}; out.kind = MeshProtocolEventKind::MessageReceived; out.peer = NodeId{header.from}; out.packet_id = header.id; - out.setPayload(data.payload.bytes, data.payload.size); + out.setPayload(data_scratch_.payload.bytes, data_scratch_.payload.size); return ProtocolResult::success(); } diff --git a/modules/core_mesh/src/usecase/direct_message_service.cpp b/modules/core_mesh/src/usecase/direct_message_service.cpp index a9e871fe..d933754f 100644 --- a/modules/core_mesh/src/usecase/direct_message_service.cpp +++ b/modules/core_mesh/src/usecase/direct_message_service.cpp @@ -60,8 +60,7 @@ SendResult DirectMessageService::sendDirect(const DirectMessageCommand& command) context.air_want_ack = command.air_want_ack; context.include_payload_dest = command.include_payload_dest; - EncodedPacket packet{}; - auto built = protocol_.buildDirectMessage(context, command, packet); + auto built = protocol_.buildDirectMessage(context, command, packet_scratch_); if (!built.ok) { events_.emit(MeshEvent{MeshEventKind::SendFailed, @@ -88,7 +87,7 @@ SendResult DirectMessageService::sendDirect(const DirectMessageCommand& command) return SendResult::fail(SendFailure::PacketBuildFailed); } - auto sent = radio_.send(packet.view()); + auto sent = radio_.send(packet_scratch_.view()); if (!sent.ok) { events_.emit(MeshEvent{MeshEventKind::RadioError, diff --git a/modules/core_mesh/src/usecase/mesh_session.cpp b/modules/core_mesh/src/usecase/mesh_session.cpp index 9cd2ed71..5cfd4f61 100644 --- a/modules/core_mesh/src/usecase/mesh_session.cpp +++ b/modules/core_mesh/src/usecase/mesh_session.cpp @@ -44,11 +44,10 @@ void MeshSession::tick() return; } - RadioRxPacket packet{}; - while (radio_.poll(packet)) + while (radio_.poll(rx_packet_scratch_)) { - receive_.onRadioPacket(packet); - packet = RadioRxPacket{}; + receive_.onRadioPacket(rx_packet_scratch_); + rx_packet_scratch_ = RadioRxPacket{}; } } diff --git a/modules/core_mesh/src/usecase/receive_packet_service.cpp b/modules/core_mesh/src/usecase/receive_packet_service.cpp index 6f60c748..76f35b1a 100644 --- a/modules/core_mesh/src/usecase/receive_packet_service.cpp +++ b/modules/core_mesh/src/usecase/receive_packet_service.cpp @@ -18,8 +18,7 @@ ReceivePacketService::ReceivePacketService(MeshProtocolStrategy& protocol, void ReceivePacketService::onRadioPacket(const RadioRxPacket& packet) { - MeshProtocolEvent event{}; - auto parsed = protocol_.parseRadioPacket(packet, event); + auto parsed = protocol_.parseRadioPacket(packet, event_scratch_); if (!parsed.ok) { events_.emit(MeshEvent{MeshEventKind::ProtocolError, @@ -28,13 +27,15 @@ void ReceivePacketService::onRadioPacket(const RadioRxPacket& packet) return; } - if (dedup_ && event.packet_id != 0 && - !dedup_->accept(event.peer, event.packet_id, clock_.nowMs())) + if (dedup_ && event_scratch_.packet_id != 0 && + !dedup_->accept(event_scratch_.peer, + event_scratch_.packet_id, + clock_.nowMs())) { return; } - handleProtocolEvent(event); + handleProtocolEvent(event_scratch_); } void ReceivePacketService::handleProtocolEvent(const MeshProtocolEvent& event) diff --git a/modules/core_sys/include/sys/bus_access_scope.h b/modules/core_sys/include/sys/bus_access_scope.h index f6a07ffd..05da55b4 100644 --- a/modules/core_sys/include/sys/bus_access_scope.h +++ b/modules/core_sys/include/sys/bus_access_scope.h @@ -1,6 +1,6 @@ #pragma once -#include "sys/runtime_async.h" +#include "sys/shared_spi_access.h" namespace sys::runtime { diff --git a/modules/core_sys/include/sys/persistence_runtime.h b/modules/core_sys/include/sys/persistence_runtime.h index 3b3a4559..0b77eea9 100644 --- a/modules/core_sys/include/sys/persistence_runtime.h +++ b/modules/core_sys/include/sys/persistence_runtime.h @@ -1,7 +1,5 @@ #pragma once -#include "sys/runtime_async.h" - #include #include #include @@ -68,7 +66,6 @@ class PersistencePolicy virtual PersistencePolicyMode modeFor(const char* store_key) const = 0; virtual uint32_t delayFor(PersistencePolicyMode policy) const = 0; - virtual BusAccessPolicy busPolicyFor(PersistencePolicyMode policy) const = 0; }; class DefaultPersistencePolicy : public PersistencePolicy @@ -95,12 +92,6 @@ class DefaultPersistencePolicy : public PersistencePolicy } } - BusAccessPolicy busPolicyFor(PersistencePolicyMode policy) const override - { - return policy == PersistencePolicyMode::ImmediateCriticalSave - ? BusAccessPolicy::DurableCommit - : BusAccessPolicy::BackgroundWorkerBounded; - } }; class IStoreSnapshotProvider @@ -243,10 +234,9 @@ class PersistenceWorker public: PersistenceWorker(IStoreSnapshotProvider& snapshots, IStoreStorageAdapter& storage, - IBusArbiter& bus, IPersistenceEventSink& events, PersistencePolicy& policy) - : snapshots_(snapshots), storage_(storage), bus_(bus), events_(events), policy_(policy) + : snapshots_(snapshots), storage_(storage), events_(events), policy_(policy) { } @@ -275,18 +265,6 @@ class PersistenceWorker started.timestamp_ms = now_ms; (void)events_.publish(started); - BusAcquireRequest request{}; - request.policy = policy_.busPolicyFor(pending_command_.policy); - request.command_id = pending_command_.command_id; - request.deadline_ms = pending_command_.deadline_ms; - const BusAcquireResult acquire = bus_.acquire(request); - if (acquire.status != BusAcquireStatus::Acquired) - { - publishResult(PersistenceEventKind::SaveFailed, now_ms, -11); - pending_ = false; - return; - } - StoreSnapshot snapshot = snapshots_.snapshot(pending_command_.store_key); StoreStorageResult result{}; if (snapshot.valid) @@ -297,8 +275,6 @@ class PersistenceWorker { result.error = -12; } - bus_.release(acquire.token); - publishResult(result.ok ? PersistenceEventKind::SaveSucceeded : PersistenceEventKind::SaveFailed, now_ms, @@ -325,7 +301,6 @@ class PersistenceWorker IStoreSnapshotProvider& snapshots_; IStoreStorageAdapter& storage_; - IBusArbiter& bus_; IPersistenceEventSink& events_; PersistencePolicy& policy_; PersistenceCommand pending_command_{}; diff --git a/modules/core_sys/include/sys/runtime_async.h b/modules/core_sys/include/sys/runtime_async.h index 224f9198..40f9481c 100644 --- a/modules/core_sys/include/sys/runtime_async.h +++ b/modules/core_sys/include/sys/runtime_async.h @@ -84,33 +84,6 @@ struct RuntimeIntent RuntimePriority priority_hint = RuntimePriority::Normal; }; -enum class BusAccessPolicy : uint8_t -{ - UiNeverBlock, - DisplayFrameCritical, - InteractiveWorkerBounded, - BackgroundWorkerBounded, - DurableCommit, - RecoveryExclusive, -}; - -enum class BusAcquireStatus : uint8_t -{ - Acquired, - Busy, - TimedOut, - Unavailable, -}; - -enum class StorageHealthStatus : uint8_t -{ - Healthy, - Slow, - Degraded, - Unavailable, - Recovering, -}; - struct RuntimeCommand { uint32_t command_id = 0; @@ -143,51 +116,6 @@ struct RuntimeState int32_t last_error = 0; }; -struct BusAcquireRequest -{ - uint32_t resource = 0; - BusAccessPolicy policy = BusAccessPolicy::BackgroundWorkerBounded; - uint32_t command_id = 0; - uint32_t deadline_ms = 0; - uint32_t origin = 0; - const char* owner_label = nullptr; -}; - -struct BusAccessToken -{ - uint32_t resource = 0; - uint32_t owner = 0; - uint32_t acquired_ms = 0; - uint32_t generation = 0; - uint32_t depth = 0; - uintptr_t task_id = 0; - bool valid = false; -}; - -struct BusDiagnostics -{ - uint32_t resource = 0; - uint32_t owner = 0; - uint32_t command_id = 0; - uint32_t wait_ms = 0; - uint32_t hold_ms = 0; - BusAccessPolicy policy = BusAccessPolicy::BackgroundWorkerBounded; -}; - -struct BusAcquireResult -{ - BusAcquireStatus status = BusAcquireStatus::Unavailable; - BusAccessToken token{}; - BusDiagnostics diagnostics{}; -}; - -struct StorageHealthState -{ - StorageHealthStatus status = StorageHealthStatus::Healthy; - int32_t last_error = 0; - uint32_t last_transition_ms = 0; -}; - struct RuntimeRetryDecision { bool retry = false; @@ -265,179 +193,6 @@ class IActiveWorker virtual bool submit(const RuntimeCommand& command) = 0; }; -class IBusArbiter -{ - public: - virtual ~IBusArbiter() = default; - - virtual BusAcquireResult acquire(const BusAcquireRequest& request) = 0; - virtual void release(const BusAccessToken& token) = 0; - virtual StorageHealthState health() const = 0; -}; - -class IBusAdapter -{ - public: - virtual ~IBusAdapter() = default; - - virtual bool tryAcquire(uint32_t timeout_ms) = 0; - virtual void release() = 0; - virtual uint32_t nowMs() const = 0; - virtual uint32_t owner() const = 0; -}; - -class BusPolicyStrategy -{ - public: - virtual ~BusPolicyStrategy() = default; - - virtual BusAccessPolicy select(const RuntimeCommand& command) const = 0; - virtual uint32_t timeoutFor(BusAccessPolicy policy) const = 0; -}; - -class DefaultBusPolicyStrategy : public BusPolicyStrategy -{ - public: - BusAccessPolicy select(const RuntimeCommand& command) const override - { - if (command.kind == RuntimeCommandKind::MapTileLoad) - { - return BusAccessPolicy::DisplayFrameCritical; - } - if (command.priority == RuntimePriority::Realtime || - command.priority == RuntimePriority::Interactive) - { - return BusAccessPolicy::InteractiveWorkerBounded; - } - if (command.kind == RuntimeCommandKind::TrackStop || - command.kind == RuntimeCommandKind::TrackFlush) - { - return BusAccessPolicy::DurableCommit; - } - return BusAccessPolicy::BackgroundWorkerBounded; - } - - uint32_t timeoutFor(BusAccessPolicy policy) const override - { - switch (policy) - { - case BusAccessPolicy::UiNeverBlock: - case BusAccessPolicy::DisplayFrameCritical: - return 0; - case BusAccessPolicy::InteractiveWorkerBounded: - return 2; - case BusAccessPolicy::BackgroundWorkerBounded: - return 25; - case BusAccessPolicy::DurableCommit: - return 150; - case BusAccessPolicy::RecoveryExclusive: - return 500; - default: - return 0; - } - } -}; - -class StorageBusArbiter : public IBusArbiter -{ - public: - StorageBusArbiter(IBusAdapter& adapter, BusPolicyStrategy& policy) - : adapter_(adapter), policy_(policy) - { - } - - BusAcquireResult acquire(const BusAcquireRequest& request) override - { - const uint32_t start_ms = adapter_.nowMs(); - uint32_t timeout_ms = policy_.timeoutFor(request.policy); - if (request.deadline_ms != 0) - { - const uint32_t remaining = - static_cast(request.deadline_ms - start_ms) > 0 - ? request.deadline_ms - start_ms - : 0; - if (remaining < timeout_ms) - { - timeout_ms = remaining; - } - } - - const bool acquired = adapter_.tryAcquire(timeout_ms); - const uint32_t end_ms = adapter_.nowMs(); - - BusAcquireResult result{}; - result.status = acquired ? BusAcquireStatus::Acquired - : (timeout_ms == 0 ? BusAcquireStatus::Busy - : BusAcquireStatus::TimedOut); - result.token.resource = request.resource; - result.token.owner = request.command_id; - result.token.acquired_ms = acquired ? end_ms : 0; - result.token.valid = acquired; - result.diagnostics.resource = request.resource; - result.diagnostics.owner = adapter_.owner(); - result.diagnostics.command_id = request.command_id; - result.diagnostics.wait_ms = end_ms - start_ms; - result.diagnostics.policy = request.policy; - - updateHealth(result.status, end_ms); - return result; - } - - void release(const BusAccessToken& token) override - { - if (!token.valid) - { - return; - } - adapter_.release(); - consecutive_timeouts_ = 0; - if (health_.status == StorageHealthStatus::Slow || - health_.status == StorageHealthStatus::Recovering) - { - health_.status = StorageHealthStatus::Healthy; - health_.last_error = 0; - health_.last_transition_ms = adapter_.nowMs(); - } - } - - StorageHealthState health() const override - { - return health_; - } - - BusAccessPolicy selectPolicy(const RuntimeCommand& command) const - { - return policy_.select(command); - } - - private: - void updateHealth(BusAcquireStatus status, uint32_t now_ms) - { - if (status == BusAcquireStatus::Acquired) - { - return; - } - - health_.last_transition_ms = now_ms; - if (status == BusAcquireStatus::Unavailable) - { - health_.status = StorageHealthStatus::Unavailable; - health_.last_error = -3; - return; - } - - ++consecutive_timeouts_; - health_.last_error = status == BusAcquireStatus::TimedOut ? -2 : -1; - health_.status = consecutive_timeouts_ >= 3 ? StorageHealthStatus::Degraded - : StorageHealthStatus::Slow; - } - - IBusAdapter& adapter_; - BusPolicyStrategy& policy_; - StorageHealthState health_{}; - uint8_t consecutive_timeouts_ = 0; -}; - class IPlatformStorageAdapter { public: @@ -472,7 +227,6 @@ class RuntimePolicyStrategy virtual ~RuntimePolicyStrategy() = default; virtual RuntimePriority selectPriority(const RuntimeIntent& intent) const = 0; - virtual BusAccessPolicy selectBusPolicy(const RuntimeCommand& command) const = 0; virtual RuntimeRetryDecision selectRetry(const RuntimeCommand& command, const PlatformStorageResult& result) const = 0; }; @@ -485,25 +239,6 @@ class DefaultRuntimePolicyStrategy : public RuntimePolicyStrategy return intent.priority_hint; } - BusAccessPolicy selectBusPolicy(const RuntimeCommand& command) const override - { - if (command.kind == RuntimeCommandKind::MapTileLoad) - { - return BusAccessPolicy::DisplayFrameCritical; - } - if (command.priority == RuntimePriority::Realtime || - command.priority == RuntimePriority::Interactive) - { - return BusAccessPolicy::InteractiveWorkerBounded; - } - if (command.priority == RuntimePriority::Idle || - command.priority == RuntimePriority::Background) - { - return BusAccessPolicy::BackgroundWorkerBounded; - } - return BusAccessPolicy::BackgroundWorkerBounded; - } - RuntimeRetryDecision selectRetry(const RuntimeCommand& command, const PlatformStorageResult& result) const override { diff --git a/modules/core_sys/include/sys/runtime_harness.h b/modules/core_sys/include/sys/runtime_harness.h index 58aa4fee..2f640b77 100644 --- a/modules/core_sys/include/sys/runtime_harness.h +++ b/modules/core_sys/include/sys/runtime_harness.h @@ -3,6 +3,7 @@ #include "sys/feedback_runtime.h" #include "sys/persistence_runtime.h" #include "sys/runtime_async.h" +#include "sys/shared_spi_access.h" #include #include diff --git a/modules/core_sys/include/sys/shared_spi_access.h b/modules/core_sys/include/sys/shared_spi_access.h new file mode 100644 index 00000000..b75eaf19 --- /dev/null +++ b/modules/core_sys/include/sys/shared_spi_access.h @@ -0,0 +1,92 @@ +#pragma once + +#include + +namespace sys::runtime +{ + +// Technical shared-bus primitives. Business/runtime modules must depend on +// semantic device services instead of including this header. +enum class BusAccessPolicy : uint8_t +{ + UiNeverBlock, + DisplayFrameCritical, + InteractiveWorkerBounded, + BackgroundWorkerBounded, + DurableCommit, + RecoveryExclusive, +}; + +enum class BusAcquireStatus : uint8_t +{ + Acquired, + Busy, + TimedOut, + Unavailable, +}; + +enum class StorageHealthStatus : uint8_t +{ + Healthy, + Slow, + Degraded, + Unavailable, + Recovering, +}; + +struct BusAcquireRequest +{ + uint32_t resource = 0; + BusAccessPolicy policy = BusAccessPolicy::BackgroundWorkerBounded; + uint32_t command_id = 0; + uint32_t deadline_ms = 0; + uint32_t origin = 0; + const char* owner_label = nullptr; +}; + +struct BusAccessToken +{ + uint32_t resource = 0; + uint32_t owner = 0; + uint32_t acquired_ms = 0; + uint32_t generation = 0; + uint32_t depth = 0; + uintptr_t task_id = 0; + bool valid = false; +}; + +struct BusDiagnostics +{ + uint32_t resource = 0; + uint32_t owner = 0; + uint32_t command_id = 0; + uint32_t wait_ms = 0; + uint32_t hold_ms = 0; + BusAccessPolicy policy = BusAccessPolicy::BackgroundWorkerBounded; +}; + +struct BusAcquireResult +{ + BusAcquireStatus status = BusAcquireStatus::Unavailable; + BusAccessToken token{}; + BusDiagnostics diagnostics{}; +}; + +struct StorageHealthState +{ + StorageHealthStatus status = StorageHealthStatus::Healthy; + int32_t last_error = 0; + uint32_t last_transition_ms = 0; +}; + +class IBusArbiter +{ + public: + virtual ~IBusArbiter() = default; + + virtual BusAcquireResult acquire(const BusAcquireRequest& request) = 0; + virtual void release(const BusAccessToken& token) = 0; + virtual StorageHealthState health() const = 0; +}; + +} // namespace sys::runtime diff --git a/modules/core_sys/tests/test_runtime_async.cpp b/modules/core_sys/tests/test_runtime_async.cpp index dc7c6ba0..8b0e0a14 100644 --- a/modules/core_sys/tests/test_runtime_async.cpp +++ b/modules/core_sys/tests/test_runtime_async.cpp @@ -42,37 +42,44 @@ class FakeUiEffectSink final : public sys::runtime::IUiEffectSink sys::runtime::RuntimeUiEffect last_effect{}; }; -class FakeBusAdapter final : public sys::runtime::IBusAdapter +class FakeBusArbiter final : public sys::runtime::IBusArbiter { public: bool acquire_ok = true; - uint32_t now_ms = 100; - uint32_t last_timeout_ms = 0; int acquire_count = 0; int release_count = 0; - bool tryAcquire(uint32_t timeout_ms) override + sys::runtime::BusAcquireResult acquire( + const sys::runtime::BusAcquireRequest& request) override { - last_timeout_ms = timeout_ms; ++acquire_count; - now_ms += timeout_ms; - return acquire_ok; + last_request = request; + sys::runtime::BusAcquireResult result{}; + result.status = acquire_ok ? sys::runtime::BusAcquireStatus::Acquired + : sys::runtime::BusAcquireStatus::TimedOut; + result.token.resource = request.resource; + result.token.owner = request.command_id; + result.token.valid = acquire_ok; + result.diagnostics = {}; + result.diagnostics.resource = request.resource; + result.diagnostics.owner = 77; + result.diagnostics.command_id = request.command_id; + result.diagnostics.policy = request.policy; + return result; } - void release() override + void release(const sys::runtime::BusAccessToken& token) override { + assert(token.valid); ++release_count; } - uint32_t nowMs() const override + sys::runtime::StorageHealthState health() const override { - return now_ms; + return {}; } - uint32_t owner() const override - { - return 77; - } + sys::runtime::BusAcquireRequest last_request{}; }; void test_priority_pop_order() @@ -258,69 +265,9 @@ void test_event_to_ui_effect_bridge() assert(out.event_id == event.event_id); } -void test_storage_bus_arbiter_uses_policy_timeout() -{ - FakeBusAdapter adapter; - sys::runtime::DefaultBusPolicyStrategy policy; - sys::runtime::StorageBusArbiter arbiter(adapter, policy); - - sys::runtime::BusAcquireRequest request{}; - request.resource = 3; - request.command_id = 9; - request.policy = sys::runtime::BusAccessPolicy::InteractiveWorkerBounded; - - const sys::runtime::BusAcquireResult result = arbiter.acquire(request); - assert(result.status == sys::runtime::BusAcquireStatus::Acquired); - assert(result.token.valid); - assert(result.token.owner == 9); - assert(adapter.last_timeout_ms == 2); - assert(result.diagnostics.owner == 77); - - arbiter.release(result.token); - assert(adapter.release_count == 1); -} - -void test_map_tile_policy_is_display_frame_critical() -{ - FakeBusAdapter adapter; - sys::runtime::DefaultBusPolicyStrategy policy; - sys::runtime::StorageBusArbiter arbiter(adapter, policy); - - sys::runtime::RuntimeCommand command{}; - command.kind = sys::runtime::RuntimeCommandKind::MapTileLoad; - command.priority = sys::runtime::RuntimePriority::Normal; - assert(policy.select(command) == sys::runtime::BusAccessPolicy::DisplayFrameCritical); - - sys::runtime::BusAcquireRequest request{}; - request.policy = policy.select(command); - const sys::runtime::BusAcquireResult result = arbiter.acquire(request); - assert(result.status == sys::runtime::BusAcquireStatus::Acquired); - assert(adapter.last_timeout_ms == 0); - arbiter.release(result.token); -} - -void test_storage_bus_arbiter_reports_degraded_after_timeouts() -{ - FakeBusAdapter adapter; - adapter.acquire_ok = false; - sys::runtime::DefaultBusPolicyStrategy policy; - sys::runtime::StorageBusArbiter arbiter(adapter, policy); - - sys::runtime::BusAcquireRequest request{}; - request.policy = sys::runtime::BusAccessPolicy::BackgroundWorkerBounded; - - assert(arbiter.acquire(request).status == sys::runtime::BusAcquireStatus::TimedOut); - assert(arbiter.health().status == sys::runtime::StorageHealthStatus::Slow); - assert(arbiter.acquire(request).status == sys::runtime::BusAcquireStatus::TimedOut); - assert(arbiter.acquire(request).status == sys::runtime::BusAcquireStatus::TimedOut); - assert(arbiter.health().status == sys::runtime::StorageHealthStatus::Degraded); -} - void test_scoped_bus_access_token_releases_on_destruction() { - FakeBusAdapter adapter; - sys::runtime::DefaultBusPolicyStrategy policy; - sys::runtime::StorageBusArbiter arbiter(adapter, policy); + FakeBusArbiter arbiter; sys::runtime::BusAcquireRequest request{}; request.resource = 8; @@ -334,18 +281,16 @@ void test_scoped_bus_access_token_releases_on_destruction() assert(scope.status() == sys::runtime::BusAcquireStatus::Acquired); assert(scope.token().owner == 12); assert(scope.diagnostics().owner == 77); - assert(adapter.acquire_count == 1); - assert(adapter.release_count == 0); + assert(arbiter.acquire_count == 1); + assert(arbiter.release_count == 0); } - assert(adapter.release_count == 1); + assert(arbiter.release_count == 1); } void test_scoped_bus_access_token_release_is_idempotent() { - FakeBusAdapter adapter; - sys::runtime::DefaultBusPolicyStrategy policy; - sys::runtime::StorageBusArbiter arbiter(adapter, policy); + FakeBusArbiter arbiter; sys::runtime::BusAcquireRequest request{}; request.policy = sys::runtime::BusAccessPolicy::BackgroundWorkerBounded; @@ -355,14 +300,12 @@ void test_scoped_bus_access_token_release_is_idempotent() scope.release(); scope.release(); assert(!scope.acquired()); - assert(adapter.release_count == 1); + assert(arbiter.release_count == 1); } void test_scoped_bus_access_token_move_transfers_release() { - FakeBusAdapter adapter; - sys::runtime::DefaultBusPolicyStrategy policy; - sys::runtime::StorageBusArbiter arbiter(adapter, policy); + FakeBusArbiter arbiter; sys::runtime::BusAcquireRequest request{}; request.policy = sys::runtime::BusAccessPolicy::BackgroundWorkerBounded; @@ -374,20 +317,18 @@ void test_scoped_bus_access_token_move_transfers_release() sys::runtime::ScopedBusAccessToken moved(std::move(original)); assert(!original.acquired()); assert(moved.acquired()); - assert(adapter.release_count == 0); + assert(arbiter.release_count == 0); } - assert(adapter.release_count == 1); + assert(arbiter.release_count == 1); } - assert(adapter.release_count == 1); + assert(arbiter.release_count == 1); } void test_scoped_bus_access_token_does_not_release_failed_acquire() { - FakeBusAdapter adapter; - adapter.acquire_ok = false; - sys::runtime::DefaultBusPolicyStrategy policy; - sys::runtime::StorageBusArbiter arbiter(adapter, policy); + FakeBusArbiter arbiter; + arbiter.acquire_ok = false; sys::runtime::BusAcquireRequest request{}; request.policy = sys::runtime::BusAccessPolicy::BackgroundWorkerBounded; @@ -398,7 +339,7 @@ void test_scoped_bus_access_token_does_not_release_failed_acquire() assert(scope.status() == sys::runtime::BusAcquireStatus::TimedOut); } - assert(adapter.release_count == 0); + assert(arbiter.release_count == 0); } } // namespace @@ -412,9 +353,6 @@ int main() test_runtime_facade_submit_tick(); test_runtime_facade_dedupe_cancel_policy(); test_event_to_ui_effect_bridge(); - test_storage_bus_arbiter_uses_policy_timeout(); - test_map_tile_policy_is_display_frame_critical(); - test_storage_bus_arbiter_reports_degraded_after_timeouts(); test_scoped_bus_access_token_releases_on_destruction(); test_scoped_bus_access_token_release_is_idempotent(); test_scoped_bus_access_token_move_transfers_release(); diff --git a/modules/core_sys/tests/test_ui_storage_event_runtime_contract.cpp b/modules/core_sys/tests/test_ui_storage_event_runtime_contract.cpp index ce1eda35..9c935878 100644 --- a/modules/core_sys/tests/test_ui_storage_event_runtime_contract.cpp +++ b/modules/core_sys/tests/test_ui_storage_event_runtime_contract.cpp @@ -91,7 +91,7 @@ class TrackFiles final : public gps::runtime::ITrackFileAdapter active_track = storage.track_id; last_storage = storage; ++open_count; - return true; + return open_ok; } bool append(const gps::runtime::TrackStorageDescriptor& storage, @@ -138,6 +138,7 @@ class TrackFiles final : public gps::runtime::ITrackFileAdapter std::size_t open_count = 0; std::size_t flush_count = 0; std::size_t close_count = 0; + bool open_ok = true; }; gps::runtime::TrackStorageDescriptor trackDescriptor() @@ -196,7 +197,6 @@ void test_persistence_runtime_contract() sys::runtime::DefaultPersistencePolicy policy; sys::runtime::PersistenceWorker worker(harness.storage(), harness.storage(), - harness.bus(), harness.events(), policy); sys::runtime::PersistenceRuntime<4> runtime(registry, @@ -209,7 +209,6 @@ void test_persistence_runtime_contract() assert(harness.storage().writeCount() == 0); runtime.tick(150); assert(harness.storage().writeCount() == 1); - assert(harness.bus().acquireCount() == 1); assert(harness.events().persistenceCount() >= 3); } @@ -242,7 +241,7 @@ void test_track_runtime_contract() TrackFiles files; TrackEvents events; gps::runtime::DefaultTrackFlushPolicy policy; - gps::runtime::TrackStorageWorker worker(files, harness.bus(), events, policy); + gps::runtime::TrackStorageWorker worker(files, events, policy); gps::runtime::TrackPointBuffer<8> points; gps::runtime::TrackStateMachine states; gps::runtime::TrackRuntime<8> runtime(points, states, policy, worker, events); @@ -270,36 +269,26 @@ void test_track_runtime_contract() assert(files.flush_count == 1); } -void test_track_worker_resource_busy_retains_pending_command() +void test_track_worker_failure_completes_semantically() { sys::runtime::RuntimeHarness harness; TrackFiles files; + files.open_ok = false; TrackEvents events; gps::runtime::DefaultTrackFlushPolicy policy; - gps::runtime::TrackStorageWorker worker(files, harness.bus(), events, policy); + gps::runtime::TrackStorageWorker worker(files, events, policy); gps::runtime::TrackCommand command{}; command.command_id = 44; command.kind = gps::runtime::TrackCommandKind::StartNewTrack; command.storage = trackDescriptor(); - harness.bus().scriptAcquire(sys::runtime::BusAcquireStatus::Busy); assert(worker.submit(command)); worker.tick(10); - assert(worker.busy()); - assert(files.open_count == 0); - assert(events.count == 1); - assert(events.events[0].kind == gps::runtime::TrackEventKind::ResourceBusy); - - worker.tick(20); - assert(events.count == 1); - - harness.bus().scriptAcquire(sys::runtime::BusAcquireStatus::Acquired); - worker.tick(40); assert(!worker.busy()); assert(files.open_count == 1); - assert(events.count == 2); - assert(events.events[1].kind == gps::runtime::TrackEventKind::Started); + assert(events.count == 1); + assert(events.events[0].kind == gps::runtime::TrackEventKind::Failed); } void test_track_runtime_keeps_buffered_points_while_worker_busy() @@ -308,7 +297,7 @@ void test_track_runtime_keeps_buffered_points_while_worker_busy() TrackFiles files; TrackEvents events; gps::runtime::DefaultTrackFlushPolicy policy; - gps::runtime::TrackStorageWorker worker(files, harness.bus(), events, policy); + gps::runtime::TrackStorageWorker worker(files, events, policy); gps::runtime::TrackPointBuffer<8> points; gps::runtime::TrackStateMachine states; gps::runtime::TrackRuntime<8> runtime(points, states, policy, worker, events); @@ -323,15 +312,14 @@ void test_track_runtime_keeps_buffered_points_while_worker_busy() point.latitude = 1.0; point.longitude = 2.0; - harness.bus().scriptAcquire(sys::runtime::BusAcquireStatus::Busy); for (int i = 0; i < 8; ++i) { point.timestamp_ms = static_cast(i); assert(runtime.appendPoint(point, 10 + static_cast(i))); } runtime.tick(30); - assert(worker.busy()); - assert(files.appended == 0); + assert(!worker.busy()); + assert(files.appended == 8); for (int i = 0; i < 8; ++i) { @@ -339,11 +327,7 @@ void test_track_runtime_keeps_buffered_points_while_worker_busy() assert(runtime.appendPoint(point, 40 + static_cast(i))); } - harness.bus().scriptAcquire(sys::runtime::BusAcquireStatus::Acquired); runtime.tick(60); - assert(files.appended == 8); - assert(worker.busy()); - runtime.tick(90); assert(files.appended == 16); assert(!worker.busy()); } @@ -354,7 +338,7 @@ void test_track_stop_appends_pending_points_before_close() TrackFiles files; TrackEvents events; gps::runtime::DefaultTrackFlushPolicy policy; - gps::runtime::TrackStorageWorker worker(files, harness.bus(), events, policy); + gps::runtime::TrackStorageWorker worker(files, events, policy); gps::runtime::TrackPointBuffer<8> points; gps::runtime::TrackStateMachine states; gps::runtime::TrackRuntime<8> runtime(points, states, policy, worker, events); @@ -398,7 +382,7 @@ int main() test_persistence_runtime_contract(); test_feedback_runtime_contract(); test_track_runtime_contract(); - test_track_worker_resource_busy_retains_pending_command(); + test_track_worker_failure_completes_semantically(); test_track_runtime_keeps_buffered_points_while_worker_busy(); test_track_stop_appends_pending_points_before_close(); test_runtime_harness_keeps_ui_drain_separate(); diff --git a/modules/ui_map_runtime/include/ui_map_runtime/map_tiles/filesystem_map_tile_source.h b/modules/ui_map_runtime/include/ui_map_runtime/map_tiles/filesystem_map_tile_source.h index 883a423c..425e0bb4 100644 --- a/modules/ui_map_runtime/include/ui_map_runtime/map_tiles/filesystem_map_tile_source.h +++ b/modules/ui_map_runtime/include/ui_map_runtime/map_tiles/filesystem_map_tile_source.h @@ -18,11 +18,9 @@ class FilesystemMapTileSource final : public IMapTileSource MapTileLookupResult lookup(const MapTileRef& ref) const override; - bool read(const MapTileRef& ref, - uint8_t* buffer, - std::size_t capacity, - std::size_t& out_size, - MapTileFormat& out_format) const override; + MapTileReadResult read(const MapTileRef& ref, + uint8_t* buffer, + std::size_t capacity) const override; bool resolvePath(const MapTileRef& ref, char* out_path, std::size_t out_size) const; bool resolveDirectory(MapTileLayer layer, char* out_path, std::size_t out_size) const; diff --git a/modules/ui_map_runtime/include/ui_map_runtime/map_tiles/map_tile_async_runtime.h b/modules/ui_map_runtime/include/ui_map_runtime/map_tiles/map_tile_async_runtime.h index 7a608272..fdd6b523 100644 --- a/modules/ui_map_runtime/include/ui_map_runtime/map_tiles/map_tile_async_runtime.h +++ b/modules/ui_map_runtime/include/ui_map_runtime/map_tiles/map_tile_async_runtime.h @@ -23,7 +23,7 @@ enum class MapTileAsyncEventKind : uint8_t Ready, Failed, Cancelled, - ResourceBusy, + RetryLater, }; struct MapViewportPlan @@ -105,22 +105,6 @@ class IMapTileEventSink virtual bool publish(const MapTileAsyncEvent& event) = 0; }; -enum class MapTileReadStatus : uint8_t -{ - Ready, - Failed, - ResourceBusy, -}; - -struct MapTileReadResult -{ - MapTileReadStatus status = MapTileReadStatus::Failed; - std::size_t size = 0; - MapTileFormat format = MapTileFormat::Unknown; - int32_t error = -1; - bool bus_access_retained = true; -}; - class IMapTileWorkerBackend { public: @@ -152,7 +136,7 @@ class MapTileStateMachine ++ready_count_; } else if (event.kind == MapTileAsyncEventKind::Failed || - event.kind == MapTileAsyncEventKind::ResourceBusy) + event.kind == MapTileAsyncEventKind::RetryLater) { ++failed_count_; } @@ -186,8 +170,7 @@ class MapTileStateMachine class MapTileAsyncRuntime { public: - explicit MapTileAsyncRuntime(IMapTileCommandSink& commands, - sys::runtime::RuntimePolicyStrategy* policy = nullptr); + explicit MapTileAsyncRuntime(IMapTileCommandSink& commands); uint32_t activeGeneration() const; std::size_t requestVisibleTiles(const MapViewportPlan& plan, uint32_t now_ms); @@ -199,8 +182,6 @@ class MapTileAsyncRuntime sys::runtime::RuntimeCommand commandFromIntent(const sys::runtime::RuntimeIntent& intent); IMapTileCommandSink& commands_; - sys::runtime::DefaultRuntimePolicyStrategy default_policy_{}; - sys::runtime::RuntimePolicyStrategy* policy_ = nullptr; sys::runtime::RuntimeState state_{}; uint32_t active_generation_ = 0; uint32_t next_command_id_ = 1; @@ -258,20 +239,15 @@ class MapTileWorker { public: MapTileWorker(IMapTileWorkerBackend& backend, - sys::runtime::IBusArbiter& bus, IMapTileEventSink& events, uint8_t* scratch, - std::size_t scratch_size, - sys::runtime::RuntimePolicyStrategy* policy = nullptr); + std::size_t scratch_size); bool execute(const LoadTileCommand& command, uint32_t now_ms); private: IMapTileWorkerBackend& backend_; - sys::runtime::IBusArbiter& bus_; IMapTileEventSink& events_; - sys::runtime::DefaultRuntimePolicyStrategy default_policy_{}; - sys::runtime::RuntimePolicyStrategy* policy_ = nullptr; uint8_t* scratch_ = nullptr; std::size_t scratch_size_ = 0; }; diff --git a/modules/ui_map_runtime/include/ui_map_runtime/map_tiles/map_tile_source.h b/modules/ui_map_runtime/include/ui_map_runtime/map_tiles/map_tile_source.h index 610d4fff..1e70b33d 100644 --- a/modules/ui_map_runtime/include/ui_map_runtime/map_tiles/map_tile_source.h +++ b/modules/ui_map_runtime/include/ui_map_runtime/map_tiles/map_tile_source.h @@ -10,6 +10,23 @@ namespace ui namespace map_tiles { +enum class MapTileReadStatus : uint8_t +{ + Ready, + Missing, + RetryLater, + Error, + Invalid, +}; + +struct MapTileReadResult +{ + MapTileReadStatus status = MapTileReadStatus::Error; + std::size_t size = 0; + int32_t error = -1; + MapTileFormat format = MapTileFormat::Unknown; +}; + class IMapTileSource { public: @@ -17,11 +34,9 @@ class IMapTileSource virtual MapTileLookupResult lookup(const MapTileRef& ref) const = 0; - virtual bool read(const MapTileRef& ref, - uint8_t* buffer, - std::size_t capacity, - std::size_t& out_size, - MapTileFormat& out_format) const = 0; + virtual MapTileReadResult read(const MapTileRef& ref, + uint8_t* buffer, + std::size_t capacity) const = 0; }; class IMapTileFileSystem @@ -31,10 +46,9 @@ class IMapTileFileSystem virtual bool exists(const char* path) const = 0; virtual bool isDirectory(const char* path) const = 0; - virtual bool readFile(const char* path, - uint8_t* buffer, - std::size_t capacity, - std::size_t& out_size) const = 0; + virtual MapTileReadResult readFile(const char* path, + uint8_t* buffer, + std::size_t capacity) const = 0; }; } // namespace map_tiles diff --git a/modules/ui_map_runtime/src/map_tiles/filesystem_map_tile_source.cpp b/modules/ui_map_runtime/src/map_tiles/filesystem_map_tile_source.cpp index ec96c79c..0e50adb4 100644 --- a/modules/ui_map_runtime/src/map_tiles/filesystem_map_tile_source.cpp +++ b/modules/ui_map_runtime/src/map_tiles/filesystem_map_tile_source.cpp @@ -30,23 +30,20 @@ MapTileLookupResult FilesystemMapTileSource::lookup(const MapTileRef& ref) const return result; } -bool FilesystemMapTileSource::read(const MapTileRef& ref, - uint8_t* buffer, - std::size_t capacity, - std::size_t& out_size, - MapTileFormat& out_format) const +MapTileReadResult FilesystemMapTileSource::read( + const MapTileRef& ref, + uint8_t* buffer, + std::size_t capacity) const { - out_size = 0; - out_format = mapTileFormatForLayer(ref.layer); - char path[160]{}; if (!resolver_.resolvePath(ref, path, sizeof(path))) { - out_format = MapTileFormat::Unknown; - return false; + return {MapTileReadStatus::Invalid, 0, -1, MapTileFormat::Unknown}; } - return file_system_.readFile(path, buffer, capacity, out_size); + MapTileReadResult result = file_system_.readFile(path, buffer, capacity); + result.format = mapTileFormatForLayer(ref.layer); + return result; } bool FilesystemMapTileSource::resolvePath(const MapTileRef& ref, diff --git a/modules/ui_map_runtime/src/map_tiles/map_tile_async_runtime.cpp b/modules/ui_map_runtime/src/map_tiles/map_tile_async_runtime.cpp index 3e60eec9..1201213b 100644 --- a/modules/ui_map_runtime/src/map_tiles/map_tile_async_runtime.cpp +++ b/modules/ui_map_runtime/src/map_tiles/map_tile_async_runtime.cpp @@ -26,9 +26,8 @@ uint32_t dedupeKeyForTile(const MapTileRef& ref) } // namespace -MapTileAsyncRuntime::MapTileAsyncRuntime(IMapTileCommandSink& commands, - sys::runtime::RuntimePolicyStrategy* policy) - : commands_(commands), policy_(policy ? policy : &default_policy_) +MapTileAsyncRuntime::MapTileAsyncRuntime(IMapTileCommandSink& commands) + : commands_(commands) { } @@ -103,7 +102,7 @@ bool MapTileAsyncRuntime::handleEvent(const MapTileAsyncEvent& event, MapTileRen case MapTileAsyncEventKind::Cancelled: ref.state = MapTileRenderState::Cancelled; break; - case MapTileAsyncEventKind::ResourceBusy: + case MapTileAsyncEventKind::RetryLater: ref.state = MapTileRenderState::Loading; break; case MapTileAsyncEventKind::Failed: @@ -126,7 +125,7 @@ sys::runtime::RuntimeCommand MapTileAsyncRuntime::commandFromIntent( sys::runtime::RuntimeCommand command{}; command.command_id = next_command_id_++; command.kind = intent.kind; - command.priority = policy_->selectPriority(intent); + command.priority = intent.priority_hint; command.cancel_policy = intent.cancel_policy; command.created_at_ms = intent.submitted_at_ms; command.deadline_ms = intent.deadline_ms; @@ -137,15 +136,11 @@ sys::runtime::RuntimeCommand MapTileAsyncRuntime::commandFromIntent( } MapTileWorker::MapTileWorker(IMapTileWorkerBackend& backend, - sys::runtime::IBusArbiter& bus, IMapTileEventSink& events, uint8_t* scratch, - std::size_t scratch_size, - sys::runtime::RuntimePolicyStrategy* policy) + std::size_t scratch_size) : backend_(backend), - bus_(bus), events_(events), - policy_(policy ? policy : &default_policy_), scratch_(scratch), scratch_size_(scratch_size) { @@ -153,31 +148,6 @@ MapTileWorker::MapTileWorker(IMapTileWorkerBackend& backend, bool MapTileWorker::execute(const LoadTileCommand& command, uint32_t now_ms) { - sys::runtime::BusAcquireRequest request{}; - request.resource = command.runtime.origin; - request.policy = policy_->selectBusPolicy(command.runtime); - request.command_id = command.runtime.command_id; - request.deadline_ms = command.runtime.deadline_ms; - request.owner_label = "map_tile_sd"; - - const auto acquired = bus_.acquire(request); - if (acquired.status != sys::runtime::BusAcquireStatus::Acquired) - { - MapTileAsyncEvent event{}; - event.kind = MapTileAsyncEventKind::ResourceBusy; - event.command_id = command.runtime.command_id; - event.generation = command.runtime.generation; - event.tile = command.tile; - event.error = static_cast(acquired.status); - (void)events_.publish(event); - return false; - } - // The bus grant is an admission check only. The backend owns its - // transaction boundaries and must reacquire the shared bus for each - // filesystem or hardware-safe operation. Holding the bus across tile - // reads would allow a large SD read to starve display frames. - bus_.release(acquired.token); - MapTileAsyncEvent event{}; event.command_id = command.runtime.command_id; event.generation = command.runtime.generation; @@ -186,9 +156,9 @@ bool MapTileWorker::execute(const LoadTileCommand& command, uint32_t now_ms) const MapTileReadResult read_result = backend_.read(command.tile, scratch_, scratch_size_); const bool ok = read_result.status == MapTileReadStatus::Ready; - if (read_result.status == MapTileReadStatus::ResourceBusy) + if (read_result.status == MapTileReadStatus::RetryLater) { - event.kind = MapTileAsyncEventKind::ResourceBusy; + event.kind = MapTileAsyncEventKind::RetryLater; } else { diff --git a/modules/ui_map_runtime/tests/test_filesystem_map_tile_source.cpp b/modules/ui_map_runtime/tests/test_filesystem_map_tile_source.cpp index 6b3ac6c9..54b59dc5 100644 --- a/modules/ui_map_runtime/tests/test_filesystem_map_tile_source.cpp +++ b/modules/ui_map_runtime/tests/test_filesystem_map_tile_source.cpp @@ -24,21 +24,19 @@ class FakeFileSystem final : public ui::map_tiles::IMapTileFileSystem return contains(dirs, path); } - bool readFile(const char* path, - uint8_t* buffer, - std::size_t capacity, - std::size_t& out_size) const override + ui::map_tiles::MapTileReadResult readFile( + const char* path, + uint8_t* buffer, + std::size_t capacity) const override { - out_size = 0; if (!exists(path) || capacity < 3 || buffer == nullptr) { - return false; + return {ui::map_tiles::MapTileReadStatus::Missing, 0, -1}; } buffer[0] = 1; buffer[1] = 2; buffer[2] = 3; - out_size = 3; - return true; + return {ui::map_tiles::MapTileReadStatus::Ready, 3, 0}; } private: @@ -76,16 +74,17 @@ void test_lookup_and_read() assert(hit.format == ui::map_tiles::MapTileFormat::Png); uint8_t buffer[4]{}; - std::size_t out_size = 0; - ui::map_tiles::MapTileFormat format = ui::map_tiles::MapTileFormat::Unknown; - assert(source.read(ref, buffer, sizeof(buffer), out_size, format)); - assert(out_size == 3); - assert(format == ui::map_tiles::MapTileFormat::Png); + const auto read_result = source.read(ref, buffer, sizeof(buffer)); + assert(read_result.status == ui::map_tiles::MapTileReadStatus::Ready); + assert(read_result.size == 3); + assert(read_result.format == ui::map_tiles::MapTileFormat::Png); assert(buffer[0] == 1 && buffer[1] == 2 && buffer[2] == 3); ref.y = 7; const auto miss = source.lookup(ref); assert(miss.status == ui::map_tiles::MapTileStatus::Missing); + const auto missing_read = source.read(ref, buffer, sizeof(buffer)); + assert(missing_read.status == ui::map_tiles::MapTileReadStatus::Missing); } void test_directories() diff --git a/modules/ui_map_runtime/tests/test_map_tile_async_runtime.cpp b/modules/ui_map_runtime/tests/test_map_tile_async_runtime.cpp index 5d29db4c..b9dbf7f0 100644 --- a/modules/ui_map_runtime/tests/test_map_tile_async_runtime.cpp +++ b/modules/ui_map_runtime/tests/test_map_tile_async_runtime.cpp @@ -57,40 +57,6 @@ class FakeCommandSink final : public ui::map_tiles::IMapTileCommandSink } }; -class FakeBusArbiter final : public sys::runtime::IBusArbiter -{ - public: - sys::runtime::BusAcquireStatus next_status = sys::runtime::BusAcquireStatus::Acquired; - sys::runtime::BusAccessPolicy last_policy = sys::runtime::BusAccessPolicy::BackgroundWorkerBounded; - uint32_t last_resource = 0; - int acquire_count = 0; - int release_count = 0; - - sys::runtime::BusAcquireResult acquire(const sys::runtime::BusAcquireRequest& request) override - { - ++acquire_count; - last_policy = request.policy; - last_resource = request.resource; - sys::runtime::BusAcquireResult result{}; - result.status = next_status; - result.token.valid = next_status == sys::runtime::BusAcquireStatus::Acquired; - result.token.resource = request.resource; - result.token.owner = request.command_id; - return result; - } - - void release(const sys::runtime::BusAccessToken& token) override - { - assert(token.valid); - ++release_count; - } - - sys::runtime::StorageHealthState health() const override - { - return {}; - } -}; - class FakeBackend final : public ui::map_tiles::IMapTileWorkerBackend { public: @@ -98,7 +64,6 @@ class FakeBackend final : public ui::map_tiles::IMapTileWorkerBackend bool read_ok = true; ui::map_tiles::MapTileReadStatus read_status = ui::map_tiles::MapTileReadStatus::Ready; int32_t read_error = -1; - bool bus_access_retained = true; int lookup_count = 0; int read_count = 0; @@ -123,16 +88,14 @@ class FakeBackend final : public ui::map_tiles::IMapTileWorkerBackend result.status = read_status; result.format = ui::map_tiles::MapTileFormat::Png; result.error = read_error; - result.bus_access_retained = bus_access_retained; if (read_status != ui::map_tiles::MapTileReadStatus::Ready) { return result; } if (!available || !read_ok || !buffer || capacity < 3) { - result.status = ui::map_tiles::MapTileReadStatus::Failed; + result.status = ui::map_tiles::MapTileReadStatus::Error; result.error = -1; - result.bus_access_retained = true; return result; } buffer[0] = 1; @@ -161,33 +124,6 @@ class FakeEventSink final : public ui::map_tiles::IMapTileEventSink } }; -class FakePolicy final : public sys::runtime::RuntimePolicyStrategy -{ - public: - sys::runtime::RuntimePriority selectPriority( - const sys::runtime::RuntimeIntent& intent) const override - { - (void)intent; - return sys::runtime::RuntimePriority::Realtime; - } - - sys::runtime::BusAccessPolicy selectBusPolicy( - const sys::runtime::RuntimeCommand& command) const override - { - (void)command; - return sys::runtime::BusAccessPolicy::RecoveryExclusive; - } - - sys::runtime::RuntimeRetryDecision selectRetry( - const sys::runtime::RuntimeCommand& command, - const sys::runtime::PlatformStorageResult& result) const override - { - (void)command; - (void)result; - return {}; - } -}; - void test_generation_cancels_old_commands() { FakeCommandSink sink; @@ -308,39 +244,12 @@ void test_tile_dedupe_key_uses_full_tile_identity() assert(sink.commands[1].runtime.dedupe_key != sink.commands[2].runtime.dedupe_key); } -void test_worker_busy_does_not_read_storage() -{ - FakeBackend backend; - FakeBusArbiter bus; - FakeEventSink events; - uint8_t scratch[8]{}; - ui::map_tiles::MapTileWorker worker(backend, bus, events, scratch, sizeof(scratch)); - - ui::map_tiles::LoadTileCommand command{}; - command.runtime.command_id = 7; - command.runtime.kind = sys::runtime::RuntimeCommandKind::MapTileLoad; - command.runtime.generation = 3; - command.runtime.priority = sys::runtime::RuntimePriority::Interactive; - command.tile = make_tile(30); - - bus.next_status = sys::runtime::BusAcquireStatus::Busy; - assert(!worker.execute(command, 200)); - assert(bus.acquire_count == 1); - assert(bus.release_count == 0); - assert(backend.lookup_count == 0); - assert(backend.read_count == 0); - assert(events.count == 1); - assert(events.events[0].kind == ui::map_tiles::MapTileAsyncEventKind::ResourceBusy); - assert(bus.last_policy == sys::runtime::BusAccessPolicy::DisplayFrameCritical); -} - void test_worker_success_publishes_ready() { FakeBackend backend; - FakeBusArbiter bus; FakeEventSink events; uint8_t scratch[8]{}; - ui::map_tiles::MapTileWorker worker(backend, bus, events, scratch, sizeof(scratch)); + ui::map_tiles::MapTileWorker worker(backend, events, scratch, sizeof(scratch)); ui::map_tiles::LoadTileCommand command{}; command.runtime.command_id = 9; @@ -348,12 +257,10 @@ void test_worker_success_publishes_ready() command.runtime.generation = 4; command.runtime.priority = sys::runtime::RuntimePriority::Normal; command.runtime.origin = 42; + command.runtime.deadline_ms = 333; command.tile = make_tile(40); assert(worker.execute(command, 300)); - assert(bus.acquire_count == 1); - assert(bus.release_count == 1); - assert(bus.last_resource == 42); assert(backend.lookup_count == 0); assert(backend.read_count == 1); assert(events.count == 1); @@ -368,10 +275,9 @@ void test_worker_missing_reads_once_without_lookup_probe() { FakeBackend backend; backend.available = false; - FakeBusArbiter bus; FakeEventSink events; uint8_t scratch[8]{}; - ui::map_tiles::MapTileWorker worker(backend, bus, events, scratch, sizeof(scratch)); + ui::map_tiles::MapTileWorker worker(backend, events, scratch, sizeof(scratch)); ui::map_tiles::LoadTileCommand command{}; command.runtime.command_id = 11; @@ -381,8 +287,6 @@ void test_worker_missing_reads_once_without_lookup_probe() command.tile = make_tile(41); assert(!worker.execute(command, 310)); - assert(bus.acquire_count == 1); - assert(bus.release_count == 1); assert(backend.lookup_count == 0); assert(backend.read_count == 1); assert(events.count == 1); @@ -391,16 +295,14 @@ void test_worker_missing_reads_once_without_lookup_probe() assert(events.events[0].payload_size == 0); } -void test_worker_resource_busy_read_publishes_resource_busy() +void test_worker_retry_later_read_publishes_retry_later() { FakeBackend backend; - backend.read_status = ui::map_tiles::MapTileReadStatus::ResourceBusy; - backend.read_error = - static_cast(sys::runtime::BusAcquireStatus::TimedOut); - FakeBusArbiter bus; + backend.read_status = ui::map_tiles::MapTileReadStatus::RetryLater; + backend.read_error = -2; FakeEventSink events; uint8_t scratch[8]{}; - ui::map_tiles::MapTileWorker worker(backend, bus, events, scratch, sizeof(scratch)); + ui::map_tiles::MapTileWorker worker(backend, events, scratch, sizeof(scratch)); ui::map_tiles::LoadTileCommand command{}; command.runtime.command_id = 12; @@ -410,66 +312,14 @@ void test_worker_resource_busy_read_publishes_resource_busy() command.tile = make_tile(42); assert(!worker.execute(command, 320)); - assert(bus.acquire_count == 1); - assert(bus.release_count == 1); assert(backend.read_count == 1); assert(events.count == 1); - assert(events.events[0].kind == ui::map_tiles::MapTileAsyncEventKind::ResourceBusy); - assert(events.events[0].error == - static_cast(sys::runtime::BusAcquireStatus::TimedOut)); + assert(events.events[0].kind == ui::map_tiles::MapTileAsyncEventKind::RetryLater); + assert(events.events[0].error == -2); assert(events.events[0].payload.data == nullptr); assert(events.events[0].payload_size == 0); } -void test_worker_released_bus_read_skips_release() -{ - FakeBackend backend; - backend.read_status = ui::map_tiles::MapTileReadStatus::ResourceBusy; - backend.bus_access_retained = false; - backend.read_error = - static_cast(sys::runtime::BusAcquireStatus::TimedOut); - FakeBusArbiter bus; - FakeEventSink events; - uint8_t scratch[8]{}; - ui::map_tiles::MapTileWorker worker(backend, bus, events, scratch, sizeof(scratch)); - - ui::map_tiles::LoadTileCommand command{}; - command.runtime.command_id = 13; - command.runtime.kind = sys::runtime::RuntimeCommandKind::MapTileLoad; - command.runtime.generation = 4; - command.runtime.priority = sys::runtime::RuntimePriority::Normal; - command.tile = make_tile(43); - - assert(!worker.execute(command, 330)); - assert(bus.acquire_count == 1); - assert(bus.release_count == 0); - assert(backend.read_count == 1); - assert(events.count == 1); - assert(events.events[0].kind == ui::map_tiles::MapTileAsyncEventKind::ResourceBusy); -} - -void test_runtime_and_worker_use_policy_strategy() -{ - FakeCommandSink sink; - FakePolicy policy; - ui::map_tiles::MapTileAsyncRuntime runtime(sink, &policy); - - ui::map_tiles::MapViewportPlan plan{}; - plan.generation = 5; - plan.tile_count = 1; - plan.tiles[0] = make_tile(50); - assert(runtime.requestVisibleTiles(plan, 500) == 1); - assert(sink.commands[0].runtime.priority == sys::runtime::RuntimePriority::Realtime); - - FakeBackend backend; - FakeBusArbiter bus; - FakeEventSink events; - uint8_t scratch[8]{}; - ui::map_tiles::MapTileWorker worker(backend, bus, events, scratch, sizeof(scratch), &policy); - assert(worker.execute(sink.commands[0], 510)); - assert(bus.last_policy == sys::runtime::BusAccessPolicy::RecoveryExclusive); -} - } // namespace int main() @@ -479,11 +329,8 @@ int main() test_map_tile_runtime_does_not_transition_state_for_stale_event(); test_interactive_plan_uses_interactive_priority(); test_tile_dedupe_key_uses_full_tile_identity(); - test_worker_busy_does_not_read_storage(); test_worker_success_publishes_ready(); test_worker_missing_reads_once_without_lookup_probe(); - test_worker_resource_busy_read_publishes_resource_busy(); - test_worker_released_bus_read_skips_release(); - test_runtime_and_worker_use_policy_strategy(); + test_worker_retry_later_read_publishes_retry_later(); return 0; } diff --git a/modules/ui_shared/src/ui/i18n/resource_pack_registry.cpp b/modules/ui_shared/src/ui/i18n/resource_pack_registry.cpp index 91e416f2..f749e271 100644 --- a/modules/ui_shared/src/ui/i18n/resource_pack_registry.cpp +++ b/modules/ui_shared/src/ui/i18n/resource_pack_registry.cpp @@ -1529,6 +1529,7 @@ bool load_font_pack(FontPackRecord& pack) static_cast(pack.estimated_ram_bytes)); #endif ScopedFontLoadOverlay overlay(pack); + bool font_fs_busy = false; { ScopedExternalFontLoadFs fs_scope; if (!fs_scope.active()) @@ -1557,23 +1558,27 @@ bool load_font_pack(FontPackRecord& pack) return false; } pack.owned_font = lv_binfont_create(pack.source_path.c_str()); +#if UI_I18N_HAVE_EXTERNAL_FONT_LOAD_FS_SCOPE + font_fs_busy = lv_external_font_load_fs_was_busy(); +#endif } if (pack.owned_font == nullptr) { - record_font_load_failure(pack, - sys::millis_now(), - FontLoadFailureKind::Permanent); + const FontLoadFailureKind failure_kind = + font_fs_busy ? FontLoadFailureKind::TransientBusBusy + : FontLoadFailureKind::Permanent; + record_font_load_failure(pack, sys::millis_now(), failure_kind); std::printf("%s font load failed id=%s source=%s reason=%s\n", kLogTag, pack.id.c_str(), pack.source_path.c_str(), - font_load_failure_kind_name(FontLoadFailureKind::Permanent)); + font_load_failure_kind_name(failure_kind)); #if UI_I18N_ROUTE_LOG_ENABLE std::printf("%s[route] route=pack_load state=failed id=%s source=%s reason=%s failures=%u\n", kLogTag, pack.id.c_str(), pack.source_path.c_str(), - font_load_failure_kind_name(FontLoadFailureKind::Permanent), + font_load_failure_kind_name(failure_kind), static_cast(pack.load_failure_count)); #endif return false; diff --git a/modules/ui_shared/src/ui/screens/chat/chat_message_list_layout.cpp b/modules/ui_shared/src/ui/screens/chat/chat_message_list_layout.cpp index cebbd891..0d5c6da8 100644 --- a/modules/ui_shared/src/ui/screens/chat/chat_message_list_layout.cpp +++ b/modules/ui_shared/src/ui/screens/chat/chat_message_list_layout.cpp @@ -5,7 +5,6 @@ */ #include "ui/screens/chat/chat_message_list_layout.h" -#include "chat/infra/mesh_protocol_utils.h" #include "ui/assets/fonts/font_utils.h" #include "ui/components/air_status_footer.h" #include "ui/components/info_card.h" @@ -172,8 +171,7 @@ std::string compact_list_name(const std::string& name) std::string build_list_title(const chat::ConversationMeta& conv) { - return "[" + std::string(chat::infra::meshProtocolShortName(conv.id.protocol)) + - "] " + compact_list_name(conv.name); + return compact_list_name(conv.name); } void style_filter_label(lv_obj_t* label) diff --git a/modules/ui_shared/src/ui/screens/contacts/contacts_page_components.cpp b/modules/ui_shared/src/ui/screens/contacts/contacts_page_components.cpp index 0c01d8c5..6b7b108c 100644 --- a/modules/ui_shared/src/ui/screens/contacts/contacts_page_components.cpp +++ b/modules/ui_shared/src/ui/screens/contacts/contacts_page_components.cpp @@ -660,13 +660,12 @@ static std::string format_broadcast_target_label(const BroadcastTargetSpec& spec { if (spec.protocol == chat::MeshProtocol::Meshtastic) { - return std::string("[MT] ") + - chat::meshtastic::channelName(app::appFacade().getConfig().meshtastic_config, + return chat::meshtastic::channelName(app::appFacade().getConfig().meshtastic_config, spec.channel); } if (chat::infra::isReticulumMeshProtocol(spec.protocol)) { - return std::string("[RT] ") + ::ui::i18n::tr("Primary Group"); + return ::ui::i18n::tr("Primary Group"); } const chat::MeshConfig& cfg = app::appFacade().getConfig().meshcore_config; const chat::MeshCoreChannelConfig& channel = @@ -679,7 +678,7 @@ static std::string format_broadcast_target_label(const BroadcastTargetSpec& spec static_cast(spec.channel_index)); name = fallback; } - return std::string("[MC] ") + name; + return name; } static std::string format_broadcast_target_status(const BroadcastTargetSpec& spec) diff --git a/modules/ui_shared/src/ui/screens/contacts/contacts_page_layout.cpp b/modules/ui_shared/src/ui/screens/contacts/contacts_page_layout.cpp index 07ad8ca3..e9a49c80 100644 --- a/modules/ui_shared/src/ui/screens/contacts/contacts_page_layout.cpp +++ b/modules/ui_shared/src/ui/screens/contacts/contacts_page_layout.cpp @@ -8,7 +8,6 @@ #include "app/app_config.h" #include "app/app_facade_access.h" #include "chat/domain/chat_types.h" -#include "chat/infra/mesh_protocol_utils.h" #include "ui/assets/fonts/font_utils.h" #include "ui/components/air_status_footer.h" @@ -51,11 +50,6 @@ bool is_dense_profile() return ::ui::page_profile::current().filter_button_height <= 24; } -lv_coord_t dense_protocol_width() -{ - return 28; -} - lv_coord_t dense_status_width() { return 52; @@ -118,24 +112,6 @@ void apply_single_line(lv_obj_t* label) lv_obj_set_style_text_font(label, ::ui::page_profile::resolve_body_font(), 0); } -const char* node_protocol_short_label(chat::contacts::NodeProtocolType protocol) -{ - return chat::infra::nodeProtocolShortName(protocol); -} - -bool should_prefix_node_protocol(ContactsMode mode, - chat::contacts::NodeProtocolType protocol) -{ - if (protocol == chat::contacts::NodeProtocolType::Unknown) - { - return false; - } - return mode == ContactsMode::Contacts || - mode == ContactsMode::Nearby || - mode == ContactsMode::Groups || - mode == ContactsMode::Ignored; -} - lv_obj_t* create_root(lv_obj_t* parent) { const auto& profile = ::ui::page_profile::current(); @@ -276,7 +252,7 @@ void ensure_list_subcontainers() lv_obj_t* create_list_item(lv_obj_t* parent, const chat::contacts::PeerDirectoryItem& node, - ContactsMode mode, + ContactsMode, const char* status_text) { const auto& profile = ::ui::page_profile::current(); @@ -295,8 +271,6 @@ lv_obj_t* create_list_item(lv_obj_t* parent, style::apply_list_item(item); std::string display_name = preferred_node_display_name(node); - const bool show_protocol = should_prefix_node_protocol(mode, node.protocol); - const char* proto = show_protocol ? node_protocol_short_label(node.protocol) : ""; if (::ui::components::info_card::use_tdeck_layout()) { @@ -320,17 +294,6 @@ lv_obj_t* create_list_item(lv_obj_t* parent, lv_obj_set_style_pad_right(item, 4, LV_PART_MAIN); lv_obj_set_style_pad_column(item, 3, LV_PART_MAIN); - if (proto[0] != '\0') - { - lv_obj_t* proto_label = lv_label_create(item); - std::string proto_text = "[" + std::string(proto) + "]"; - ::ui::i18n::set_label_text_raw(proto_label, proto_text.c_str()); - style::apply_label_muted(proto_label); - lv_obj_set_width(proto_label, dense_protocol_width()); - lv_obj_set_style_text_align(proto_label, LV_TEXT_ALIGN_LEFT, 0); - lv_obj_set_style_text_font(proto_label, ::ui::page_profile::resolve_caption_font(), 0); - } - lv_obj_t* name_label = lv_label_create(item); ::ui::i18n::set_content_label_text_raw(name_label, display_name.c_str()); lv_obj_set_width(name_label, 0); @@ -347,11 +310,6 @@ lv_obj_t* create_list_item(lv_obj_t* parent, } else { - if (proto[0] != '\0') - { - display_name = "[" + std::string(proto) + "] " + display_name; - } - lv_obj_t* name_label = lv_label_create(item); ::ui::i18n::set_content_label_text_raw(name_label, display_name.c_str()); lv_obj_align(name_label, LV_ALIGN_LEFT_MID, 10, 0); diff --git a/modules/ui_shared/src/ui/screens/settings/settings_page_components.cpp b/modules/ui_shared/src/ui/screens/settings/settings_page_components.cpp index 5cccf583..cddd1c5a 100644 --- a/modules/ui_shared/src/ui/screens/settings/settings_page_components.cpp +++ b/modules/ui_shared/src/ui/screens/settings/settings_page_components.cpp @@ -466,23 +466,38 @@ static void rebuild_wifi_scan_options(size_t result_count) kWifiNetworkOptionCount = limit; } +static void refresh_wifi_status_from_runtime() +{ + const wifi_runtime::Status status = wifi_runtime::status(); + if (status.connected && status.ip[0] != '\0') + { + copy_bounded(g_settings.wifi_status, sizeof(g_settings.wifi_status), status.ip); + return; + } + + if (!status.supported && status.message[0] == '\0') + { + copy_bounded(g_settings.wifi_status, + sizeof(g_settings.wifi_status), + "Wi-Fi unsupported"); + } + else + { + copy_bounded(g_settings.wifi_status, + sizeof(g_settings.wifi_status), + status.message); + } +} + static void refresh_wifi_state_from_runtime() { wifi_runtime::Config config{}; (void)wifi_runtime::load_config(config); - const wifi_runtime::Status status = wifi_runtime::status(); g_settings.wifi_enabled = config.enabled; copy_bounded(g_settings.wifi_ssid, sizeof(g_settings.wifi_ssid), config.ssid); copy_bounded(g_settings.wifi_password, sizeof(g_settings.wifi_password), config.password); - if (!status.supported && status.message[0] == '\0') - { - copy_bounded(g_settings.wifi_status, sizeof(g_settings.wifi_status), "Wi-Fi unsupported"); - } - else - { - copy_bounded(g_settings.wifi_status, sizeof(g_settings.wifi_status), status.message); - } + refresh_wifi_status_from_runtime(); } static void firmware_status_summary(const firmware_update_runtime::Status& status, @@ -859,6 +874,7 @@ static void sync_firmware_update_ui(bool notify_completion) static void firmware_update_timer_cb(lv_timer_t* /*timer*/) { + refresh_wifi_status_from_runtime(); sync_firmware_update_ui(true); } diff --git a/modules/ui_shared/src/ui/startup_shell.cpp b/modules/ui_shared/src/ui/startup_shell.cpp index 6550f9d3..c600b68a 100644 --- a/modules/ui_shared/src/ui/startup_shell.cpp +++ b/modules/ui_shared/src/ui/startup_shell.cpp @@ -5,9 +5,6 @@ #include #include "lvgl.h" -#if defined(ARDUINO_ARCH_ESP32) -#include "platform/esp/common/shared_spi_coordinator.h" -#endif #include "platform/ui/screen_runtime.h" #include "platform/ui/time_runtime.h" #include "sys/clock.h" @@ -71,18 +68,11 @@ bool resolve_display_time(struct tm* out_tm) return true; } -// This counter only proves that the display transaction path returned success. -// It cannot prove that the panel's controller rendered pixels; there is no -// readback/TE acknowledgement in this SPI write-only path. -bool s_boot_display_transaction_completed = false; +bool s_boot_presentation_completed = false; void present_boot_overlay_now() { #if TRAIL_MATE_BOOT_UI_SYNC_PRESENT -#if defined(ARDUINO_ARCH_ESP32) - const uint32_t completed_before = - ::platform::esp::common::shared_spi_coordinator().displayFrameCompletions(); -#endif for (uint8_t frame = 0; frame < kBootPresentFrameCount; ++frame) { if (lv_obj_t* top = lv_layer_top()) @@ -96,19 +86,13 @@ void present_boot_overlay_now() sys::sleep_ms(kBootPresentFrameDelayMs); } } -#if defined(ARDUINO_ARCH_ESP32) - s_boot_display_transaction_completed = - ::platform::esp::common::shared_spi_coordinator().displayFrameCompletions() > - completed_before; -#else - s_boot_display_transaction_completed = true; -#endif + s_boot_presentation_completed = true; #else if (lv_obj_t* top = lv_layer_top()) { lv_obj_invalidate(top); } - s_boot_display_transaction_completed = false; + s_boot_presentation_completed = false; #endif } @@ -262,7 +246,7 @@ void initializeShell(const Hooks& hooks) void finalizeStartup(bool waking_from_sleep) { - if (!waking_from_sleep && !s_boot_display_transaction_completed) + if (!waking_from_sleep && !s_boot_presentation_completed) { std::printf("[BOOT][UI] first_frame_retry reason=no_display_transaction\n"); std::fflush(stdout); @@ -270,8 +254,8 @@ void finalizeStartup(bool waking_from_sleep) } std::printf("[BOOT][UI] ready waking=%d\n", waking_from_sleep ? 1 : 0); std::fflush(stdout); - std::printf("[BOOT][UI] display_transaction_completed=%d\n", - s_boot_display_transaction_completed ? 1 : 0); + std::printf("[BOOT][UI] presentation_completed=%d\n", + s_boot_presentation_completed ? 1 : 0); std::fflush(stdout); if (waking_from_sleep) { diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/meshtastic/mt_adapter.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/meshtastic/mt_adapter.h index d5ef1f9e..fc1694dd 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/meshtastic/mt_adapter.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/meshtastic/mt_adapter.h @@ -16,6 +16,7 @@ #include "chat/ports/i_mesh_adapter.h" #include "chat/ports/i_mesh_peer_directory.h" #include "chat/runtime/meshtastic_runtime.h" +#include "chat/runtime/meshtastic_self_announcement_core.h" #include "freertos/FreeRTOS.h" #include "freertos/queue.h" #include "mesh/domain/peer_identity.h" @@ -298,6 +299,7 @@ class MtAdapter : public chat::IMeshAdapter TxScratchBuffers tx_scratch_; RxScratchBuffers rx_scratch_; meshtastic_MeshPacket protocol_effect_packet_scratch_ = meshtastic_MeshPacket_init_zero; + chat::runtime::MeshtasticAnnouncementPacket node_info_packet_scratch_{}; PendingAckTable pending_ack_states_; std::unique_ptr<::platform::esp::arduino_common::mesh::EspMeshtasticAdapterBridge> core_bridge_; diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/reticulum/reticulum_interfaces.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/reticulum/reticulum_interfaces.h index 7987be97..a18a3bd9 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/reticulum/reticulum_interfaces.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/reticulum/reticulum_interfaces.h @@ -218,6 +218,7 @@ class AutoReticulumInterface static constexpr uint32_t kAnnounceIntervalMs = 1600; static constexpr uint32_t kPeerTimeoutMs = 22000; static constexpr uint32_t kReverseAnnounceIntervalMs = 5200; + static constexpr uint32_t kWifiConnectRetryIntervalMs = 10000; bool enabled_ = false; bool transport_enabled_ = true; @@ -235,6 +236,8 @@ class AutoReticulumInterface uint8_t discovery_token_[reticulum::kFullHashSize] = {}; uint32_t last_announce_ms_ = 0; uint32_t last_socket_attempt_ms_ = 0; + uint32_t wifi_connect_retry_not_before_ms_ = 0; + bool wifi_connect_suspended_ = false; std::array peers_{}; RxPacket rx_scratch_{}; sys::RingBuffer rx_queue_; diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/store/fixed_slot_journal.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/store/fixed_slot_journal.h index 6274f7ed..cefc8660 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/store/fixed_slot_journal.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/store/fixed_slot_journal.h @@ -26,6 +26,14 @@ class FixedSlotJournalEngine final uint32_t slot_count = 0; }; + enum class ReadStatus : uint8_t + { + Ok = 0, + InvalidArgument, + OutOfRange, + Unavailable, + }; + Inspection inspect(const char* path, MeshProtocol protocol, JournalKind kind, @@ -49,6 +57,14 @@ class FixedSlotJournalEngine final uint32_t slot_index, void* out_slot) const; + ReadStatus readStatus(const char* path, + MeshProtocol protocol, + JournalKind kind, + std::size_t slot_size, + const Inspection& inspection, + uint32_t slot_index, + void* out_slot) const; + static constexpr std::size_t headerSize(); private: diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/mesh/esp_meshtastic_adapter_bridge.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/mesh/esp_meshtastic_adapter_bridge.h index 95a987cb..363bf11b 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/mesh/esp_meshtastic_adapter_bridge.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/mesh/esp_meshtastic_adapter_bridge.h @@ -116,6 +116,7 @@ class EspMeshtasticAdapterBridge final ::mesh::ReceivePacketService receive_; ::mesh::DirectMessageService direct_; ::mesh::MeshSession session_; + ::mesh::RadioRxPacket rx_packet_scratch_{}; }; } // namespace platform::esp::arduino_common::mesh diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/storage/persistence_bus_gate.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/storage/persistence_bus_gate.h deleted file mode 100644 index c66a76ba..00000000 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/storage/persistence_bus_gate.h +++ /dev/null @@ -1,59 +0,0 @@ -#pragma once - -#include "sys/bus_access_scope.h" -#include "sys/clock.h" -#include "sys/runtime_async.h" - -#include - -namespace platform::esp::arduino_common::storage -{ - -class PersistenceBusGate final -{ - public: - PersistenceBusGate(sys::runtime::IBusArbiter& arbiter, - sys::runtime::BusAccessPolicy policy, - uint32_t wait_ms, - uint32_t resource, - uint32_t command_id, - uint32_t origin, - const char* owner_label = "persistence") - : scope_(arbiter, - makeRequest(policy, wait_ms, resource, command_id, origin, owner_label)) - { - } - - bool locked() const - { - return scope_.acquired(); - } - - sys::runtime::BusAcquireStatus status() const - { - return scope_.status(); - } - - private: - static sys::runtime::BusAcquireRequest makeRequest( - sys::runtime::BusAccessPolicy policy, - uint32_t wait_ms, - uint32_t resource, - uint32_t command_id, - uint32_t origin, - const char* owner_label) - { - sys::runtime::BusAcquireRequest request{}; - request.resource = resource; - request.policy = policy; - request.command_id = command_id; - request.origin = origin; - request.deadline_ms = sys::millis_now() + wait_ms; - request.owner_label = owner_label; - return request; - } - - sys::runtime::ScopedBusAccessToken scope_; -}; - -} // namespace platform::esp::arduino_common::storage diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/storage/sd_card_runtime.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/storage/sd_card_runtime.h index b7d9aa17..a3ece666 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/storage/sd_card_runtime.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/storage/sd_card_runtime.h @@ -50,6 +50,30 @@ bool sd_rmdir(const char* path); bool sd_remove(const char* path); bool sd_rename(const char* old_path, const char* new_path); +enum class SdFileReadStatus : uint8_t +{ + Ready, + Missing, + Busy, + Unavailable, + IoError, + Invalid, +}; + +struct SdFileReadResult +{ + SdFileReadStatus status = SdFileReadStatus::IoError; + std::size_t bytes_read = 0; + uint64_t file_size = 0; + int32_t error = -1; +}; + +// Reads a file through bounded device-owned transactions. Callers receive a +// semantic storage result and do not provide SPI policy or lock metadata. +SdFileReadResult sd_read_file(const char* path, + uint8_t* buffer, + std::size_t capacity); + class SdRuntimeFile { public: diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/storage/sd_spi_bus_hooks.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/storage/sd_spi_bus_hooks.h new file mode 100644 index 00000000..54cd7ff8 --- /dev/null +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/storage/sd_spi_bus_hooks.h @@ -0,0 +1,13 @@ +#pragma once + +#include "sys/shared_spi_access.h" + +namespace platform::esp::arduino_common::storage +{ + +// Used only by the board-enabled SdFat SPI driver. Business and filesystem +// callers must not acquire the physical bus through this interface. +bool sd_spi_bus_acquire(sys::runtime::BusAccessToken& token); +void sd_spi_bus_release(const sys::runtime::BusAccessToken& token); + +} // namespace platform::esp::arduino_common::storage diff --git a/platform/esp/arduino_common/include/ui/LV_Helper.h b/platform/esp/arduino_common/include/ui/LV_Helper.h index 28838db8..3fac7f8c 100644 --- a/platform/esp/arduino_common/include/ui/LV_Helper.h +++ b/platform/esp/arduino_common/include/ui/LV_Helper.h @@ -17,3 +17,4 @@ lv_indev_t* lv_get_encoder_indev(); bool lv_begin_external_font_load_fs_scope(); void lv_end_external_font_load_fs_scope(); +bool lv_external_font_load_fs_was_busy(); diff --git a/platform/esp/arduino_common/src/LV_Helper_v9.cpp b/platform/esp/arduino_common/src/LV_Helper_v9.cpp index 4c6ece1c..803de85c 100644 --- a/platform/esp/arduino_common/src/LV_Helper_v9.cpp +++ b/platform/esp/arduino_common/src/LV_Helper_v9.cpp @@ -63,11 +63,6 @@ static_assert(kTDeckDmaDrawBufferLines >= 10, "TDeck draw buffers below 10 lines // bus must surface as LV_FS_RES_BUSY/failed open, not as a blocked UI frame. constexpr TickType_t kLvglSdFsWait = pdMS_TO_TICKS(2); constexpr TickType_t kLvglSdFsCloseWait = pdMS_TO_TICKS(10); -constexpr uint32_t kLvglExternalFontBusAcquireMs = 6000; -constexpr uint32_t kLvglExternalFontSlowHoldMs = 750; -constexpr uint32_t kLvglExternalFontLogIntervalMs = 5000; -constexpr uint32_t kLvglExternalFontBusResource = 0x4C56464EU; // "LVFN" -constexpr uint32_t kLvglExternalFontCommandId = 0x4C564653U; // "LVFS" constexpr const char* kLvglExternalFontOwner = "lvgl_font_sd"; lv_fs_drv_t s_flash_fs_drv; @@ -75,23 +70,20 @@ lv_fs_drv_t s_sd_fs_drv; bool s_flash_fs_ready = false; bool s_sd_fs_ready = false; std::atomic s_external_font_load_fs_depth{0}; -sys::runtime::BusAccessToken s_external_font_bus_token{}; +std::atomic s_external_font_fs_busy{false}; TaskHandle_t s_external_font_owner_task = nullptr; -uint32_t s_external_font_acquired_ms = 0; -uint32_t s_external_font_last_busy_log_ms = 0; bool external_font_load_fs_scope_active() { return s_external_font_load_fs_depth.load(std::memory_order_relaxed) > 0 && - s_external_font_bus_token.valid; + s_external_font_owner_task == xTaskGetCurrentTaskHandle(); } TickType_t current_sd_fs_wait(TickType_t normal_wait) { - // A font load scope already owns the shared bus. Same-task callback reentry - // succeeds immediately; cross-task access should fail fast instead of - // waiting behind the UI-visible font transaction. - return external_font_load_fs_scope_active() ? 0 : normal_wait; + // Font loading is a sequence of short filesystem transactions. Do not + // carry one SPI token across the whole lv_binfont_create() call. + return normal_wait; } const char* current_sd_fs_owner() @@ -99,6 +91,14 @@ const char* current_sd_fs_owner() return external_font_load_fs_scope_active() ? kLvglExternalFontOwner : nullptr; } +void mark_external_font_fs_busy() +{ + if (external_font_load_fs_scope_active()) + { + s_external_font_fs_busy.store(true, std::memory_order_release); + } +} + inline int flash_fs_fd_from_ptr(void* file_p) { return static_cast(reinterpret_cast(file_p) - 1U); @@ -201,6 +201,7 @@ void* sd_fs_open(lv_fs_drv_t* drv, const char* path, lv_fs_mode_t mode) current_sd_fs_wait(kLvglSdFsWait), current_sd_fs_owner()); if (!spi_guard.locked()) { + mark_external_font_fs_busy(); return nullptr; } @@ -239,6 +240,11 @@ lv_fs_res_t sd_fs_close(lv_fs_drv_t* drv, void* file_p) current_sd_fs_wait(kLvglSdFsCloseWait), current_sd_fs_owner()); file->close(); delete file; + if (!spi_guard.locked()) + { + mark_external_font_fs_busy(); + return LV_FS_RES_BUSY; + } return LV_FS_RES_OK; } @@ -254,12 +260,14 @@ lv_fs_res_t sd_fs_read(lv_fs_drv_t* drv, void* file_p, void* buf, uint32_t btr, current_sd_fs_wait(kLvglSdFsWait), current_sd_fs_owner()); if (!spi_guard.locked()) { + mark_external_font_fs_busy(); return LV_FS_RES_BUSY; } const int result = file->read(buf, btr); if (result < 0) { + mark_external_font_fs_busy(); return sd_fs_error_to_res(); } if (br != nullptr) @@ -285,6 +293,7 @@ lv_fs_res_t sd_fs_write(lv_fs_drv_t* drv, current_sd_fs_wait(kLvglSdFsWait), current_sd_fs_owner()); if (!spi_guard.locked()) { + mark_external_font_fs_busy(); return LV_FS_RES_BUSY; } @@ -308,6 +317,7 @@ lv_fs_res_t sd_fs_seek(lv_fs_drv_t* drv, void* file_p, uint32_t pos, lv_fs_whenc current_sd_fs_wait(kLvglSdFsWait), current_sd_fs_owner()); if (!spi_guard.locked()) { + mark_external_font_fs_busy(); return LV_FS_RES_BUSY; } @@ -326,7 +336,12 @@ lv_fs_res_t sd_fs_seek(lv_fs_drv_t* drv, void* file_p, uint32_t pos, lv_fs_whenc break; } - return file->seek(target) ? LV_FS_RES_OK : sd_fs_error_to_res(); + if (!file->seek(target)) + { + mark_external_font_fs_busy(); + return sd_fs_error_to_res(); + } + return LV_FS_RES_OK; } lv_fs_res_t sd_fs_tell(lv_fs_drv_t* drv, void* file_p, uint32_t* pos_p) @@ -341,6 +356,7 @@ lv_fs_res_t sd_fs_tell(lv_fs_drv_t* drv, void* file_p, uint32_t* pos_p) current_sd_fs_wait(kLvglSdFsWait), current_sd_fs_owner()); if (!spi_guard.locked()) { + mark_external_font_fs_busy(); return LV_FS_RES_BUSY; } *pos_p = static_cast(file->position()); @@ -1068,7 +1084,7 @@ static void keypad_read(lv_indev_t* drv, lv_indev_data_t* data) } } -#if defined(ARDUINO_T_DECK) || defined(ARDUINO_T_DECK_PRO) +#if defined(ARDUINO_T_LORA_PAGER) || defined(ARDUINO_T_DECK) || defined(ARDUINO_T_DECK_PRO) if (!from_nav && state == KEYBOARD_PRESSED && c == ' ' && ui_get_active_app() == nullptr) { ::platform::ui::screen::record_activity(); @@ -1358,94 +1374,37 @@ lv_indev_t* lv_get_encoder_indev() return indev_encoder; } -const char* bus_acquire_status_name(sys::runtime::BusAcquireStatus status) -{ - switch (status) - { - case sys::runtime::BusAcquireStatus::Acquired: - return "acquired"; - case sys::runtime::BusAcquireStatus::Busy: - return "busy"; - case sys::runtime::BusAcquireStatus::TimedOut: - return "timeout"; - case sys::runtime::BusAcquireStatus::Unavailable: - return "unavailable"; - default: - return "unknown"; - } -} - bool lv_begin_external_font_load_fs_scope() { - TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); - unsigned depth = s_external_font_load_fs_depth.load(std::memory_order_acquire); + const TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); + const unsigned depth = + s_external_font_load_fs_depth.load(std::memory_order_acquire); if (depth > 0) { - if (s_external_font_owner_task == current_task && s_external_font_bus_token.valid) + if (s_external_font_owner_task != current_task) { - s_external_font_load_fs_depth.fetch_add(1, std::memory_order_acq_rel); - return true; + return false; } - - const uint32_t now_ms = sys::millis_now(); - if (s_external_font_last_busy_log_ms == 0 || - static_cast(now_ms - s_external_font_last_busy_log_ms) >= - kLvglExternalFontLogIntervalMs) - { - Serial.printf("[SPI][LVGL_FONT] acquire failed reason=owned_by_other depth=%lu owner_task=%s current_task=%s\n", - static_cast(depth), - s_external_font_owner_task ? pcTaskGetName(s_external_font_owner_task) - : "-", - current_task ? pcTaskGetName(current_task) : "-"); - s_external_font_last_busy_log_ms = now_ms; - } - return false; + s_external_font_load_fs_depth.fetch_add(1, std::memory_order_acq_rel); + return true; } - sys::runtime::BusAcquireRequest request{}; - const uint32_t start_ms = sys::millis_now(); - request.resource = kLvglExternalFontBusResource; - request.policy = sys::runtime::BusAccessPolicy::RecoveryExclusive; - request.command_id = kLvglExternalFontCommandId; - request.deadline_ms = start_ms + kLvglExternalFontBusAcquireMs; - request.origin = kLvglExternalFontCommandId; - request.owner_label = kLvglExternalFontOwner; - - sys::runtime::BusAcquireResult result = - ::platform::esp::common::shared_spi_coordinator().acquire(request); - if (result.status != sys::runtime::BusAcquireStatus::Acquired || !result.token.valid) - { - const uint32_t now_ms = sys::millis_now(); - if (s_external_font_last_busy_log_ms == 0 || - static_cast(now_ms - s_external_font_last_busy_log_ms) >= - kLvglExternalFontLogIntervalMs) - { - Serial.printf("[SPI][LVGL_FONT] acquire failed status=%s wait_ms=%lu owner=%s task=%s\n", - bus_acquire_status_name(result.status), - static_cast(result.diagnostics.wait_ms), - kLvglExternalFontOwner, - current_task ? pcTaskGetName(current_task) : "-"); - s_external_font_last_busy_log_ms = now_ms; - } - return false; - } - - s_external_font_bus_token = result.token; + s_external_font_fs_busy.store(false, std::memory_order_release); s_external_font_owner_task = current_task; - s_external_font_acquired_ms = result.token.acquired_ms; s_external_font_load_fs_depth.store(1, std::memory_order_release); - s_external_font_last_busy_log_ms = 0; - - Serial.printf("[SPI][LVGL_FONT] acquire ok wait_ms=%lu owner=%s task=%s\n", - static_cast(result.diagnostics.wait_ms), - kLvglExternalFontOwner, - current_task ? pcTaskGetName(current_task) : "-"); return true; } void lv_end_external_font_load_fs_scope() { - unsigned depth = s_external_font_load_fs_depth.load(std::memory_order_acquire); + const TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); + if (s_external_font_owner_task != current_task) + { + return; + } + + unsigned depth = + s_external_font_load_fs_depth.load(std::memory_order_acquire); while (depth > 0) { const unsigned next_depth = depth - 1U; @@ -1454,31 +1413,17 @@ void lv_end_external_font_load_fs_scope() { continue; } - if (next_depth > 0) + if (next_depth == 0) { - return; - } - - sys::runtime::BusAccessToken token = s_external_font_bus_token; - s_external_font_bus_token = {}; - s_external_font_owner_task = nullptr; - const uint32_t now_ms = sys::millis_now(); - const uint32_t hold_ms = s_external_font_acquired_ms == 0 - ? 0 - : static_cast(now_ms - s_external_font_acquired_ms); - s_external_font_acquired_ms = 0; - - if (token.valid) - { - ::platform::esp::common::shared_spi_coordinator().release(token); - Serial.printf("[SPI][LVGL_FONT] release hold_ms=%lu slow=%d\n", - static_cast(hold_ms), - hold_ms >= kLvglExternalFontSlowHoldMs ? 1 : 0); + s_external_font_owner_task = nullptr; } return; } +} - Serial.printf("[SPI][LVGL_FONT] release skipped reason=no_active_scope\n"); +bool lv_external_font_load_fs_was_busy() +{ + return s_external_font_fs_busy.exchange(false, std::memory_order_acq_rel); } #if LV_USE_STDLIB_MALLOC == LV_STDLIB_CUSTOM diff --git a/platform/esp/arduino_common/src/chat/infra/meshtastic/mt_adapter.cpp b/platform/esp/arduino_common/src/chat/infra/meshtastic/mt_adapter.cpp index 2e5bb8fc..81c2d5d7 100644 --- a/platform/esp/arduino_common/src/chat/infra/meshtastic/mt_adapter.cpp +++ b/platform/esp/arduino_common/src/chat/infra/meshtastic/mt_adapter.cpp @@ -541,7 +541,13 @@ bool MtAdapter::sendAppDataNow(ChannelId channel, uint32_t portnum, const auto send_policy = chat::runtime::resolveMeshtasticAppDataSendPolicy(dest, want_ack, want_response); bool effective_want_response = send_policy.effective_want_response; - if (!encodeAppData(portnum, payload, len, effective_want_response, data_buffer.data(), &data_size)) + if (!encodeAppData(portnum, + payload, + len, + effective_want_response, + data_buffer.data(), + &data_size, + &scratch.decoded)) { last_send_error_ = meshtastic_Routing_Error_BAD_REQUEST; return false; @@ -2489,7 +2495,10 @@ void MtAdapter::processReceivedPacket(const uint8_t* data, size_t size) if (decoded.portnum == meshtastic_PortNum_KEY_VERIFICATION_APP && decoded.payload.size > 0) { meshtastic_KeyVerification kv = meshtastic_KeyVerification_init_default; - if (decodeKeyVerificationMessage(plaintext, plaintext_len, &kv)) + if (decodeKeyVerificationMessage(plaintext, + plaintext_len, + &kv, + &scratch.candidate_decoded)) { mt_diag_log("[MT][KEY_VERIFY] from=%08lX id=%08lX stage=%s hash1=%u hash2=%u\n", static_cast(header.from), @@ -3065,7 +3074,8 @@ bool MtAdapter::sendPacket(const PendingSend& pending) pending.msg_id, pending.dest, data_buffer.data(), - &data_size)) + &data_size, + &scratch.decoded)) { last_send_error_ = meshtastic_Routing_Error_BAD_REQUEST; return false; @@ -3293,8 +3303,8 @@ bool MtAdapter::sendNodeInfoTo(uint32_t dest, bool want_response, ChannelId chan request.public_key_len = pki_public_key_.size(); } - chat::runtime::MeshtasticAnnouncementPacket packet{}; - if (!chat::runtime::MeshtasticSelfAnnouncementCore::buildNodeInfoPacket(request, &packet)) + if (!chat::runtime::MeshtasticSelfAnnouncementCore::buildNodeInfoPacket( + request, &node_info_packet_scratch_)) { return false; } @@ -3309,23 +3319,24 @@ bool MtAdapter::sendNodeInfoTo(uint32_t dest, bool want_response, ChannelId chan LORA_LOG("[LORA] NodeInfo user_id=%s short=%s long=%s\n", logged_user_id, identity.short_name, identity.long_name); LORA_LOG("[LORA] TX nodeinfo wire ch=0x%02X idx=%u hop=%u wire=%u\n", - packet.channel_hash, + node_info_packet_scratch_.channel_hash, (unsigned)(channel == ChannelId::SECONDARY ? 1 : 0), request.hop_limit, - (unsigned)packet.wire_size); + (unsigned)node_info_packet_scratch_.wire_size); if (!board_.isRadioOnline()) { return false; } - bool ok = transmitWirePacket(packet.wire, packet.wire_size); + bool ok = transmitWirePacket(node_info_packet_scratch_.wire, + node_info_packet_scratch_.wire_size); if (ok && dest == kBroadcastNodeId) { last_nodeinfo_ms_ = millis(); } LORA_LOG("[LORA] TX nodeinfo id=%08lX len=%u ok=%d\n", (unsigned long)request.packet_id, - (unsigned)packet.wire_size, + (unsigned)node_info_packet_scratch_.wire_size, ok ? 1 : 0); return ok; } diff --git a/platform/esp/arduino_common/src/chat/infra/reticulum/reticulum_interfaces.cpp b/platform/esp/arduino_common/src/chat/infra/reticulum/reticulum_interfaces.cpp index ccc70583..cd461680 100644 --- a/platform/esp/arduino_common/src/chat/infra/reticulum/reticulum_interfaces.cpp +++ b/platform/esp/arduino_common/src/chat/infra/reticulum/reticulum_interfaces.cpp @@ -934,6 +934,8 @@ void AutoReticulumInterface::applyConfig( { stop(); last_socket_attempt_ms_ = 0; + wifi_connect_retry_not_before_ms_ = 0; + wifi_connect_suspended_ = false; rx_queue_.clear(); } Serial.printf("[Reticulum][IF][Auto] enabled=%s group=%s discovery=%u data=%u available=%s\n", @@ -947,6 +949,11 @@ void AutoReticulumInterface::applyConfig( void AutoReticulumInterface::setTransportEnabled(bool enabled) { transport_enabled_ = enabled; + if (transport_enabled_) + { + wifi_connect_retry_not_before_ms_ = 0; + wifi_connect_suspended_ = false; + } if (!transport_enabled_) { stop(); @@ -963,16 +970,59 @@ void AutoReticulumInterface::maintain() platform::ui::wifi::Status wifi_status = platform::ui::wifi::status(); const uint32_t now_ms = millis(); + if (wifi_status.connected) + { + wifi_connect_retry_not_before_ms_ = 0; + wifi_connect_suspended_ = false; + } if (!wifi_status.connected && auto_connect_wifi_) { + if (wifi_connect_suspended_) + { + stop(); + return; + } + if (wifi_connect_retry_not_before_ms_ != 0 && + static_cast(wifi_connect_retry_not_before_ms_ - now_ms) > 0) + { + stop(); + return; + } + platform::ui::wifi_access::Request request{}; request.client = platform::ui::wifi_access::Client::ReticulumGateway; request.kind = platform::ui::wifi_access::AccessKind::WifiConnect; request.priority = platform::ui::wifi_access::Priority::Messaging; request.allow_connect = true; request.reason = "reticulum_auto_interface"; - (void)platform::ui::wifi_access::ensure_connected(request, nullptr); + platform::ui::wifi_access::ConnectResult connect_result{}; + const bool connect_allowed = + platform::ui::wifi_access::ensure_connected(request, &connect_result); wifi_status = platform::ui::wifi::status(); + if (!connect_allowed && !wifi_status.connected) + { + if (connect_result.decision == + platform::ui::wifi_access::Decision::WifiDisabled) + { + // Wi-Fi disabled is a policy state, not a transient connect + // failure. Wait for the explicit enable transition instead of + // polling the policy on every maintain() tick. + wifi_connect_suspended_ = true; + } + else + { + const uint32_t retry_after_ms = + connect_result.retry_after_ms != 0 + ? connect_result.retry_after_ms + : kWifiConnectRetryIntervalMs; + wifi_connect_retry_not_before_ms_ = now_ms + retry_after_ms; + } + } + else if (wifi_status.connected) + { + wifi_connect_retry_not_before_ms_ = 0; + wifi_connect_suspended_ = false; + } } if (!wifi_status.connected) { diff --git a/platform/esp/arduino_common/src/chat/infra/store/fixed_slot_journal.cpp b/platform/esp/arduino_common/src/chat/infra/store/fixed_slot_journal.cpp index ffbc1a5b..7b605a2d 100644 --- a/platform/esp/arduino_common/src/chat/infra/store/fixed_slot_journal.cpp +++ b/platform/esp/arduino_common/src/chat/infra/store/fixed_slot_journal.cpp @@ -139,23 +139,49 @@ bool FixedSlotJournalEngine::read(const char* path, uint32_t slot_index, void* out_slot) const { - if (!out_slot) - { - return false; - } const Inspection inspection = inspect(path, protocol, kind, slot_size); + return readStatus(path, + protocol, + kind, + slot_size, + inspection, + slot_index, + out_slot) == ReadStatus::Ok; +} + +FixedSlotJournalEngine::ReadStatus FixedSlotJournalEngine::readStatus( + const char* path, + MeshProtocol protocol, + JournalKind kind, + std::size_t slot_size, + const Inspection& inspection, + uint32_t slot_index, + void* out_slot) const +{ + if (!out_slot || !validDescriptor(protocol, kind, slot_size)) + { + return ReadStatus::InvalidArgument; + } if ((inspection.state != State::Ready && inspection.state != State::PartialTail) || slot_index >= inspection.slot_count) { - return false; + return ReadStatus::OutOfRange; } storage::SdRuntimeFile file; const uint64_t offset = sizeof(Header) + static_cast(slot_index) * slot_size; - return file.open(path, "r") && file.seek(offset) && - readExact(file, out_slot, slot_size); + if (!file.open(path, "r") || !file.seek(offset) || + !readExact(file, out_slot, slot_size)) + { + // The caller already inspected this journal. A subsequent open/seek/ + // read failure is most commonly a bounded shared-SPI miss, not a + // corrupt slot. Let the owner retry the journal instead of scanning + // and logging every remaining slot. + return ReadStatus::Unavailable; + } + return ReadStatus::Ok; } bool FixedSlotJournalEngine::validDescriptor(MeshProtocol protocol, diff --git a/platform/esp/arduino_common/src/chat/infra/store/sd_protocol_peer_repository.cpp b/platform/esp/arduino_common/src/chat/infra/store/sd_protocol_peer_repository.cpp index f6117dd2..4f2aadc9 100644 --- a/platform/esp/arduino_common/src/chat/infra/store/sd_protocol_peer_repository.cpp +++ b/platform/esp/arduino_common/src/chat/infra/store/sd_protocol_peer_repository.cpp @@ -278,27 +278,49 @@ bool SdProtocolPeerRepository::loadPeerJournal(MeshProtocol protocol, { return false; } + uint32_t decode_failures = 0U; + uint32_t first_decode_failure = inspection.slot_count; + uint32_t last_decode_failure = 0U; for (uint32_t index = 0; index < inspection.slot_count; ++index) { storage_v2::PeerProjection projection{}; - if (!journal_.read(path, - protocol, - kind, - slot_size, - index, - slot_scratch_.data()) || - !storage_v2::decodePeerSlot(protocol, + const auto read_status = journal_.readStatus(path, + protocol, + kind, + slot_size, + inspection, + index, + slot_scratch_.data()); + if (read_status != + storage_v2::FixedSlotJournalEngine::ReadStatus::Ok) + { + Serial.printf("[PeerStoreV2] hydration deferred path=%s index=%lu read_status=%u\n", + path, + static_cast(index), + static_cast(read_status)); + return false; + } + if (!storage_v2::decodePeerSlot(protocol, slot_scratch_.data(), slot_size, projection)) { - Serial.printf("[PeerStoreV2] corrupt peer slot path=%s index=%lu\n", - path, - static_cast(index)); + ++decode_failures; + first_decode_failure = + std::min(first_decode_failure, index); + last_decode_failure = index; continue; } (void)applyPeerProjection(projection); } + if (decode_failures > 0U) + { + Serial.printf("[PeerStoreV2] invalid peer slots path=%s count=%lu first=%lu last=%lu\n", + path, + static_cast(decode_failures), + static_cast(first_decode_failure), + static_cast(last_decode_failure)); + } if (kind == storage_v2::JournalKind::PeerDelta) { partitions_[protocolIndex(protocol)].peer_delta_count = @@ -342,27 +364,49 @@ bool SdProtocolPeerRepository::loadContactJournal(MeshProtocol protocol, { return false; } + uint32_t decode_failures = 0U; + uint32_t first_decode_failure = inspection.slot_count; + uint32_t last_decode_failure = 0U; for (uint32_t index = 0; index < inspection.slot_count; ++index) { storage_v2::ContactProjection projection{}; - if (!journal_.read(path, - protocol, - kind, - slot_size, - index, - slot_scratch_.data()) || - !storage_v2::decodeContactSlot(protocol, + const auto read_status = journal_.readStatus(path, + protocol, + kind, + slot_size, + inspection, + index, + slot_scratch_.data()); + if (read_status != + storage_v2::FixedSlotJournalEngine::ReadStatus::Ok) + { + Serial.printf("[PeerStoreV2] hydration deferred path=%s index=%lu read_status=%u\n", + path, + static_cast(index), + static_cast(read_status)); + return false; + } + if (!storage_v2::decodeContactSlot(protocol, slot_scratch_.data(), slot_size, projection)) { - Serial.printf("[PeerStoreV2] corrupt contact slot path=%s index=%lu\n", - path, - static_cast(index)); + ++decode_failures; + first_decode_failure = + std::min(first_decode_failure, index); + last_decode_failure = index; continue; } (void)applyContactProjection(projection); } + if (decode_failures > 0U) + { + Serial.printf("[PeerStoreV2] invalid contact slots path=%s count=%lu first=%lu last=%lu\n", + path, + static_cast(decode_failures), + static_cast(first_decode_failure), + static_cast(last_decode_failure)); + } if (kind == storage_v2::JournalKind::ContactDelta) { partitions_[protocolIndex(protocol)].contact_delta_count = diff --git a/platform/esp/arduino_common/src/gps/track_recorder.cpp b/platform/esp/arduino_common/src/gps/track_recorder.cpp index 82854b2e..da095d40 100644 --- a/platform/esp/arduino_common/src/gps/track_recorder.cpp +++ b/platform/esp/arduino_common/src/gps/track_recorder.cpp @@ -1,7 +1,5 @@ #include "platform/esp/arduino_common/gps/track_recorder.h" #include "platform/esp/arduino_common/storage/sd_card_runtime.h" -#include "platform/esp/common/shared_spi_coordinator.h" -#include "sys/bus_access_scope.h" #include #include @@ -42,44 +40,9 @@ constexpr uint8_t kActiveVersion = 1; constexpr uint8_t kActiveFlagManual = 0x01; constexpr uint8_t kActiveFlagAuto = 0x02; constexpr const char* kActivePath = "/trackers/active.bin"; -constexpr uint32_t kTrackBusResource = 3; -constexpr uint32_t kTrackBusOwnerId = 0x54524B; // 'TRK' -constexpr const char* kTrackBusOwner = "track_sd"; constexpr uint32_t kPendingFlushIntervalMs = 5000; constexpr size_t kPendingFlushThreshold = 6; -class TrackRecorderBusGate final -{ - public: - TrackRecorderBusGate(sys::runtime::RuntimeCommandKind kind, - sys::runtime::BusAccessPolicy policy) - : scope_(::platform::esp::common::shared_spi_coordinator(), - makeRequest(kind, policy)) - { - } - - bool locked() const - { - return scope_.acquired(); - } - - private: - static sys::runtime::BusAcquireRequest makeRequest( - sys::runtime::RuntimeCommandKind kind, - sys::runtime::BusAccessPolicy policy) - { - sys::runtime::BusAcquireRequest request{}; - request.resource = kTrackBusResource; - request.policy = policy; - request.command_id = static_cast(kind); - request.origin = kTrackBusOwnerId; - request.owner_label = kTrackBusOwner; - return request; - } - - sys::runtime::ScopedBusAccessToken scope_; -}; - double deg2rad(double deg) { return deg * 0.017453292519943295; // pi / 180 @@ -285,12 +248,6 @@ bool TrackRecorder::start() bool ok = false; do { - TrackRecorderBusGate bus_gate(sys::runtime::RuntimeCommandKind::TrackStart, - sys::runtime::BusAccessPolicy::DurableCommit); - if (!bus_gate.locked()) - { - break; - } if (!ensureDir()) { break; @@ -322,17 +279,6 @@ void TrackRecorder::stop() return; } - TrackRecorderBusGate bus_gate(sys::runtime::RuntimeCommandKind::TrackStop, - sys::runtime::BusAccessPolicy::DurableCommit); - if (!bus_gate.locked()) - { - if (mutex_) - { - xSemaphoreGive(mutex_); - } - return; - } - manual_recording_ = false; if (auto_recording_) { @@ -380,20 +326,6 @@ void TrackRecorder::setAutoRecording(bool enabled) return; } - const sys::runtime::RuntimeCommandKind command_kind = - enabled ? sys::runtime::RuntimeCommandKind::TrackStart - : sys::runtime::RuntimeCommandKind::TrackStop; - TrackRecorderBusGate bus_gate(command_kind, - sys::runtime::BusAccessPolicy::DurableCommit); - if (!bus_gate.locked()) - { - if (mutex_) - { - xSemaphoreGive(mutex_); - } - return; - } - auto_recording_ = enabled; if (auto_recording_ && !recording_) { @@ -475,17 +407,6 @@ void TrackRecorder::setFormat(TrackFormat format) return; } - TrackRecorderBusGate bus_gate(sys::runtime::RuntimeCommandKind::TrackFlush, - sys::runtime::BusAccessPolicy::DurableCommit); - if (!bus_gate.locked()) - { - if (mutex_) - { - xSemaphoreGive(mutex_); - } - return; - } - format_ = format; if (current_path_.length() > 0) @@ -615,20 +536,6 @@ void TrackRecorder::flushPending(bool force) return; } - const sys::runtime::BusAccessPolicy policy = - force ? sys::runtime::BusAccessPolicy::DurableCommit - : sys::runtime::BusAccessPolicy::UiNeverBlock; - TrackRecorderBusGate bus_gate(sys::runtime::RuntimeCommandKind::TrackFlush, - policy); - if (!bus_gate.locked()) - { - if (mutex_) - { - xSemaphoreGive(mutex_); - } - return; - } - (void)writePendingPointsLocked(force); if (mutex_) @@ -711,12 +618,6 @@ bool TrackRecorder::restoreActiveSession() bool ok = false; do { - TrackRecorderBusGate bus_gate(sys::runtime::RuntimeCommandKind::TrackStart, - sys::runtime::BusAccessPolicy::DurableCommit); - if (!bus_gate.locked()) - { - break; - } if (!sd_card_ready()) { break; @@ -891,14 +792,6 @@ size_t TrackRecorder::listTracks(String* out_names, size_t max_names) const } }; - TrackRecorderBusGate bus_gate(sys::runtime::RuntimeCommandKind::TrackList, - sys::runtime::BusAccessPolicy::UiNeverBlock); - if (!bus_gate.locked()) - { - release_mutex(); - return 0; - } - if (!sd_card_ready()) { release_mutex(); diff --git a/platform/esp/arduino_common/src/mesh/esp_meshtastic_adapter_bridge.cpp b/platform/esp/arduino_common/src/mesh/esp_meshtastic_adapter_bridge.cpp index eb7f1ef1..99ce0bde 100644 --- a/platform/esp/arduino_common/src/mesh/esp_meshtastic_adapter_bridge.cpp +++ b/platform/esp/arduino_common/src/mesh/esp_meshtastic_adapter_bridge.cpp @@ -284,13 +284,13 @@ void EspMeshtasticAdapterBridge::onRadioPacket(const uint8_t* data, return; } - ::mesh::RadioRxPacket packet{}; - std::memcpy(packet.bytes, data, size); - packet.size = size; - packet.rssi = rssi; - packet.snr = snr; - packet.received_at_ms = clock_.nowMs(); - receive_.onRadioPacket(packet); + std::memcpy(rx_packet_scratch_.bytes, data, size); + rx_packet_scratch_.size = size; + rx_packet_scratch_.rssi = rssi; + rx_packet_scratch_.snr = snr; + rx_packet_scratch_.received_at_ms = clock_.nowMs(); + receive_.onRadioPacket(rx_packet_scratch_); + rx_packet_scratch_ = ::mesh::RadioRxPacket{}; } void EspMeshtasticAdapterBridge::tick() diff --git a/platform/esp/arduino_common/src/platform_ui_reticulum_directory_runtime.cpp b/platform/esp/arduino_common/src/platform_ui_reticulum_directory_runtime.cpp index eebff3f4..9d37ec76 100644 --- a/platform/esp/arduino_common/src/platform_ui_reticulum_directory_runtime.cpp +++ b/platform/esp/arduino_common/src/platform_ui_reticulum_directory_runtime.cpp @@ -1948,10 +1948,6 @@ constexpr const char* kPagesDir = "/trailmate/reticulum/pages"; constexpr const char* kPagesDir = "/fs/trailmate/reticulum/pages"; #endif constexpr const char* kDefaultPagePath = "/page/index.mu"; -constexpr uint32_t kPageCacheReadWaitMs = 60; -constexpr uint32_t kPageCacheWriteWaitMs = 120; -SemaphoreHandle_t s_page_storage_mutex = nullptr; - RequestStartHandler s_request_handler = nullptr; void* s_request_context = nullptr; RequestCancelHandler s_request_cancel_handler = nullptr; @@ -2002,58 +1998,6 @@ PageRequestProgressSlot s_request_progress[kPageProgressDepth]{}; uint32_t s_request_progress_order = 1; portMUX_TYPE s_request_progress_lock = portMUX_INITIALIZER_UNLOCKED; -enum class PageCacheBusAccess : uint8_t -{ - Read = 1, - Write, -}; - -bool ensure_page_storage_mutex() -{ - if (s_page_storage_mutex) - { - return true; - } - s_page_storage_mutex = xSemaphoreCreateRecursiveMutex(); - return s_page_storage_mutex != nullptr; -} - -class PageCacheBusGate final -{ - public: - explicit PageCacheBusGate(PageCacheBusAccess access) - { - if (!ensure_page_storage_mutex()) - { - return; - } - const uint32_t wait_ms = access == PageCacheBusAccess::Write - ? kPageCacheWriteWaitMs - : kPageCacheReadWaitMs; - locked_ = xSemaphoreTakeRecursive(s_page_storage_mutex, - pdMS_TO_TICKS(wait_ms)) == pdTRUE; - } - - ~PageCacheBusGate() - { - if (locked_) - { - xSemaphoreGiveRecursive(s_page_storage_mutex); - } - } - - bool locked() const - { - return locked_; - } - - PageCacheBusGate(const PageCacheBusGate&) = delete; - PageCacheBusGate& operator=(const PageCacheBusGate&) = delete; - - private: - bool locked_ = false; -}; - void copy_text(char* out, std::size_t out_len, const char* text) { if (!out || out_len == 0) @@ -2832,14 +2776,6 @@ Status load_cached_page(const char* destination_hash, } const std::string path_text = cache_path(destination, normalized_path); - PageCacheBusGate bus_gate(PageCacheBusAccess::Read); - if (!bus_gate.locked()) - { - out.busy = true; - set_status(out, "Nomad page cache busy", path_text.c_str()); - return out; - } - out.file_present = page_storage_exists(path_text.c_str()) && !page_storage_is_directory(path_text.c_str()); out.cache_checked = true; @@ -3085,13 +3021,6 @@ Status store_cached_page_now(const char* destination_hash, return out; } - PageCacheBusGate bus_gate(PageCacheBusAccess::Write); - if (!bus_gate.locked()) - { - set_status(out, "Nomad page cache busy", kPagesDir); - return out; - } - if (!ensure_page_parent_dirs(destination, normalized_path)) { set_status(out, "Cannot create Nomad page cache directory", kPagesDir); @@ -3143,14 +3072,6 @@ Status clear_cached_page(const char* destination_hash, const char* path) } const std::string path_text = cache_path(destination, normalized_path); - PageCacheBusGate bus_gate(PageCacheBusAccess::Write); - if (!bus_gate.locked()) - { - out.busy = true; - set_status(out, "Nomad page cache busy", path_text.c_str()); - return out; - } - out.cache_checked = true; out.file_present = page_storage_exists(path_text.c_str()) && !page_storage_is_directory(path_text.c_str()); diff --git a/platform/esp/arduino_common/src/sstv/sstv_service.cpp b/platform/esp/arduino_common/src/sstv/sstv_service.cpp index f38a7c4f..7781742a 100644 --- a/platform/esp/arduino_common/src/sstv/sstv_service.cpp +++ b/platform/esp/arduino_common/src/sstv/sstv_service.cpp @@ -26,8 +26,6 @@ #include "platform/esp/arduino_common/storage/sd_card_runtime.h" #endif -#include "platform/esp/common/shared_spi_coordinator.h" -#include "sys/bus_access_scope.h" #include "sys/clock.h" #include @@ -233,51 +231,7 @@ constexpr bool kEnableSlantCorrection = true; constexpr bool kStretch = true; constexpr int kSamplesPerBlock = 1024; constexpr int kResampleMaxOut = 4096; -constexpr uint32_t kSstvSaveDeadlineMs = 2000; constexpr uint32_t kSstvSaveRowsPerChunk = 8; -constexpr uint32_t kSstvSaveBusResource = 8; -constexpr uint32_t kSstvSaveBusOwnerId = 0x53535456u; // 'SSTV' -constexpr const char* kSstvSaveBusOwner = "sstv_save_sd"; - -enum class SstvSaveBusCommand : uint8_t -{ - Prepare = 1, - Header, - Rows, - Flush, - Close, -}; - -class SstvSaveBusGate final -{ - public: - explicit SstvSaveBusGate(SstvSaveBusCommand command, uint32_t deadline_ms) - : scope_(::platform::esp::common::shared_spi_coordinator(), - makeRequest(command, deadline_ms)) - { - } - - bool locked() const - { - return scope_.acquired(); - } - - private: - static sys::runtime::BusAcquireRequest makeRequest(SstvSaveBusCommand command, - uint32_t deadline_ms) - { - sys::runtime::BusAcquireRequest request{}; - request.resource = kSstvSaveBusResource; - request.policy = sys::runtime::BusAccessPolicy::DurableCommit; - request.command_id = kSstvSaveBusOwnerId + static_cast(command); - request.origin = kSstvSaveBusOwnerId; - request.deadline_ms = deadline_ms; - request.owner_label = kSstvSaveBusOwner; - return request; - } - - sys::runtime::ScopedBusAccessToken scope_; -}; struct LinearResampler { @@ -505,10 +459,8 @@ bool flush_save_file(FileT& file) } template -void close_save_file(FileT& file, uint32_t deadline_ms) +void close_save_file(FileT& file) { - SstvSaveBusGate close_gate(SstvSaveBusCommand::Close, deadline_ms); - (void)close_gate.locked(); file.close(); } @@ -612,7 +564,6 @@ bool save_frame_to_sd() return false; } - const uint32_t deadline_ms = sys::millis_now() + kSstvSaveDeadlineMs; char path[64]; #if defined(TRAIL_MATE_ESP_BOARD_TAB5) || defined(TRAIL_MATE_ESP_BOARD_T_DISPLAY_P4) @@ -621,44 +572,36 @@ bool save_frame_to_sd() ::platform::esp::arduino_common::storage::SdRuntimeFile f; #endif +#if defined(TRAIL_MATE_ESP_BOARD_TAB5) || defined(TRAIL_MATE_ESP_BOARD_T_DISPLAY_P4) + if (s_tab5_storage.cardType() == CARD_NONE) +#else + if (!::platform::esp::arduino_common::storage::sd_card_ready()) +#endif { - SstvSaveBusGate prepare_gate(SstvSaveBusCommand::Prepare, deadline_ms); - if (!prepare_gate.locked()) - { - set_error("SD busy"); - return false; - } -#if defined(TRAIL_MATE_ESP_BOARD_TAB5) || defined(TRAIL_MATE_ESP_BOARD_T_DISPLAY_P4) - if (s_tab5_storage.cardType() == CARD_NONE) -#else - if (!::platform::esp::arduino_common::storage::sd_card_ready()) -#endif - { - set_error("SD not ready"); - return false; - } - if (!ensure_sstv_dir()) - { - set_error("SD mkdir failed"); - return false; - } + set_error("SD not ready"); + return false; + } + if (!ensure_sstv_dir()) + { + set_error("SD mkdir failed"); + return false; + } - if (!build_save_path(path, sizeof(path))) - { - set_error("SD path failed"); - return false; - } + if (!build_save_path(path, sizeof(path))) + { + set_error("SD path failed"); + return false; + } #if defined(TRAIL_MATE_ESP_BOARD_TAB5) || defined(TRAIL_MATE_ESP_BOARD_T_DISPLAY_P4) - f = s_tab5_storage.open(path, FILE_WRITE); - if (!f) + f = s_tab5_storage.open(path, FILE_WRITE); + if (!f) #else - if (!f.open(path, "w")) + if (!f.open(path, "w")) #endif - { - set_error("SD open failed"); - return false; - } + { + set_error("SD open failed"); + return false; } auto fail_after_open = [&](uint8_t* buffer, const char* message) -> bool @@ -667,7 +610,7 @@ bool save_frame_to_sd() { free(buffer); } - close_save_file(f, deadline_ms); + close_save_file(f); set_error(message); return false; }; @@ -709,23 +652,16 @@ bool save_frame_to_sd() info_hdr[22] = static_cast((pixel_bytes >> 16) & 0xFF); info_hdr[23] = static_cast((pixel_bytes >> 24) & 0xFF); + if (!write_exact(f, file_hdr, sizeof(file_hdr)) || + !write_exact(f, info_hdr, sizeof(info_hdr))) { - SstvSaveBusGate header_gate(SstvSaveBusCommand::Header, deadline_ms); - if (!header_gate.locked()) - { - return fail_after_open(nullptr, "SD busy"); - } - if (!write_exact(f, file_hdr, sizeof(file_hdr)) || - !write_exact(f, info_hdr, sizeof(info_hdr))) - { - return fail_after_open(nullptr, "SD write failed"); - } + return fail_after_open(nullptr, "SD write failed"); } uint8_t* rowbuf = static_cast(malloc(row24)); if (!rowbuf) { - close_save_file(f, deadline_ms); + close_save_file(f); set_error("SD buffer fail"); return false; } @@ -734,26 +670,18 @@ bool save_frame_to_sd() uint32_t y = 0; while (y < h) { + uint32_t chunk_end = y + kSstvSaveRowsPerChunk; + if (chunk_end > h) { - SstvSaveBusGate rows_gate(SstvSaveBusCommand::Rows, deadline_ms); - if (!rows_gate.locked()) + chunk_end = h; + } + for (; y < chunk_end; ++y) + { + const uint16_t* row = s_frame + (h - 1 - y) * w; + fill_bmp_row(rowbuf, row24, row, w); + if (!write_exact(f, rowbuf, row24)) { - return fail_after_open(rowbuf, "SD busy"); - } - - uint32_t chunk_end = y + kSstvSaveRowsPerChunk; - if (chunk_end > h) - { - chunk_end = h; - } - for (; y < chunk_end; ++y) - { - const uint16_t* row = s_frame + (h - 1 - y) * w; - fill_bmp_row(rowbuf, row24, row, w); - if (!write_exact(f, rowbuf, row24)) - { - return fail_after_open(rowbuf, "SD write failed"); - } + return fail_after_open(rowbuf, "SD write failed"); } } @@ -763,19 +691,12 @@ bool save_frame_to_sd() } } + if (!flush_save_file(f)) { - SstvSaveBusGate flush_gate(SstvSaveBusCommand::Flush, deadline_ms); - if (!flush_gate.locked()) - { - return fail_after_open(rowbuf, "SD busy"); - } - if (!flush_save_file(f)) - { - return fail_after_open(rowbuf, "SD flush failed"); - } + return fail_after_open(rowbuf, "SD flush failed"); } - close_save_file(f, deadline_ms); + close_save_file(f); free(rowbuf); snprintf(s_saved_path, sizeof(s_saved_path), "%s", path); diff --git a/platform/esp/arduino_common/src/storage/sd_card_runtime.cpp b/platform/esp/arduino_common/src/storage/sd_card_runtime.cpp index 7f9ac803..ee0e4af1 100644 --- a/platform/esp/arduino_common/src/storage/sd_card_runtime.cpp +++ b/platform/esp/arduino_common/src/storage/sd_card_runtime.cpp @@ -1,9 +1,12 @@ #include "platform/esp/arduino_common/storage/sd_card_runtime.h" +#include "platform/esp/arduino_common/storage/sd_spi_bus_hooks.h" #include "platform/esp/common/shared_spi_coordinator.h" +#include "sys/bus_access_scope.h" #include "sys/clock.h" -#include "sys/runtime_async.h" +#include "esp_heap_caps.h" +#include "freertos/semphr.h" #include "freertos/task.h" #include #include @@ -16,7 +19,6 @@ #include #include #include -#include #if SDFAT_FILE_TYPE != 3 #error "TrailMate requires SdFs with FAT/FAT32/exFAT support (SDFAT_FILE_TYPE=3)." @@ -36,7 +38,10 @@ constexpr uint32_t kDefaultSharedSpiSdHz = 4000000U; constexpr uint32_t kMaxSharedSpiSdHz = 10000000U; constexpr uint32_t kSdInitHz = 400000U; constexpr uint8_t kSdR1IdleState = 0x01U; -constexpr uint32_t kSdRuntimeLockWaitMs = 250U; +constexpr uint32_t kSdRuntimeLockWaitMs = 25U; +constexpr uint32_t kSdInteractiveReadLockWaitMs = 200U; +constexpr uint32_t kSdDurableLockWaitMs = 250U; +constexpr std::size_t kSdTransferSliceBytes = kSdSectorSize; #ifndef TRAIL_MATE_SD_IO_LOG_ENABLE #define TRAIL_MATE_SD_IO_LOG_ENABLE 1 @@ -60,42 +65,145 @@ bool s_sdfat_mounted = false; volatile bool s_external_block_owner_active = false; uint32_t s_last_sd_io_log_ms = 0; uint32_t s_suppressed_sd_io_logs = 0; +StaticSemaphore_t s_filesystem_mutex_storage{}; +SemaphoreHandle_t s_filesystem_mutex = nullptr; +FsFile* s_transient_file = nullptr; -class SdRuntimeBusGuard +struct SdSpiOperationProfile +{ + sys::runtime::BusAccessPolicy policy = + sys::runtime::BusAccessPolicy::BackgroundWorkerBounded; + uint32_t wait_ms = kSdRuntimeLockWaitMs; + const char* owner = "sd_spi_unscoped"; + sys::runtime::BusAcquireStatus last_bus_status = + sys::runtime::BusAcquireStatus::Unavailable; + bool active = false; +}; + +SdSpiOperationProfile s_spi_operation_profile{}; + +bool ensure_filesystem_mutex() +{ + if (s_filesystem_mutex == nullptr) + { + s_filesystem_mutex = + xSemaphoreCreateRecursiveMutexStatic(&s_filesystem_mutex_storage); + } + return s_filesystem_mutex != nullptr; +} + +template +T* psram_preferred_object() +{ + void* storage = heap_caps_malloc_prefer(sizeof(T), + 2, + MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, + MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + return storage != nullptr ? new (storage) T() : nullptr; +} + +void* psram_preferred_bytes(std::size_t bytes) +{ + return heap_caps_malloc_prefer(bytes, + 2, + MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, + MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); +} + +template +void destroy_psram_preferred_object(T*& object) +{ + if (object == nullptr) + { + return; + } + object->~T(); + heap_caps_free(object); + object = nullptr; +} + +bool ensure_transient_file() +{ + if (s_transient_file == nullptr) + { + s_transient_file = psram_preferred_object(); + } + return s_transient_file != nullptr; +} + +class SdRuntimeOperationGuard { public: - explicit SdRuntimeBusGuard(const char* owner = "sd_runtime") + explicit SdRuntimeOperationGuard( + const char* owner = "sd_runtime", + sys::runtime::BusAccessPolicy policy = + sys::runtime::BusAccessPolicy::BackgroundWorkerBounded, + uint32_t wait_ms = kSdRuntimeLockWaitMs) { - sys::runtime::BusAcquireRequest request{}; - request.resource = - ::platform::esp::common::SharedSpiCoordinator::kSharedBusResource; - request.policy = sys::runtime::BusAccessPolicy::BackgroundWorkerBounded; - request.command_id = 0x53440000U; - request.origin = request.command_id; - request.deadline_ms = sys::millis_now() + kSdRuntimeLockWaitMs; - request.owner_label = owner; - result_ = ::platform::esp::common::shared_spi_coordinator().acquire(request); - token_ = result_.token; - locked_ = result_.status == sys::runtime::BusAcquireStatus::Acquired && - token_.valid; + if (!ensure_filesystem_mutex()) + { + status_ = sys::runtime::BusAcquireStatus::Unavailable; + return; + } + + const TickType_t wait_ticks = + wait_ms == 0U ? 0 : std::max(1, pdMS_TO_TICKS(wait_ms)); + if (xSemaphoreTakeRecursive(s_filesystem_mutex, wait_ticks) != pdTRUE) + { + status_ = wait_ms == 0U ? sys::runtime::BusAcquireStatus::Busy + : sys::runtime::BusAcquireStatus::TimedOut; + return; + } + + previous_profile_ = s_spi_operation_profile; + s_spi_operation_profile.policy = policy; + s_spi_operation_profile.wait_ms = wait_ms; + s_spi_operation_profile.owner = + owner != nullptr && owner[0] != '\0' ? owner : "sd_runtime"; + s_spi_operation_profile.last_bus_status = + sys::runtime::BusAcquireStatus::Acquired; + s_spi_operation_profile.active = true; + status_ = sys::runtime::BusAcquireStatus::Acquired; + locked_ = true; } - ~SdRuntimeBusGuard() + ~SdRuntimeOperationGuard() { if (locked_) { - ::platform::esp::common::shared_spi_coordinator().release(token_); + s_spi_operation_profile = previous_profile_; + xSemaphoreGiveRecursive(s_filesystem_mutex); } } bool locked() const { return locked_; } + sys::runtime::BusAcquireStatus status() const { return status_; } + sys::runtime::BusAcquireStatus busStatus() const + { + return locked_ ? s_spi_operation_profile.last_bus_status : status_; + } private: - sys::runtime::BusAcquireResult result_{}; - sys::runtime::BusAccessToken token_{}; + SdSpiOperationProfile previous_profile_{}; + sys::runtime::BusAcquireStatus status_ = + sys::runtime::BusAcquireStatus::Unavailable; bool locked_ = false; }; +int32_t bus_acquire_error(sys::runtime::BusAcquireStatus status) +{ + switch (status) + { + case sys::runtime::BusAcquireStatus::Busy: + return -4; + case sys::runtime::BusAcquireStatus::Unavailable: + return -3; + case sys::runtime::BusAcquireStatus::TimedOut: + default: + return -2; + } +} + const char* backend_name_from_info() { switch (s_info.backend) @@ -295,6 +403,10 @@ void clear_sdfat() { if (s_sdfat_mounted) { + if (s_transient_file != nullptr && *s_transient_file) + { + (void)s_transient_file->close(); + } s_sdfat.end(); // SdFat's Arduino driver ends the shared SPIClass. Restore the board // pin mapping immediately so display/radio users keep a valid bus. @@ -351,6 +463,13 @@ uint8_t sd_send_cmd0(SPIClass& spi) bool sd_preflight_go_idle(int sd_cs, SPIClass& spi) { + sys::runtime::BusAccessToken bus_token{}; + if (!sd_spi_bus_acquire(bus_token)) + { + Serial.println("[SD] SdFat preflight skipped: shared SPI unavailable"); + return false; + } + uint8_t last_token = 0xFF; bool ok = false; uint8_t attempt = 0; @@ -383,6 +502,7 @@ bool sd_preflight_go_idle(int sd_cs, SPIClass& spi) } spi.endTransaction(); + sd_spi_bus_release(bus_token); Serial.printf("[SD] SdFat preflight CMD0 -> %d token=0x%02X attempts=%u\n", ok ? 1 : 0, static_cast(last_token), @@ -429,6 +549,52 @@ void record_sdfat_info() } // namespace +bool sd_spi_bus_acquire(sys::runtime::BusAccessToken& token) +{ +#if defined(TRAIL_MATE_SDFAT_SHARED_SPI) + const uint32_t now_ms = sys::millis_now(); + const SdSpiOperationProfile& profile = s_spi_operation_profile; + sys::runtime::BusAcquireRequest request{}; + request.resource = + ::platform::esp::common::SharedSpiCoordinator::kSharedBusResource; + request.policy = profile.active + ? profile.policy + : sys::runtime::BusAccessPolicy::BackgroundWorkerBounded; + request.command_id = 0x53445049U; + request.origin = request.command_id; + const uint32_t wait_ms = + profile.active ? profile.wait_ms : kSdRuntimeLockWaitMs; + request.deadline_ms = now_ms + wait_ms; + request.owner_label = + profile.active ? profile.owner : "sd_spi_unscoped"; + + const sys::runtime::BusAcquireResult result = + ::platform::esp::common::shared_spi_coordinator().acquire(request); + if (result.status != sys::runtime::BusAcquireStatus::Acquired) + { + s_spi_operation_profile.last_bus_status = result.status; + } + token = result.token; + return result.status == sys::runtime::BusAcquireStatus::Acquired && + token.valid; +#else + token = {}; + return true; +#endif +} + +void sd_spi_bus_release(const sys::runtime::BusAccessToken& token) +{ +#if defined(TRAIL_MATE_SDFAT_SHARED_SPI) + if (token.valid) + { + ::platform::esp::common::shared_spi_coordinator().release(token); + } +#else + (void)token; +#endif +} + bool mount_sd_card(int sd_cs, SPIClass& spi, uint32_t spi_hz, @@ -437,6 +603,16 @@ bool mount_sd_card(int sd_cs, { (void)mount_point; (void)max_files; + SdRuntimeOperationGuard operation( + "sd_mount", + sys::runtime::BusAccessPolicy::RecoveryExclusive, + 500U); + if (!operation.locked()) + { + Serial.println("[SD] mount skipped: filesystem session unavailable"); + return false; + } + s_external_block_owner_active = false; clear_sdfat(); reset_info(); @@ -494,10 +670,13 @@ bool mount_sd_card(int sd_cs, void unmount_sd_card() { s_external_block_owner_active = false; - SdRuntimeBusGuard guard("sd_unmount"); + SdRuntimeOperationGuard guard( + "sd_unmount", + sys::runtime::BusAccessPolicy::RecoveryExclusive, + 500U); if (!guard.locked()) { - Serial.println("[SD] unmount skipped: shared SPI lock unavailable"); + Serial.println("[SD] unmount skipped: filesystem session unavailable"); return; } clear_sdfat(); @@ -578,7 +757,7 @@ bool sd_exists(const char* path) const char* normalized = normalize_sd_path(path); const uint32_t start_ms = sd_io_begin("exists", normalized); bool result = false; - SdRuntimeBusGuard guard("sd_exists"); + SdRuntimeOperationGuard guard("sd_exists"); if (!guard.locked()) { sd_io_end("exists", normalized, start_ms, false, 0, -2); @@ -594,12 +773,157 @@ bool sd_exists(const char* path) return false; } +SdFileReadResult sd_read_file(const char* path, + uint8_t* buffer, + std::size_t capacity) +{ + const char* normalized = normalize_sd_path(path); + const uint32_t start_ms = sd_io_begin("map_file_read", normalized, capacity); + SdFileReadResult result{}; + + auto finish = [&](SdFileReadStatus status, + std::size_t bytes_read, + uint64_t file_size, + int32_t error) + { + result.status = status; + result.bytes_read = bytes_read; + result.file_size = file_size; + result.error = error; + sd_io_end("map_file_read", + normalized, + start_ms, + status == SdFileReadStatus::Ready, + bytes_read, + error); + return result; + }; + + if (path_empty(path) || buffer == nullptr || capacity == 0) + { + return finish(SdFileReadStatus::Invalid, 0, 0, -4); + } + if (!sd_card_ready() || s_info.backend != SdCardBackend::SdFat) + { + return finish(SdFileReadStatus::Unavailable, 0, 0, -3); + } + + SdRuntimeOperationGuard operation( + "sd_map_file", + sys::runtime::BusAccessPolicy::InteractiveWorkerBounded, + kSdInteractiveReadLockWaitMs); + if (!operation.locked()) + { + return finish(SdFileReadStatus::Busy, + 0, + 0, + bus_acquire_error(operation.status())); + } + if (!ensure_transient_file()) + { + return finish(SdFileReadStatus::IoError, 0, 0, -8); + } + + FsFile& file = *s_transient_file; + if (file && !file.close()) + { + const sys::runtime::BusAcquireStatus status = operation.busStatus(); + return finish(status == sys::runtime::BusAcquireStatus::Acquired + ? SdFileReadStatus::IoError + : SdFileReadStatus::Busy, + 0, + 0, + status == sys::runtime::BusAcquireStatus::Acquired + ? -7 + : bus_acquire_error(status)); + } + + uint64_t file_size = 0; + file = s_sdfat.open(normalized, O_RDONLY); + if (!file) + { + const sys::runtime::BusAcquireStatus status = operation.busStatus(); + // Map tile paths are immutable generated artifacts. Only a completed + // open may establish that the artifact is missing. + return finish(status == sys::runtime::BusAcquireStatus::Acquired + ? SdFileReadStatus::Missing + : SdFileReadStatus::Busy, + 0, + 0, + status == sys::runtime::BusAcquireStatus::Acquired + ? -1 + : bus_acquire_error(status)); + } + + file_size = file.fileSize(); + if (file_size == 0 || file_size > capacity) + { + (void)file.close(); + return finish(SdFileReadStatus::Invalid, 0, file_size, -5); + } + + const std::size_t target_size = static_cast(file_size); + std::size_t total_read = 0; + while (total_read < target_size) + { + const std::size_t chunk_size = + std::min(kSdTransferSliceBytes, + target_size - total_read); + const uint32_t chunk_start_ms = + sd_io_begin("map_file_read_chunk", normalized, chunk_size); + const int bytes_read = file.read(buffer + total_read, chunk_size); + if (bytes_read <= 0) + { + const sys::runtime::BusAcquireStatus status = operation.busStatus(); + const int32_t error = + status == sys::runtime::BusAcquireStatus::Acquired + ? -6 + : bus_acquire_error(status); + sd_io_end("map_file_read_chunk", + normalized, + chunk_start_ms, + false, + 0, + error); + (void)file.close(); + return finish(status == sys::runtime::BusAcquireStatus::Acquired + ? SdFileReadStatus::IoError + : SdFileReadStatus::Busy, + total_read, + file_size, + error); + } + sd_io_end("map_file_read_chunk", + normalized, + chunk_start_ms, + true, + static_cast(bytes_read), + bytes_read); + total_read += static_cast(bytes_read); + } + + if (!file.close()) + { + const sys::runtime::BusAcquireStatus status = operation.busStatus(); + return finish(status == sys::runtime::BusAcquireStatus::Acquired + ? SdFileReadStatus::IoError + : SdFileReadStatus::Busy, + total_read, + file_size, + status == sys::runtime::BusAcquireStatus::Acquired + ? -7 + : bus_acquire_error(status)); + } + + return finish(SdFileReadStatus::Ready, total_read, file_size, 0); +} + bool sd_is_directory(const char* path) { const char* normalized = normalize_sd_path(path); const uint32_t start_ms = sd_io_begin("is_dir", normalized); bool result = false; - SdRuntimeBusGuard guard("sd_is_dir"); + SdRuntimeOperationGuard guard("sd_is_dir"); if (!guard.locked()) { sd_io_end("is_dir", normalized, start_ms, false, 0, -2); @@ -607,9 +931,19 @@ bool sd_is_directory(const char* path) } if (s_info.backend == SdCardBackend::SdFat) { - FsFile dir = s_sdfat.open(normalized, O_RDONLY); + if (!ensure_transient_file()) + { + sd_io_end("is_dir", normalized, start_ms, false, 0, -8); + return false; + } + FsFile& dir = *s_transient_file; + if (dir) + { + (void)dir.close(); + } + dir = s_sdfat.open(normalized, O_RDONLY); result = dir && dir.isDir(); - dir.close(); + (void)dir.close(); sd_io_end("is_dir", normalized, start_ms, true, 0, result ? 1 : 0); return result; } @@ -626,7 +960,10 @@ bool sd_mkdir(const char* path) return false; } bool result = false; - SdRuntimeBusGuard guard("sd_mkdir"); + SdRuntimeOperationGuard guard( + "sd_mkdir", + sys::runtime::BusAccessPolicy::DurableCommit, + kSdDurableLockWaitMs); if (!guard.locked()) { sd_io_end("mkdir", normalized, start_ms, false, 0, -2); @@ -651,7 +988,10 @@ bool sd_rmdir(const char* path) return false; } bool result = false; - SdRuntimeBusGuard guard("sd_rmdir"); + SdRuntimeOperationGuard guard( + "sd_rmdir", + sys::runtime::BusAccessPolicy::DurableCommit, + kSdDurableLockWaitMs); if (!guard.locked()) { sd_io_end("rmdir", normalized, start_ms, false, 0, -2); @@ -676,7 +1016,10 @@ bool sd_remove(const char* path) return false; } bool result = false; - SdRuntimeBusGuard guard("sd_remove"); + SdRuntimeOperationGuard guard( + "sd_remove", + sys::runtime::BusAccessPolicy::DurableCommit, + kSdDurableLockWaitMs); if (!guard.locked()) { sd_io_end("remove", normalized, start_ms, false, 0, -2); @@ -702,7 +1045,10 @@ bool sd_rename(const char* old_path, const char* new_path) return false; } bool result = false; - SdRuntimeBusGuard guard("sd_rename"); + SdRuntimeOperationGuard guard( + "sd_rename", + sys::runtime::BusAccessPolicy::DurableCommit, + kSdDurableLockWaitMs); if (!guard.locked()) { sd_io_end("rename", normalized_old, start_ms, false, 0, -2); @@ -728,14 +1074,14 @@ class SdRuntimeFile::Impl }; SdRuntimeFile::SdRuntimeFile() - : impl_(new (std::nothrow) Impl()) + : impl_(psram_preferred_object()) { } SdRuntimeFile::~SdRuntimeFile() { close(); - delete impl_; + destroy_psram_preferred_object(impl_); } bool SdRuntimeFile::open(const char* path, const char* mode) @@ -755,7 +1101,12 @@ bool SdRuntimeFile::open(const char* path, const char* mode) { return false; } - SdRuntimeBusGuard guard("sd_file_open"); + const bool mutating = open_mode_mutates(mode); + SdRuntimeOperationGuard guard( + "sd_file_open", + mutating ? sys::runtime::BusAccessPolicy::DurableCommit + : sys::runtime::BusAccessPolicy::BackgroundWorkerBounded, + mutating ? kSdDurableLockWaitMs : kSdRuntimeLockWaitMs); if (!guard.locked()) { sd_io_end("file_open", impl_->path, start_ms, false, 0, -2); @@ -782,7 +1133,12 @@ void SdRuntimeFile::close() if (impl_->backend == SdCardBackend::SdFat) { const uint32_t start_ms = sd_io_begin("file_close", impl_->path); - SdRuntimeBusGuard guard("sd_file_close"); + const bool mutating = open_mode_mutates(impl_->mode); + SdRuntimeOperationGuard guard( + "sd_file_close", + mutating ? sys::runtime::BusAccessPolicy::DurableCommit + : sys::runtime::BusAccessPolicy::BackgroundWorkerBounded, + mutating ? kSdDurableLockWaitMs : kSdRuntimeLockWaitMs); if (guard.locked()) { impl_->sdfat_file.close(); @@ -811,7 +1167,7 @@ int SdRuntimeFile::available() const } if (impl_->backend == SdCardBackend::SdFat) { - SdRuntimeBusGuard guard("sd_file_available"); + SdRuntimeOperationGuard guard("sd_file_available"); if (!guard.locked()) { return 0; @@ -830,14 +1186,44 @@ int SdRuntimeFile::read(void* buffer, std::size_t bytes_to_read) if (impl_->backend == SdCardBackend::SdFat) { const uint32_t start_ms = sd_io_begin("file_read", impl_->path, bytes_to_read); - SdRuntimeBusGuard guard("sd_file_read"); + SdRuntimeOperationGuard guard("sd_file_read"); if (!guard.locked()) { sd_io_end("file_read", impl_->path, start_ms, false, bytes_to_read, -2); return -1; } - const int result = impl_->sdfat_file.read(buffer, bytes_to_read); - sd_io_end("file_read", impl_->path, start_ms, result >= 0, bytes_to_read, result); + std::size_t total_read = 0; + auto* out = static_cast(buffer); + while (total_read < bytes_to_read) + { + const std::size_t slice = + std::min(kSdTransferSliceBytes, bytes_to_read - total_read); + const int current = impl_->sdfat_file.read(out + total_read, slice); + if (current <= 0) + { + const int result = + total_read > 0 ? static_cast(total_read) : current; + sd_io_end("file_read", + impl_->path, + start_ms, + current == 0, + total_read, + result); + return result; + } + total_read += static_cast(current); + if (static_cast(current) < slice) + { + break; + } + } + const int result = static_cast(total_read); + sd_io_end("file_read", + impl_->path, + start_ms, + true, + total_read, + result); return result; } return -1; @@ -851,7 +1237,7 @@ int SdRuntimeFile::read_byte() } if (impl_->backend == SdCardBackend::SdFat) { - SdRuntimeBusGuard guard("sd_file_read_byte"); + SdRuntimeOperationGuard guard("sd_file_read_byte"); if (!guard.locked()) { return -1; @@ -870,15 +1256,42 @@ std::size_t SdRuntimeFile::read_bytes(char* buffer, std::size_t bytes_to_read) if (impl_->backend == SdCardBackend::SdFat) { const uint32_t start_ms = sd_io_begin("file_read_bytes", impl_->path, bytes_to_read); - SdRuntimeBusGuard guard("sd_file_read_bytes"); + SdRuntimeOperationGuard guard("sd_file_read_bytes"); if (!guard.locked()) { sd_io_end("file_read_bytes", impl_->path, start_ms, false, bytes_to_read, -2); return 0; } - int result = impl_->sdfat_file.read(buffer, bytes_to_read); - sd_io_end("file_read_bytes", impl_->path, start_ms, result >= 0, bytes_to_read, result); - return result > 0 ? static_cast(result) : 0; + std::size_t total_read = 0; + while (total_read < bytes_to_read) + { + const std::size_t slice = + std::min(kSdTransferSliceBytes, bytes_to_read - total_read); + const int current = + impl_->sdfat_file.read(buffer + total_read, slice); + if (current <= 0) + { + sd_io_end("file_read_bytes", + impl_->path, + start_ms, + current == 0, + total_read, + current); + return total_read; + } + total_read += static_cast(current); + if (static_cast(current) < slice) + { + break; + } + } + sd_io_end("file_read_bytes", + impl_->path, + start_ms, + true, + total_read, + static_cast(total_read)); + return total_read; } return 0; } @@ -897,15 +1310,37 @@ std::size_t SdRuntimeFile::write(const void* buffer, std::size_t bytes_to_write) { return 0; } - SdRuntimeBusGuard guard("sd_file_write"); + SdRuntimeOperationGuard guard( + "sd_file_write", + sys::runtime::BusAccessPolicy::DurableCommit, + kSdDurableLockWaitMs); if (!guard.locked()) { sd_io_end("file_write", impl_->path, start_ms, false, bytes_to_write, -2); return 0; } - const std::size_t result = impl_->sdfat_file.write(buffer, bytes_to_write); - sd_io_end("file_write", impl_->path, start_ms, result == bytes_to_write, bytes_to_write, result); - return result; + std::size_t total_written = 0; + const auto* input = static_cast(buffer); + while (total_written < bytes_to_write) + { + const std::size_t slice = + std::min(kSdTransferSliceBytes, + bytes_to_write - total_written); + const std::size_t current = + impl_->sdfat_file.write(input + total_written, slice); + total_written += current; + if (current != slice) + { + break; + } + } + sd_io_end("file_write", + impl_->path, + start_ms, + total_written == bytes_to_write, + bytes_to_write, + static_cast(total_written)); + return total_written; } return 0; } @@ -922,7 +1357,10 @@ std::size_t SdRuntimeFile::write_byte(uint8_t value) { return 0; } - SdRuntimeBusGuard guard("sd_file_write_byte"); + SdRuntimeOperationGuard guard( + "sd_file_write_byte", + sys::runtime::BusAccessPolicy::DurableCommit, + kSdDurableLockWaitMs); if (!guard.locked()) { return 0; @@ -938,20 +1376,7 @@ std::size_t SdRuntimeFile::print(const char* text) { return 0; } - if (impl_->backend == SdCardBackend::SdFat) - { - if (s_external_block_owner_active) - { - return 0; - } - SdRuntimeBusGuard guard("sd_file_print"); - if (!guard.locked()) - { - return 0; - } - return impl_->sdfat_file.print(text); - } - return 0; + return write(text, std::strlen(text)); } std::size_t SdRuntimeFile::print(double value, int digits) @@ -967,7 +1392,10 @@ std::size_t SdRuntimeFile::print(double value, int digits) { return 0; } - SdRuntimeBusGuard guard("sd_file_print"); + SdRuntimeOperationGuard guard( + "sd_file_print", + sys::runtime::BusAccessPolicy::DurableCommit, + kSdDurableLockWaitMs); if (!guard.locked()) { return 0; @@ -996,10 +1424,18 @@ std::size_t SdRuntimeFile::printf(const char* format, ...) return 0; } - std::vector buffer(static_cast(len) + 1U); - std::vsnprintf(buffer.data(), buffer.size(), format, args); + const std::size_t buffer_size = static_cast(len) + 1U; + auto* buffer = static_cast(psram_preferred_bytes(buffer_size)); + if (buffer == nullptr) + { + va_end(args); + return 0; + } + std::vsnprintf(buffer, buffer_size, format, args); va_end(args); - return write(buffer.data(), static_cast(len)); + const std::size_t written = write(buffer, static_cast(len)); + heap_caps_free(buffer); + return written; } bool SdRuntimeFile::seek(uint64_t offset) @@ -1010,7 +1446,7 @@ bool SdRuntimeFile::seek(uint64_t offset) } if (impl_->backend == SdCardBackend::SdFat) { - SdRuntimeBusGuard guard("sd_file_seek"); + SdRuntimeOperationGuard guard("sd_file_seek"); if (!guard.locked()) { return false; @@ -1028,7 +1464,7 @@ uint64_t SdRuntimeFile::position() const } if (impl_->backend == SdCardBackend::SdFat) { - SdRuntimeBusGuard guard("sd_file_position"); + SdRuntimeOperationGuard guard("sd_file_position"); if (!guard.locked()) { return 0; @@ -1046,7 +1482,7 @@ uint64_t SdRuntimeFile::size() const } if (impl_->backend == SdCardBackend::SdFat) { - SdRuntimeBusGuard guard("sd_file_size"); + SdRuntimeOperationGuard guard("sd_file_size"); if (!guard.locked()) { return 0; @@ -1069,7 +1505,10 @@ bool SdRuntimeFile::flush() { return false; } - SdRuntimeBusGuard guard("sd_file_flush"); + SdRuntimeOperationGuard guard( + "sd_file_flush", + sys::runtime::BusAccessPolicy::DurableCommit, + kSdDurableLockWaitMs); if (!guard.locked()) { sd_io_end("file_flush", impl_->path, start_ms, false, 0, -2); @@ -1086,19 +1525,20 @@ class SdRuntimeDir::Impl { public: FsFile sdfat_dir; + FsFile entry_scratch; SdCardBackend backend = SdCardBackend::None; char path[128]{}; }; SdRuntimeDir::SdRuntimeDir() - : impl_(new (std::nothrow) Impl()) + : impl_(psram_preferred_object()) { } SdRuntimeDir::~SdRuntimeDir() { close(); - delete impl_; + destroy_psram_preferred_object(impl_); } bool SdRuntimeDir::open(const char* path) @@ -1111,7 +1551,7 @@ bool SdRuntimeDir::open(const char* path) const char* normalized = normalize_sd_path(path); copy_path(impl_->path, sizeof(impl_->path), normalized); const uint32_t start_ms = sd_io_begin("dir_open", impl_->path); - SdRuntimeBusGuard guard("sd_dir_open"); + SdRuntimeOperationGuard guard("sd_dir_open"); if (!guard.locked()) { sd_io_end("dir_open", impl_->path, start_ms, false, 0, -2); @@ -1139,9 +1579,13 @@ void SdRuntimeDir::close() if (impl_->backend == SdCardBackend::SdFat) { const uint32_t start_ms = sd_io_begin("dir_close", impl_->path); - SdRuntimeBusGuard guard("sd_dir_close"); + SdRuntimeOperationGuard guard("sd_dir_close"); if (guard.locked()) { + if (impl_->entry_scratch) + { + (void)impl_->entry_scratch.close(); + } impl_->sdfat_dir.close(); sd_io_end("dir_close", impl_->path, start_ms, true); } @@ -1174,13 +1618,18 @@ bool SdRuntimeDir::read_next(char* name, std::size_t name_size, bool* is_dir) if (impl_->backend == SdCardBackend::SdFat) { const uint32_t start_ms = sd_io_begin("dir_read", impl_->path); - SdRuntimeBusGuard guard("sd_dir_read"); + SdRuntimeOperationGuard guard("sd_dir_read"); if (!guard.locked()) { sd_io_end("dir_read", impl_->path, start_ms, false, 0, -2); return false; } - FsFile entry = impl_->sdfat_dir.openNextFile(O_RDONLY); + if (impl_->entry_scratch) + { + (void)impl_->entry_scratch.close(); + } + impl_->entry_scratch = impl_->sdfat_dir.openNextFile(O_RDONLY); + FsFile& entry = impl_->entry_scratch; if (!entry) { sd_io_end("dir_read", impl_->path, start_ms, true, 0, 0); @@ -1205,7 +1654,7 @@ bool sd_read_raw(uint32_t lba, uint8_t* buffer) std::snprintf(path, sizeof(path), "raw:%lu", static_cast(lba)); const uint32_t start_ms = sd_io_begin("raw_read", path, kSdSectorSize); bool result = false; - SdRuntimeBusGuard guard("sd_raw_read"); + SdRuntimeOperationGuard guard("sd_raw_read"); if (!guard.locked()) { sd_io_end("raw_read", path, start_ms, false, kSdSectorSize, -2); @@ -1228,7 +1677,10 @@ bool sd_write_raw(uint32_t lba, const uint8_t* buffer) std::snprintf(path, sizeof(path), "raw:%lu", static_cast(lba)); const uint32_t start_ms = sd_io_begin("raw_write", path, kSdSectorSize); bool result = false; - SdRuntimeBusGuard guard("sd_raw_write"); + SdRuntimeOperationGuard guard( + "sd_raw_write", + sys::runtime::BusAccessPolicy::DurableCommit, + kSdDurableLockWaitMs); if (!guard.locked()) { sd_io_end("raw_write", path, start_ms, false, kSdSectorSize, -2); diff --git a/platform/esp/arduino_common/src/ui/screens/team/team_ui_store.cpp b/platform/esp/arduino_common/src/ui/screens/team/team_ui_store.cpp index 65c1a8e0..e2494408 100644 --- a/platform/esp/arduino_common/src/ui/screens/team/team_ui_store.cpp +++ b/platform/esp/arduino_common/src/ui/screens/team/team_ui_store.cpp @@ -6,9 +6,7 @@ #include "platform/ui/team_ui_store_runtime.h" #include "ui/team_persistence/team_ui_snapshot_codec.h" -#include "platform/esp/arduino_common/storage/persistence_bus_gate.h" #include "platform/esp/arduino_common/storage/sd_card_runtime.h" -#include "platform/esp/common/shared_spi_coordinator.h" #include "sys/clock.h" #include #include @@ -69,72 +67,9 @@ constexpr uint32_t kPosHeaderSize = 24; constexpr uint32_t kPosMinIntervalSec = 15; constexpr uint32_t kPosMaxIntervalSec = 30; constexpr float kPosMinDistanceM = 20.0f; -constexpr uint32_t kTeamStoreLoadWaitMs = 60; -constexpr uint32_t kTeamStoreReadWaitMs = 20; -constexpr uint32_t kTeamStoreWriteWaitMs = 20; -constexpr uint32_t kTeamStoreBusResource = 4; -constexpr uint32_t kTeamStoreBusOwnerId = 0x5445414Du; // 'TEAM' -constexpr const char* kTeamStoreBusOwner = "team_store_sd"; - constexpr size_t kChatlogMaxBytes = 256 * 1024; constexpr uint32_t kMinValidEpoch = 1577836800U; // 2020-01-01 -enum class TeamStoreBusAccess : uint8_t -{ - Load = 1, - Read, - Write, -}; - -class TeamStoreBusGate final -{ - public: - explicit TeamStoreBusGate(TeamStoreBusAccess access) - : gate_(::platform::esp::common::shared_spi_coordinator(), - policyFor(access), - waitMsFor(access), - kTeamStoreBusResource, - commandIdFor(access), - kTeamStoreBusOwnerId) - { - } - - bool locked() const - { - return gate_.locked(); - } - - private: - static sys::runtime::BusAccessPolicy policyFor(TeamStoreBusAccess access) - { - return access == TeamStoreBusAccess::Write - ? sys::runtime::BusAccessPolicy::DurableCommit - : sys::runtime::BusAccessPolicy::BackgroundWorkerBounded; - } - - static uint32_t waitMsFor(TeamStoreBusAccess access) - { - switch (access) - { - case TeamStoreBusAccess::Load: - return kTeamStoreLoadWaitMs; - case TeamStoreBusAccess::Read: - return kTeamStoreReadWaitMs; - case TeamStoreBusAccess::Write: - return kTeamStoreWriteWaitMs; - default: - return 0; - } - } - - static uint32_t commandIdFor(TeamStoreBusAccess access) - { - return kTeamStoreBusOwnerId + static_cast(access); - } - - ::platform::esp::arduino_common::storage::PersistenceBusGate gate_; -}; - uint32_t now_secs() { return sys::uptime_seconds_now(); @@ -974,12 +909,6 @@ class TeamUiSnapshotStorePersisted : public ITeamUiSnapshotStore return false; } - TeamStoreBusGate bus_gate(TeamStoreBusAccess::Load); - if (!bus_gate.locked()) - { - return false; - } - TeamUiSnapshot snap; std::string dir; bool has_current = read_current_dir(dir); @@ -1033,11 +962,7 @@ class TeamUiSnapshotStorePersisted : public ITeamUiSnapshotStore } if (!in.has_team_id || !in.in_team) { - TeamStoreBusGate bus_gate(TeamStoreBusAccess::Write); - if (bus_gate.locked()) - { - clear_current_dir(); - } + clear_current_dir(); return; } @@ -1056,12 +981,6 @@ class TeamUiSnapshotStorePersisted : public ITeamUiSnapshotStore return; } - TeamStoreBusGate bus_gate(TeamStoreBusAccess::Write); - if (!bus_gate.locked()) - { - return; - } - std::string dir = team_dir_from_id(in.team_id); std::string dir_path = std::string(kBaseDir) + "/" + dir; write_current_dir(dir); @@ -1085,11 +1004,7 @@ class TeamUiSnapshotStorePersisted : public ITeamUiSnapshotStore void clear() override { s_has_cached_snapshot = false; - TeamStoreBusGate bus_gate(TeamStoreBusAccess::Write); - if (bus_gate.locked()) - { - clear_current_dir(); - } + clear_current_dir(); } private: @@ -1205,11 +1120,6 @@ bool team_ui_append_key_event(const TeamId& team_id, const uint8_t* payload, size_t len) { - TeamStoreBusGate bus_gate(TeamStoreBusAccess::Write); - if (!bus_gate.locked()) - { - return false; - } return append_event(team_id, type, event_seq, ts, payload, len); } @@ -1226,12 +1136,6 @@ bool team_ui_posring_append(const TeamId& team_id, return false; } - TeamStoreBusGate bus_gate(TeamStoreBusAccess::Write); - if (!bus_gate.locked()) - { - return false; - } - std::string dir_path; if (!ensure_team_dir_for_id(team_id, dir_path)) { @@ -1292,11 +1196,6 @@ bool team_ui_posring_load_latest(const TeamId& team_id, { return false; } - TeamStoreBusGate bus_gate(TeamStoreBusAccess::Read); - if (!bus_gate.locked()) - { - return false; - } std::string dir = team_dir_from_id(team_id); std::string dir_path = std::string(kBaseDir) + "/" + dir; std::string path = dir_path + "/" + kPosringName; @@ -1406,12 +1305,6 @@ bool TeamUiSdChatLogStore::appendStructured(const TeamId& team_id, team::proto::TeamChatType type, const std::vector& payload) { - TeamStoreBusGate bus_gate(TeamStoreBusAccess::Write); - if (!bus_gate.locked()) - { - return false; - } - std::string dir_path; if (!ensure_team_dir_for_id(team_id, dir_path)) { @@ -1489,11 +1382,6 @@ bool TeamUiSdChatLogStore::loadRecent(const TeamId& team_id, { return false; } - TeamStoreBusGate bus_gate(TeamStoreBusAccess::Read); - if (!bus_gate.locked()) - { - return false; - } std::string dir_path; if (!ensure_team_dir_for_id(team_id, dir_path)) { @@ -1631,11 +1519,6 @@ bool team_ui_save_keys_now(const TeamId& team_id, { return false; } - TeamStoreBusGate bus_gate(TeamStoreBusAccess::Write); - if (!bus_gate.locked()) - { - return false; - } std::string dir_path; if (!ensure_team_dir_for_id(team_id, dir_path)) { @@ -1658,11 +1541,6 @@ bool team_ui_get_member_track_path(const TeamId& team_id, { return false; } - TeamStoreBusGate bus_gate(TeamStoreBusAccess::Write); - if (!bus_gate.locked()) - { - return false; - } std::string dir_path; if (!ensure_team_dir_for_id_internal(team_id, dir_path, false)) { @@ -1705,12 +1583,6 @@ bool team_ui_append_member_track(const TeamId& team_id, return false; } - TeamStoreBusGate bus_gate(TeamStoreBusAccess::Write); - if (!bus_gate.locked()) - { - return false; - } - std::string dir_path; if (!ensure_team_dir_for_id_internal(team_id, dir_path, true)) { diff --git a/platform/esp/arduino_common/src/ui/widgets/map/map_tiles.cpp b/platform/esp/arduino_common/src/ui/widgets/map/map_tiles.cpp index 064f180d..a5fd785d 100644 --- a/platform/esp/arduino_common/src/ui/widgets/map/map_tiles.cpp +++ b/platform/esp/arduino_common/src/ui/widgets/map/map_tiles.cpp @@ -9,7 +9,6 @@ #include "freertos/task.h" #include "lvgl.h" #include "platform/esp/arduino_common/storage/sd_card_runtime.h" -#include "platform/esp/common/shared_spi_coordinator.h" #include "src/draw/lv_image_decoder_private.h" #include "src/misc/cache/instance/lv_image_cache.h" #include "sys/clock.h" @@ -63,8 +62,6 @@ static uint32_t g_cache_full_log_ms = 0; static uint8_t g_requested_map_source = 0; -constexpr std::size_t kMapTileSdReadChunkBytes = 2U * 1024U; - static void style_placeholder_card(lv_obj_t* card); static void style_placeholder_text(lv_obj_t* label); @@ -145,23 +142,19 @@ class PathOnlyMapTileFileSystem final : public ui::map_tiles::IMapTileFileSystem return false; } - bool readFile(const char* path, - uint8_t* buffer, - std::size_t capacity, - std::size_t& out_size) const override + ui::map_tiles::MapTileReadResult readFile( + const char* path, + uint8_t* buffer, + std::size_t capacity) const override { - out_size = 0; (void)path; (void)buffer; (void)capacity; - return false; + return {ui::map_tiles::MapTileReadStatus::Missing, 0, -1}; } }; #if defined(ARDUINO) || defined(ARDUINO_ARCH_ESP32) -void reset_map_tile_sd_read_backpressure_state(); -void note_map_tile_sd_read_resource_busy(bool bus_access_retained); - class SdMapTileFileSystem final : public ui::map_tiles::IMapTileFileSystem { public: @@ -175,51 +168,40 @@ class SdMapTileFileSystem final : public ui::map_tiles::IMapTileFileSystem return ::platform::esp::arduino_common::storage::sd_is_directory(path); } - bool readFile(const char* path, - uint8_t* buffer, - std::size_t capacity, - std::size_t& out_size) const override + ui::map_tiles::MapTileReadResult readFile( + const char* path, + uint8_t* buffer, + std::size_t capacity) const override { - out_size = 0; - reset_map_tile_sd_read_backpressure_state(); - if (!path || !buffer || capacity == 0) + const auto result = + ::platform::esp::arduino_common::storage::sd_read_file( + path, + buffer, + capacity); + ui::map_tiles::MapTileReadResult mapped{}; + mapped.size = result.bytes_read; + mapped.error = result.error; + switch (result.status) { - return false; + case ::platform::esp::arduino_common::storage::SdFileReadStatus::Ready: + mapped.status = ui::map_tiles::MapTileReadStatus::Ready; + break; + case ::platform::esp::arduino_common::storage::SdFileReadStatus::Missing: + mapped.status = ui::map_tiles::MapTileReadStatus::Missing; + break; + case ::platform::esp::arduino_common::storage::SdFileReadStatus::Busy: + mapped.status = ui::map_tiles::MapTileReadStatus::RetryLater; + break; + case ::platform::esp::arduino_common::storage::SdFileReadStatus::Invalid: + mapped.status = ui::map_tiles::MapTileReadStatus::Invalid; + break; + case ::platform::esp::arduino_common::storage::SdFileReadStatus::Unavailable: + case ::platform::esp::arduino_common::storage::SdFileReadStatus::IoError: + default: + mapped.status = ui::map_tiles::MapTileReadStatus::Error; + break; } - - ::platform::esp::arduino_common::storage::SdRuntimeFile file; - if (!file.open(path, "r")) - { - return false; - } - - const uint64_t file_size = file.size(); - if (file_size == 0 || file_size > capacity) - { - file.close(); - return false; - } - - const std::size_t target_size = static_cast(file_size); - std::size_t total_read = 0; - while (total_read < target_size) - { - const std::size_t chunk_size = - std::min(kMapTileSdReadChunkBytes, target_size - total_read); - const int bytes_read = file.read(buffer + total_read, chunk_size); - if (bytes_read <= 0) - { - out_size = total_read; - file.close(); - return false; - } - - total_read += static_cast(bytes_read); - } - - file.close(); - out_size = total_read; - return true; + return mapped; } }; #endif @@ -237,7 +219,6 @@ constexpr uint32_t kMapTileLayerTransientBackoffMs = 450; constexpr uint32_t kMapTileLayerCacheBackoffMs = 350; constexpr uint32_t kMapTileMissingCacheTtlMs = 5U * 60U * 1000U; constexpr uint32_t kMapTileGenerationInitial = 1; -constexpr uint32_t kMapTileBusResource = 1; constexpr uint32_t kMapTileDiagnosticLogIntervalMs = 1000; constexpr TickType_t kMapTileWorkerPostCommandYieldTicks = pdMS_TO_TICKS(32); StaticTask_t s_map_tile_worker_task_tcb{}; @@ -246,8 +227,6 @@ StackType_t s_map_tile_worker_task_stack[(kMapTileWorkerTaskStackBytes + sizeof( uint32_t g_map_tile_decode_log_ms = 0; uint32_t g_map_tile_event_log_ms = 0; uint32_t g_map_tile_next_event_drain_ms = 0; -bool g_map_tile_sd_read_resource_busy = false; -bool g_map_tile_sd_read_bus_access_retained = true; bool should_log_map_tile_diagnostic(uint32_t& last_ms, uint32_t now_ms) { @@ -259,28 +238,6 @@ bool should_log_map_tile_diagnostic(uint32_t& last_ms, uint32_t now_ms) return false; } -void reset_map_tile_sd_read_backpressure_state() -{ - g_map_tile_sd_read_resource_busy = false; - g_map_tile_sd_read_bus_access_retained = true; -} - -void note_map_tile_sd_read_resource_busy(bool bus_access_retained) -{ - g_map_tile_sd_read_resource_busy = true; - g_map_tile_sd_read_bus_access_retained = bus_access_retained; -} - -bool map_tile_sd_read_resource_busy() -{ - return g_map_tile_sd_read_resource_busy; -} - -bool map_tile_sd_read_bus_access_retained() -{ - return g_map_tile_sd_read_bus_access_retained; -} - const char* map_tile_format_name(ui::map_tiles::MapTileFormat format) { switch (format) @@ -336,7 +293,7 @@ const char* map_tile_event_kind_name(ui::map_tiles::MapTileAsyncEventKind kind) return "ready"; case ui::map_tiles::MapTileAsyncEventKind::Failed: return "failed"; - case ui::map_tiles::MapTileAsyncEventKind::ResourceBusy: + case ui::map_tiles::MapTileAsyncEventKind::RetryLater: return "busy"; case ui::map_tiles::MapTileAsyncEventKind::Cancelled: return "cancelled"; @@ -898,36 +855,6 @@ class MapTileEventQueue final : public ui::map_tiles::IMapTileEventSink queue_{}; }; -class EspMapTilePolicyStrategy final : public sys::runtime::RuntimePolicyStrategy -{ - public: - sys::runtime::RuntimePriority selectPriority( - const sys::runtime::RuntimeIntent& intent) const override - { - return intent.priority_hint; - } - - sys::runtime::BusAccessPolicy selectBusPolicy( - const sys::runtime::RuntimeCommand& command) const override - { - if (command.priority == sys::runtime::RuntimePriority::Interactive || - command.priority == sys::runtime::RuntimePriority::Realtime) - { - return sys::runtime::BusAccessPolicy::InteractiveWorkerBounded; - } - return sys::runtime::BusAccessPolicy::BackgroundWorkerBounded; - } - - sys::runtime::RuntimeRetryDecision selectRetry( - const sys::runtime::RuntimeCommand& command, - const sys::runtime::PlatformStorageResult& result) const override - { - (void)command; - (void)result; - return {}; - } -}; - class EspMapTileWorkerBackend final : public ui::map_tiles::IMapTileWorkerBackend { public: @@ -942,50 +869,44 @@ class EspMapTileWorkerBackend final : public ui::map_tiles::IMapTileWorkerBacken return source_.lookup(ref); } - ui::map_tiles::MapTileReadResult read(const ui::map_tiles::MapTileRef& ref, - uint8_t* buffer, - std::size_t capacity) override + ui::map_tiles::MapTileReadResult read( + const ui::map_tiles::MapTileRef& ref, + uint8_t* buffer, + std::size_t capacity) override { ui::map_tiles::MapTileReadResult result{}; result.format = ui::map_tiles::mapTileFormatForLayer(ref.layer); - reset_map_tile_sd_read_backpressure_state(); if (map_tile_availability_memory().knownMissing(ref)) { result.error = -1; return result; } - std::size_t out_size = 0; - ui::map_tiles::MapTileFormat out_format = result.format; - if (source_.read(ref, buffer, capacity, out_size, out_format)) + const ui::map_tiles::MapTileReadResult storage_result = + source_.read(ref, buffer, capacity); + result.format = storage_result.format; + result.size = storage_result.size; + result.error = storage_result.error; + switch (storage_result.status) { + case ui::map_tiles::MapTileReadStatus::Ready: map_tile_availability_memory().markAvailable(ref); result.status = ui::map_tiles::MapTileReadStatus::Ready; - result.size = out_size; - result.format = out_format; result.error = 0; return result; - } - - if (map_tile_sd_read_resource_busy()) - { - result.status = ui::map_tiles::MapTileReadStatus::ResourceBusy; - result.format = out_format; - result.error = - static_cast(sys::runtime::BusAcquireStatus::TimedOut); - result.bus_access_retained = false; + case ui::map_tiles::MapTileReadStatus::RetryLater: + result.status = ui::map_tiles::MapTileReadStatus::RetryLater; + return result; + case ui::map_tiles::MapTileReadStatus::Missing: + map_tile_availability_memory().markMissing(ref); + result.status = ui::map_tiles::MapTileReadStatus::Error; + return result; + case ui::map_tiles::MapTileReadStatus::Invalid: + case ui::map_tiles::MapTileReadStatus::Error: + default: + result.status = ui::map_tiles::MapTileReadStatus::Error; return result; } - - const ui::map_tiles::MapTileLookupResult lookup = source_.lookup(ref); - if (lookup.status == ui::map_tiles::MapTileStatus::Missing) - { - map_tile_availability_memory().markMissing(ref); - } - result.status = ui::map_tiles::MapTileReadStatus::Failed; - result.format = out_format; - result.error = -1; - return result; } private: @@ -1198,10 +1119,7 @@ ui::map_tiles::FilesystemMapTileSource& worker_tile_source() class MapTileAsyncHost final { public: - MapTileAsyncHost() - : bus_(::platform::esp::common::shared_spi_coordinator()) - { - } + MapTileAsyncHost() = default; void acquire() { @@ -1268,7 +1186,6 @@ class MapTileAsyncHost final ui::map_tiles::LoadTileCommand command{}; if (commands_.pop(sys::millis_now(), command)) { - command.runtime.origin = kMapTileBusResource; if (worker_ != nullptr) { (void)worker_->execute(command, sys::millis_now()); @@ -1341,11 +1258,9 @@ class MapTileAsyncHost final { worker_ = new (std::nothrow) ui::map_tiles::MapTileWorker(backend_, - bus_, events_, scratch_, - kMapTileWorkerScratchBytes, - &policy_); + kMapTileWorkerScratchBytes); if (worker_ == nullptr) { if (!task_start_failed_logged_) @@ -1390,8 +1305,6 @@ class MapTileAsyncHost final MapTileCommandQueue commands_{}; MapTileEventQueue events_{}; - ::platform::esp::common::SharedSpiCoordinator& bus_; - EspMapTilePolicyStrategy policy_{}; EspMapTileWorkerBackend backend_{worker_tile_source()}; uint8_t* scratch_ = nullptr; ui::map_tiles::MapTileWorker* worker_ = nullptr; @@ -2581,7 +2494,7 @@ static bool apply_map_tile_event(TileContext& ctx, ui::map_tiles::MapTileAsyncEv pending = false; const uint32_t now_ms = sys::millis_now(); - if (event.kind == ui::map_tiles::MapTileAsyncEventKind::ResourceBusy) + if (event.kind == ui::map_tiles::MapTileAsyncEventKind::RetryLater) { retry_not_before = now_ms + kMapTileLayerBusyBackoffMs; log_map_tile_event_failure("resource_busy", event, event.error); diff --git a/platform/esp/boards/src/display/DisplayInterface.cpp b/platform/esp/boards/src/display/DisplayInterface.cpp index 0d3651ed..88690a7f 100644 --- a/platform/esp/boards/src/display/DisplayInterface.cpp +++ b/platform/esp/boards/src/display/DisplayInterface.cpp @@ -155,31 +155,6 @@ bool LilyGoDispArduinoSPI::init(int sck, return false; } - uint8_t display_id[3]{}; - uint8_t display_status[4]{}; - uint8_t display_madctl[1]{}; - uint8_t display_colmod[1]{}; - const bool id_read = readRegister(0x04, display_id, sizeof(display_id)); - const bool status_read = readRegister(0x09, display_status, sizeof(display_status)); - const bool madctl_read = readRegister(0x0B, display_madctl, sizeof(display_madctl)); - const bool colmod_read = readRegister(0x0C, display_colmod, sizeof(display_colmod)); - Serial.printf("[DISPLAY][READBACK] id_ok=%d id=%02X:%02X:%02X " - "status_ok=%d status=%02X:%02X:%02X:%02X " - "madctl_ok=%d madctl=%02X colmod_ok=%d colmod=%02X\n", - id_read ? 1 : 0, - static_cast(display_id[0]), - static_cast(display_id[1]), - static_cast(display_id[2]), - status_read ? 1 : 0, - static_cast(display_status[0]), - static_cast(display_status[1]), - static_cast(display_status[2]), - static_cast(display_status[3]), - madctl_read ? 1 : 0, - static_cast(display_madctl[0]), - colmod_read ? 1 : 0, - static_cast(display_colmod[0])); - setRotation(0); Serial.printf("[DISPLAY][INIT] rotation=0 logical=%ux%u offset=(%u,%u) " "coord_requests=%lu busy=%lu failures=%lu\n", diff --git a/platform/esp/boards/src/display/drivers/ST7796.cpp b/platform/esp/boards/src/display/drivers/ST7796.cpp index a2a427e1..1e10acb6 100644 --- a/platform/esp/boards/src/display/drivers/ST7796.cpp +++ b/platform/esp/boards/src/display/drivers/ST7796.cpp @@ -14,6 +14,7 @@ static const CommandTable_t st7796_init_commands[] = { {0xF0, {0xC3}, 0x01}, // Command Set Control 1 {0xF0, {0xC3}, 0x01}, // Command Set Control 1 {0xF0, {0x96}, 0x01}, // Command Set Control 1 + {0x36, {0x48}, 0x01}, // Initial memory access control {0x3A, {0x55}, 0x01}, // Pixel Format Set (16-bit/pixel) {0xB4, {0x01}, 0x01}, // Display Inversion Control {0xB6, {0x80, 0x02, 0x3B}, 0x03}, // Display Function Control diff --git a/platform/esp/common/include/platform/esp/common/shared_spi_coordinator.h b/platform/esp/common/include/platform/esp/common/shared_spi_coordinator.h index 6bc910cd..e4424915 100644 --- a/platform/esp/common/include/platform/esp/common/shared_spi_coordinator.h +++ b/platform/esp/common/include/platform/esp/common/shared_spi_coordinator.h @@ -3,7 +3,7 @@ #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #include "freertos/task.h" -#include "sys/runtime_async.h" +#include "sys/shared_spi_access.h" #include @@ -45,6 +45,7 @@ class SharedSpiCoordinator final : public sys::runtime::IBusArbiter private: static constexpr uint8_t kMaxWaiters = 8U; static constexpr uint8_t kOwnerLabelCapacity = 31U; + static constexpr uint32_t kSlowHoldBudgetMs = 45U; struct Waiter { diff --git a/platform/esp/common/src/shared_spi_coordinator.cpp b/platform/esp/common/src/shared_spi_coordinator.cpp index e8809a8d..74537ab9 100644 --- a/platform/esp/common/src/shared_spi_coordinator.cpp +++ b/platform/esp/common/src/shared_spi_coordinator.cpp @@ -377,10 +377,20 @@ void SharedSpiCoordinator::release(const sys::runtime::BusAccessToken& token) return; } + const uint32_t hold_ms = + owner_acquired_ms_ == 0U ? 0U : static_cast(now_ms - owner_acquired_ms_); clearOwnerLocked(now_ms); consecutive_timeouts_ = 0; - health_.status = sys::runtime::StorageHealthStatus::Healthy; - health_.last_error = 0; + if (hold_ms > kSlowHoldBudgetMs) + { + health_.status = sys::runtime::StorageHealthStatus::Slow; + health_.last_error = -4; + } + else + { + health_.status = sys::runtime::StorageHealthStatus::Healthy; + health_.last_error = 0; + } health_.last_transition_ms = now_ms; notify = findBestWaiterLocked() >= 0; portEXIT_CRITICAL(&mux_); diff --git a/platform/esp/idf_common/src/lv_helper.cpp b/platform/esp/idf_common/src/lv_helper.cpp index 91e60425..869291aa 100644 --- a/platform/esp/idf_common/src/lv_helper.cpp +++ b/platform/esp/idf_common/src/lv_helper.cpp @@ -51,3 +51,8 @@ bool lv_begin_external_font_load_fs_scope() void lv_end_external_font_load_fs_scope() { } + +bool lv_external_font_load_fs_was_busy() +{ + return false; +} diff --git a/platform/esp/idf_common/src/sd_card_runtime_sdfat_adapter.cpp b/platform/esp/idf_common/src/sd_card_runtime_sdfat_adapter.cpp index 68338dd6..6eb50183 100644 --- a/platform/esp/idf_common/src/sd_card_runtime_sdfat_adapter.cpp +++ b/platform/esp/idf_common/src/sd_card_runtime_sdfat_adapter.cpp @@ -41,7 +41,7 @@ constexpr uint8_t kRuntimeCardNone = 0; constexpr uint8_t kRuntimeCardSdhc = 3; constexpr uint8_t kRuntimeCardUnknown = 4; constexpr uint32_t kSdSectorSize = 512; -constexpr TickType_t kSdRuntimeLockWait = pdMS_TO_TICKS(250); +constexpr TickType_t kSdRuntimeLockWait = pdMS_TO_TICKS(25); #ifndef TRAIL_MATE_SD_IO_LOG_ENABLE #define TRAIL_MATE_SD_IO_LOG_ENABLE 1 @@ -628,6 +628,100 @@ bool sd_exists(const char* path) return false; } +SdFileReadResult sd_read_file(const char* path, + uint8_t* buffer, + std::size_t capacity) +{ + const char* normalized = normalize_sd_path(path); + const uint32_t start_ms = sd_io_begin("map_file_read", normalized, capacity); + SdFileReadResult result{}; + + auto finish = [&](SdFileReadStatus status, + std::size_t bytes_read, + uint64_t file_size, + int32_t error) + { + result.status = status; + result.bytes_read = bytes_read; + result.file_size = file_size; + result.error = error; + sd_io_end("map_file_read", + normalized, + start_ms, + status == SdFileReadStatus::Ready, + bytes_read, + error); + return result; + }; + + if (path_empty(path) || buffer == nullptr || capacity == 0) + { + return finish(SdFileReadStatus::Invalid, 0, 0, -4); + } + if (!sd_card_ready() || s_info.backend != SdCardBackend::SdFat) + { + return finish(SdFileReadStatus::Unavailable, 0, 0, -3); + } + + FsFile file; + uint64_t file_size = 0; + { + SdRuntimeBusGuard guard("sd_map_file_open"); + if (!guard.locked()) + { + return finish(SdFileReadStatus::Busy, 0, 0, -2); + } + + file = s_volume.open(normalized, O_RDONLY); + if (!file) + { + return finish(SdFileReadStatus::Missing, 0, 0, -1); + } + + file_size = file.fileSize(); + if (file_size == 0 || file_size > capacity) + { + file.close(); + return finish(SdFileReadStatus::Invalid, 0, file_size, -5); + } + } + + const std::size_t target_size = static_cast(file_size); + std::size_t total_read = 0; + while (total_read < target_size) + { + const std::size_t chunk_size = + std::min(2048U, target_size - total_read); + int bytes_read = -1; + { + SdRuntimeBusGuard guard("sd_map_file_read_chunk"); + if (!guard.locked()) + { + return finish(SdFileReadStatus::Busy, total_read, file_size, -2); + } + bytes_read = file.read(buffer + total_read, chunk_size); + if (bytes_read <= 0) + { + file.close(); + return finish(SdFileReadStatus::IoError, total_read, file_size, -6); + } + } + + total_read += static_cast(bytes_read); + } + + { + SdRuntimeBusGuard guard("sd_map_file_close"); + if (!guard.locked()) + { + return finish(SdFileReadStatus::Busy, total_read, file_size, -2); + } + file.close(); + } + + return finish(SdFileReadStatus::Ready, total_read, file_size, 0); +} + bool sd_is_directory(const char* path) { const char* normalized = normalize_sd_path(path); diff --git a/platform/esp/radio/meshtastic_radio_adapter.cpp b/platform/esp/radio/meshtastic_radio_adapter.cpp index c1a038be..5ecc7ee0 100644 --- a/platform/esp/radio/meshtastic_radio_adapter.cpp +++ b/platform/esp/radio/meshtastic_radio_adapter.cpp @@ -10,6 +10,7 @@ #include "chat/runtime/meshtastic_self_announcement_core.h" #include "chat/runtime/self_identity_policy.h" #include "chat/time_utils.h" +#include "esp_heap_caps.h" #include "esp_log.h" #include "esp_mac.h" #include "esp_timer.h" @@ -22,6 +23,7 @@ #include #include #include +#include namespace platform::esp::radio { @@ -104,6 +106,25 @@ MeshtasticRadioAdapter::MeshtasticRadioAdapter(LoraBoard& board) initNodeIdentity(); } +void* MeshtasticRadioAdapter::operator new(std::size_t size) +{ + void* ptr = heap_caps_malloc_prefer(size, + 2, + MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, + MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + return ptr != nullptr ? ptr : ::operator new(size); +} + +void MeshtasticRadioAdapter::operator delete(void* ptr) noexcept +{ + heap_caps_free(ptr); +} + +void MeshtasticRadioAdapter::operator delete(void* ptr, std::size_t) noexcept +{ + operator delete(ptr); +} + chat::MeshCapabilities MeshtasticRadioAdapter::getCapabilities() const { chat::MeshCapabilities caps{}; @@ -135,8 +156,14 @@ bool MeshtasticRadioAdapter::sendText(chat::ChannelId channel, const chat::NodeId dest = (peer != 0) ? peer : kBroadcastNodeId; const chat::MessageId msg_id = next_packet_id_++; size_t data_size = data_scratch_.size(); - if (!chat::meshtastic::encodeTextMessage(channel, text, node_id_, msg_id, dest, - data_scratch_.data(), &data_size)) + if (!chat::meshtastic::encodeTextMessage(channel, + text, + node_id_, + msg_id, + dest, + data_scratch_.data(), + &data_size, + &tx_data_scratch_)) { return false; } @@ -168,8 +195,13 @@ bool MeshtasticRadioAdapter::sendAppData(chat::ChannelId channel, } size_t data_size = data_scratch_.size(); - if (!chat::meshtastic::encodeAppData(portnum, payload, len, want_response, - data_scratch_.data(), &data_size)) + if (!chat::meshtastic::encodeAppData(portnum, + payload, + len, + want_response, + data_scratch_.data(), + &data_size, + &tx_data_scratch_)) { return false; } @@ -360,13 +392,14 @@ bool MeshtasticRadioAdapter::sendNodeInfoTo(chat::NodeId dest, request.hw_model = meshtastic_HardwareModel_PRIVATE_HW; request.mac_addr = mac_addr_; - chat::runtime::MeshtasticAnnouncementPacket packet{}; - if (!chat::runtime::MeshtasticSelfAnnouncementCore::buildNodeInfoPacket(request, &packet)) + if (!chat::runtime::MeshtasticSelfAnnouncementCore::buildNodeInfoPacket( + request, &node_info_packet_scratch_)) { return false; } - const int state = board_.transmitRadio(packet.wire, packet.wire_size); + const int state = board_.transmitRadio(node_info_packet_scratch_.wire, + node_info_packet_scratch_.wire_size); const bool ok = (state == static_cast(kRadioOk)); if (ok) { @@ -377,8 +410,8 @@ bool MeshtasticRadioAdapter::sendNodeInfoTo(chat::NodeId dest, static_cast(node_id_), static_cast(dest), static_cast(request.packet_id), - static_cast(packet.channel_hash), - static_cast(packet.wire_size), + static_cast(node_info_packet_scratch_.channel_hash), + static_cast(node_info_packet_scratch_.wire_size), ok ? 1 : 0); return ok; } @@ -398,19 +431,21 @@ bool MeshtasticRadioAdapter::sendRoutingAck(chat::NodeId dest, return false; } - meshtastic_Data data = meshtastic_Data_init_default; - data.portnum = meshtastic_PortNum_ROUTING_APP; - data.dest = dest; - data.source = node_id_; - data.request_id = request_id; - data.has_bitfield = true; - data.bitfield = 0; - data.payload.size = routing_stream.bytes_written; - std::memcpy(data.payload.bytes, routing_buf, data.payload.size); + tx_data_scratch_ = meshtastic_Data_init_default; + tx_data_scratch_.portnum = meshtastic_PortNum_ROUTING_APP; + tx_data_scratch_.dest = dest; + tx_data_scratch_.source = node_id_; + tx_data_scratch_.request_id = request_id; + tx_data_scratch_.has_bitfield = true; + tx_data_scratch_.bitfield = 0; + tx_data_scratch_.payload.size = routing_stream.bytes_written; + std::memcpy(tx_data_scratch_.payload.bytes, + routing_buf, + tx_data_scratch_.payload.size); uint8_t data_buf[128]; pb_ostream_t data_stream = pb_ostream_from_buffer(data_buf, sizeof(data_buf)); - if (!pb_encode(&data_stream, meshtastic_Data_fields, &data)) + if (!pb_encode(&data_stream, meshtastic_Data_fields, &tx_data_scratch_)) { return false; } @@ -496,9 +531,9 @@ void MeshtasticRadioAdapter::processReceivedPacket(const uint8_t* data, size_t s fill_rx_meta(rx_meta, header, last_rx_rssi_, last_rx_snr_, radio_freq_hz_, radio_bw_hz_, radio_sf_, radio_cr_); - meshtastic_Data decoded = meshtastic_Data_init_default; + rx_data_scratch_ = meshtastic_Data_init_default; pb_istream_t stream = pb_istream_from_buffer(plaintext_scratch_.data(), plaintext_len); - if (!pb_decode(&stream, meshtastic_Data_fields, &decoded)) + if (!pb_decode(&stream, meshtastic_Data_fields, &rx_data_scratch_)) { ESP_LOGW(kTag, "rx drop data_decode_fail from=%08lX id=%08lX plain=%u", @@ -514,19 +549,20 @@ void MeshtasticRadioAdapter::processReceivedPacket(const uint8_t* data, size_t s const bool is_broadcast = (header.to == kBroadcastNodeId); const bool want_ack = (header.flags & chat::meshtastic::PACKET_FLAGS_WANT_ACK_MASK) != 0; const bool want_response = - decoded.want_response || - (decoded.has_bitfield && ((decoded.bitfield & kBitfieldWantResponseMask) != 0)); + rx_data_scratch_.want_response || + (rx_data_scratch_.has_bitfield && + ((rx_data_scratch_.bitfield & kBitfieldWantResponseMask) != 0)); if (want_ack && to_us) { (void)sendRoutingAck(header.from, header.id, channel); } - if (chat::meshtastic::isNodeMetadataPayload(decoded.portnum)) + if (chat::meshtastic::isNodeMetadataPayload(rx_data_scratch_.portnum)) { - if (decoded.payload.size > 0) + if (rx_data_scratch_.payload.size > 0) { - const bool published = publishNodePayload(decoded, + const bool published = publishNodePayload(rx_data_scratch_, rx_meta, header.from, to_channel_index(channel)); @@ -534,11 +570,11 @@ void MeshtasticRadioAdapter::processReceivedPacket(const uint8_t* data, size_t s "rx node payload from=%08lX id=%08lX port=%u len=%u published=%u", static_cast(header.from), static_cast(header.id), - static_cast(decoded.portnum), - static_cast(decoded.payload.size), + static_cast(rx_data_scratch_.portnum), + static_cast(rx_data_scratch_.payload.size), published ? 1U : 0U); } - if (decoded.portnum == meshtastic_PortNum_NODEINFO_APP && + if (rx_data_scratch_.portnum == meshtastic_PortNum_NODEINFO_APP && want_response && (to_us || is_broadcast)) { (void)sendNodeInfoTo(header.from, false, channel); @@ -546,11 +582,12 @@ void MeshtasticRadioAdapter::processReceivedPacket(const uint8_t* data, size_t s return; } - if (decoded.portnum == meshtastic_PortNum_POSITION_APP && decoded.payload.size > 0) + if (rx_data_scratch_.portnum == meshtastic_PortNum_POSITION_APP && + rx_data_scratch_.payload.size > 0) { chat::meshtastic::DecodedPositionPayload position{}; if (chat::meshtastic::decodePositionPayload( - decoded, + rx_data_scratch_, header.from, rx_meta.rx_timestamp_s, &position)) @@ -568,22 +605,25 @@ void MeshtasticRadioAdapter::processReceivedPacket(const uint8_t* data, size_t s "rx position decode_fail from=%08lX id=%08lX len=%u", static_cast(header.from), static_cast(header.id), - static_cast(decoded.payload.size)); + static_cast(rx_data_scratch_.payload.size)); } } - if (decoded.portnum == meshtastic_PortNum_ROUTING_APP) + if (rx_data_scratch_.portnum == meshtastic_PortNum_ROUTING_APP) { ESP_LOGI(kTag, "rx routing from=%08lX id=%08lX len=%u", static_cast(header.from), static_cast(header.id), - static_cast(decoded.payload.size)); + static_cast(rx_data_scratch_.payload.size)); return; } chat::MeshIncomingText incoming_text{}; - if (chat::meshtastic::decodeTextMessage(plaintext_scratch_.data(), plaintext_len, &incoming_text)) + if (chat::meshtastic::decodeTextMessage(plaintext_scratch_.data(), + plaintext_len, + &incoming_text, + &rx_data_scratch_)) { incoming_text.from = header.from; incoming_text.to = header.to; @@ -622,14 +662,14 @@ void MeshtasticRadioAdapter::processReceivedPacket(const uint8_t* data, size_t s return; } - if (decoded.payload.size > 0) + if (rx_data_scratch_.payload.size > 0) { chat::MeshIncomingData incoming_data{}; - incoming_data.portnum = decoded.portnum; + incoming_data.portnum = rx_data_scratch_.portnum; incoming_data.from = header.from; incoming_data.to = header.to; incoming_data.packet_id = header.id; - incoming_data.request_id = decoded.request_id; + incoming_data.request_id = rx_data_scratch_.request_id; incoming_data.channel = channel; incoming_data.channel_hash = header.channel; incoming_data.hop_limit = header.flags & chat::meshtastic::PACKET_FLAGS_HOP_LIMIT_MASK; @@ -637,8 +677,8 @@ void MeshtasticRadioAdapter::processReceivedPacket(const uint8_t* data, size_t s incoming_data.rx_meta = rx_meta; chat::infra::IncomingQueuePushReport report{}; if (!data_queue_.push(incoming_data, - decoded.payload.bytes, - decoded.payload.size, + rx_data_scratch_.payload.bytes, + rx_data_scratch_.payload.size, chat::infra::IncomingQueuePriority::P1User, &report)) { @@ -646,8 +686,8 @@ void MeshtasticRadioAdapter::processReceivedPacket(const uint8_t* data, size_t s "rx appdata queue drop from=%08lX id=%08lX port=%u len=%u depth=%u", static_cast(header.from), static_cast(header.id), - static_cast(decoded.portnum), - static_cast(decoded.payload.size), + static_cast(rx_data_scratch_.portnum), + static_cast(rx_data_scratch_.payload.size), static_cast(data_queue_.size())); return; } @@ -663,8 +703,8 @@ void MeshtasticRadioAdapter::processReceivedPacket(const uint8_t* data, size_t s static_cast(header.from), static_cast(header.to), static_cast(header.id), - static_cast(decoded.portnum), - static_cast(decoded.payload.size)); + static_cast(rx_data_scratch_.portnum), + static_cast(rx_data_scratch_.payload.size)); } else { @@ -672,7 +712,7 @@ void MeshtasticRadioAdapter::processReceivedPacket(const uint8_t* data, size_t s "rx no business payload from=%08lX id=%08lX port=%u", static_cast(header.from), static_cast(header.id), - static_cast(decoded.portnum)); + static_cast(rx_data_scratch_.portnum)); } } diff --git a/platform/esp/radio/meshtastic_radio_adapter.h b/platform/esp/radio/meshtastic_radio_adapter.h index 8b76932d..593b2630 100644 --- a/platform/esp/radio/meshtastic_radio_adapter.h +++ b/platform/esp/radio/meshtastic_radio_adapter.h @@ -7,6 +7,7 @@ #include "chat/infra/meshtastic/mt_codec_pb.h" #include "chat/infra/meshtastic/mt_dedup.h" #include "chat/ports/i_mesh_adapter.h" +#include "chat/runtime/meshtastic_self_announcement_core.h" #include "idf_lora_radio_pump.h" #include @@ -21,6 +22,10 @@ class MeshtasticRadioAdapter final : public chat::IMeshAdapter public: explicit MeshtasticRadioAdapter(LoraBoard& board); + static void* operator new(std::size_t size); + static void operator delete(void* ptr) noexcept; + static void operator delete(void* ptr, std::size_t size) noexcept; + chat::MeshCapabilities getCapabilities() const override; bool sendText(chat::ChannelId channel, const std::string& text, chat::MessageId* out_msg_id, chat::NodeId peer = 0) override; @@ -105,6 +110,9 @@ class MeshtasticRadioAdapter final : public chat::IMeshAdapter std::array wire_scratch_{}; std::array payload_scratch_{}; std::array plaintext_scratch_{}; + meshtastic_Data tx_data_scratch_ = meshtastic_Data_init_default; + meshtastic_Data rx_data_scratch_ = meshtastic_Data_init_default; + chat::runtime::MeshtasticAnnouncementPacket node_info_packet_scratch_{}; }; } // namespace platform::esp::radio diff --git a/platform/linux/common/src/ui/widgets/map/map_tiles.cpp b/platform/linux/common/src/ui/widgets/map/map_tiles.cpp index 910466f6..9b0bcbf1 100644 --- a/platform/linux/common/src/ui/widgets/map/map_tiles.cpp +++ b/platform/linux/common/src/ui/widgets/map/map_tiles.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -87,27 +88,39 @@ class StdMapTileFileSystem final : public ui::map_tiles::IMapTileFileSystem return path && std::filesystem::is_directory(std::filesystem::path(path)); } - bool readFile(const char* path, - uint8_t* buffer, - std::size_t capacity, - std::size_t& out_size) const override + ui::map_tiles::MapTileReadResult readFile( + const char* path, + uint8_t* buffer, + std::size_t capacity) const override { - out_size = 0; if (!path || !buffer || capacity == 0) { - return false; + return {ui::map_tiles::MapTileReadStatus::Invalid, 0, -4}; } FILE* file = std::fopen(path, "rb"); if (!file) { - return false; + return {errno == ENOENT ? ui::map_tiles::MapTileReadStatus::Missing + : ui::map_tiles::MapTileReadStatus::Error, + 0, + errno == ENOENT ? -1 : -2}; } - out_size = std::fread(buffer, 1, capacity, file); + const std::size_t bytes_read = std::fread(buffer, 1, capacity, file); const bool ok = std::ferror(file) == 0; - std::fclose(file); - return ok; + const int close_result = std::fclose(file); + if (!ok || close_result != 0) + { + return {ui::map_tiles::MapTileReadStatus::Error, + bytes_read, + -2}; + } + if (bytes_read == 0) + { + return {ui::map_tiles::MapTileReadStatus::Invalid, 0, -5}; + } + return {ui::map_tiles::MapTileReadStatus::Ready, bytes_read, 0}; } }; diff --git a/platform/shared/include/board/sd_utils.h b/platform/shared/include/board/sd_utils.h index 875fd65b..339658c6 100644 --- a/platform/shared/include/board/sd_utils.h +++ b/platform/shared/include/board/sd_utils.h @@ -37,8 +37,10 @@ inline void resetSharedSpiForSd(int sd_cs, const int* extra_cs, size_t extra_cs_ { releaseSdBusDevices(sd_cs, extra_cs, extra_cs_count); pinMode(MISO, INPUT_PULLUP); - SPI.end(); - delay(2); + // SPIClass::begin() is idempotent while the shared controller is active. + // Do not call SPI.end() here: the display already owns the controller + // configuration, and tearing it down between SD retries can invalidate + // the next display transaction. SPI.begin(SCK, MISO, MOSI); releaseSdBusDevices(sd_cs, extra_cs, extra_cs_count); delay(2); diff --git a/platformio.ini b/platformio.ini index cdbbe308..24d2dd20 100644 --- a/platformio.ini +++ b/platformio.ini @@ -84,7 +84,7 @@ lib_deps = lewisxhe/SensorLib @ 0.3.3 mikalhart/TinyGPSPlus @ 1.0.3 earlephilhower/ESP8266Audio @ 2.0.0 - greiman/SdFat @ 2.3.1 + file://third_party/sdfat nanopb/nanopb @ ^0.4.8 rweather/Crypto @ 0.4.0 diff --git a/scripts/check_esp_stack_hygiene.py b/scripts/check_esp_stack_hygiene.py index c02b7e1a..e1075600 100644 --- a/scripts/check_esp_stack_hygiene.py +++ b/scripts/check_esp_stack_hygiene.py @@ -20,6 +20,11 @@ HOT_PATH_PREFIXES = ( "platform/esp/arduino_common/src/chat/infra/meshtastic/", "platform/esp/arduino_common/src/chat/infra/reticulum/", "platform/esp/arduino_common/src/chat/infra/rnode/", + "platform/esp/radio/", + "modules/core_chat/src/infra/meshtastic/", + "modules/core_chat/src/runtime/", + "modules/core_mesh/src/protocol/meshtastic/", + "modules/core_mesh/src/usecase/", "modules/core_phone/src/meshtastic/", ) @@ -64,6 +69,9 @@ HIGH_RISK_PROTOCOL_TYPES = ( "EncodedAirPacketSet", "chat::rnode::EncodedAirPacketSet", "QueuedPacket", + "RadioRxPacket", + "EncodedPacket", + "MeshProtocolEvent", ) HIGH_RISK_CONFIG_TYPES = ( diff --git a/scripts/platformio-pre.py b/scripts/platformio-pre.py index fd4752ea..c3e3c121 100644 --- a/scripts/platformio-pre.py +++ b/scripts/platformio-pre.py @@ -466,7 +466,7 @@ def configure_sdfat_for_sx1262_esp32(): ): return - sdfat_dir = os.path.join(project_dir, ".pio", "libdeps", pio_env, "SdFat") + sdfat_dir = os.path.join(project_dir, "third_party", "sdfat") library_json_path = os.path.join(sdfat_dir, "library.json") # Preserve FAT32/exFAT and the generic Arduino SPI path while excluding # formatters, streams, debug helpers, and other architectures' drivers. diff --git a/third_party/sdfat/library.json b/third_party/sdfat/library.json new file mode 100644 index 00000000..0e96d553 --- /dev/null +++ b/third_party/sdfat/library.json @@ -0,0 +1,39 @@ +{ + "name": "SdFat", + "version": "2.3.1-trailmate.1", + "frameworks": [ + "arduino" + ], + "platforms": [ + "*" + ], + "build": { + "srcFilter": [ + "-<*>", + "+", + "+", + "+", + "+", + "+", + "+", + "+", + "+", + "+", + "+", + "+", + "+", + "+", + "+", + "+", + "+", + "+", + "+", + "+", + "+", + "+", + "+", + "+", + "+" + ] + } +} diff --git a/third_party/sdfat/src/SdCard/SdSpiCard/SdSpiCard.cpp b/third_party/sdfat/src/SdCard/SdSpiCard/SdSpiCard.cpp index bb491a11..caaac212 100644 --- a/third_party/sdfat/src/SdCard/SdSpiCard/SdSpiCard.cpp +++ b/third_party/sdfat/src/SdCard/SdSpiCard/SdSpiCard.cpp @@ -145,7 +145,14 @@ bool SdSpiCard::begin(SdSpiConfig spiConfig) { spiBegin(spiConfig); m_beginCalled = true; +#if defined(TRAIL_MATE_SDFAT_SHARED_SPI) + if (!spiStart()) { + sdError(SD_CARD_ERROR_INIT_NOT_CALLED); + goto fail; + } +#else spiStart(); +#endif // must supply min of 74 clock cycles with CS high. spiUnselect(); @@ -248,7 +255,13 @@ uint8_t SdSpiCard::cardCommand(uint8_t cmd, uint32_t arg) { } // select card if (!m_spiActive) { +#if defined(TRAIL_MATE_SDFAT_SHARED_SPI) + if (!spiStart()) { + return 0XFF; + } +#else spiStart(); +#endif } if (cmd != CMD0 && cmd != CMD12 && !waitReady(SD_CMD_TIMEOUT)) { return 0XFF; @@ -347,7 +360,13 @@ bool SdSpiCard::isBusy() { } bool spiActive = m_spiActive; if (!spiActive) { +#if defined(TRAIL_MATE_SDFAT_SHARED_SPI) + if (!spiStart()) { + return true; + } +#else spiStart(); +#endif } bool rtn = 0XFF != spiReceive(); if (!spiActive) { @@ -578,15 +597,28 @@ bool SdSpiCard::setDedicatedSpi(bool value) { #endif // ENABLE_DEDICATED_SPI } //------------------------------------------------------------------------------ +#if defined(TRAIL_MATE_SDFAT_SHARED_SPI) +bool SdSpiCard::spiStart() { +#else void SdSpiCard::spiStart() { +#endif SPI_ASSERT_NOT_ACTIVE; if (!m_spiActive) { +#if defined(TRAIL_MATE_SDFAT_SHARED_SPI) + if (!spiActivate()) { + return false; + } +#else spiActivate(); +#endif m_spiActive = true; spiSelect(); // Dummy byte to drive MISO busy status. spiSend(0XFF); } +#if defined(TRAIL_MATE_SDFAT_SHARED_SPI) + return true; +#endif } //------------------------------------------------------------------------------ void SdSpiCard::spiStop() { diff --git a/third_party/sdfat/src/SdCard/SdSpiCard/SdSpiCard.h b/third_party/sdfat/src/SdCard/SdSpiCard/SdSpiCard.h index 4be12a8b..363462e0 100644 --- a/third_party/sdfat/src/SdCard/SdSpiCard/SdSpiCard.h +++ b/third_party/sdfat/src/SdCard/SdSpiCard/SdSpiCard.h @@ -297,13 +297,21 @@ class SdSpiCard { bool readData(uint8_t* dst, size_t count); bool readRegister(uint8_t cmd, void* buf); void spiSelect() { sdCsWrite(m_csPin, false); } +#if defined(TRAIL_MATE_SDFAT_SHARED_SPI) + bool spiStart(); +#else void spiStart(); +#endif void spiStop(); void spiUnselect() { sdCsWrite(m_csPin, true); } bool waitReady(uint16_t ms); bool writeData(uint8_t token, const uint8_t* src); #if SPI_DRIVER_SELECT < 2 +#if defined(TRAIL_MATE_SDFAT_SHARED_SPI) + bool spiActivate() { return m_spiDriver.activate(); } +#else void spiActivate() { m_spiDriver.activate(); } +#endif void spiBegin(SdSpiConfig spiConfig) { m_spiDriver.begin(spiConfig); } void spiDeactivate() { m_spiDriver.deactivate(); } void spiEnd() { m_spiDriver.end(); } diff --git a/third_party/sdfat/src/SdCard/SdSpiCard/SpiDriver/SdSpiArduinoDriver.h b/third_party/sdfat/src/SdCard/SdSpiCard/SpiDriver/SdSpiArduinoDriver.h index 561aaa8c..db18e89d 100644 --- a/third_party/sdfat/src/SdCard/SdSpiCard/SpiDriver/SdSpiArduinoDriver.h +++ b/third_party/sdfat/src/SdCard/SdSpiCard/SpiDriver/SdSpiArduinoDriver.h @@ -27,6 +27,9 @@ * \brief SpiDriver classes for Arduino compatible systems. */ #pragma once +#if defined(TRAIL_MATE_SDFAT_SHARED_SPI) +#include "platform/esp/arduino_common/storage/sd_spi_bus_hooks.h" +#endif //============================================================================== #if SPI_DRIVER_SELECT == 0 && SD_HAS_CUSTOM_SPI #define SD_USE_CUSTOM_SPI 1 @@ -40,7 +43,11 @@ class SdSpiArduinoDriver { /** Constructor. */ SdSpiArduinoDriver() = default; /** Activate SPI hardware. */ +#if defined(TRAIL_MATE_SDFAT_SHARED_SPI) + bool activate(); +#else void activate(); +#endif /** Initialize the SPI bus. * * \param[in] spiConfig SD card configuration. @@ -85,6 +92,9 @@ class SdSpiArduinoDriver { private: SPIClass* m_spi = nullptr; SPISettings m_spiSettings; +#if defined(TRAIL_MATE_SDFAT_SHARED_SPI) + sys::runtime::BusAccessToken m_busToken{}; +#endif }; /** Typedef for use of SdSpiArduinoDriver */ typedef SdSpiArduinoDriver SdSpiDriver; diff --git a/third_party/sdfat/src/SdCard/SdSpiCard/SpiDriver/SdSpiLibDriver.h b/third_party/sdfat/src/SdCard/SdSpiCard/SpiDriver/SdSpiLibDriver.h index 6197171a..89d8c201 100644 --- a/third_party/sdfat/src/SdCard/SdSpiCard/SpiDriver/SdSpiLibDriver.h +++ b/third_party/sdfat/src/SdCard/SdSpiCard/SpiDriver/SdSpiLibDriver.h @@ -28,9 +28,19 @@ */ #pragma once //------------------------------------------------------------------------------ +#if defined(TRAIL_MATE_SDFAT_SHARED_SPI) +inline bool SdSpiArduinoDriver::activate() { + if (!platform::esp::arduino_common::storage::sd_spi_bus_acquire(m_busToken)) { + return false; + } + m_spi->beginTransaction(m_spiSettings); + return true; +} +#else inline void SdSpiArduinoDriver::activate() { m_spi->beginTransaction(m_spiSettings); } +#endif //------------------------------------------------------------------------------ inline void SdSpiArduinoDriver::begin(SdSpiConfig spiConfig) { if (spiConfig.spiPort) { @@ -49,7 +59,13 @@ inline void SdSpiArduinoDriver::begin(SdSpiConfig spiConfig) { //------------------------------------------------------------------------------ inline void SdSpiArduinoDriver::end() { m_spi->end(); } //------------------------------------------------------------------------------ -inline void SdSpiArduinoDriver::deactivate() { m_spi->endTransaction(); } +inline void SdSpiArduinoDriver::deactivate() { + m_spi->endTransaction(); +#if defined(TRAIL_MATE_SDFAT_SHARED_SPI) + platform::esp::arduino_common::storage::sd_spi_bus_release(m_busToken); + m_busToken = {}; +#endif +} //------------------------------------------------------------------------------ inline uint8_t SdSpiArduinoDriver::receive() { return m_spi->transfer(0XFF); } //------------------------------------------------------------------------------ diff --git a/variants/lilygo_tlora_pager/envs/tlora_pager.ini b/variants/lilygo_tlora_pager/envs/tlora_pager.ini index 38a05f70..9cb2fdd7 100644 --- a/variants/lilygo_tlora_pager/envs/tlora_pager.ini +++ b/variants/lilygo_tlora_pager/envs/tlora_pager.ini @@ -26,6 +26,7 @@ build_flags = -D MESHCORE_LOG_ENABLE=1 -D LORA_LOG_ENABLE=1 -D APP_EVENT_LOG_ENABLE=1 + -DTRAIL_MATE_SDFAT_SHARED_SPI=1 -I variants/lilygo_tlora_pager lib_deps = ${arduino_base.lib_deps} @@ -70,6 +71,7 @@ build_flags = -D MESHCORE_LOG_ENABLE=1 -D LORA_LOG_ENABLE=1 -D APP_EVENT_LOG_ENABLE=1 + -DTRAIL_MATE_SDFAT_SHARED_SPI=1 -I variants/lilygo_tlora_pager lib_deps = ${arduino_base.lib_deps} @@ -112,6 +114,7 @@ build_flags = -D MESHCORE_LOG_ENABLE=1 -D LORA_LOG_ENABLE=1 -D APP_EVENT_LOG_ENABLE=1 + -DTRAIL_MATE_SDFAT_SHARED_SPI=1 -I variants/lilygo_tlora_pager lib_deps = ${arduino_base.lib_deps} diff --git a/variants/tdeck/envs/tdeck.ini b/variants/tdeck/envs/tdeck.ini index 77f34ca1..a00b6700 100644 --- a/variants/tdeck/envs/tdeck.ini +++ b/variants/tdeck/envs/tdeck.ini @@ -32,6 +32,7 @@ build_flags = -D MESHCORE_LOG_ENABLE=1 -D LORA_LOG_ENABLE=1 -D APP_EVENT_LOG_ENABLE=1 + -DTRAIL_MATE_SDFAT_SHARED_SPI=1 -I variants/tdeck lib_deps = ${arduino_base.lib_deps} @@ -78,6 +79,7 @@ build_flags = -D MESHCORE_LOG_ENABLE=1 -D LORA_LOG_ENABLE=1 -D APP_EVENT_LOG_ENABLE=1 + -DTRAIL_MATE_SDFAT_SHARED_SPI=1 -I variants/tdeck lib_deps = ${arduino_base.lib_deps} diff --git a/variants/tdeck_pro/envs/tdeck_pro.ini b/variants/tdeck_pro/envs/tdeck_pro.ini index 871bfcc4..bc022119 100644 --- a/variants/tdeck_pro/envs/tdeck_pro.ini +++ b/variants/tdeck_pro/envs/tdeck_pro.ini @@ -37,6 +37,7 @@ build_flags = -DUSING_INPUT_DEV_KEYBOARD -DTRAIL_MATE_TDECK_PRO=1 -DTRAIL_MATE_TDECK_PRO_A7682E=1 + -DTRAIL_MATE_SDFAT_SHARED_SPI=1 lib_deps = ${arduino_base.lib_deps} zinggjm/GxEPD2 @ 1.5.9 @@ -94,6 +95,7 @@ build_flags = -DUSING_INPUT_DEV_KEYBOARD -DTRAIL_MATE_TDECK_PRO=1 -DTRAIL_MATE_TDECK_PRO_PCM512A=1 + -DTRAIL_MATE_SDFAT_SHARED_SPI=1 lib_deps = ${arduino_base.lib_deps} zinggjm/GxEPD2 @ 1.5.9