-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
fix POV Display usermod #4427
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+393
−76
Merged
fix POV Display usermod #4427
Changes from 4 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
41f31b8
POV Display usermod
Liliputech a5d1c37
Apply suggestions from code review
Liliputech 0ce9ac6
improve file extension checks
Liliputech 4ef95ee
improve README, remove PNGdec reference, clean usermod
Liliputech 4cb6e8f
improve readme
Liliputech 1311102
restrain to esp32 platform + reduce memory footprint with malloc
Liliputech 804dca8
Update usermods/pov_display/README.md
Liliputech 9275b6c
update readme
Liliputech File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| ## POV Display usermod | ||
|
|
||
| This usermod adds a new effect called “POV Image”. | ||
|
|
||
|  | ||
|
|
||
| To get it working: | ||
| - Resize your image. The height must match the number of LEDs in your strip/segment. | ||
| - Rotate your image 90° clockwise (height becomes width). | ||
| - Upload a BMP image (24-bit, uncompressed) to the ESP filesystem using the “/edit” URL. | ||
| - Select the “POV Image” effect. | ||
| - Set the segment name to the absolute filesystem path of the image (e.g., “/myimage.bmp”). | ||
| - The path is case-sensitive and must start with “/”. | ||
| - Rotate the segment at approximately 20 RPM. | ||
| - Tune as needed so that one full revolution maps to the image width (if the image appears stretched or compressed, adjust RPM slightly). | ||
| - Enjoy the show! | ||
|
|
||
| Notes: | ||
| - Only 24-bit uncompressed BMP files are supported. | ||
| - The image must fit into ~64 KB of RAM (width × height × 3 bytes, plus row padding to a 4-byte boundary). | ||
| - Examples (approximate, excluding row padding): | ||
| - 128×128 (49,152 bytes) fits. | ||
| - 160×160 (76,800 bytes) does NOT fit. | ||
| - 96×192 (55,296 bytes) fits; padding may add a small overhead. | ||
| - If the rendered image appears mirrored or upside‑down, rotate 90° the other way or flip horizontally in your editor and try again. | ||
| - The path must be absolute. | ||
|
|
||
| ### Requirements | ||
| - 1D rotating LED strip/segment (POV setup). Ensure the segment length equals the number of physical LEDs. | ||
| - BMP image saved as 24‑bit, uncompressed (no alpha, no palette). | ||
| - Sufficient free RAM (~64 KB) for the image buffer. | ||
|
|
||
| ### Troubleshooting | ||
| - Nothing displays: verify the file exists at the exact absolute path (case‑sensitive) and is a 24‑bit uncompressed BMP. | ||
| - Garbled colors or wrong orientation: re‑export as 24‑bit BMP and retry the rotation/flip guidance above. | ||
| - Image too large: reduce width and/or height until it fits within ~64 KB (see examples). | ||
| - Path issues: confirm you uploaded the file via the “/edit” URL and can see it in the filesystem browser. | ||
|
|
||
| ### Safety | ||
| - Secure the rotating assembly and keep clear of moving parts. | ||
| - Balance the strip/hub to minimize vibration before running at speed. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| #include "bmpimage.h" | ||
| #define BUF_SIZE 64000 | ||
|
|
||
| byte _buffer[BUF_SIZE]; | ||
|
|
||
| uint16_t read16(File &f) { | ||
| uint16_t result; | ||
| f.read((uint8_t *)&result,2); | ||
| return result; | ||
| } | ||
|
|
||
| uint32_t read32(File &f) { | ||
| uint32_t result; | ||
| f.read((uint8_t *)&result,4); | ||
| return result; | ||
| } | ||
|
|
||
| bool BMPimage::init(const char * fn) { | ||
| File bmpFile; | ||
| int bmpDepth; | ||
| //first, check if filename exists | ||
| if (!WLED_FS.exists(fn)) { | ||
| return false; | ||
| } | ||
|
|
||
| bmpFile = WLED_FS.open(fn); | ||
| if (!bmpFile) { | ||
| _valid=false; | ||
| return false; | ||
| } | ||
|
|
||
| //so, the file exists and is opened | ||
| // Parse BMP header | ||
| uint16_t header = read16(bmpFile); | ||
| if(header != 0x4D42) { // BMP signature | ||
| _valid=false; | ||
| bmpFile.close(); | ||
| return false; | ||
| } | ||
|
|
||
| //read and ingnore file size | ||
| read32(bmpFile); | ||
| (void)read32(bmpFile); // Read & ignore creator bytes | ||
| _imageOffset = read32(bmpFile); // Start of image data | ||
| // Read DIB header | ||
| read32(bmpFile); | ||
| _width = read32(bmpFile); | ||
| _height = read32(bmpFile); | ||
| if(read16(bmpFile) != 1) { // # planes -- must be '1' | ||
| _valid=false; | ||
| bmpFile.close(); | ||
| return false; | ||
| } | ||
| bmpDepth = read16(bmpFile); // bits per pixel | ||
| if((bmpDepth != 24) || (read32(bmpFile) != 0)) { // 0 = uncompressed { | ||
| _width=0; | ||
| _valid=false; | ||
| bmpFile.close(); | ||
| return false; | ||
| } | ||
| // If _height is negative, image is in top-down order. | ||
| // This is not canon but has been observed in the wild. | ||
| if(_height < 0) { | ||
| _height = -_height; | ||
| } | ||
| //now, we have successfully got all the basics | ||
| // BMP rows are padded (if needed) to 4-byte boundary | ||
| _rowSize = (_width * 3 + 3) & ~3; | ||
| //check image size - if it is too large, it will be unusable | ||
| if (_rowSize*_height>BUF_SIZE) { | ||
| _valid=false; | ||
| bmpFile.close(); | ||
| return false; | ||
| } | ||
|
|
||
| bmpFile.close(); | ||
| // Ensure filename fits our buffer (segment name length constraint). | ||
| size_t len = strlen(fn); | ||
| if (len > WLED_MAX_SEGNAME_LEN) { | ||
| return false; | ||
| } | ||
| strncpy(filename, fn, sizeof(filename)); | ||
| filename[sizeof(filename) - 1] = '\0'; | ||
| _valid = true; | ||
| return true; | ||
| } | ||
|
|
||
| void BMPimage::clear(){ | ||
| strcpy(filename, ""); | ||
| _width=0; | ||
| _height=0; | ||
| _rowSize=0; | ||
| _imageOffset=0; | ||
| _loaded=false; | ||
| _valid=false; | ||
| } | ||
|
|
||
| bool BMPimage::load(){ | ||
| const size_t size = (size_t)_rowSize * (size_t)_height; | ||
| if (size > BUF_SIZE) { | ||
| return false; | ||
| } | ||
| File bmpFile = WLED_FS.open(filename); | ||
| if (!bmpFile) { | ||
| return false; | ||
| } | ||
| bmpFile.seek(_imageOffset); | ||
| const size_t readBytes = bmpFile.read(_buffer, size); | ||
| bmpFile.close(); | ||
| if (readBytes != size) { | ||
| _loaded = false; | ||
| return false; | ||
| } | ||
| _loaded = true; | ||
| return true; | ||
| } | ||
|
|
||
| byte* BMPimage::line(uint16_t n){ | ||
| if (_loaded) { | ||
| return (_buffer+n*_rowSize); | ||
| } else { | ||
| return NULL; | ||
| } | ||
| } | ||
|
|
||
| uint32_t BMPimage::pixelColor(uint16_t x, uint16_t y){ | ||
| uint32_t pos; | ||
| byte b,g,r; //colors | ||
| if (! _loaded) { | ||
| return 0; | ||
| } | ||
| if ( (x>=_width) || (y>=_height) ) { | ||
| return 0; | ||
| } | ||
| pos=y*_rowSize + 3*x; | ||
| //get colors. Note that in BMP files, they go in BGR order | ||
| b= _buffer[pos++]; | ||
| g= _buffer[pos++]; | ||
| r= _buffer[pos]; | ||
| return (r<<16|g<<8|b); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| #ifndef _BMPIMAGE_H | ||
| #define _BMPIMAGE_H | ||
| #include "Arduino.h" | ||
| #include "wled.h" | ||
|
|
||
| /* | ||
| * This class describes a bitmap image. Each object refers to a bmp file on | ||
| * filesystem fatfs. | ||
| * To initialize, call init(), passign to it name of a bitmap file | ||
| * at the root of fatfs filesystem: | ||
| * | ||
| * BMPimage myImage; | ||
| * myImage.init("logo.bmp"); | ||
| * | ||
| * For performance reasons, before actually usign the image, you need to load | ||
| * it from filesystem to RAM: | ||
| * myImage.load(); | ||
| * All load() operations use the same reserved buffer in RAM, so you can only | ||
| * have one file loaded at a time. Before loading a new file, always unload the | ||
| * previous one: | ||
| * myImage.unload(); | ||
| */ | ||
|
|
||
| class BMPimage { | ||
| public: | ||
| int height() {return _height; } | ||
| int width() {return _width; } | ||
| int rowSize() {return _rowSize;} | ||
| bool isLoaded() {return _loaded; } | ||
| bool load(); | ||
| void unload() {_loaded=false; } | ||
| byte * line(uint16_t n); | ||
| uint32_t pixelColor(uint16_t x,uint16_t y); | ||
| bool init(const char* fn); | ||
| void clear(); | ||
| char * getFilename() {return filename;}; | ||
|
|
||
| private: | ||
| char filename[WLED_MAX_SEGNAME_LEN+1]=""; | ||
| int _width=0; | ||
| int _height=0; | ||
| int _rowSize=0; | ||
| int _imageOffset=0; | ||
| bool _loaded=false; | ||
| bool _valid=false; | ||
| }; | ||
|
|
||
| extern byte _buffer[]; | ||
|
|
||
| #endif |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,5 @@ | |
| "name:": "pov_display", | ||
| "build": { "libArchive": false}, | ||
| "dependencies": { | ||
| "bitbank2/PNGdec":"^1.0.3" | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| #include "pov.h" | ||
|
|
||
| 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; | ||
| return true; | ||
| } | ||
|
|
||
| int16_t POV::showNextLine(){ | ||
| if (!image.isLoaded()) return 0; | ||
| //move to next line | ||
| showLine(image.line(currentLine), image.width()); | ||
| currentLine++; | ||
| if (currentLine == image.height()) {currentLine=0;} | ||
| return currentLine; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| #ifndef _POV_H | ||
| #define _POV_H | ||
| #include "bmpimage.h" | ||
|
|
||
|
|
||
| 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) | ||
| */ | ||
| void showLine(const byte * line, uint16_t size); | ||
|
|
||
| /* 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);} | ||
|
|
||
|
|
||
| BMPimage * currentImage() {return ℑ} | ||
|
|
||
| char * getFilename() {return image.getFilename();} | ||
|
|
||
| private: | ||
| BMPimage image; | ||
| int16_t currentLine=0; //next line to be shown | ||
| uint32_t lastLineUpdate=0; //time in microseconds | ||
| }; | ||
|
|
||
|
|
||
|
|
||
| #endif |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.