Files
pyxis/lib/tdeck_ui/UI/LXMF/OutgoingSendMailbox.h
T
Torlando 1c688608b5 fix(lxmf): move outgoing-send persistence off the LVGL task
Every message send on the device was deterministically rebooting it:
send_message() ran the full pipeline (identity recall, message
construction, RouterLock-scoped router admission, and LittleFS
persistence) synchronously on LVGL's 8 KiB task while holding the LVGL
mutex. On this device's degraded filesystem a single save takes ~7s of
400ms-2s per-op gaps, tripping the 5s LVGL deadlock guard and asserting
at LVGLLock.h:45 (assert failed: LVGL mutex timeout (5s)). The receive
path already carries the fix pattern for exactly this failure class
(see on_message_received); the send path never got it.

Restructure the send path as a mailbox handoff, following the existing
CallStartMailbox / LocationShareCommandMailbox precedent:

- send_message() (LVGL task) now only validates and publishes
  (destination, content, source) into a mutex-guarded single-slot
  OutgoingSendMailbox. No router lock, no I/O, no message construction.
- update() services the mailbox in service_pending_sends() on the main
  loop, before the big LVGL_LOCK() — the only place in the send path
  that may take the router lock, block on admission, or wait on
  LittleFS.
- On acceptance, a brief LVGL_LOCK in apply_outbound_result() commits
  the UI (add_message / clear_composer / compose->chat navigation,
  route-guarded). The admitted packed form is unpacked for display
  with incoming/state flags restored.
- On rejection (storage error, router busy, queue full) the user's
  input is retained for retry, matching the old behavior.

The 500-char UI cap bounds the mailbox payload.

Build tdeck SUCCESS, 170/170 contract tests pass.
2026-09-05 05:23:02 +00:00

76 lines
2.4 KiB
C++

// Copyright (c) 2024 microReticulum contributors
// SPDX-License-Identifier: MIT
#ifndef UI_LXMF_OUTGOINGSENDBOARD_H
#define UI_LXMF_OUTGOINGSENDBOARD_H
#include <cstddef>
#include <cstdint>
#include <mutex>
#include <string>
namespace UI {
namespace LXMF {
// Single-slot handoff for one outgoing message from the LVGL task to the
// Arduino main loop. The LVGL task only publishes (destination, content,
// source); the main loop performs identity recall, signing/pack, the
// RouterLock-scoped router admission, and the LittleFS persistence there, so
// a multi-second save can never hold the LVGL mutex past the 5s deadlock
// guard (LVGLLock.h:45) and trip the assert. The user's input is retained on
// the screen until the main loop confirms acceptance (see
// UIManager::apply_outbound_result).
//
// One producer (LVGL task), one consumer (main loop). The mutex covers a
// small copy (strings are moved out on take). A second send while one is
// pending is rejected and its input is retained for retry — the same UX as
// the router-busy path.
class OutgoingSendMailbox {
public:
enum class Source : uint8_t {
None,
Chat,
Compose,
};
struct Slot {
Source source = Source::None;
std::string destination; // raw peer-hash bytes (binary-safe)
std::string content; // UTF-8 message text
};
// Returns false when a send is already pending (caller retains input).
bool request(Source source, const void* destination, size_t destinationSize,
const char* content, size_t contentSize) {
std::lock_guard<std::mutex> guard(_mutex);
if (_slot.source != Source::None) return false;
_slot.source = source;
_slot.destination.assign(static_cast<const char*>(destination), destinationSize);
_slot.content.assign(content, contentSize);
return true;
}
// Returns false when nothing is pending; the slot is consumed on success.
bool take(Slot& slot) {
std::lock_guard<std::mutex> guard(_mutex);
if (_slot.source == Source::None) return false;
slot = std::move(_slot);
_slot = Slot{};
return true;
}
bool hasPending() const {
std::lock_guard<std::mutex> guard(_mutex);
return _slot.source != Source::None;
}
private:
mutable std::mutex _mutex;
Slot _slot{};
};
} // namespace LXMF
} // namespace UI
#endif // UI_LXMF_OUTGOINGSENDBOARD_H