mirror of
https://github.com/meshtastic/firmware.git
synced 2026-11-07 23:04:49 -05:00
feat(screen): stream the TFT color-region palette for color-accurate mirroring
ScreenMirror captures the color-region table with each snapshot when the frame signature changes (regions byte-swapped from panel order to logical RGB565, defaults = White / theme body background) and streams it as FromRadio.display_palette chunks ahead of frames, with per-client (signature, region) cursors like the frame drain. Frames reference the palette via palette_signature; monochrome-only builds send 0 and no palettes. Also classify USE_TFTDISPLAY/HAS_SPI_TFT panels (T-Deck) as TFT in DeviceMetadata.display.
This commit is contained in:
1 parent
270bf00f6c
commit
1ed930ec2d
5 files changed
+150
-3
No files matched your search
@@ -8,11 +8,26 @@
|
||||
#include <OLEDDisplay.h>
|
||||
#include <cstring>
|
||||
|
||||
#include "TFTColorRegions.h" // self-defines GRAPHICS_TFT_COLORING_ENABLED; API is safe when coloring is compiled out
|
||||
#include "TFTPalette.h"
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
ScreenMirror screenMirror;
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
namespace
|
||||
{
|
||||
// Region colors are stored 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
|
||||
#endif
|
||||
|
||||
void ScreenMirror::freeSnapshotLocked()
|
||||
{
|
||||
if (snapshot) {
|
||||
@@ -21,6 +36,13 @@ void ScreenMirror::freeSnapshotLocked()
|
||||
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;
|
||||
}
|
||||
|
||||
void ScreenMirror::setMirror(bool enabled)
|
||||
@@ -83,12 +105,81 @@ void ScreenMirror::onRendered(OLEDDisplay *display)
|
||||
memcpy(snapshot, display->buffer, frameSize);
|
||||
frameId++;
|
||||
oneShot = false;
|
||||
capturePaletteLocked();
|
||||
readyId = frameId;
|
||||
}
|
||||
// Notify outside the lock: observers (PhoneAPI) may re-enter hasChunkFor.
|
||||
frameReady.notifyObservers(readyId);
|
||||
}
|
||||
|
||||
void ScreenMirror::capturePaletteLocked()
|
||||
{
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
uint32_t sig = getTFTColorFrameSignature();
|
||||
if (sig == 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);
|
||||
}
|
||||
uint8_t count = getTFTColorRegionCount();
|
||||
if (count > MAX_TFT_COLOR_REGIONS)
|
||||
count = MAX_TFT_COLOR_REGIONS;
|
||||
for (uint8_t i = 0; i < count; i++) {
|
||||
const TFTColorRegion &r = colorRegions[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 = TFTPalette::White;
|
||||
paletteDefaultOff = getThemeBodyBg();
|
||||
paletteSig = sig;
|
||||
#endif
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
bool ScreenMirror::hasChunkFor(uint32_t clientFrameId, uint16_t clientOffset)
|
||||
{
|
||||
concurrency::LockGuard g(&lock);
|
||||
@@ -114,6 +205,7 @@ bool ScreenMirror::copyChunk(uint32_t &clientFrameId, uint16_t &clientOffset, me
|
||||
out.width = width;
|
||||
out.height = height;
|
||||
out.format = meshtastic_DisplayFrame_Format_MONO_VLSB;
|
||||
out.palette_signature = paletteSig; // 0 on monochrome-only builds
|
||||
out.frame_id = frameId;
|
||||
out.offset = clientOffset;
|
||||
out.total_size = frameSize;
|
||||
|
||||
@@ -42,8 +42,16 @@ class ScreenMirror
|
||||
/// the client was mid-drain restarts it at offset 0 of the new frame.
|
||||
bool copyChunk(uint32_t &clientFrameId, uint16_t &clientOffset, meshtastic_DisplayFrame &out);
|
||||
|
||||
/// 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);
|
||||
|
||||
private:
|
||||
void freeSnapshotLocked();
|
||||
void capturePaletteLocked();
|
||||
|
||||
concurrency::Lock lock;
|
||||
bool mirroring = false;
|
||||
@@ -54,6 +62,16 @@ class ScreenMirror
|
||||
uint16_t width = 0;
|
||||
uint16_t height = 0;
|
||||
uint32_t frameId = 0;
|
||||
// Color-region palette captured with the frame (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;
|
||||
};
|
||||
|
||||
extern ScreenMirror screenMirror;
|
||||
|
||||
@@ -1392,6 +1392,33 @@ extern meshtastic_DeviceMetadata getDeviceMetadata()
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
|
||||
deviceMetadata.has_xeddsa = true;
|
||||
#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(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;
|
||||
#elif defined(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
|
||||
#ifdef HAS_TOUCHSCREEN
|
||||
deviceMetadata.display.has_touch = true;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return deviceMetadata;
|
||||
}
|
||||
|
||||
|
||||
@@ -390,6 +390,8 @@ void PhoneAPI::close()
|
||||
graphics::screenMirror.setMirror(false);
|
||||
mirrorFrameId = 0;
|
||||
mirrorOffset = 0;
|
||||
mirrorPaletteSig = 0;
|
||||
mirrorPaletteOffset = 0;
|
||||
#endif
|
||||
releasePhonePacket(); // Don't leak phone packets on shutdown
|
||||
releaseQueueStatusPhonePacket();
|
||||
@@ -1111,6 +1113,10 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
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(mirrorFrameId, mirrorOffset, fromRadioScratch.display_frame)) {
|
||||
// Lowest priority: mesh traffic and notifications outrank pixels.
|
||||
@@ -1764,7 +1770,8 @@ bool PhoneAPI::available()
|
||||
return true;
|
||||
|
||||
#if HAS_SCREEN_MIRROR
|
||||
return screenMirrorAuthorized() && graphics::screenMirror.hasChunkFor(mirrorFrameId, mirrorOffset);
|
||||
return screenMirrorAuthorized() && (graphics::screenMirror.hasPaletteChunkFor(mirrorPaletteSig, mirrorPaletteOffset) ||
|
||||
graphics::screenMirror.hasChunkFor(mirrorFrameId, mirrorOffset));
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
|
||||
+5
-2
@@ -91,10 +91,13 @@ class PhoneAPI
|
||||
meshtastic_XModem xmodemPacketForPhone = meshtastic_XModem_init_zero;
|
||||
|
||||
#if HAS_SCREEN_MIRROR
|
||||
// Per-connection drain cursor into ScreenMirror's current frame, so
|
||||
// coexisting clients (BLE + serial + TCP) each receive complete frames.
|
||||
// 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).
|
||||
|
||||
Reference in new issue
Block a user