Skip to content

Commit

Permalink
Added POV image effect
Browse files Browse the repository at this point in the history
Setup is really easy, after first boot and WiFi/LEDs setup:
go to wled.local/edit and upload a couple image to WLed's filesystem.
Only PNG is supported right now, further support for GIF is planned.
The image should be as wide as the 1D segment you want to apply to.

When done, go to the Effect page on the UI, select "POV Image" effect.

There should be a new selector, near the Effect Speed slider.
You can use that selector to set the image for display on POV.

You could also update the image with a post to the JSON-API like this:
curl -X POST http://[wled]/json/state -d '{"seg":{"id":0,"fx":114,"f":"/axel.png"}}'

The segment should move at around 120RPM (that's 2revolutions per seconds) for an image to showup.
More informations and pictures here : https://lumina.toys
  • Loading branch information
Arthur Suzuki committed Dec 2, 2023
1 parent 1dab26b commit 9a3fd3f
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 18 deletions.
5 changes: 5 additions & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ build_flags =
; -D USERMOD_SENSORSTOMQTT
#For ADS1115 sensor uncomment following
; -D USERMOD_ADS1115
#For POV Display uncomment following
-D USERMOD_POV_DISPLAY

build_unflags =

Expand Down Expand Up @@ -195,6 +197,9 @@ lib_deps =
#For ADS1115 sensor uncomment following
; adafruit/Adafruit BusIO @ 1.13.2
; adafruit/Adafruit ADS1X15 @ 2.4.0
#For POV and Image display uncomment following
bitbank2/PNGdec@^1.0.1
bitbank2/AnimatedGIF@^1.4.7

extra_scripts = ${scripts_defaults.extra_scripts}

Expand Down
164 changes: 164 additions & 0 deletions usermods/pov_display/usermod_pov_display.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
#pragma once
#include "wled.h"
#include <PNGdec.h>
#include <AnimatedGIF.h>
#define BRI 3
#define FX_MODE_POV_IMAGE 255
static const char _data_FX_MODE_POV_IMAGE[] PROGMEM = "POV Image@!;;;1";

AnimatedGIF gif;
PNG png;
File f;

typedef struct file_tag
{
int32_t iPos; // current file position
int32_t iSize; // file size
uint8_t *pData; // memory file pointer
void * fHandle; // class pointer to File/SdFat or whatever you want
} IMGFILE;

void * openFile(const char *filename, int32_t *size) {
f = WLED_FS.open(filename);
*size = f.size();
return &f;
}

void closeFile(void *handle) {
if (f) f.close();
}

int32_t readFile(IMGFILE *pFile, uint8_t *pBuf, int32_t iLen)
{
int32_t iBytesRead;
iBytesRead = iLen;
File *f = static_cast<File *>(pFile->fHandle);
// Note: If you read a file all the way to the last byte, seek() stops working
if ((pFile->iSize - pFile->iPos) < iLen)
iBytesRead = pFile->iSize - pFile->iPos - 1; // <-- ugly work-around
if (iBytesRead <= 0)
return 0;
iBytesRead = (int32_t)f->read(pBuf, iBytesRead);
pFile->iPos = f->position();
return iBytesRead;
}

int32_t readPNG(PNGFILE *pFile, uint8_t *pBuf, int32_t iLen)
{ return readFile((IMGFILE*) pFile, pBuf, iLen); }

int32_t readGIF(GIFFILE *pFile, uint8_t *pBuf, int32_t iLen)
{ return readFile((IMGFILE*) pFile, pBuf, iLen); }

int32_t seekFile(IMGFILE *pFile, int32_t iPosition)
{
int i = micros();
File *f = static_cast<File *>(pFile->fHandle);
f->seek(iPosition);
pFile->iPos = (int32_t)f->position();
i = micros() - i;
return pFile->iPos;
}

int32_t seekPNG(PNGFILE *pFile, int32_t iPos)
{ return seekFile((IMGFILE*) pFile, iPos); }

int32_t seekGIF(GIFFILE *pFile, int32_t iPos)
{ return seekFile((IMGFILE*) pFile, iPos); }

void pngDraw(PNGDRAW *pDraw) {
uint16_t usPixels[SEGLEN];
png.getLineAsRGB565(pDraw, usPixels, PNG_RGB565_LITTLE_ENDIAN, 0xffffffff);
for(int x=0; x < SEGLEN; x++) {
uint16_t color = usPixels[x];
byte r = ((color >> 11) & 0x1F);
byte g = ((color >> 5) & 0x3F);
byte b = (color & 0x1F);
SEGMENT.setPixelColor(x, RGBW32(r,g,b,0));
}
busses.show();
}

void gifDraw(GIFDRAW *pDraw) {
uint8_t r, g, b, *s, *p, *pPal = (uint8_t *)pDraw->pPalette;
int x, y = pDraw->iY + pDraw->y;

s = pDraw->pPixels;
if (pDraw->ucDisposalMethod == 2) {
p = &pPal[pDraw->ucBackground * 3];
r = p[0]; g = p[1]; b = p[2];
for (x=0; x<pDraw->iWidth; x++)
{
if (s[x] == pDraw->ucTransparent) {
SEGMENT.setPixelColor(x, RGBW32(r, g, b, 0));
}
}
pDraw->ucHasTransparency = 0;
}

if (pDraw->ucHasTransparency) {
const uint8_t ucTransparent = pDraw->ucTransparent;
for (x=0; x<pDraw->iWidth; x++) {
if (s[x] != ucTransparent) {
p = &pPal[s[x] * 3];
SEGMENT.setPixelColor(x, RGBW32(p[0]>>BRI, p[1]>>BRI, p[2]>>BRI, 0));
}
}
}

else // no transparency, just copy them all
{
for (x=0; x<pDraw->iWidth; x++)
{
p = &pPal[s[x] * 3];
SEGMENT.setPixelColor(x, RGBW32(p[0], p[1], p[2], 0));
}
}
busses.show();
}

void pov_image() {
const char * filepath = SEGMENT.name;
int rc = png.open(filepath, openFile, closeFile, readPNG, seekPNG, pngDraw);
if (rc == PNG_SUCCESS) {
if (png.getWidth() != SEGLEN) return;
rc = png.decode(NULL, 0);
png.close();
return;
}

gif.begin(GIF_PALETTE_RGB888);
rc = gif.open(filepath, openFile, closeFile, readGIF, seekGIF, gifDraw);
if (rc) {
if (gif.getCanvasWidth() != SEGLEN) return;
while (gif.playFrame(true, NULL)) {}
gif.close();
}
}

uint16_t mode_pov_image(void) {
pov_image();
return FRAMETIME;
}

class PovDisplayUsermod : public Usermod
{
protected:
bool enabled = false; //WLEDMM
bool initDone = false; //WLEDMM
unsigned long lastTime = 0; //WLEDMM

public:
void setup() {
strip.addEffect(FX_MODE_POV_IMAGE, &mode_pov_image, _data_FX_MODE_POV_IMAGE);
initDone=true;
}

void loop() {
if (!enabled || strip.isUpdating()) return;
if (millis() - lastTime > 1000) {
lastTime = millis();
}
}

void connected() {}
};
1 change: 1 addition & 0 deletions wled00/const.h
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
#define USERMOD_ID_KLIPPER 40 //Usermod Klipper percentage
#define USERMOD_ID_WIREGUARD 41 //Usermod "wireguard.h"
#define USERMOD_ID_INTERNAL_TEMPERATURE 42 //Usermod "usermod_internal_temperature.h"
#define USERMOD_ID_POV_DISPLAY 43 //Usermod "usermod_pov_display.h"

//Access point behavior
#define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot
Expand Down
75 changes: 57 additions & 18 deletions wled00/data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ var csel = 0; // selected color slot (0-2)
var currentPreset = -1;
var lastUpdate = 0;
var segCount = 0, ledCount = 0, lowestUnused = 0, maxSeg = 0, lSeg = 0;
var image_list = [];
var pcMode = false, pcModeA = false, lastw = 0, wW;
var tr = 7;
var d = document;
Expand Down Expand Up @@ -203,6 +204,7 @@ function loadSkinCSS(cId)
function getURL(path) {
return (loc ? locproto + "//" + locip : "") + path;
}

function onLoad()
{
let l = window.location;
Expand Down Expand Up @@ -261,21 +263,23 @@ function onLoad()
updateTablinks(0);
pmtLS = localStorage.getItem('wledPmt');

// Load initial data
loadPalettes(()=>{
// fill effect extra data array
loadFXData(()=>{
// load and populate effects
loadFX(()=>{
setTimeout(()=>{ // ESP8266 can't handle quick requests
loadPalettesData(()=>{
requestJson();// will load presets and create WS
});
},100);
// Load initial data
loadPalettes(()=>{
// fill effect extra data array
loadFXData(()=>{
// load and populate effects
loadFX(()=>{
// load available files for file selector
setTimeout(()=>{ // ESP8266 can't handle quick requests
loadPalettesData(()=>{
requestJson();// will load presets and create WS
});
},100);
});
});
});
resetUtil();

resetUtil();

d.addEventListener("visibilitychange", handleVisibilityChange, false);
//size();
Expand Down Expand Up @@ -545,6 +549,34 @@ function loadFXData(callback = null)
});
}

function getImages()
{
if (image_list.length === 0)
fetch(getURL('/edit?list=/'), {
method: 'get'
}).then((res)=>{
if (!res.ok) showErrorToast();
return res.json();
}).then((json)=>{
allowed_extensions = ['png', 'gif'];
for (const [key, file] of Object.entries(json))
if ( allowed_extensions.includes(file.name.substr(-3)))
image_list.push(file.name);
}).catch((e)=>{
fxdata = [];
showToast(e, true);
});

html = '<datalist id="images">';
for (file of image_list) {
basename = file.substr(1, file.length - 5);
html+= '<option value="' + file + '">' + basename + '</option>';
}

html+='</datalist>';
return html;
}

var pQL = [];
function populateQL()
{
Expand Down Expand Up @@ -702,7 +734,7 @@ ${inforow("Environment",i.arch + " " + i.core + " (" + i.lwip + ")")}

function populateSegments(s)
{
var cn = "";
var cn = getImages();
let li = lastinfo;
segCount = 0; lowestUnused = 0; lSeg = 0;

Expand Down Expand Up @@ -774,8 +806,8 @@ function populateSegments(s)
`<i class="icons e-icon flr" id="sege${i}" onclick="expand(${i})">&#xe395;</i>`+
(cfg.comp.segpwr ? segp : '') +
`<div class="segin" id="seg${i}in">`+
`<input type="text" class="ptxt" id="seg${i}t" autocomplete="off" maxlength=${li.arch=="esp8266"?32:64} value="${inst.n?inst.n:""}" placeholder="Enter name..."/>`+
`<table class="infot segt">`+
`<input type="text" class="ptxt" id="seg${i}t" autocomplete="off" maxlength=${li.arch=="esp8266"?32:64} value="${inst.n?inst.n:""}" placeholder="Enter name..." list="images"/>`+
`<table class="infot segt">`+
`<tr>`+
`<td>${isMSeg?'Start X':'Start LED'}</td>`+
`<td>${isMSeg?(cfg.comp.seglen?"Width":"Stop X"):(cfg.comp.seglen?"LED count":"Stop LED")}</td>`+
Expand Down Expand Up @@ -822,7 +854,7 @@ function populateSegments(s)
`</div>`;
}

gId('segcont').innerHTML = cn;
gId('segcont').innerHTML = cn;
let noNewSegs = (lowestUnused >= maxSeg);
resetUtil(noNewSegs);
if (gId('selall')) gId('selall').checked = true;
Expand Down Expand Up @@ -888,7 +920,7 @@ function populateEffects()
if (m.includes('1')) nm += "&#8942;"; // 1D effects
if (m.includes('2')) nm += "&#9638;"; // 2D effects
if (m.includes('v')) nm += "&#9834;"; // volume effects
if (m.includes('f')) nm += "&#9835;"; // frequency effects
if (m.includes('f')) nm += "&#9835;"; // frequency effects
}
}
html += generateListItemHtml('fx',id,nm,'setFX','',fd);
Expand Down Expand Up @@ -1483,7 +1515,8 @@ function setEffectParameters(idx)
var slOnOff = (effectPars.length==0 || effectPars[0]=='')?[]:effectPars[0].split(",");
var coOnOff = (effectPars.length<2 || effectPars[1]=='')?[]:effectPars[1].split(",");
var paOnOff = (effectPars.length<3 || effectPars[2]=='')?[]:effectPars[2].split(",");

var flags = (effectPars.length<4 || effectPars[3]=='')?[]:effectPars[3];
if(flags.includes('s')) toggleFileSelector();
// set html slider items on/off
let nSliders = 5;
for (let i=0; i<nSliders; i++) {
Expand Down Expand Up @@ -2290,6 +2323,12 @@ function setCustom(i=1)
requestJson(obj);
}

function setFile($seg)
{
var obj = {"seg":{"id":0,"fx":114,"name":gId('selectFile').value}};
requestJson(obj);
}

function setOption(i=1, v=false)
{
if (i<1 || i>3) return;
Expand Down
8 changes: 8 additions & 0 deletions wled00/usermods_list.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@
#include "../usermods/pwm_outputs/usermod_pwm_outputs.h"
#endif

#ifdef USERMOD_POV_DISPLAY
#include "../usermods/pov_display/usermod_pov_display.h"
#endif


void registerUsermods()
{
Expand Down Expand Up @@ -373,4 +377,8 @@ void registerUsermods()
#ifdef USERMOD_INTERNAL_TEMPERATURE
usermods.add(new InternalTemperatureUsermod());
#endif

#ifdef USERMOD_POV_DISPLAY
usermods.add(new PovDisplayUsermod());
#endif
}

0 comments on commit 9a3fd3f

Please sign in to comment.