Skip to content

Commit da7f107

Browse files
authored
fix POV Display usermod (#4427)
* POV Display usermod this usermod adds a new effect called "POV Image". To get it to work: - read the README :) - upload a bmp image to the ESP filesystem using "/edit" url. - select "POV Image" effect. - set the filename (ie: "/myimage.bmp") as segment name. - rotate the segment at approximately 20 RPM. - enjoy the show! * improve file extension checks * improve README, remove PNGdec reference, clean usermod * restrain to esp32 platform + reduce memory footprint with malloc
1 parent d5d7fde commit da7f107

File tree

9 files changed

+393
-76
lines changed

9 files changed

+393
-76
lines changed

platformio_override.sample.ini

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ lib_deps = ${esp8266.lib_deps}
2828
; robtillaart/SHT85@~0.3.3
2929
; ;gmag11/QuickESPNow @ ~0.7.0 # will also load QuickDebug
3030
; https://github.com/blazoncek/QuickESPNow.git#optional-debug ;; exludes debug library
31-
; bitbank2/PNGdec@^1.0.1 ;; used for POV display uncomment following
3231
; ${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE
3332

3433
build_unflags = ${common.build_unflags}

usermods/pov_display/README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
## POV Display usermod
2+
3+
This usermod adds a new effect called “POV Image”.
4+
5+
![the usermod at work](pov_display.gif?raw=true)
6+
7+
###How does it work?
8+
With proper configuration (see below) the main segment will display a single row of pixels from an image stored on the ESP.
9+
It displays the image row by row at a high refresh rate.
10+
If you move the pixel segment at the right speed, you will see the full image floating in the air thanks to the persistence of vision.
11+
RGB LEDs only (no RGBW), with grouping set to 1 and spacing set to 0.
12+
Best results with high-density strips (e.g., 144 LEDs/m).
13+
14+
To get it working:
15+
- Resize your image. The height must match the number of LEDs in your strip/segment.
16+
- Rotate your image 90° clockwise (height becomes width).
17+
- Upload a BMP image (24-bit, uncompressed) to the ESP filesystem using the “/edit” URL.
18+
- Select the “POV Image” effect.
19+
- Set the segment name to the absolute filesystem path of the image (e.g., “/myimage.bmp”).
20+
- The path is case-sensitive and must start with “/”.
21+
- Rotate the pixel strip at approximately 20 RPM.
22+
- Tune as needed so that one full revolution maps to the image width (if the image appears stretched or compressed, adjust RPM slightly).
23+
- Enjoy the show!
24+
25+
Notes:
26+
- Only 24-bit uncompressed BMP files are supported.
27+
- The image must fit into ~64 KB of RAM (width × height × 3 bytes, plus row padding to a 4-byte boundary).
28+
- Examples (approximate, excluding row padding):
29+
- 128×128 (49,152 bytes) fits.
30+
- 160×160 (76,800 bytes) does NOT fit.
31+
- 96×192 (55,296 bytes) fits; padding may add a small overhead.
32+
- If the rendered image appears mirrored or upside‑down, rotate 90° the other way or flip horizontally in your editor and try again.
33+
- The path must be absolute.
34+
35+
### Requirements
36+
- 1D rotating LED strip/segment (POV setup). Ensure the segment length equals the number of physical LEDs.
37+
- BMP image saved as 24‑bit, uncompressed (no alpha, no palette).
38+
- Sufficient free RAM (~64 KB) for the image buffer.
39+
40+
### Troubleshooting
41+
- Nothing displays: verify the file exists at the exact absolute path (case‑sensitive) and is a 24‑bit uncompressed BMP.
42+
- Garbled colors or wrong orientation: re‑export as 24‑bit BMP and retry the rotation/flip guidance above.
43+
- Image too large: reduce width and/or height until it fits within ~64 KB (see examples).
44+
- Path issues: confirm you uploaded the file via the “/edit” URL and can see it in the filesystem browser.
45+
46+
### Safety
47+
- Secure the rotating assembly and keep clear of moving parts.
48+
- Balance the strip/hub to minimize vibration before running at speed.

usermods/pov_display/bmpimage.cpp

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#include "bmpimage.h"
2+
#define BUF_SIZE 64000
3+
4+
byte * _buffer = nullptr;
5+
6+
uint16_t read16(File &f) {
7+
uint16_t result;
8+
f.read((uint8_t *)&result,2);
9+
return result;
10+
}
11+
12+
uint32_t read32(File &f) {
13+
uint32_t result;
14+
f.read((uint8_t *)&result,4);
15+
return result;
16+
}
17+
18+
bool BMPimage::init(const char * fn) {
19+
File bmpFile;
20+
int bmpDepth;
21+
//first, check if filename exists
22+
if (!WLED_FS.exists(fn)) {
23+
return false;
24+
}
25+
26+
bmpFile = WLED_FS.open(fn);
27+
if (!bmpFile) {
28+
_valid=false;
29+
return false;
30+
}
31+
32+
//so, the file exists and is opened
33+
// Parse BMP header
34+
uint16_t header = read16(bmpFile);
35+
if(header != 0x4D42) { // BMP signature
36+
_valid=false;
37+
bmpFile.close();
38+
return false;
39+
}
40+
41+
//read and ingnore file size
42+
read32(bmpFile);
43+
(void)read32(bmpFile); // Read & ignore creator bytes
44+
_imageOffset = read32(bmpFile); // Start of image data
45+
// Read DIB header
46+
read32(bmpFile);
47+
_width = read32(bmpFile);
48+
_height = read32(bmpFile);
49+
if(read16(bmpFile) != 1) { // # planes -- must be '1'
50+
_valid=false;
51+
bmpFile.close();
52+
return false;
53+
}
54+
bmpDepth = read16(bmpFile); // bits per pixel
55+
if((bmpDepth != 24) || (read32(bmpFile) != 0)) { // 0 = uncompressed {
56+
_width=0;
57+
_valid=false;
58+
bmpFile.close();
59+
return false;
60+
}
61+
// If _height is negative, image is in top-down order.
62+
// This is not canon but has been observed in the wild.
63+
if(_height < 0) {
64+
_height = -_height;
65+
}
66+
//now, we have successfully got all the basics
67+
// BMP rows are padded (if needed) to 4-byte boundary
68+
_rowSize = (_width * 3 + 3) & ~3;
69+
//check image size - if it is too large, it will be unusable
70+
if (_rowSize*_height>BUF_SIZE) {
71+
_valid=false;
72+
bmpFile.close();
73+
return false;
74+
}
75+
76+
bmpFile.close();
77+
// Ensure filename fits our buffer (segment name length constraint).
78+
size_t len = strlen(fn);
79+
if (len > WLED_MAX_SEGNAME_LEN) {
80+
return false;
81+
}
82+
strncpy(filename, fn, sizeof(filename));
83+
filename[sizeof(filename) - 1] = '\0';
84+
_valid = true;
85+
return true;
86+
}
87+
88+
void BMPimage::clear(){
89+
strcpy(filename, "");
90+
_width=0;
91+
_height=0;
92+
_rowSize=0;
93+
_imageOffset=0;
94+
_loaded=false;
95+
_valid=false;
96+
}
97+
98+
bool BMPimage::load(){
99+
const size_t size = (size_t)_rowSize * (size_t)_height;
100+
if (size > BUF_SIZE) {
101+
return false;
102+
}
103+
File bmpFile = WLED_FS.open(filename);
104+
if (!bmpFile) {
105+
return false;
106+
}
107+
108+
if (_buffer != nullptr) free(_buffer);
109+
_buffer = (byte*)malloc(size);
110+
if (_buffer == nullptr) return false;
111+
112+
bmpFile.seek(_imageOffset);
113+
const size_t readBytes = bmpFile.read(_buffer, size);
114+
bmpFile.close();
115+
if (readBytes != size) {
116+
_loaded = false;
117+
return false;
118+
}
119+
_loaded = true;
120+
return true;
121+
}
122+
123+
byte* BMPimage::line(uint16_t n){
124+
if (_loaded) {
125+
return (_buffer+n*_rowSize);
126+
} else {
127+
return NULL;
128+
}
129+
}
130+
131+
uint32_t BMPimage::pixelColor(uint16_t x, uint16_t y){
132+
uint32_t pos;
133+
byte b,g,r; //colors
134+
if (! _loaded) {
135+
return 0;
136+
}
137+
if ( (x>=_width) || (y>=_height) ) {
138+
return 0;
139+
}
140+
pos=y*_rowSize + 3*x;
141+
//get colors. Note that in BMP files, they go in BGR order
142+
b= _buffer[pos++];
143+
g= _buffer[pos++];
144+
r= _buffer[pos];
145+
return (r<<16|g<<8|b);
146+
}

usermods/pov_display/bmpimage.h

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#ifndef _BMPIMAGE_H
2+
#define _BMPIMAGE_H
3+
#include "Arduino.h"
4+
#include "wled.h"
5+
6+
/*
7+
* This class describes a bitmap image. Each object refers to a bmp file on
8+
* filesystem fatfs.
9+
* To initialize, call init(), passign to it name of a bitmap file
10+
* at the root of fatfs filesystem:
11+
*
12+
* BMPimage myImage;
13+
* myImage.init("logo.bmp");
14+
*
15+
* For performance reasons, before actually usign the image, you need to load
16+
* it from filesystem to RAM:
17+
* myImage.load();
18+
* All load() operations use the same reserved buffer in RAM, so you can only
19+
* have one file loaded at a time. Before loading a new file, always unload the
20+
* previous one:
21+
* myImage.unload();
22+
*/
23+
24+
class BMPimage {
25+
public:
26+
int height() {return _height; }
27+
int width() {return _width; }
28+
int rowSize() {return _rowSize;}
29+
bool isLoaded() {return _loaded; }
30+
bool load();
31+
void unload() {_loaded=false; }
32+
byte * line(uint16_t n);
33+
uint32_t pixelColor(uint16_t x,uint16_t y);
34+
bool init(const char* fn);
35+
void clear();
36+
char * getFilename() {return filename;};
37+
38+
private:
39+
char filename[WLED_MAX_SEGNAME_LEN+1]="";
40+
int _width=0;
41+
int _height=0;
42+
int _rowSize=0;
43+
int _imageOffset=0;
44+
bool _loaded=false;
45+
bool _valid=false;
46+
};
47+
48+
extern byte * _buffer;
49+
50+
#endif
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
{
22
"name:": "pov_display",
33
"build": { "libArchive": false},
4-
"dependencies": {
5-
"bitbank2/PNGdec":"^1.0.3"
6-
}
4+
"platforms": ["espressif32"]
75
}

usermods/pov_display/pov.cpp

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#include "pov.h"
2+
3+
POV::POV() {}
4+
5+
void POV::showLine(const byte * line, uint16_t size){
6+
uint16_t i, pos;
7+
uint8_t r, g, b;
8+
if (!line) {
9+
// All-black frame on null input
10+
for (i = 0; i < SEGLEN; i++) {
11+
SEGMENT.setPixelColor(i, CRGB::Black);
12+
}
13+
strip.show();
14+
lastLineUpdate = micros();
15+
return;
16+
}
17+
for (i = 0; i < SEGLEN; i++) {
18+
if (i < size) {
19+
pos = 3 * i;
20+
// using bgr order
21+
b = line[pos++];
22+
g = line[pos++];
23+
r = line[pos];
24+
SEGMENT.setPixelColor(i, CRGB(r, g, b));
25+
} else {
26+
SEGMENT.setPixelColor(i, CRGB::Black);
27+
}
28+
}
29+
strip.show();
30+
lastLineUpdate = micros();
31+
}
32+
33+
bool POV::loadImage(const char * filename){
34+
if(!image.init(filename)) return false;
35+
if(!image.load()) return false;
36+
currentLine=0;
37+
return true;
38+
}
39+
40+
int16_t POV::showNextLine(){
41+
if (!image.isLoaded()) return 0;
42+
//move to next line
43+
showLine(image.line(currentLine), image.width());
44+
currentLine++;
45+
if (currentLine == image.height()) {currentLine=0;}
46+
return currentLine;
47+
}

usermods/pov_display/pov.h

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#ifndef _POV_H
2+
#define _POV_H
3+
#include "bmpimage.h"
4+
5+
6+
class POV {
7+
public:
8+
POV();
9+
10+
/* Shows one line. line should be pointer to array which holds pixel colors
11+
* (3 bytes per pixel, in BGR order). Note: 3, not 4!!!
12+
* size should be size of array (number of pixels, not number of bytes)
13+
*/
14+
void showLine(const byte * line, uint16_t size);
15+
16+
/* Reads from file an image and making it current image */
17+
bool loadImage(const char * filename);
18+
19+
/* Show next line of active image
20+
Retunrs the index of next line to be shown (not yet shown!)
21+
If it retunrs 0, it means we have completed showing the image and
22+
next call will start again
23+
*/
24+
int16_t showNextLine();
25+
26+
//time since strip was last updated, in micro sec
27+
uint32_t timeSinceUpdate() {return (micros()-lastLineUpdate);}
28+
29+
30+
BMPimage * currentImage() {return &image;}
31+
32+
char * getFilename() {return image.getFilename();}
33+
34+
private:
35+
BMPimage image;
36+
int16_t currentLine=0; //next line to be shown
37+
uint32_t lastLineUpdate=0; //time in microseconds
38+
};
39+
40+
41+
42+
#endif

0 commit comments

Comments
 (0)