mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
fix(screenshot): use GxEPD2 visible dimensions; fix phys_x formula
Firmware: add screenshotWidth()/screenshotHeight() virtual pair to DisplayDriver (default: width()/height()). GxEPDDisplay overrides to return display.width()/display.height() — the GxEPD2-reported values that use WIDTH_VISIBLE (e.g. 122 for GxEPD2_213_B74), not the full physical WIDTH stored in DisplayDriver (128). MyMesh uses these instead of display->width()/height() for the screenshot header bytes. Result for GxEPD2_213_B74 + DISPLAY_ROTATION=1: header was 250×128 → now 250×122 (correct visible canvas) Python decoder (tools/screenshot.py): - Remove hardcoded EINK_PHYS_WIDTH=128 / EINK_VISIBLE_W=122 constants. - Derive phys_stride from buffer_size / log_width (works for any panel). - Fix phys_x formula: was (EINK_PHYS_WIDTH-1-ly = 127-ly), now (vis_w-1-ly = log_height-1-ly = 121-ly for 213_B74). Old formula addressed invisible columns 122-127; new formula correctly maps logical rows 0..121 to visible physical columns 121..0. - vis_w = log_height (no hardcoded trim; header now carries correct value). This also works for square panels (e.g. 128×128 where WIDTH=WIDTH_VISIBLE): header will carry 128×128, decoder produces a correct 128×128 image. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,32 +193,39 @@ 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
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user