diff --git a/protobufs b/protobufs index 7b2464c9b8c..575536388c0 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 7b2464c9b8c1521f93852261e4123826e5b25e11 +Subproject commit 575536388c04966249db9faee7b1b486b1f35aa3 diff --git a/src/configuration.h b/src/configuration.h index 0c99b6631fc..aca028095ce 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -632,6 +632,24 @@ along with this program. If not, see . #define HAS_SCREEN 0 #endif +// Display mirroring to the client (FromRadio.display_frame) rides the screen +// and can be excluded independently with MESHTASTIC_EXCLUDE_SCREEN_MIRROR. +#if HAS_SCREEN && !defined(MESHTASTIC_EXCLUDE_SCREEN_MIRROR) +#define HAS_SCREEN_MIRROR 1 +#else +#define HAS_SCREEN_MIRROR 0 +#endif + +// Mirroring MUI (device-ui/LVGL) streams RGB565 dirty rects instead of the +// 1bpp framebuffer, and needs a device-ui carrying the flush observer. Until +// that lands upstream the whole path - queue, pool and input seam - compiles +// out, so a color build that does not opt in carries none of it. +#if HAS_SCREEN_MIRROR && HAS_TFT && defined(MESHTASTIC_MUI_MIRROR) +#define HAS_MUI_MIRROR 1 +#else +#define HAS_MUI_MIRROR 0 +#endif + #ifndef USE_ETHERNET_DEFAULT #define USE_ETHERNET_DEFAULT 0 #endif diff --git a/src/graphics/HUB75Display.cpp b/src/graphics/HUB75Display.cpp index f1b0528f1f5..d6e27a484ba 100644 --- a/src/graphics/HUB75Display.cpp +++ b/src/graphics/HUB75Display.cpp @@ -3,6 +3,7 @@ #if defined(USE_HUB75) #include "HUB75Display.h" +#include "ScreenMirror.h" #include "TFTColorRegions.h" #include "TFTPalette.h" #include @@ -121,6 +122,9 @@ void HUB75Display::display() firstFrame = false; #if GRAPHICS_TFT_COLORING_ENABLED lastColorSig = colorSig; +#if HAS_SCREEN_MIRROR + graphics::screenMirror.capturePalette(colorSig, onBe, offBe, graphics::colorRegions, graphics::getTFTColorRegionCount()); +#endif // Regions are re-registered every frame by the renderers; clear so they // don't accumulate across frames. graphics::clearTFTColorRegions(); diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 689004dff85..dcbebd868de 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -23,6 +23,7 @@ along with this program. If not, see . #include "Screen.h" #include "NodeDB.h" #include "PowerMon.h" +#include "ScreenMirror.h" #include "Throttle.h" #include "configuration.h" #include "meshUtils.h" @@ -181,6 +182,18 @@ static void drawLockdownLockScreen(OLEDDisplay *display) } #endif +// Give ScreenMirror a look at the committed framebuffer (no-op unless armed). +// Deliberately scoped to updateUiFrame commits: the few direct display() +// paths it bypasses (EInk re-commits, boot logo) immediately follow or +// precede a mirrored frame. +static inline void screenMirrorCapture() +{ +#if HAS_SCREEN_MIRROR + if (screen) + screenMirror.onRendered(screen->getDisplayDevice()); +#endif +} + static inline void updateUiFrame(OLEDDisplayUi *ui) { #ifdef MESHTASTIC_LOCKDOWN @@ -208,6 +221,8 @@ static inline void updateUiFrame(OLEDDisplayUi *ui) NotificationRenderer::drawBannercallback(display, ui->getUiState()); } display->display(); + // The mirror sees the LOCKED frame, matching the panel's redaction. + screenMirrorCapture(); return; } #endif @@ -215,6 +230,7 @@ static inline void updateUiFrame(OLEDDisplayUi *ui) prepareFrameColorRegions(); #endif ui->update(); + screenMirrorCapture(); } // Global variables for alert banner - explicitly define with extern "C" linkage to prevent optimization diff --git a/src/graphics/ScreenMirror.cpp b/src/graphics/ScreenMirror.cpp new file mode 100644 index 00000000000..f56274d4980 --- /dev/null +++ b/src/graphics/ScreenMirror.cpp @@ -0,0 +1,364 @@ +#include "ScreenMirror.h" + +#if HAS_SCREEN_MIRROR + +#include "DebugConfiguration.h" +#include "concurrency/LockGuard.h" +#include "memory/MemAudit.h" +#include +#include + +#include "TFTColorRegions.h" + +namespace graphics +{ + +ScreenMirror screenMirror; + +namespace +{ +// Region colors arrive panel-byte-order (big-endian RGB565); the wire +// carries logical bit layout. +inline uint16_t swap16(uint16_t v) +{ + return (uint16_t)((v >> 8) | (v << 8)); +} +} // namespace + +void ScreenMirror::freeSnapshotLocked() +{ + if (snapshot) { + memaudit::add("display", -(int32_t)frameSize); + free(snapshot); + snapshot = nullptr; + } + frameSize = 0; + if (paletteRegions) { + memaudit::add("display", -(int32_t)(sizeof(PaletteRegion) * MAX_TFT_COLOR_REGIONS)); + free(paletteRegions); + paletteRegions = nullptr; + } + paletteSig = 0; + paletteCount = 0; +#if HAS_MUI_MIRROR + if (muiPool) { + memaudit::add("display", -(int32_t)MUI_POOL_BYTES); + free(muiPool); + muiPool = nullptr; + } + muiHead = muiCount = 0; + muiPoolUsed = 0; + muiRectSendOffset = 0; + muiOwner = nullptr; +#endif +} + +void ScreenMirror::setMirror(bool enabled) +{ + concurrency::LockGuard g(&lock); + // Cleanup must stay unconditional: a one-shot request leaves mirroring + // false, so an early return would strand the pool and the snapshot. + const bool changed = mirroring != enabled || oneShot; + mirroring = enabled; + if (enabled) { + // Force an immediate frame so the client doesn't wait for the next + // on-screen change. + oneShot = true; +#if HAS_MUI_MIRROR + if (muiRefresh) + muiRefresh(); // MUI delivers "a frame" as a full-repaint rect burst +#endif + } else { + oneShot = false; + freeSnapshotLocked(); // also releases the MUI pool and rect ownership + } + if (changed) + LOG_INFO("Screen mirror %s", enabled ? "enabled" : "disabled"); +} + +void ScreenMirror::requestFrame() +{ + concurrency::LockGuard g(&lock); + oneShot = true; +#if HAS_MUI_MIRROR + if (muiRefresh) + muiRefresh(); +#endif +} + +void ScreenMirror::onRendered(OLEDDisplay *display) +{ + uint32_t readyId = 0; + { + concurrency::LockGuard g(&lock); + if (!mirroring && !oneShot) + return; + if (!display || !display->buffer) + return; + + uint16_t w = display->getWidth(); + uint16_t h = display->getHeight(); + uint32_t fullSize = (uint32_t)w * ((h + 7) / 8); + if (fullSize == 0) + return; + if (fullSize > UINT16_MAX) { + LOG_WARN("Screen mirror: %ux%u framebuffer too large to stream", w, h); + mirroring = oneShot = false; + return; + } + uint16_t size = (uint16_t)fullSize; + + if (snapshot && size != frameSize) + freeSnapshotLocked(); // display geometry changed; start over + + bool firstFrame = !snapshot; + if (firstFrame) { + snapshot = (uint8_t *)malloc(size); + if (!snapshot) { + LOG_ERROR("Screen mirror: no memory for %u byte snapshot", size); + mirroring = oneShot = false; + return; + } + memaudit::add("display", size); + frameSize = size; + width = w; + height = h; + } + + // A palette-only change (theme recolor) is frame-worthy even when the + // mono bits are identical: the client keys colors off the frame's signature. + bool paletteChanged = snapshotPaletteSig != paletteSig; + if (!firstFrame && !oneShot && !paletteChanged && memcmp(display->buffer, snapshot, frameSize) == 0) + return; + + memcpy(snapshot, display->buffer, frameSize); + snapshotPaletteSig = paletteSig; + frameId++; + oneShot = false; + readyId = frameId; + } + // Notify outside the lock: observers (PhoneAPI) may re-enter hasChunkFor. + frameReady.notifyObservers(readyId); +} + +void ScreenMirror::capturePalette(uint32_t signature, uint16_t defaultOnBe, uint16_t defaultOffBe, const TFTColorRegion *regions, + uint8_t count) +{ + concurrency::LockGuard g(&lock); + // Cheap when nothing changed and when the mirror is idle: paint-time + // callers hit this every frame, but clients only exist while mirroring. + if (!mirroring && !oneShot && !snapshot) + return; + if (signature == paletteSig && paletteRegions) + return; + if (!paletteRegions) { + paletteRegions = (PaletteRegion *)malloc(sizeof(PaletteRegion) * MAX_TFT_COLOR_REGIONS); + if (!paletteRegions) + return; // frames still stream; clients render monochrome + memaudit::add("display", sizeof(PaletteRegion) * MAX_TFT_COLOR_REGIONS); + } + if (count > MAX_TFT_COLOR_REGIONS) + count = MAX_TFT_COLOR_REGIONS; + for (uint8_t i = 0; i < count; i++) { + const TFTColorRegion &r = regions[i]; + paletteRegions[i] = {(uint16_t)r.x, (uint16_t)r.y, (uint16_t)r.width, + (uint16_t)r.height, swap16(r.onColorBe), swap16(r.offColorBe)}; + } + paletteCount = count; + paletteDefaultOn = swap16(defaultOnBe); + paletteDefaultOff = swap16(defaultOffBe); + paletteSig = signature; +} + +bool ScreenMirror::hasPaletteChunkFor(uint32_t clientPaletteSig, uint8_t clientRegionOffset) +{ + concurrency::LockGuard g(&lock); + if (!paletteRegions || !snapshot) + return false; + return clientPaletteSig != paletteSig || clientRegionOffset < paletteCount; +} + +bool ScreenMirror::copyPaletteChunk(uint32_t &clientPaletteSig, uint8_t &clientRegionOffset, meshtastic_DisplayPalette &out) +{ + concurrency::LockGuard g(&lock); + if (!paletteRegions || !snapshot) + return false; + if (clientPaletteSig != paletteSig) { + clientPaletteSig = paletteSig; + clientRegionOffset = 0; + } else if (clientRegionOffset >= paletteCount) { + return false; + } + + out.signature = paletteSig; + out.default_on_color = paletteDefaultOn; + out.default_off_color = paletteDefaultOff; + out.region_offset = clientRegionOffset; + out.region_total = paletteCount; + uint8_t n = 0; + while (n < (sizeof(out.regions) / sizeof(out.regions[0])) && clientRegionOffset + n < paletteCount) { + const PaletteRegion &r = paletteRegions[clientRegionOffset + n]; + out.regions[n].x = r.x; + out.regions[n].y = r.y; + out.regions[n].width = r.w; + out.regions[n].height = r.h; + out.regions[n].on_color = r.onColor; + out.regions[n].off_color = r.offColor; + n++; + } + out.regions_count = n; + clientRegionOffset += n; + return true; +} + +#if HAS_MUI_MIRROR +void ScreenMirror::onMuiRect(int16_t x, int16_t y, uint16_t w, uint16_t h, const uint16_t *pixels) +{ + uint32_t readyId = 0; + { + concurrency::LockGuard g(&lock); + if (!mirroring && !oneShot) + return; + // Panel size comes from LVGL at registration, so frames always carry the + // full display dimensions the wire contract promises. + if (x < 0 || y < 0 || w == 0 || h == 0 || muiPanelW == 0) + return; + + uint32_t bytes = (uint32_t)w * h * 2; + if (!muiPool) { + bool psram = false; +#ifdef ESP32 + muiPool = (uint8_t *)ps_malloc(MUI_POOL_BYTES); // PSRAM first; this is a big buffer + psram = muiPool != nullptr; +#endif + if (!muiPool) + muiPool = (uint8_t *)malloc(MUI_POOL_BYTES); + if (!muiPool) { + LOG_ERROR("Screen mirror: no memory for the %u byte rect pool", (unsigned)MUI_POOL_BYTES); + mirroring = oneShot = false; + return; + } + if (!psram) + memaudit::add("display", MUI_POOL_BYTES); // PSRAM is not the constrained budget + } + if (bytes > MUI_POOL_BYTES) + return; // a rect larger than the whole pool can never be sent + if (muiCount >= MUI_MAX_RECTS || muiPoolUsed + bytes > MUI_POOL_BYTES) { + // The consumer is behind. Drop the backlog rather than the newest + // pixels and repaint once: stale history has no value, and dropping + // only the incoming rect wedges the pool until the queue empties. + muiHead = muiCount = 0; + muiPoolUsed = 0; + muiRectSendOffset = 0; + if (muiRefresh) + muiRefresh(); + return; + } + memcpy(muiPool + muiPoolUsed, pixels, bytes); + MuiRect &r = muiRects[(muiHead + muiCount) % MUI_MAX_RECTS]; + r = {(uint16_t)x, (uint16_t)y, w, h, bytes, muiPoolUsed, ++frameId}; + muiPoolUsed += bytes; + muiCount++; + // Only the empty->non-empty transition needs a wakeup; available() + // keeps the client draining, and a notify per flush is a storm. + if (muiCount == 1) + readyId = frameId; + } + if (readyId) + frameReady.notifyObservers(readyId); +} + +// Fills one chunk of the oldest queued rect; pops it when fully drained. +bool ScreenMirror::copyMuiChunkLocked(meshtastic_DisplayFrame &out) +{ + MuiRect &r = muiRects[muiHead]; + uint32_t len = r.bytes - muiRectSendOffset; + if (len > sizeof(out.data.bytes)) + len = sizeof(out.data.bytes); + + out.width = muiPanelW; + out.height = muiPanelH; + out.format = meshtastic_DisplayFrame_Format_RGB565; + out.palette_signature = 0; + out.frame_id = r.id; + out.rect_x = r.x; + out.rect_y = r.y; + out.rect_width = r.w; + out.rect_height = r.h; + out.offset = muiRectSendOffset; + out.total_size = r.bytes; + out.data.size = len; + memcpy(out.data.bytes, muiPool + r.poolOffset + muiRectSendOffset, len); + muiRectSendOffset += len; + + if (muiRectSendOffset >= r.bytes) { + muiHead = (muiHead + 1) % MUI_MAX_RECTS; + muiCount--; + muiRectSendOffset = 0; + if (muiCount == 0) { + muiPoolUsed = 0; + if (!mirroring) + oneShot = false; // one-shot fully delivered + } + } + return true; +} +#endif + +bool ScreenMirror::hasChunkFor(const void *client, uint32_t clientFrameId, uint16_t clientOffset) +{ + concurrency::LockGuard g(&lock); +#if HAS_MUI_MIRROR + // One shared rect cursor means one consumer; another connection simply + // sees no rects rather than stealing half of each frame. + if (muiCount && (muiOwner == nullptr || muiOwner == client)) + return true; +#endif + return snapshot && (clientFrameId != frameId || clientOffset < frameSize); +} + +bool ScreenMirror::copyChunk(const void *client, uint32_t &clientFrameId, uint16_t &clientOffset, meshtastic_DisplayFrame &out) +{ + concurrency::LockGuard g(&lock); +#if HAS_MUI_MIRROR + // MUI rects drain ahead of (and on MUI builds, instead of) mono snapshots. + // Spike scope: the rect queue has a single consumer, not per-client cursors. + if (muiCount) { + if (muiOwner == nullptr) + muiOwner = client; // first drainer claims the stream + if (muiOwner != client) + return false; + return copyMuiChunkLocked(out); + } +#endif + if (!snapshot) + return false; + if (clientFrameId != frameId) { + clientFrameId = frameId; + clientOffset = 0; + } + if (clientOffset >= frameSize) + return false; + + uint16_t len = frameSize - clientOffset; + if (len > sizeof(out.data.bytes)) + len = sizeof(out.data.bytes); + + out.width = width; + out.height = height; + out.format = meshtastic_DisplayFrame_Format_MONO_VLSB; + // The signature captured WITH this snapshot, not the live one: a drain can + // span a display() that already advanced paletteSig for the next frame. + out.palette_signature = snapshotPaletteSig; + out.frame_id = frameId; + out.offset = clientOffset; + out.total_size = frameSize; + out.data.size = len; + memcpy(out.data.bytes, snapshot + clientOffset, len); + clientOffset += len; + return true; +} + +} // namespace graphics + +#endif diff --git a/src/graphics/ScreenMirror.h b/src/graphics/ScreenMirror.h new file mode 100644 index 00000000000..73d42a3aa94 --- /dev/null +++ b/src/graphics/ScreenMirror.h @@ -0,0 +1,149 @@ +#pragma once +#include "configuration.h" + +#if HAS_SCREEN_MIRROR + +#include "Observer.h" +#include "concurrency/Lock.h" +#include "mesh/generated/meshtastic/mesh.pb.h" + +class OLEDDisplay; + +namespace graphics +{ + +struct TFTColorRegion; + +/** + * Streams 1bpp framebuffer snapshots to local clients as + * FromRadio.display_frame chunks. Armed via AdminMessage + * set_display_mirror (continuous) or get_display_frame_request (one-shot). + * + * Holds only the latest captured frame; each PhoneAPI instance keeps its own + * (frameId, offset) drain cursor and pulls chunks via copyChunk, so multiple + * clients receive complete frames independently. + */ +class ScreenMirror +{ + public: + /// Fired when a new frame is ready to drain; PhoneAPI observes this. + Observable frameReady; + + void setMirror(bool enabled); + void requestFrame(); + + /// Called by Screen after each frame commit. Snapshots the framebuffer + /// when armed and the contents changed since the last captured frame. + void onRendered(OLEDDisplay *display); + + /// True while this client has undelivered bytes of the current frame. + /// `client` identifies the connection (its PhoneAPI instance). + bool hasChunkFor(const void *client, uint32_t clientFrameId, uint16_t clientOffset); + + /// Fills the next chunk for a client cursor, advancing it; false when the + /// client is fully caught up (or no frame exists). A frame captured while + /// the client was mid-drain restarts it at offset 0 of the new frame. + bool copyChunk(const void *client, uint32_t &clientFrameId, uint16_t &clientOffset, meshtastic_DisplayFrame &out); + + /// Called by the color display drivers at paint time, before they clear + /// the per-frame region table: stores the palette the frame was painted + /// with. Colors arrive panel-byte-order (big-endian RGB565). + void capturePalette(uint32_t signature, uint16_t defaultOnBe, uint16_t defaultOffBe, const TFTColorRegion *regions, + uint8_t count); + + /// True while the client cursor lacks regions of the current color palette. + bool hasPaletteChunkFor(uint32_t clientPaletteSig, uint8_t clientRegionOffset); + + /// Fills the next palette chunk for a client cursor, advancing it; false + /// when the client holds the full current palette (or coloring is off). + bool copyPaletteChunk(uint32_t &clientPaletteSig, uint8_t &clientRegionOffset, meshtastic_DisplayPalette &out); + +#if HAS_MUI_MIRROR + /// MUI path: queues one LVGL dirty rect (native little-endian RGB565). + /// Called on the LVGL thread via the device-ui flush observer; copies and returns. + void onMuiRect(int16_t x, int16_t y, uint16_t w, uint16_t h, const uint16_t *pixels); + + /// Registers device-ui's thread-safe full-repaint request plus the panel + /// size, so streamed frames carry the full display dimensions from the + /// first rect rather than growing into them. + using FullRefreshFn = void (*)(); + void setMuiSource(FullRefreshFn fn, uint16_t panelWidth, uint16_t panelHeight) + { + muiRefresh = fn; + muiPanelW = panelWidth; + muiPanelH = panelHeight; + } +#endif + + private: + void freeSnapshotLocked(); + + concurrency::Lock lock; + bool mirroring = false; + bool oneShot = false; + // Latest captured frame; doubles as the change-detection baseline. + uint8_t *snapshot = nullptr; + uint16_t frameSize = 0; + uint16_t width = 0; + uint16_t height = 0; + uint32_t frameId = 0; + // Signature of the palette the current snapshot was painted with; frames + // carry this, not the live paletteSig, so mid-drain captures stay coherent. + uint32_t snapshotPaletteSig = 0; + // Color-region palette captured at paint time (TFT/HUB75 builds only). + uint32_t paletteSig = 0; + uint8_t paletteCount = 0; + struct PaletteRegion { + uint16_t x, y, w, h; + uint16_t onColor, offColor; // logical RGB565 + }; + PaletteRegion *paletteRegions = nullptr; + uint16_t paletteDefaultOn = 0; + uint16_t paletteDefaultOff = 0; + +#if HAS_MUI_MIRROR + // MUI dirty-rect queue: FIFO rect headers over a linear pixel pool, + // compacted whenever it drains. Spike scope: single consumer. + struct MuiRect { + uint16_t x, y, w, h; + uint32_t bytes; + uint32_t poolOffset; + uint32_t id; + }; + static constexpr uint8_t MUI_MAX_RECTS = 64; + // Must hold one full repaint (320x240 RGB565 = 150 KB) plus concurrent + // incremental rects, or arming can never deliver a complete first frame. + static constexpr uint32_t MUI_POOL_BYTES = 192 * 1024; + MuiRect muiRects[MUI_MAX_RECTS]; + uint8_t muiHead = 0; + uint8_t muiCount = 0; + uint8_t *muiPool = nullptr; + uint32_t muiPoolUsed = 0; + uint32_t muiRectSendOffset = 0; + uint16_t muiPanelW = 0; + uint16_t muiPanelH = 0; + FullRefreshFn muiRefresh = nullptr; + const void *muiOwner = nullptr; // connection currently draining rects + + bool copyMuiChunkLocked(meshtastic_DisplayFrame &out); +#endif +}; + +extern ScreenMirror screenMirror; + +#if HAS_MUI_MIRROR +/** + * 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); + +/** Fills MUI's panel geometry for DeviceMetadata; false when MUI is not active. */ +bool muiDisplayInfo(uint16_t &width, uint16_t &height, bool &hasTouch); +#endif + +} // namespace graphics + +#endif diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index 07bb7e6350d..2d83e3a641e 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -1455,6 +1455,7 @@ static LGFX *tft = nullptr; #endif #include "SPILock.h" +#include "ScreenMirror.h" #include "TFTColorRegions.h" #include "TFTDisplay.h" #include "TFTPalette.h" @@ -1620,6 +1621,11 @@ void TFTDisplay::display(bool fromBlank) haveLastDefaults = true; lastDefaultOnColor = defaultOnColor; lastDefaultOffColor = defaultOffColor; +#if HAS_SCREEN_MIRROR + // Regions are cleared below; hand the mirror the palette this frame was painted with. + graphics::screenMirror.capturePalette(colorFrameSignature, defaultOnColor, defaultOffColor, graphics::colorRegions, + graphics::getTFTColorRegionCount()); +#endif graphics::clearTFTColorRegions(); return; } @@ -1798,6 +1804,10 @@ void TFTDisplay::display(bool fromBlank) haveLastDefaults = true; lastDefaultOnColor = defaultOnColor; lastDefaultOffColor = defaultOffColor; +#if HAS_SCREEN_MIRROR && GRAPHICS_TFT_COLORING_ENABLED + graphics::screenMirror.capturePalette(colorFrameSignature, defaultOnColor, defaultOffColor, graphics::colorRegions, + graphics::getTFTColorRegionCount()); +#endif graphics::clearTFTColorRegions(); } diff --git a/src/graphics/tftSetup.cpp b/src/graphics/tftSetup.cpp index 8e971cb60b1..b761458a2bc 100644 --- a/src/graphics/tftSetup.cpp +++ b/src/graphics/tftSetup.cpp @@ -3,11 +3,18 @@ #include "SPILock.h" #include "sleep.h" +#include "NodeDB.h" // config #include "api/PacketAPI.h" #include "comms/PacketClient.h" #include "comms/PacketServer.h" #include "graphics/DeviceScreen.h" +#include "graphics/ScreenMirror.h" +#include "graphics/driver/DisplayDriver.h" #include "graphics/driver/DisplayDriverConfig.h" +#include "input/InputBroker.h" +#if HAS_MUI_MIRROR +#include "input/InputDriver.h" +#endif #include "util/ISpiLock.h" #ifdef ARCH_PORTDUINO @@ -311,6 +318,79 @@ class ReentrantSpiLock : public ISpiLock static ReentrantSpiLock reentrantSpiLock; +#if HAS_MUI_MIRROR +namespace graphics +{ +// Reports MUI's logical panel geometry for DeviceMetadata.display. The BaseUI +// `screen` object does not exist on MUI builds, so the dimensions come from +// LVGL itself (already rotated to the logical orientation). +bool muiDisplayInfo(uint16_t &width, uint16_t &height, bool &hasTouch) +{ + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) + return false; + lv_display_t *disp = lv_display_get_default(); + if (!disp) + return false; + width = (uint16_t)lv_display_get_horizontal_resolution(disp); + height = (uint16_t)lv_display_get_vertical_resolution(disp); +#if HAS_TOUCHSCREEN + hasTouch = true; +#else + hasTouch = false; +#endif + return width > 0 && height > 0; +} + +// 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; + + constexpr uint16_t longPressHoldMs = 600; + // Mirrors the trackball driver's semantics (EncoderInputDriver, type 3): + // vertical is encoder rotation, which is what actually moves focus in a + // group; horizontal becomes the slider keys, deliberately inverted there. + switch (eventCode) { + case INPUT_BROKER_UP: + InputDriver::injectEncoder(-1); + break; + case INPUT_BROKER_DOWN: + InputDriver::injectEncoder(1); + break; + case INPUT_BROKER_LEFT: + InputDriver::injectKey(LV_KEY_DOWN); + break; + case INPUT_BROKER_RIGHT: + InputDriver::injectKey(LV_KEY_UP); + 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; + } + return true; +} +} // namespace graphics +#endif + void tft_task_handler(void *param = nullptr) { while (true) { @@ -329,9 +409,28 @@ void tftSetup(void) I2CKeyboardScanner::setSecondaryBus(i2cProxy); #endif #ifndef ARCH_PORTDUINO +#if HAS_MUI_MIRROR + // Must precede DeviceScreen::init: device-ui only builds its virtual input + // devices (and the default focus group they need) when injection is asked for. + InputDriver::enableInjection(); +#endif deviceScreen = &DeviceScreen::create(reentrantSpiLock); PacketAPI::create(PacketServer::init()); deviceScreen->init(new PacketClient); +#if HAS_MUI_MIRROR + // Stream MUI's dirty rects to local clients (see graphics::ScreenMirror). + // Gated on MESHTASTIC_MUI_MIRROR until the device-ui flush observer merges + // (jamesarich/device-ui screen-mirror-poc); the vendored pin lacks it. + { + lv_display_t *disp = lv_display_get_default(); + graphics::screenMirror.setMuiSource([]() { DisplayDriver::requestFullRefresh(); }, + disp ? (uint16_t)lv_display_get_horizontal_resolution(disp) : 0, + disp ? (uint16_t)lv_display_get_vertical_resolution(disp) : 0); + } + DisplayDriver::setFlushObserver([](int16_t x, int16_t y, uint16_t w, uint16_t h, const uint16_t *px) { + graphics::screenMirror.onMuiRect(x, y, w, h, px); + }); +#endif #else if (portduino_config.displayPanel != no_screen) { DisplayDriverConfig displayConfig; diff --git a/src/main.cpp b/src/main.cpp index 80b8e4f4a93..81406600b27 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -44,6 +44,7 @@ #endif #include "detect/einkScan.h" #include "graphics/Screen.h" +#include "graphics/ScreenMirror.h" #include "main.h" #include "memory/MemAudit.h" #include "mesh/generated/meshtastic/config.pb.h" @@ -1358,6 +1359,52 @@ extern meshtastic_DeviceMetadata getDeviceMetadata() #if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA) deviceMetadata.has_xeddsa = true; #endif + +#if HAS_MUI_MIRROR + // MUI owns the panel and leaves `screen` null, so ask LVGL instead. + uint16_t muiW = 0, muiH = 0; + bool muiTouch = false; + if (graphics::muiDisplayInfo(muiW, muiH, muiTouch)) { + deviceMetadata.has_display = true; + deviceMetadata.display.width = muiW; + deviceMetadata.display.height = muiH; + deviceMetadata.display.format = meshtastic_DisplayFrame_Format_RGB565; + deviceMetadata.display.panel_class = meshtastic_DisplayInfo_PanelClass_TFT; + deviceMetadata.display.has_touch = muiTouch; + } else +#endif +#if HAS_SCREEN + if (screen) { + OLEDDisplay *dispdev = screen->getDisplayDevice(); + if (dispdev && dispdev->getWidth() > 0) { + deviceMetadata.has_display = true; + deviceMetadata.display.width = dispdev->getWidth(); + deviceMetadata.display.height = dispdev->getHeight(); + deviceMetadata.display.format = meshtastic_DisplayFrame_Format_MONO_VLSB; +#if defined(ARCH_PORTDUINO) + // The native panel is selected at runtime; UNSPECIFIED is honest until derived from portduino_config. + deviceMetadata.display.panel_class = meshtastic_DisplayInfo_PanelClass_PANEL_CLASS_UNSPECIFIED; +#elif defined(USE_EINK) + deviceMetadata.display.panel_class = meshtastic_DisplayInfo_PanelClass_EINK; +#elif defined(USE_HUB75) || defined(HAS_HUB75_NATIVE) + deviceMetadata.display.panel_class = meshtastic_DisplayInfo_PanelClass_HUB75; +// USE_TFTDISPLAY is value-tested, not defined()-tested: configuration.h +// defaults it to 0, so it is always defined and every screen build would +// otherwise report itself as a TFT. +#elif USE_TFTDISPLAY || defined(HAS_SPI_TFT) || defined(USE_ST7789) || defined(USE_ST7796) || defined(ILI9341_DRIVER) || \ + defined(ILI9342_DRIVER) + deviceMetadata.display.panel_class = meshtastic_DisplayInfo_PanelClass_TFT; +#elif defined(USE_ST7567) + deviceMetadata.display.panel_class = meshtastic_DisplayInfo_PanelClass_LCD; +#else + deviceMetadata.display.panel_class = meshtastic_DisplayInfo_PanelClass_OLED; +#endif +#if HAS_TOUCHSCREEN + deviceMetadata.display.has_touch = true; +#endif + } + } +#endif return deviceMetadata; } diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index f6757206c5f..1c2518d2661 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -23,6 +23,7 @@ #include "SPILock.h" #include "TypeConversions.h" #include "concurrency/LockGuard.h" +#include "graphics/ScreenMirror.h" #include "main.h" #include "modules/NodeInfoModule.h" #include "xmodem.h" @@ -273,6 +274,9 @@ void PhoneAPI::handleStartConfig() #ifdef FSCom observe(&xModem.packetReady); #endif +#if HAS_SCREEN_MIRROR + observe(&graphics::screenMirror.frameReady); +#endif #ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL // New physical connection: clear this PhoneAPI's auth slot so the new // client must present a passphrase or PKC admin signature before @@ -376,6 +380,17 @@ void PhoneAPI::close() unobserve(&service->fromNumChanged); #ifdef FSCom unobserve(&xModem.packetReady); +#endif +#if HAS_SCREEN_MIRROR + unobserve(&graphics::screenMirror.frameReady); + // This client is gone; PoC keeps one arming flag, so disarm and free + // the snapshot rather than stream to nobody. A surviving client + // re-arms with another set_display_mirror. + graphics::screenMirror.setMirror(false); + mirrorFrameId = 0; + mirrorOffset = 0; + mirrorPaletteSig = 0; + mirrorPaletteOffset = 0; #endif releasePhonePacket(); // Don't leak phone packets on shutdown releaseQueueStatusPhonePacket(); @@ -1094,6 +1109,16 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag; fromRadioScratch.packet = replayPkt; } +#if HAS_SCREEN_MIRROR + } else if (screenMirrorAuthorized() && graphics::screenMirror.copyPaletteChunk(mirrorPaletteSig, mirrorPaletteOffset, + fromRadioScratch.display_palette)) { + // Palette before frames so the client can colorize the first frame it renders. + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_display_palette_tag; + } else if (screenMirrorAuthorized() && + graphics::screenMirror.copyChunk(this, mirrorFrameId, mirrorOffset, fromRadioScratch.display_frame)) { + // Lowest priority: mesh traffic and notifications outrank pixels. + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_display_frame_tag; +#endif } break; @@ -1720,7 +1745,15 @@ bool PhoneAPI::available() return true; // Trailing replay drain - feeds cached satellite-DB packets alongside // (lower priority than) live traffic. - return replayPending(); + if (replayPending()) + return true; + +#if HAS_SCREEN_MIRROR + return screenMirrorAuthorized() && (graphics::screenMirror.hasPaletteChunkFor(mirrorPaletteSig, mirrorPaletteOffset) || + graphics::screenMirror.hasChunkFor(this, mirrorFrameId, mirrorOffset)); +#else + return false; +#endif } default: LOG_ERROR("PhoneAPI::available unexpected state %d", state); @@ -1904,7 +1937,9 @@ int PhoneAPI::onNotify(uint32_t newValue) // doesn't call this from idle) if (state == STATE_SEND_PACKETS) { - LOG_INFO("Tell client new packets %u", newValue); + // TRACE, not INFO: with display mirroring active this fires once per + // captured frame per client, at screen-change rate. + LOG_TRACE("Tell client new packets %u", newValue); onNowHasData(newValue); } else { LOG_DEBUG("Client not yet interested in packets (state=%d)", state); diff --git a/src/mesh/PhoneAPI.h b/src/mesh/PhoneAPI.h index ab04c178b0b..730cc2b5ae8 100644 --- a/src/mesh/PhoneAPI.h +++ b/src/mesh/PhoneAPI.h @@ -89,6 +89,27 @@ class PhoneAPI // file transfer packets destined for phone. Push it to the queue then free it. meshtastic_XModem xmodemPacketForPhone = meshtastic_XModem_init_zero; +#if HAS_SCREEN_MIRROR + // Per-connection drain cursors into ScreenMirror's current frame and + // color palette, so coexisting clients (BLE + serial + TCP) each receive + // complete frames and palettes. + uint32_t mirrorFrameId = 0; + uint16_t mirrorOffset = 0; + uint32_t mirrorPaletteSig = 0; + uint8_t mirrorPaletteOffset = 0; + + // Screen pixels carry operator content; under access control only an + // authorized client may receive them (same rule as mesh packets). + bool screenMirrorAuthorized() + { +#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL + return getAdminAuthorized(); +#else + return true; +#endif + } +#endif + // Keep QueueStatus packet just as packetForPhone meshtastic_QueueStatus *queueStatusPacketForPhone = NULL; diff --git a/src/mesh/generated/meshtastic/admin.pb.h b/src/mesh/generated/meshtastic/admin.pb.h index ccf6f54c8bc..049ca7689b6 100644 --- a/src/mesh/generated/meshtastic/admin.pb.h +++ b/src/mesh/generated/meshtastic/admin.pb.h @@ -186,7 +186,7 @@ typedef struct _meshtastic_LockdownAuth { token at unlock time: the client-supplied boots_remaining when non-zero, otherwise the firmware default (TOKEN_DEFAULT_BOOTS). Note that boots_remaining == 0 in this message means "use firmware - default", NOT "zero boots" — a client computing the ceiling for + default", NOT "zero boots" - a client computing the ceiling for display should mirror that resolution rather than multiplying the raw request value. @@ -196,7 +196,7 @@ typedef struct _meshtastic_LockdownAuth { Uses millis() (CPU uptime), not wall-clock time, so the cap is immune to GPS spoofing, RTC backup-battery removal, and Faraday - cage isolation — none of those move the uptime counter. The only + cage isolation - none of those move the uptime counter. The only way to reset the session clock is a reboot, which costs a boot from the on-flash, HMAC-bound counter. */ uint32_t max_session_seconds; @@ -213,7 +213,7 @@ typedef struct _meshtastic_LockdownAuth { NOT reversed by this operation: APPROTECT. Once the debug port lockout has been burned (on silicon where it is effective) it is - permanent — disabling lockdown decrypts your data and removes the + permanent - disabling lockdown decrypts your data and removes the access gates, but the SWD/JTAG port stays locked for the life of the device (recoverable only via a full chip erase over a debug probe, which destroys all data). Clients should make this @@ -502,6 +502,21 @@ typedef struct _meshtastic_AdminMessage { uint32_t remove_ignored_node; /* Set specified node-num to be muted */ uint32_t toggle_muted_node; + /* Request a single frame of the device's display framebuffer. + The frame is delivered to the local client as FromRadio.display_frame + chunks (see DisplayFrame in mesh.proto) - there is no AdminMessage + response. Local connection only: a node receiving this over the mesh, + or a build without a display, ignores it. During active mirroring it + forces one frame on the next redraw even if the screen is unchanged. */ + bool get_display_frame_request; + /* Enable (true) or disable (false) continuous mirroring of the device + display - unlike most bool verbs in this oneof, false is meaningful. + While enabled, the device sends a DisplayFrame after each screen + redraw that changed the framebuffer, as FromRadio.display_frame + chunks; the first frame arrives immediately and acts as the + acknowledgement. Local connection only (see get_display_frame_request) + and not persisted across reboot. */ + bool set_display_mirror; /* Begins an edit transaction for config, module config, owner, and channel settings changes This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings) */ bool begin_edit_settings; @@ -738,6 +753,8 @@ extern "C" { #define meshtastic_AdminMessage_set_ignored_node_tag 47 #define meshtastic_AdminMessage_remove_ignored_node_tag 48 #define meshtastic_AdminMessage_toggle_muted_node_tag 49 +#define meshtastic_AdminMessage_get_display_frame_request_tag 50 +#define meshtastic_AdminMessage_set_display_mirror_tag 51 #define meshtastic_AdminMessage_begin_edit_settings_tag 64 #define meshtastic_AdminMessage_commit_edit_settings_tag 65 #define meshtastic_AdminMessage_add_contact_tag 66 @@ -800,6 +817,8 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,store_ui_config,store_ui_con X(a, STATIC, ONEOF, UINT32, (payload_variant,set_ignored_node,set_ignored_node), 47) \ X(a, STATIC, ONEOF, UINT32, (payload_variant,remove_ignored_node,remove_ignored_node), 48) \ X(a, STATIC, ONEOF, UINT32, (payload_variant,toggle_muted_node,toggle_muted_node), 49) \ +X(a, STATIC, ONEOF, BOOL, (payload_variant,get_display_frame_request,get_display_frame_request), 50) \ +X(a, STATIC, ONEOF, BOOL, (payload_variant,set_display_mirror,set_display_mirror), 51) \ X(a, STATIC, ONEOF, BOOL, (payload_variant,begin_edit_settings,begin_edit_settings), 64) \ X(a, STATIC, ONEOF, BOOL, (payload_variant,commit_edit_settings,commit_edit_settings), 65) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,add_contact,add_contact), 66) \ diff --git a/src/mesh/generated/meshtastic/mesh.pb.cpp b/src/mesh/generated/meshtastic/mesh.pb.cpp index 4cf8e980cf2..e632dba9a77 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.cpp +++ b/src/mesh/generated/meshtastic/mesh.pb.cpp @@ -60,6 +60,15 @@ PB_BIND(meshtastic_QueueStatus, meshtastic_QueueStatus, AUTO) PB_BIND(meshtastic_FromRadio, meshtastic_FromRadio, 2) +PB_BIND(meshtastic_DisplayFrame, meshtastic_DisplayFrame, 2) + + +PB_BIND(meshtastic_DisplayPalette, meshtastic_DisplayPalette, 2) + + +PB_BIND(meshtastic_DisplayPalette_ColorRegion, meshtastic_DisplayPalette_ColorRegion, AUTO) + + PB_BIND(meshtastic_LockdownStatus, meshtastic_LockdownStatus, AUTO) @@ -99,6 +108,9 @@ PB_BIND(meshtastic_Neighbor, meshtastic_Neighbor, AUTO) PB_BIND(meshtastic_DeviceMetadata, meshtastic_DeviceMetadata, AUTO) +PB_BIND(meshtastic_DisplayInfo, meshtastic_DisplayInfo, AUTO) + + PB_BIND(meshtastic_LoRaPresetGroup, meshtastic_LoRaPresetGroup, AUTO) @@ -149,6 +161,10 @@ PB_BIND(meshtastic_ChunkedPayloadResponse, meshtastic_ChunkedPayloadResponse, AU + + + + diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index 375ff4861b6..5a751710143 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -670,6 +670,19 @@ typedef enum _meshtastic_LogRecord_Level { meshtastic_LogRecord_Level_TRACE = 5 } meshtastic_LogRecord_Level; +/* Pixel encodings of the framebuffer bytes. */ +typedef enum _meshtastic_DisplayFrame_Format { + /* Default; should not be sent. */ + meshtastic_DisplayFrame_Format_FORMAT_UNSPECIFIED = 0, + /* 1 bit per pixel, vertical LSB-first pages (SSD1306/OLEDDisplay layout): + byte index = x + (y / 8) * width, bit index = y % 8. */ + meshtastic_DisplayFrame_Format_MONO_VLSB = 1, + /* 16 bits per pixel, RGB565 in little-endian byte order, rows tightly + packed. Used with the partial-update rect fields: data covers only + the rectangle. Streamed by LVGL-based color UIs. */ + meshtastic_DisplayFrame_Format_RGB565 = 2 +} meshtastic_DisplayFrame_Format; + typedef enum _meshtastic_LockdownStatus_State { /* Default; should not be sent. */ meshtastic_LockdownStatus_State_STATE_UNSPECIFIED = 0, @@ -696,6 +709,24 @@ typedef enum _meshtastic_LockdownStatus_State { meshtastic_LockdownStatus_State_DISABLED = 5 } meshtastic_LockdownStatus_State; +/* Physical panel technology, as a hint for client rendering and refresh + expectations. */ +typedef enum _meshtastic_DisplayInfo_PanelClass { + /* Default; should not be sent. */ + meshtastic_DisplayInfo_PanelClass_PANEL_CLASS_UNSPECIFIED = 0, + /* Monochrome OLED (SSD1306/SH1106 family). */ + meshtastic_DisplayInfo_PanelClass_OLED = 1, + /* Monochrome LCD (ST7567 family). */ + meshtastic_DisplayInfo_PanelClass_LCD = 2, + /* Color TFT. */ + meshtastic_DisplayInfo_PanelClass_TFT = 3, + /* E-ink panel: refresh is slow and full-screen; clients should prefer + one-shot frame requests over continuous mirroring. */ + meshtastic_DisplayInfo_PanelClass_EINK = 4, + /* LED matrix (HUB75). */ + meshtastic_DisplayInfo_PanelClass_HUB75 = 5 +} meshtastic_DisplayInfo_PanelClass; + /* Struct definitions */ /* A GPS Position */ typedef struct _meshtastic_Position { @@ -1269,6 +1300,98 @@ typedef struct _meshtastic_QueueStatus { uint32_t mesh_packet_id; } meshtastic_QueueStatus; +typedef PB_BYTES_ARRAY_T(384) meshtastic_DisplayFrame_data_t; +/* A chunk of the device's display framebuffer, streamed to the local client + over BLE/serial/TCP. Frames larger than one chunk are split by byte offset; + a chunk with offset + data length == total_size completes the frame. + Chunks of one frame arrive contiguously (no other display_frame between + them; display_palette messages may interleave) and in offset order + (FromRadio is a reliable ordered stream), so clients may reassemble into + a single buffer without reordering. A frame whose streaming has begun is always drained to + completion, even if mirroring is disabled mid-frame. */ +typedef struct _meshtastic_DisplayFrame { + /* Display width in pixels. */ + uint16_t width; + /* Display height in pixels. */ + uint16_t height; + /* Pixel encoding of data. */ + meshtastic_DisplayFrame_Format format; + /* Frame counter, constant across the chunks of one frame so the client + can detect interleaving or loss. Increments per captured frame, wraps + at uint32 range, and restarts from 1 on device reboot - treat any + change as "a new frame", not as an ordering guarantee. */ + uint32_t frame_id; + /* Byte offset of this chunk within the full frame buffer. */ + uint32_t offset; + /* Total size in bytes of the full frame buffer. */ + uint32_t total_size; + /* The framebuffer bytes for this chunk. */ + meshtastic_DisplayFrame_data_t data; + /* Optional partial-update rectangle (dirty-rect streaming from LVGL-based + color UIs, format RGB565). When rect_width > 0, data carries only the + rectangle's pixels and offset/total_size describe the rectangle's own + buffer; width/height above always remain the full display dimensions, + and each rectangle is an independently completed unit under its own + frame_id. MONO_VLSB frames never set these fields. */ + uint16_t rect_x; + /* See rect_x. */ + uint16_t rect_y; + /* See rect_x. */ + uint16_t rect_width; + /* See rect_x. */ + uint16_t rect_height; + /* Identity of the DisplayPalette that colorizes this frame, matching + DisplayPalette.signature. 0 when the device renders monochrome (no + color panel); clients without the referenced palette yet should render + monochrome until its chunks arrive. */ + uint32_t palette_signature; +} meshtastic_DisplayFrame; + +/* One colorized rectangle of the display. */ +typedef struct _meshtastic_DisplayPalette_ColorRegion { + /* Region origin and size in pixels. */ + uint16_t x; + /* See x. */ + uint16_t y; + /* See x. */ + uint16_t width; + /* See x. */ + uint16_t height; + /* RGB565 drawn for set (1) pixels inside this region. */ + uint16_t on_color; + /* RGB565 drawn for clear (0) pixels inside this region. */ + uint16_t off_color; +} meshtastic_DisplayPalette_ColorRegion; + +/* Colorization palette for DisplayFrame streams from devices that paint the + 1bpp base UI onto a color panel. The panel applies per-region on/off + colors at flush time; streaming the same region table lets a client + render the mirror in the panel's true colors at 1bpp bandwidth. + Sent as FromRadio.display_palette, split by region index when the table + exceeds one message; re-sent only when the region layout or theme changes + (the signature changes with it). Regions are ordered by table index; a + region with a higher table index overrides lower-indexed ones where they + overlap, regardless of chunk boundaries. A client holding partial chunks + of a signature that no longer matches incoming chunks should discard + them. All colors are RGB565 in logical bit layout (RRRRRGGGGGGBBBBB). */ +typedef struct _meshtastic_DisplayPalette { + /* Identity of this palette; DisplayFrame.palette_signature references it. + Changes whenever the region table or theme changes. */ + uint32_t signature; + /* RGB565 for set pixels outside all regions. */ + uint32_t default_on_color; + /* RGB565 for clear pixels outside all regions. */ + uint32_t default_off_color; + /* Table index of the first region in this chunk. */ + uint8_t region_offset; + /* Total regions in the complete palette; region_offset + regions length + == region_total completes it. */ + uint8_t region_total; + /* The regions of this chunk, in table order. */ + pb_size_t regions_count; + meshtastic_DisplayPalette_ColorRegion regions[16]; +} meshtastic_DisplayPalette; + /* Lockdown state report from firmware to client (for hardened builds with MESHTASTIC_LOCKDOWN). Sent immediately after config_complete_id to inform a freshly-connected unauthorized client what it must do, @@ -1395,6 +1518,22 @@ typedef struct _meshtastic_NeighborInfo { meshtastic_Neighbor neighbors[10]; } meshtastic_NeighborInfo; +/* Static description of a device's display, sent inside DeviceMetadata + during the connection handshake. */ +typedef struct _meshtastic_DisplayInfo { + /* Display width in pixels. */ + uint16_t width; + /* Display height in pixels. */ + uint16_t height; + /* Pixel encoding that DisplayFrame streams from this device will use. */ + meshtastic_DisplayFrame_Format format; + /* Physical panel technology. */ + meshtastic_DisplayInfo_PanelClass panel_class; + /* True when the panel accepts touch input; clients may map taps on a + mirrored frame to AdminMessage.send_input_event touch coordinates. */ + bool has_touch; +} meshtastic_DisplayInfo; + /* Device metadata response */ typedef struct _meshtastic_DeviceMetadata { /* Device firmware version string */ @@ -1425,6 +1564,12 @@ typedef struct _meshtastic_DeviceMetadata { /* Indicates whether this firmware build includes XEdDSA packet signature verification. This is a read-only capability and must be false when XEdDSA is not compiled in. */ bool has_xeddsa; + /* Describes the device's screen when one is present; absent on display-less + builds. Lets clients gate display-mirroring UI (see DisplayFrame) and + adapt to the panel - e.g. expect slow refresh from EINK, or offer + tap-to-touch when has_touch is set. */ + bool has_display; + meshtastic_DisplayInfo display; } meshtastic_DeviceMetadata; /* A distinct set of legal modem presets shared by one or more LoRa regions. @@ -1536,6 +1681,13 @@ typedef struct _meshtastic_FromRadio { illegal region+preset combination. A region that does not appear in any group carries no constraint info and should not be restricted. */ meshtastic_LoRaRegionPresetMap region_presets; + /* One chunk of the device's display framebuffer, sent while display + mirroring is active (see AdminMessage.set_display_mirror and + AdminMessage.get_display_frame_request). */ + meshtastic_DisplayFrame display_frame; + /* One chunk of the color palette referenced by display_frame's + palette_signature (see DisplayPalette). */ + meshtastic_DisplayPalette display_palette; }; } meshtastic_FromRadio; @@ -1677,10 +1829,18 @@ extern "C" { #define _meshtastic_LogRecord_Level_MAX meshtastic_LogRecord_Level_CRITICAL #define _meshtastic_LogRecord_Level_ARRAYSIZE ((meshtastic_LogRecord_Level)(meshtastic_LogRecord_Level_CRITICAL+1)) +#define _meshtastic_DisplayFrame_Format_MIN meshtastic_DisplayFrame_Format_FORMAT_UNSPECIFIED +#define _meshtastic_DisplayFrame_Format_MAX meshtastic_DisplayFrame_Format_RGB565 +#define _meshtastic_DisplayFrame_Format_ARRAYSIZE ((meshtastic_DisplayFrame_Format)(meshtastic_DisplayFrame_Format_RGB565+1)) + #define _meshtastic_LockdownStatus_State_MIN meshtastic_LockdownStatus_State_STATE_UNSPECIFIED #define _meshtastic_LockdownStatus_State_MAX meshtastic_LockdownStatus_State_DISABLED #define _meshtastic_LockdownStatus_State_ARRAYSIZE ((meshtastic_LockdownStatus_State)(meshtastic_LockdownStatus_State_DISABLED+1)) +#define _meshtastic_DisplayInfo_PanelClass_MIN meshtastic_DisplayInfo_PanelClass_PANEL_CLASS_UNSPECIFIED +#define _meshtastic_DisplayInfo_PanelClass_MAX meshtastic_DisplayInfo_PanelClass_HUB75 +#define _meshtastic_DisplayInfo_PanelClass_ARRAYSIZE ((meshtastic_DisplayInfo_PanelClass)(meshtastic_DisplayInfo_PanelClass_HUB75+1)) + #define meshtastic_Position_location_source_ENUMTYPE meshtastic_Position_LocSource #define meshtastic_Position_altitude_source_ENUMTYPE meshtastic_Position_AltSource @@ -1712,6 +1872,10 @@ extern "C" { +#define meshtastic_DisplayFrame_format_ENUMTYPE meshtastic_DisplayFrame_Format + + + #define meshtastic_LockdownStatus_state_ENUMTYPE meshtastic_LockdownStatus_State #define meshtastic_ClientNotification_level_ENUMTYPE meshtastic_LogRecord_Level @@ -1730,6 +1894,9 @@ extern "C" { #define meshtastic_DeviceMetadata_role_ENUMTYPE meshtastic_Config_DeviceConfig_Role #define meshtastic_DeviceMetadata_hw_model_ENUMTYPE meshtastic_HardwareModel +#define meshtastic_DisplayInfo_format_ENUMTYPE meshtastic_DisplayFrame_Format +#define meshtastic_DisplayInfo_panel_class_ENUMTYPE meshtastic_DisplayInfo_PanelClass + #define meshtastic_LoRaPresetGroup_presets_ENUMTYPE meshtastic_Config_LoRaConfig_ModemPreset #define meshtastic_LoRaPresetGroup_default_preset_ENUMTYPE meshtastic_Config_LoRaConfig_ModemPreset @@ -1761,6 +1928,9 @@ extern "C" { #define meshtastic_LogRecord_init_default {"", 0, "", _meshtastic_LogRecord_Level_MIN} #define meshtastic_QueueStatus_init_default {0, 0, 0, 0} #define meshtastic_FromRadio_init_default {0, 0, {meshtastic_MeshPacket_init_default}} +#define meshtastic_DisplayFrame_init_default {0, 0, _meshtastic_DisplayFrame_Format_MIN, 0, 0, 0, {0, {0}}, 0, 0, 0, 0, 0} +#define meshtastic_DisplayPalette_init_default {0, 0, 0, 0, 0, 0, {meshtastic_DisplayPalette_ColorRegion_init_default, meshtastic_DisplayPalette_ColorRegion_init_default, meshtastic_DisplayPalette_ColorRegion_init_default, meshtastic_DisplayPalette_ColorRegion_init_default, meshtastic_DisplayPalette_ColorRegion_init_default, meshtastic_DisplayPalette_ColorRegion_init_default, meshtastic_DisplayPalette_ColorRegion_init_default, meshtastic_DisplayPalette_ColorRegion_init_default, meshtastic_DisplayPalette_ColorRegion_init_default, meshtastic_DisplayPalette_ColorRegion_init_default, meshtastic_DisplayPalette_ColorRegion_init_default, meshtastic_DisplayPalette_ColorRegion_init_default, meshtastic_DisplayPalette_ColorRegion_init_default, meshtastic_DisplayPalette_ColorRegion_init_default, meshtastic_DisplayPalette_ColorRegion_init_default, meshtastic_DisplayPalette_ColorRegion_init_default}} +#define meshtastic_DisplayPalette_ColorRegion_init_default {0, 0, 0, 0, 0, 0} #define meshtastic_LockdownStatus_init_default {_meshtastic_LockdownStatus_State_MIN, "", 0, 0, 0} #define meshtastic_ClientNotification_init_default {false, 0, 0, _meshtastic_LogRecord_Level_MIN, "", 0, {meshtastic_KeyVerificationNumberInform_init_default}} #define meshtastic_KeyVerificationNumberInform_init_default {0, "", 0} @@ -1773,7 +1943,8 @@ extern "C" { #define meshtastic_Compressed_init_default {_meshtastic_PortNum_MIN, {0, {0}}} #define meshtastic_NeighborInfo_init_default {0, 0, 0, 0, {meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default}} #define meshtastic_Neighbor_init_default {0, 0, 0, 0} -#define meshtastic_DeviceMetadata_init_default {"", 0, 0, 0, 0, 0, _meshtastic_Config_DeviceConfig_Role_MIN, 0, _meshtastic_HardwareModel_MIN, 0, 0, 0, 0} +#define meshtastic_DeviceMetadata_init_default {"", 0, 0, 0, 0, 0, _meshtastic_Config_DeviceConfig_Role_MIN, 0, _meshtastic_HardwareModel_MIN, 0, 0, 0, 0, false, meshtastic_DisplayInfo_init_default} +#define meshtastic_DisplayInfo_init_default {0, 0, _meshtastic_DisplayFrame_Format_MIN, _meshtastic_DisplayInfo_PanelClass_MIN, 0} #define meshtastic_LoRaPresetGroup_init_default {0, {_meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN}, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0} #define meshtastic_LoRaRegionPresets_init_default {_meshtastic_Config_LoRaConfig_RegionCode_MIN, 0} #define meshtastic_LoRaRegionPresetMap_init_default {0, {meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default}, 0, {meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default}} @@ -1800,6 +1971,9 @@ extern "C" { #define meshtastic_LogRecord_init_zero {"", 0, "", _meshtastic_LogRecord_Level_MIN} #define meshtastic_QueueStatus_init_zero {0, 0, 0, 0} #define meshtastic_FromRadio_init_zero {0, 0, {meshtastic_MeshPacket_init_zero}} +#define meshtastic_DisplayFrame_init_zero {0, 0, _meshtastic_DisplayFrame_Format_MIN, 0, 0, 0, {0, {0}}, 0, 0, 0, 0, 0} +#define meshtastic_DisplayPalette_init_zero {0, 0, 0, 0, 0, 0, {meshtastic_DisplayPalette_ColorRegion_init_zero, meshtastic_DisplayPalette_ColorRegion_init_zero, meshtastic_DisplayPalette_ColorRegion_init_zero, meshtastic_DisplayPalette_ColorRegion_init_zero, meshtastic_DisplayPalette_ColorRegion_init_zero, meshtastic_DisplayPalette_ColorRegion_init_zero, meshtastic_DisplayPalette_ColorRegion_init_zero, meshtastic_DisplayPalette_ColorRegion_init_zero, meshtastic_DisplayPalette_ColorRegion_init_zero, meshtastic_DisplayPalette_ColorRegion_init_zero, meshtastic_DisplayPalette_ColorRegion_init_zero, meshtastic_DisplayPalette_ColorRegion_init_zero, meshtastic_DisplayPalette_ColorRegion_init_zero, meshtastic_DisplayPalette_ColorRegion_init_zero, meshtastic_DisplayPalette_ColorRegion_init_zero, meshtastic_DisplayPalette_ColorRegion_init_zero}} +#define meshtastic_DisplayPalette_ColorRegion_init_zero {0, 0, 0, 0, 0, 0} #define meshtastic_LockdownStatus_init_zero {_meshtastic_LockdownStatus_State_MIN, "", 0, 0, 0} #define meshtastic_ClientNotification_init_zero {false, 0, 0, _meshtastic_LogRecord_Level_MIN, "", 0, {meshtastic_KeyVerificationNumberInform_init_zero}} #define meshtastic_KeyVerificationNumberInform_init_zero {0, "", 0} @@ -1812,7 +1986,8 @@ extern "C" { #define meshtastic_Compressed_init_zero {_meshtastic_PortNum_MIN, {0, {0}}} #define meshtastic_NeighborInfo_init_zero {0, 0, 0, 0, {meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero}} #define meshtastic_Neighbor_init_zero {0, 0, 0, 0} -#define meshtastic_DeviceMetadata_init_zero {"", 0, 0, 0, 0, 0, _meshtastic_Config_DeviceConfig_Role_MIN, 0, _meshtastic_HardwareModel_MIN, 0, 0, 0, 0} +#define meshtastic_DeviceMetadata_init_zero {"", 0, 0, 0, 0, 0, _meshtastic_Config_DeviceConfig_Role_MIN, 0, _meshtastic_HardwareModel_MIN, 0, 0, 0, 0, false, meshtastic_DisplayInfo_init_zero} +#define meshtastic_DisplayInfo_init_zero {0, 0, _meshtastic_DisplayFrame_Format_MIN, _meshtastic_DisplayInfo_PanelClass_MIN, 0} #define meshtastic_LoRaPresetGroup_init_zero {0, {_meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN}, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0} #define meshtastic_LoRaRegionPresets_init_zero {_meshtastic_Config_LoRaConfig_RegionCode_MIN, 0} #define meshtastic_LoRaRegionPresetMap_init_zero {0, {meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero}, 0, {meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero}} @@ -1968,6 +2143,30 @@ extern "C" { #define meshtastic_QueueStatus_free_tag 2 #define meshtastic_QueueStatus_maxlen_tag 3 #define meshtastic_QueueStatus_mesh_packet_id_tag 4 +#define meshtastic_DisplayFrame_width_tag 1 +#define meshtastic_DisplayFrame_height_tag 2 +#define meshtastic_DisplayFrame_format_tag 3 +#define meshtastic_DisplayFrame_frame_id_tag 4 +#define meshtastic_DisplayFrame_offset_tag 5 +#define meshtastic_DisplayFrame_total_size_tag 6 +#define meshtastic_DisplayFrame_data_tag 7 +#define meshtastic_DisplayFrame_rect_x_tag 8 +#define meshtastic_DisplayFrame_rect_y_tag 9 +#define meshtastic_DisplayFrame_rect_width_tag 10 +#define meshtastic_DisplayFrame_rect_height_tag 11 +#define meshtastic_DisplayFrame_palette_signature_tag 12 +#define meshtastic_DisplayPalette_ColorRegion_x_tag 1 +#define meshtastic_DisplayPalette_ColorRegion_y_tag 2 +#define meshtastic_DisplayPalette_ColorRegion_width_tag 3 +#define meshtastic_DisplayPalette_ColorRegion_height_tag 4 +#define meshtastic_DisplayPalette_ColorRegion_on_color_tag 5 +#define meshtastic_DisplayPalette_ColorRegion_off_color_tag 6 +#define meshtastic_DisplayPalette_signature_tag 1 +#define meshtastic_DisplayPalette_default_on_color_tag 2 +#define meshtastic_DisplayPalette_default_off_color_tag 3 +#define meshtastic_DisplayPalette_region_offset_tag 4 +#define meshtastic_DisplayPalette_region_total_tag 5 +#define meshtastic_DisplayPalette_regions_tag 6 #define meshtastic_LockdownStatus_state_tag 1 #define meshtastic_LockdownStatus_lock_reason_tag 2 #define meshtastic_LockdownStatus_boots_remaining_tag 3 @@ -2003,6 +2202,11 @@ extern "C" { #define meshtastic_NeighborInfo_last_sent_by_id_tag 2 #define meshtastic_NeighborInfo_node_broadcast_interval_secs_tag 3 #define meshtastic_NeighborInfo_neighbors_tag 4 +#define meshtastic_DisplayInfo_width_tag 1 +#define meshtastic_DisplayInfo_height_tag 2 +#define meshtastic_DisplayInfo_format_tag 3 +#define meshtastic_DisplayInfo_panel_class_tag 4 +#define meshtastic_DisplayInfo_has_touch_tag 5 #define meshtastic_DeviceMetadata_firmware_version_tag 1 #define meshtastic_DeviceMetadata_device_state_version_tag 2 #define meshtastic_DeviceMetadata_canShutdown_tag 3 @@ -2016,6 +2220,7 @@ extern "C" { #define meshtastic_DeviceMetadata_hasPKC_tag 11 #define meshtastic_DeviceMetadata_excluded_modules_tag 12 #define meshtastic_DeviceMetadata_has_xeddsa_tag 14 +#define meshtastic_DeviceMetadata_display_tag 15 #define meshtastic_LoRaPresetGroup_presets_tag 1 #define meshtastic_LoRaPresetGroup_default_preset_tag 2 #define meshtastic_LoRaPresetGroup_licensed_only_tag 3 @@ -2042,6 +2247,8 @@ extern "C" { #define meshtastic_FromRadio_deviceuiConfig_tag 17 #define meshtastic_FromRadio_lockdown_status_tag 18 #define meshtastic_FromRadio_region_presets_tag 19 +#define meshtastic_FromRadio_display_frame_tag 20 +#define meshtastic_FromRadio_display_palette_tag 21 #define meshtastic_Heartbeat_nonce_tag 1 #define meshtastic_ToRadio_packet_tag 1 #define meshtastic_ToRadio_want_config_id_tag 3 @@ -2301,7 +2508,9 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,fileInfo,fileInfo), 15) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,clientNotification,clientNotification), 16) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,deviceuiConfig,deviceuiConfig), 17) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,lockdown_status,lockdown_status), 18) \ -X(a, STATIC, ONEOF, MESSAGE, (payload_variant,region_presets,region_presets), 19) +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,region_presets,region_presets), 19) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,display_frame,display_frame), 20) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,display_palette,display_palette), 21) #define meshtastic_FromRadio_CALLBACK NULL #define meshtastic_FromRadio_DEFAULT NULL #define meshtastic_FromRadio_payload_variant_packet_MSGTYPE meshtastic_MeshPacket @@ -2320,6 +2529,45 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,region_presets,region_preset #define meshtastic_FromRadio_payload_variant_deviceuiConfig_MSGTYPE meshtastic_DeviceUIConfig #define meshtastic_FromRadio_payload_variant_lockdown_status_MSGTYPE meshtastic_LockdownStatus #define meshtastic_FromRadio_payload_variant_region_presets_MSGTYPE meshtastic_LoRaRegionPresetMap +#define meshtastic_FromRadio_payload_variant_display_frame_MSGTYPE meshtastic_DisplayFrame +#define meshtastic_FromRadio_payload_variant_display_palette_MSGTYPE meshtastic_DisplayPalette + +#define meshtastic_DisplayFrame_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, width, 1) \ +X(a, STATIC, SINGULAR, UINT32, height, 2) \ +X(a, STATIC, SINGULAR, UENUM, format, 3) \ +X(a, STATIC, SINGULAR, UINT32, frame_id, 4) \ +X(a, STATIC, SINGULAR, UINT32, offset, 5) \ +X(a, STATIC, SINGULAR, UINT32, total_size, 6) \ +X(a, STATIC, SINGULAR, BYTES, data, 7) \ +X(a, STATIC, SINGULAR, UINT32, rect_x, 8) \ +X(a, STATIC, SINGULAR, UINT32, rect_y, 9) \ +X(a, STATIC, SINGULAR, UINT32, rect_width, 10) \ +X(a, STATIC, SINGULAR, UINT32, rect_height, 11) \ +X(a, STATIC, SINGULAR, UINT32, palette_signature, 12) +#define meshtastic_DisplayFrame_CALLBACK NULL +#define meshtastic_DisplayFrame_DEFAULT NULL + +#define meshtastic_DisplayPalette_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, signature, 1) \ +X(a, STATIC, SINGULAR, UINT32, default_on_color, 2) \ +X(a, STATIC, SINGULAR, UINT32, default_off_color, 3) \ +X(a, STATIC, SINGULAR, UINT32, region_offset, 4) \ +X(a, STATIC, SINGULAR, UINT32, region_total, 5) \ +X(a, STATIC, REPEATED, MESSAGE, regions, 6) +#define meshtastic_DisplayPalette_CALLBACK NULL +#define meshtastic_DisplayPalette_DEFAULT NULL +#define meshtastic_DisplayPalette_regions_MSGTYPE meshtastic_DisplayPalette_ColorRegion + +#define meshtastic_DisplayPalette_ColorRegion_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, x, 1) \ +X(a, STATIC, SINGULAR, UINT32, y, 2) \ +X(a, STATIC, SINGULAR, UINT32, width, 3) \ +X(a, STATIC, SINGULAR, UINT32, height, 4) \ +X(a, STATIC, SINGULAR, UINT32, on_color, 5) \ +X(a, STATIC, SINGULAR, UINT32, off_color, 6) +#define meshtastic_DisplayPalette_ColorRegion_CALLBACK NULL +#define meshtastic_DisplayPalette_ColorRegion_DEFAULT NULL #define meshtastic_LockdownStatus_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UENUM, state, 1) \ @@ -2435,9 +2683,20 @@ X(a, STATIC, SINGULAR, UENUM, hw_model, 9) \ X(a, STATIC, SINGULAR, BOOL, hasRemoteHardware, 10) \ X(a, STATIC, SINGULAR, BOOL, hasPKC, 11) \ X(a, STATIC, SINGULAR, UINT32, excluded_modules, 12) \ -X(a, STATIC, SINGULAR, BOOL, has_xeddsa, 14) +X(a, STATIC, SINGULAR, BOOL, has_xeddsa, 14) \ +X(a, STATIC, OPTIONAL, MESSAGE, display, 15) #define meshtastic_DeviceMetadata_CALLBACK NULL #define meshtastic_DeviceMetadata_DEFAULT NULL +#define meshtastic_DeviceMetadata_display_MSGTYPE meshtastic_DisplayInfo + +#define meshtastic_DisplayInfo_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, width, 1) \ +X(a, STATIC, SINGULAR, UINT32, height, 2) \ +X(a, STATIC, SINGULAR, UENUM, format, 3) \ +X(a, STATIC, SINGULAR, UENUM, panel_class, 4) \ +X(a, STATIC, SINGULAR, BOOL, has_touch, 5) +#define meshtastic_DisplayInfo_CALLBACK NULL +#define meshtastic_DisplayInfo_DEFAULT NULL #define meshtastic_LoRaPresetGroup_FIELDLIST(X, a) \ X(a, STATIC, REPEATED, UENUM, presets, 1) \ @@ -2512,6 +2771,9 @@ extern const pb_msgdesc_t meshtastic_MyNodeInfo_msg; extern const pb_msgdesc_t meshtastic_LogRecord_msg; extern const pb_msgdesc_t meshtastic_QueueStatus_msg; extern const pb_msgdesc_t meshtastic_FromRadio_msg; +extern const pb_msgdesc_t meshtastic_DisplayFrame_msg; +extern const pb_msgdesc_t meshtastic_DisplayPalette_msg; +extern const pb_msgdesc_t meshtastic_DisplayPalette_ColorRegion_msg; extern const pb_msgdesc_t meshtastic_LockdownStatus_msg; extern const pb_msgdesc_t meshtastic_ClientNotification_msg; extern const pb_msgdesc_t meshtastic_KeyVerificationNumberInform_msg; @@ -2525,6 +2787,7 @@ extern const pb_msgdesc_t meshtastic_Compressed_msg; extern const pb_msgdesc_t meshtastic_NeighborInfo_msg; extern const pb_msgdesc_t meshtastic_Neighbor_msg; extern const pb_msgdesc_t meshtastic_DeviceMetadata_msg; +extern const pb_msgdesc_t meshtastic_DisplayInfo_msg; extern const pb_msgdesc_t meshtastic_LoRaPresetGroup_msg; extern const pb_msgdesc_t meshtastic_LoRaRegionPresets_msg; extern const pb_msgdesc_t meshtastic_LoRaRegionPresetMap_msg; @@ -2553,6 +2816,9 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg; #define meshtastic_LogRecord_fields &meshtastic_LogRecord_msg #define meshtastic_QueueStatus_fields &meshtastic_QueueStatus_msg #define meshtastic_FromRadio_fields &meshtastic_FromRadio_msg +#define meshtastic_DisplayFrame_fields &meshtastic_DisplayFrame_msg +#define meshtastic_DisplayPalette_fields &meshtastic_DisplayPalette_msg +#define meshtastic_DisplayPalette_ColorRegion_fields &meshtastic_DisplayPalette_ColorRegion_msg #define meshtastic_LockdownStatus_fields &meshtastic_LockdownStatus_msg #define meshtastic_ClientNotification_fields &meshtastic_ClientNotification_msg #define meshtastic_KeyVerificationNumberInform_fields &meshtastic_KeyVerificationNumberInform_msg @@ -2566,6 +2832,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg; #define meshtastic_NeighborInfo_fields &meshtastic_NeighborInfo_msg #define meshtastic_Neighbor_fields &meshtastic_Neighbor_msg #define meshtastic_DeviceMetadata_fields &meshtastic_DeviceMetadata_msg +#define meshtastic_DisplayInfo_fields &meshtastic_DisplayInfo_msg #define meshtastic_LoRaPresetGroup_fields &meshtastic_LoRaPresetGroup_msg #define meshtastic_LoRaRegionPresets_fields &meshtastic_LoRaRegionPresets_msg #define meshtastic_LoRaRegionPresetMap_fields &meshtastic_LoRaRegionPresetMap_msg @@ -2584,7 +2851,11 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg; #define meshtastic_ClientNotification_size 482 #define meshtastic_Compressed_size 239 #define meshtastic_Data_size 335 -#define meshtastic_DeviceMetadata_size 56 +#define meshtastic_DeviceMetadata_size 72 +#define meshtastic_DisplayFrame_size 437 +#define meshtastic_DisplayInfo_size 14 +#define meshtastic_DisplayPalette_ColorRegion_size 24 +#define meshtastic_DisplayPalette_size 440 #define meshtastic_DuplicatedPublicKey_size 0 #define meshtastic_FileInfo_size 236 #define meshtastic_FromRadio_size 510 diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 07d55b74838..7a268281729 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -9,6 +9,7 @@ #include "PowerFSM.h" #include "SPILock.h" #include "gps/RTC.h" +#include "graphics/ScreenMirror.h" #include "input/InputBroker.h" #include "meshUtils.h" #include @@ -679,6 +680,25 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta handleSendInputEvent(r->send_input_event); break; } +#if HAS_SCREEN_MIRROR + // Both verbs are documented local-connection-only: frames ride FromRadio, + // which never crosses the mesh, so honoring a remote arm request would + // stream the screen to whatever local client happens to be attached. + case meshtastic_AdminMessage_get_display_frame_request_tag: { + if (mp.from != 0) + break; + LOG_INFO("Client requests display frame"); + graphics::screenMirror.requestFrame(); + break; + } + case meshtastic_AdminMessage_set_display_mirror_tag: { + if (mp.from != 0) + break; + LOG_INFO("Client sets display mirror: %d", r->set_display_mirror); + graphics::screenMirror.setMirror(r->set_display_mirror); + break; + } +#endif #ifdef ARCH_PORTDUINO case meshtastic_AdminMessage_exit_simulator_tag: LOG_INFO("Exiting simulator"); @@ -2152,7 +2172,8 @@ bool AdminModule::messageIsRequest(const meshtastic_AdminMessage *r) r->which_payload_variant == meshtastic_AdminMessage_get_ringtone_request_tag || r->which_payload_variant == meshtastic_AdminMessage_get_device_connection_status_request_tag || r->which_payload_variant == meshtastic_AdminMessage_get_node_remote_hardware_pins_request_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_ui_config_request_tag) + r->which_payload_variant == meshtastic_AdminMessage_get_ui_config_request_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_display_frame_request_tag) return true; else return false; @@ -2182,6 +2203,14 @@ void AdminModule::handleSendInputEvent(const meshtastic_AdminMessage_InputEvent // Wake the device if asleep powerFSM.trigger(EVENT_INPUT); + +#if HAS_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) {