From 7653316083f2086e22293b2998712639a428b951 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 31 Aug 2026 17:30:42 -0500 Subject: [PATCH] fix(screen): route remote input to MUI directly, not through the InputBroker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MUI builds never construct an InputBroker — Modules.cpp skips it (along with SystemCommands and buzzer feedback) whenever displaymode is COLOR — so AdminMessage.send_input_event hit AdminModule's null-broker guard and died: remote control was silently inert on every MUI device. AdminModule now offers the event to graphics::muiInjectInputEvent first, which maps it onto device-ui's virtual LVGL devices and reports whether it consumed it, falling through to the broker on BaseUI builds. Verified on a T-Deck with a headless mirror client: injecting USER_PRESS at (35,105) navigates MUI to Group Channels. --- src/graphics/ScreenMirror.h | 10 ++++ src/graphics/tftSetup.cpp | 104 ++++++++++++++++-------------------- src/modules/AdminModule.cpp | 8 +++ 3 files changed, 64 insertions(+), 58 deletions(-) diff --git a/src/graphics/ScreenMirror.h b/src/graphics/ScreenMirror.h index 5c8231a51f..ac9b053521 100644 --- a/src/graphics/ScreenMirror.h +++ b/src/graphics/ScreenMirror.h @@ -124,6 +124,16 @@ class ScreenMirror extern ScreenMirror screenMirror; +#if HAS_TFT +/** + * Routes a remote input event straight into device-ui's injection seam. + * MUI builds never construct an InputBroker (Modules.cpp skips it when + * displaymode is COLOR), so admin input cannot travel the usual path. + * Returns false when MUI is not the active UI. + */ +bool muiInjectInputEvent(uint32_t eventCode, uint32_t kbChar, uint32_t touchX, uint32_t touchY); +#endif + } // namespace graphics #endif diff --git a/src/graphics/tftSetup.cpp b/src/graphics/tftSetup.cpp index 20e097a964..722edc4d41 100644 --- a/src/graphics/tftSetup.cpp +++ b/src/graphics/tftSetup.cpp @@ -3,6 +3,7 @@ #include "SPILock.h" #include "sleep.h" +#include "NodeDB.h" // config #include "api/PacketAPI.h" #include "comms/PacketClient.h" #include "comms/PacketServer.h" @@ -11,6 +12,7 @@ #include "graphics/driver/DisplayDriver.h" #include "graphics/driver/DisplayDriverConfig.h" #include "input/InputBroker.h" +#include "mesh/Throttle.h" #if defined(MESHTASTIC_MUI_MIRROR) #include "input/InputDriver.h" #endif @@ -321,72 +323,58 @@ class ReentrantSpiLock : public ISpiLock static ReentrantSpiLock reentrantSpiLock; #if HAS_SCREEN_MIRROR && defined(MESHTASTIC_MUI_MIRROR) -// Bridges InputBroker events (remote AdminMessage.send_input_event) into -// device-ui's virtual input devices. Note the LEFT/RIGHT cross-map: the -// broker's codes were modeled on LVGL keys but those two are swapped. -class MuiInputBridge +namespace graphics { - public: - void attach() - { - if (!attached && inputBroker) { - inputObserver.observe(inputBroker); - attached = true; - } - } +// Maps a remote input event onto device-ui's virtual LVGL devices. Note the +// LEFT/RIGHT cross-map: the broker's codes were modeled on LVGL keys but +// those two are swapped. +bool muiInjectInputEvent(uint32_t eventCode, uint32_t kbChar, uint32_t touchX, uint32_t touchY) +{ + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) + return false; - private: - int onInputEvent(const InputEvent *event) - { - constexpr uint16_t longPressHoldMs = 600; - switch (event->inputEvent) { - case INPUT_BROKER_UP: - InputDriver::injectKey(LV_KEY_UP); - break; - case INPUT_BROKER_DOWN: - InputDriver::injectKey(LV_KEY_DOWN); - break; - case INPUT_BROKER_LEFT: - InputDriver::injectKey(LV_KEY_LEFT); - break; - case INPUT_BROKER_RIGHT: - InputDriver::injectKey(LV_KEY_RIGHT); - break; - case INPUT_BROKER_SELECT: - if (event->touchX || event->touchY) - InputDriver::injectTouch(event->touchX, event->touchY, longPressHoldMs); - else - InputDriver::injectKey(LV_KEY_ENTER); - break; - case INPUT_BROKER_USER_PRESS: - InputDriver::injectTouch(event->touchX, event->touchY); - break; - case INPUT_BROKER_BACK: - case INPUT_BROKER_CANCEL: - InputDriver::injectKey(LV_KEY_ESC); - break; - default: - if (event->kbchar) - InputDriver::injectKey(event->kbchar); - break; - } - return 0; + constexpr uint16_t longPressHoldMs = 600; + switch (eventCode) { + case INPUT_BROKER_UP: + InputDriver::injectKey(LV_KEY_UP); + break; + case INPUT_BROKER_DOWN: + InputDriver::injectKey(LV_KEY_DOWN); + break; + case INPUT_BROKER_LEFT: + InputDriver::injectKey(LV_KEY_LEFT); + break; + case INPUT_BROKER_RIGHT: + InputDriver::injectKey(LV_KEY_RIGHT); + break; + case INPUT_BROKER_SELECT: + if (touchX || touchY) + InputDriver::injectTouch(touchX, touchY, longPressHoldMs); + else + InputDriver::injectKey(LV_KEY_ENTER); + break; + case INPUT_BROKER_USER_PRESS: + InputDriver::injectTouch(touchX, touchY); + break; + case INPUT_BROKER_BACK: + case INPUT_BROKER_CANCEL: + InputDriver::injectKey(LV_KEY_ESC); + break; + default: + if (kbChar) + InputDriver::injectKey(kbChar); + else + return false; + break; } - - bool attached = false; - CallbackObserver inputObserver = - CallbackObserver(this, &MuiInputBridge::onInputEvent); -}; -static MuiInputBridge muiInputBridge; + return true; +} +} // namespace graphics #endif void tft_task_handler(void *param = nullptr) { while (true) { -#if HAS_SCREEN_MIRROR && defined(MESHTASTIC_MUI_MIRROR) - // inputBroker is created in setupModules, after tftSetup: attach lazily. - muiInputBridge.attach(); -#endif // No lock held here on purpose - device-ui guards its own SPI access. deviceScreen->task_handler(); deviceScreen->sleep(); diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index e2af8abb14..dbe9b817c9 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -2185,6 +2185,14 @@ void AdminModule::handleSendInputEvent(const meshtastic_AdminMessage_InputEvent // Wake the device if asleep powerFSM.trigger(EVENT_INPUT); + +#if HAS_SCREEN_MIRROR && defined(MESHTASTIC_MUI_MIRROR) + // MUI builds never construct an InputBroker (Modules.cpp skips it when + // displaymode is COLOR), so remote input reaches the LVGL UI directly. + if (graphics::muiInjectInputEvent(inputEvent.event_code, inputEvent.kb_char, inputEvent.touch_x, inputEvent.touch_y)) + return; +#endif + #if !defined(MESHTASTIC_EXCLUDE_INPUTBROKER) // Inject the event through InputBroker if (inputBroker) {