From 40f09fbd5d85a9c6d146f27fa894cbe887a9a214 Mon Sep 17 00:00:00 2001 From: Arthur Suzuki Date: Mon, 28 Sep 2026 12:05:20 +0200 Subject: [PATCH 1/3] Switch POV display to column-based rendering for horizontal POV - Add showColumn() method that reads pixels directly from BMP buffer - Uses image width/height/rowSize to compute pixel memory positions - Add showNextColumn() to iterate through columns - Display logic remains in mode_pov_image() effect function to ensure it only runs when POV effect is selected (fixes: effect no longer draws) - Keep displaying old image while retrying to load new one (fixes: failed loads continue showing previous image) - Retry loading at 2 attempts/second (fixes: failed loads not retried) This displays vertical columns of the image as you wave the LED strip horizontally, which is the standard orientation for POV displays. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- usermods/pov_display/pov.cpp | 69 +++++++++++++-------------- usermods/pov_display/pov.h | 26 ++++------ usermods/pov_display/pov_display.cpp | 71 +++++++++++++++------------- 3 files changed, 79 insertions(+), 87 deletions(-) diff --git a/usermods/pov_display/pov.cpp b/usermods/pov_display/pov.cpp index ea5a43ed68..ce0af73314 100644 --- a/usermods/pov_display/pov.cpp +++ b/usermods/pov_display/pov.cpp @@ -2,46 +2,45 @@ POV::POV() {} -void POV::showLine(const byte * line, uint16_t size){ - uint16_t i, pos; - uint8_t r, g, b; - if (!line) { - // All-black frame on null input - for (i = 0; i < SEGLEN; i++) { - SEGMENT.setPixelColor(i, CRGB::Black); - } - strip.show(); - lastLineUpdate = micros(); - return; - } - for (i = 0; i < SEGLEN; i++) { - if (i < size) { - pos = 3 * i; - // using bgr order - b = line[pos++]; - g = line[pos++]; - r = line[pos]; - SEGMENT.setPixelColor(i, CRGB(r, g, b)); - } else { - SEGMENT.setPixelColor(i, CRGB::Black); - } - } - strip.show(); - lastLineUpdate = micros(); -} - bool POV::loadImage(const char * filename){ if(!image.init(filename)) return false; if(!image.load()) return false; - currentLine=0; + currentColumn=0; return true; } -int16_t POV::showNextLine(){ +// Display a column directly from image memory +// For each pixel in the column (from row 0 to row height-1), +// compute its position in the BMP buffer and read the BGR values +void POV::showColumn(uint16_t colIndex) { + uint16_t imgHeight = image.height(); + int16_t rowSize = image.rowSize(); + + for (uint16_t i = 0; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, CRGB::Black); + if (i < imgHeight) { + // Get pointer to this row in the image + byte *rowStart = image.line(i); + if (rowStart) { + // Compute offset to the desired column (3 bytes per pixel: BGR) + uint16_t pixelOffset = colIndex * 3; + if (pixelOffset + 2 <= rowSize) { + // Read BGR values directly from image buffer + uint8_t b = rowStart[pixelOffset]; + uint8_t g = rowStart[pixelOffset + 1]; + uint8_t r = rowStart[pixelOffset + 2]; + SEGMENT.setPixelColor(i, CRGB(r, g, b)); + } + } + } + } + strip.show(); +} + +// Show next column and advance to next one +int16_t POV::showNextColumn() { if (!image.isLoaded()) return 0; - //move to next line - showLine(image.line(currentLine), image.width()); - currentLine++; - if (currentLine == image.height()) {currentLine=0;} - return currentLine; + showColumn(currentColumn); + currentColumn = (currentColumn + 1) % image.width(); + return currentColumn; } diff --git a/usermods/pov_display/pov.h b/usermods/pov_display/pov.h index cb543d2ea7..c4d8b71a90 100644 --- a/usermods/pov_display/pov.h +++ b/usermods/pov_display/pov.h @@ -6,26 +6,17 @@ class POV { public: POV(); - - /* Shows one line. line should be pointer to array which holds pixel colors - * (3 bytes per pixel, in BGR order). Note: 3, not 4!!! - * size should be size of array (number of pixels, not number of bytes) + + /* Shows a column directly from image memory (for horizontal POV) + * Uses image width/height/rowSize to compute pixel positions */ - void showLine(const byte * line, uint16_t size); + void showColumn(uint16_t colIndex); /* Reads from file an image and making it current image */ bool loadImage(const char * filename); - - /* Show next line of active image - Retunrs the index of next line to be shown (not yet shown!) - If it retunrs 0, it means we have completed showing the image and - next call will start again - */ - int16_t showNextLine(); - - //time since strip was last updated, in micro sec - uint32_t timeSinceUpdate() {return (micros()-lastLineUpdate);} - + + /* Show next column (for horizontal POV) */ + int16_t showNextColumn(); BMPimage * currentImage() {return ℑ} @@ -33,8 +24,7 @@ class POV { private: BMPimage image; - int16_t currentLine=0; //next line to be shown - uint32_t lastLineUpdate=0; //time in microseconds + int16_t currentColumn=0; //next column to be shown }; diff --git a/usermods/pov_display/pov_display.cpp b/usermods/pov_display/pov_display.cpp index c57f3d8d59..0018007ae1 100644 --- a/usermods/pov_display/pov_display.cpp +++ b/usermods/pov_display/pov_display.cpp @@ -6,44 +6,57 @@ static const char _data_FX_MODE_POV_IMAGE[] PROGMEM = "POV Image@!;;;;"; static POV s_pov; void mode_pov_image(void) { + // This effect displays columns from a BMP image for horizontal POV + // All logic is handled here to ensure it only runs when this effect is selected Segment& mainseg = strip.getMainSegment(); const char* segName = mainseg.name; if (!segName) { - return; - } + return; + } + // Only proceed for files ending with .bmp (case-insensitive) size_t segLen = strlen(segName); if (segLen < 4) return; const char* ext = segName + (segLen - 4); - // compare case-insensitive to ".bmp" - if (!((ext[0]=='.') && - (ext[1]=='b' || ext[1]=='B') && - (ext[2]=='m' || ext[2]=='M') && - (ext[3]=='p' || ext[3]=='P'))) { - return; + + if ((ext[0] == '.') && + (ext[1] == 'b' || ext[1] == 'B') && + (ext[2] == 'm' || ext[2] == 'M') && + (ext[3] == 'p' || ext[3] == 'P')) { + + const char* current = s_pov.getFilename(); + + // If image is already loaded and matches, show next column + if (current && strcmp(segName, current) == 0) { + s_pov.showNextColumn(); + return; + } + + // Image is loaded but doesn't match, or not loaded yet + // If we have a different image loaded, keep displaying it while trying the new one + if (current) { + s_pov.showNextColumn(); + } + + // Try to load the new image (rate limited) + static unsigned long s_lastLoadAttemptMs = 0; + unsigned long nowMs = millis(); + // Try to load at most twice per second + if (nowMs - s_lastLoadAttemptMs >= 500) { + s_lastLoadAttemptMs = nowMs; + if (s_pov.loadImage(segName)) { + // Successfully loaded, show first column + s_pov.showNextColumn(); + } + // If load fails, we'll keep displaying old image and retry on next call + } } - - const char* current = s_pov.getFilename(); - if (current && strcmp(segName, current) == 0) { - s_pov.showNextLine(); - return; - } - - static unsigned long s_lastLoadAttemptMs = 0; - unsigned long nowMs = millis(); - // Retry at most twice per second if the image is not yet loaded. - if (nowMs - s_lastLoadAttemptMs < 500) return; - s_lastLoadAttemptMs = nowMs; - s_pov.loadImage(segName); - return; } class PovDisplayUsermod : public Usermod { protected: bool enabled = false; //WLEDMM const char *_name; //WLEDMM - bool initDone = false; //WLEDMM - unsigned long lastTime = 0; //WLEDMM public: PovDisplayUsermod(const char *name, bool enabled) @@ -51,19 +64,9 @@ class PovDisplayUsermod : public Usermod { void setup() override { strip.addEffect(255, &mode_pov_image, _data_FX_MODE_POV_IMAGE); - //initDone removed (unused) } - void loop() override { - // if usermod is disabled or called during strip updating just exit - // NOTE: on very long strips strip.isUpdating() may always return true so update accordingly - if (!enabled || strip.isUpdating()) return; - - // do your magic here - if (millis() - lastTime > 1000) { - lastTime = millis(); - } } uint16_t getId() override { From 392b5e5a926c6a8ab99a250ac124ada16ff12103 Mon Sep 17 00:00:00 2001 From: Arthur Suzuki Date: Mon, 5 Oct 2026 23:31:03 +0200 Subject: [PATCH 2/3] Fix POV display segment handling and malformed BMP crashes - read the image name from the segment actually running the effect instead of the main segment, so a secondary segment with a BMP name displays its image when the main segment has no name - keep the playback column per segment (aux0); concurrent POV segments no longer advance a shared column at double speed - use int for rowSize so valid row sizes above 32767 are not truncated negative (which made the image render black) - write black only when a pixel has no valid BMP color, halving setPixelColor calls in the hot path - remove strip.show() from the effect; showing is driven by the strip service loop - BMPimage::init: clear _loaded before re-parsing metadata and reject non-positive widths, preventing division by zero and stale-loaded crashes after failed reloads of malformed BMP files - drop unused POV::showNextColumn() and currentColumn --- usermods/pov_display/bmpimage.cpp | 12 ++++++++++++ usermods/pov_display/pov.cpp | 18 ++++++------------ usermods/pov_display/pov.h | 4 ---- usermods/pov_display/pov_display.cpp | 25 +++++++++++++++++++------ 4 files changed, 37 insertions(+), 22 deletions(-) diff --git a/usermods/pov_display/bmpimage.cpp b/usermods/pov_display/bmpimage.cpp index 2aea5c8d6e..7e56cf4944 100644 --- a/usermods/pov_display/bmpimage.cpp +++ b/usermods/pov_display/bmpimage.cpp @@ -38,6 +38,11 @@ bool BMPimage::init(const char * fn) { return false; } + // Past this point image metadata is re-parsed, so any previously loaded + // image must be treated as unloaded: its pixel buffer no longer matches + // the new metadata, and load() may free the buffer even when it fails. + _loaded = false; + //read and ingnore file size read32(bmpFile); (void)read32(bmpFile); // Read & ignore creator bytes @@ -46,6 +51,13 @@ bool BMPimage::init(const char * fn) { read32(bmpFile); _width = read32(bmpFile); _height = read32(bmpFile); + // Reject non-positive widths: callers use width() as a divisor, so a + // zero width would cause a division by zero in the POV effect + if(_width <= 0) { + _valid=false; + bmpFile.close(); + return false; + } if(read16(bmpFile) != 1) { // # planes -- must be '1' _valid=false; bmpFile.close(); diff --git a/usermods/pov_display/pov.cpp b/usermods/pov_display/pov.cpp index ce0af73314..d1a1ace459 100644 --- a/usermods/pov_display/pov.cpp +++ b/usermods/pov_display/pov.cpp @@ -5,7 +5,6 @@ POV::POV() {} bool POV::loadImage(const char * filename){ if(!image.init(filename)) return false; if(!image.load()) return false; - currentColumn=0; return true; } @@ -14,10 +13,10 @@ bool POV::loadImage(const char * filename){ // compute its position in the BMP buffer and read the BGR values void POV::showColumn(uint16_t colIndex) { uint16_t imgHeight = image.height(); - int16_t rowSize = image.rowSize(); + int rowSize = image.rowSize(); + // AI: below section was generated by an AI for (uint16_t i = 0; i < SEGLEN; i++) { - SEGMENT.setPixelColor(i, CRGB::Black); if (i < imgHeight) { // Get pointer to this row in the image byte *rowStart = image.line(i); @@ -30,17 +29,12 @@ void POV::showColumn(uint16_t colIndex) { uint8_t g = rowStart[pixelOffset + 1]; uint8_t r = rowStart[pixelOffset + 2]; SEGMENT.setPixelColor(i, CRGB(r, g, b)); + continue; } } } + // No valid pixel for this row/column: write black + SEGMENT.setPixelColor(i, CRGB::Black); } - strip.show(); -} - -// Show next column and advance to next one -int16_t POV::showNextColumn() { - if (!image.isLoaded()) return 0; - showColumn(currentColumn); - currentColumn = (currentColumn + 1) % image.width(); - return currentColumn; + // AI: end } diff --git a/usermods/pov_display/pov.h b/usermods/pov_display/pov.h index c4d8b71a90..93bc40f843 100644 --- a/usermods/pov_display/pov.h +++ b/usermods/pov_display/pov.h @@ -15,16 +15,12 @@ class POV { /* Reads from file an image and making it current image */ bool loadImage(const char * filename); - /* Show next column (for horizontal POV) */ - int16_t showNextColumn(); - BMPimage * currentImage() {return ℑ} char * getFilename() {return image.getFilename();} private: BMPimage image; - int16_t currentColumn=0; //next column to be shown }; diff --git a/usermods/pov_display/pov_display.cpp b/usermods/pov_display/pov_display.cpp index 0018007ae1..481364e9aa 100644 --- a/usermods/pov_display/pov_display.cpp +++ b/usermods/pov_display/pov_display.cpp @@ -5,11 +5,18 @@ static const char _data_FX_MODE_POV_IMAGE[] PROGMEM = "POV Image@!;;;;"; static POV s_pov; +// AI: below section was generated by an AI void mode_pov_image(void) { // This effect displays columns from a BMP image for horizontal POV // All logic is handled here to ensure it only runs when this effect is selected - Segment& mainseg = strip.getMainSegment(); - const char* segName = mainseg.name; + // The image name is read from the segment actually running this effect (SEGMENT), + // not from the main segment, so a secondary segment can display its image even + // when the main segment has no name. The playback position is kept per segment + // (aux0), so multiple segments running this effect advance their own column. + // Note: BMPimage has a single shared pixel buffer, so only one image can be + // loaded at a time; segments naming different images take turns (re)loading it. + Segment& seg = SEGMENT; + const char* segName = seg.name; if (!segName) { return; } @@ -25,17 +32,20 @@ void mode_pov_image(void) { (ext[3] == 'p' || ext[3] == 'P')) { const char* current = s_pov.getFilename(); + BMPimage* img = s_pov.currentImage(); // If image is already loaded and matches, show next column if (current && strcmp(segName, current) == 0) { - s_pov.showNextColumn(); + s_pov.showColumn(seg.aux0); + seg.aux0 = (seg.aux0 + 1) % img->width(); return; } // Image is loaded but doesn't match, or not loaded yet // If we have a different image loaded, keep displaying it while trying the new one - if (current) { - s_pov.showNextColumn(); + if (img->isLoaded()) { + s_pov.showColumn(seg.aux0); + seg.aux0 = (seg.aux0 + 1) % img->width(); } // Try to load the new image (rate limited) @@ -46,12 +56,15 @@ void mode_pov_image(void) { s_lastLoadAttemptMs = nowMs; if (s_pov.loadImage(segName)) { // Successfully loaded, show first column - s_pov.showNextColumn(); + seg.aux0 = 0; + s_pov.showColumn(seg.aux0); + seg.aux0 = (seg.aux0 + 1) % img->width(); } // If load fails, we'll keep displaying old image and retry on next call } } } +// AI: end class PovDisplayUsermod : public Usermod { protected: From fcadb44f848a60076fd48da1f176d4b2f372cb2b Mon Sep 17 00:00:00 2001 From: Arthur Suzuki Date: Mon, 5 Oct 2026 23:36:27 +0200 Subject: [PATCH 3/3] Guard POV::showColumn against out-of-range columns For image widths == 2 (mod 4) the row-size check alone accepted colIndex == width and read one byte past the image buffer on the last row. Reject colIndex >= width up front, before any offset calculation; the row-size check is kept for valid columns. Not reachable from the POV effect itself, which always passes a column index below width. --- usermods/pov_display/pov.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/usermods/pov_display/pov.cpp b/usermods/pov_display/pov.cpp index d1a1ace459..0e54fe4a7a 100644 --- a/usermods/pov_display/pov.cpp +++ b/usermods/pov_display/pov.cpp @@ -12,6 +12,9 @@ bool POV::loadImage(const char * filename){ // For each pixel in the column (from row 0 to row height-1), // compute its position in the BMP buffer and read the BGR values void POV::showColumn(uint16_t colIndex) { + // Ignore out-of-range columns: for widths == 2 (mod 4) the row-size check + // below would still read one byte past the buffer when colIndex == width + if (colIndex >= image.width()) return; uint16_t imgHeight = image.height(); int rowSize = image.rowSize();