Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion platformio_override.sample.ini
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ lib_deps = ${esp8266.lib_deps}
; robtillaart/SHT85@~0.3.3
; ;gmag11/QuickESPNow @ ~0.7.0 # will also load QuickDebug
; https://github.com/blazoncek/QuickESPNow.git#optional-debug ;; exludes debug library
; bitbank2/PNGdec@^1.0.1 ;; used for POV display uncomment following
; ${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE

build_unflags = ${common.build_unflags}
Expand Down
48 changes: 48 additions & 0 deletions usermods/pov_display/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
## POV Display usermod

This usermod adds a new effect called “POV Image”.

![the usermod at work](pov_display.gif?raw=true)

###How does it work?
With proper configuration (see below) the main segment will display a single row of pixels from an image stored on the ESP.
It displays the image row by row at a high refresh rate.
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.
RGB LEDs only (no RGBW), with grouping set to 1 and spacing set to 0.
Best results with high-density strips (e.g., 144 LEDs/m).

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 pixel strip 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.
146 changes: 146 additions & 0 deletions usermods/pov_display/bmpimage.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
#include "bmpimage.h"
#define BUF_SIZE 64000

byte * _buffer = nullptr;

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;
}

if (_buffer != nullptr) free(_buffer);
_buffer = (byte*)malloc(size);
if (_buffer == nullptr) 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);
}
50 changes: 50 additions & 0 deletions usermods/pov_display/bmpimage.h
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
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
{
"name:": "pov_display",
"build": { "libArchive": false},
"dependencies": {
"bitbank2/PNGdec":"^1.0.3"
}
"platforms": ["espressif32"]
}
47 changes: 47 additions & 0 deletions usermods/pov_display/pov.cpp
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;
}
42 changes: 42 additions & 0 deletions usermods/pov_display/pov.h
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 &image;}

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
Loading