diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 0a1e5c16..fbb7fb91 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2107,8 +2107,8 @@ void MyMesh::sendScreenshotResponse(DisplayDriver* display, const uint8_t* buffe for (int chunkIdx = 0; chunkIdx < totalChunks; chunkIdx++) { int i = 0; out_frame[i++] = RESP_CODE_SCREENSHOT; - out_frame[i++] = (uint8_t)display->width(); - out_frame[i++] = (uint8_t)display->height(); + out_frame[i++] = (uint8_t)display->screenshotWidth(); + out_frame[i++] = (uint8_t)display->screenshotHeight(); out_frame[i++] = (uint8_t)chunkIdx; out_frame[i++] = (uint8_t)totalChunks; out_frame[i++] = display->getDisplayType(); diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index 25a6b871..8829643a 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -254,5 +254,11 @@ public: virtual const uint8_t* getBuffer() { return nullptr; } virtual uint16_t getBufferSize() { return 0; } virtual uint8_t getDisplayType() { return 0; } + // Visible pixel dimensions to embed in the screenshot header. + // Override in e-ink drivers to return the GxEPD2-reported dimensions (which use + // WIDTH_VISIBLE instead of the full physical WIDTH used by DisplayDriver). + // OLED drivers: width()/height() already reflect the visible canvas, so no override needed. + virtual int screenshotWidth() { return width(); } + virtual int screenshotHeight() { return height(); } #endif }; diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index 86439463..eb1c7d43 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -82,6 +82,10 @@ public: const uint8_t* getBuffer() override { return display.getBuffer(); } uint16_t getBufferSize() override { return display.getBufferSize(); } uint8_t getDisplayType() override { return 1; } // 1 = e-ink + // Return GxEPD2's own reported dimensions (uses WIDTH_VISIBLE, not the full physical WIDTH + // stored in DisplayDriver). After setRotation(1): width()=HEIGHT, height()=WIDTH_VISIBLE. + int screenshotWidth() override { return (int)display.width(); } + int screenshotHeight() override { return (int)display.height(); } #endif // Line height and approx. char width for each font size: diff --git a/tools/screenshot.py b/tools/screenshot.py index 3de1e811..51da14dc 100755 --- a/tools/screenshot.py +++ b/tools/screenshot.py @@ -31,9 +31,9 @@ RESP_CODE_ERR = 1 DISPLAY_TYPE_OLED = 0 # page-based, column-major (SH1106/SSD1306) DISPLAY_TYPE_EINK = 1 # row-major, MSB-first, 1=white/0=black (GxEPD2) -# E-ink panel constants for GxEPD2_213_B74 / GxEPD2_213_BN -EINK_PHYS_WIDTH = 128 # full hardware width including invisible columns -EINK_VISIBLE_W = 122 # WIDTH_VISIBLE — columns actually shown on panel +# No panel-specific constants needed — the firmware now sends GxEPD2's own reported +# dimensions (which use WIDTH_VISIBLE, not the full physical WIDTH), so the header +# already carries the correct visible height and width for any panel/rotation. def parse_args(): @@ -193,36 +193,43 @@ def oled_buffer_to_image(buffer, width, height): def eink_buffer_to_image(buffer, log_width, log_height): - """Decode GxEPD2 framebuffer for GxEPD2_213_B74 with DISPLAY_ROTATION=1 (landscape). + """Decode GxEPD2 framebuffer for any panel with DISPLAY_ROTATION=1 (landscape). - Physical panel: PHYS_WIDTH=128, HEIGHT=250. - Buffer layout: row-major, stride=PHYS_WIDTH/8=16 bytes, MSB-first. - byte index = phys_y * 16 + phys_x // 8 + The firmware now sends GxEPD2's own width()/height() in the header, which already + reflect WIDTH_VISIBLE (not the full physical WIDTH). So log_width = HEIGHT and + log_height = WIDTH_VISIBLE for this rotation. + + Buffer layout (always in physical panel coordinates, regardless of rotation): + row-major, stride = ceil(phys_width/8) bytes, MSB-first + byte index = phys_x // 8 + phys_y * stride bit = 7 - phys_x % 8 value 1 = white (paper), 0 = black (ink) - With setRotation(1), GxEPD2 maps logical (lx, ly) to physical: - phys_x = PHYS_WIDTH - 1 - ly (= 127 - ly) + With setRotation(1), GxEPD2 maps logical (lx, ly) → physical: + phys_x = WIDTH_VISIBLE - 1 - ly (= log_height - 1 - ly) phys_y = lx - Visible logical height = EINK_VISIBLE_W = 122 (PHYS_WIDTH_VISIBLE). - We trim to visible_h rows so the 6 invisible physical columns don't appear. + phys_stride is derived from buffer size so no panel-specific constants are needed: + phys_stride = buffer_size / phys_height = buffer_size / log_width """ - phys_stride = EINK_PHYS_WIDTH // 8 # 16 bytes per physical row - visible_h = min(log_height, EINK_VISIBLE_W) # trim to 122 + if log_width == 0 or log_height == 0: + return Image.new("RGB", (1, 1), (255, 255, 255)) - image = Image.new("RGB", (log_width, visible_h), (255, 255, 255)) + phys_stride = len(buffer) // log_width # bytes per physical row + vis_w = log_height # WIDTH_VISIBLE — from header after firmware fix + + image = Image.new("RGB", (log_width, vis_w), (255, 255, 255)) pixels = image.load() - for ly in range(visible_h): - phys_x = EINK_PHYS_WIDTH - 1 - ly # 127 - ly + for ly in range(vis_w): + phys_x = vis_w - 1 - ly # WIDTH_VISIBLE - 1 - ly for lx in range(log_width): - phys_y = lx - byte_idx = phys_y * phys_stride + phys_x // 8 - bit = 7 - phys_x % 8 + phys_y = lx + byte_idx = phys_x // 8 + phys_y * phys_stride + bit = 7 - phys_x % 8 if byte_idx < len(buffer): raw = (buffer[byte_idx] >> bit) & 1 - # 1=white (paper), 0=black (ink) + # 1 = white (paper), 0 = black (ink) color = (255, 255, 255) if raw else (0, 0, 0) pixels[lx, ly] = color