Skip to content

Commit daa438b

Browse files
authored
Merge pull request wled#3835 from Aircoookie/gif
Image effect (GIF support)
2 parents 2d6ad41 + 35624ab commit daa438b

File tree

7 files changed

+201
-8
lines changed

7 files changed

+201
-8
lines changed

platformio.ini

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,13 @@ lib_deps_compat =
238238
https://github.com/blazoncek/QuickESPNow.git#optional-debug
239239
https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.2.1
240240

241+
[esp32_all_variants]
242+
lib_deps =
243+
https://github.com/pbolduc/AsyncTCP.git @ 1.2.0
244+
bitbank2/AnimatedGIF@^1.4.7
245+
https://github.com/Aircoookie/GifDecoder#bc3af18
246+
build_flags =
247+
-D WLED_ENABLE_GIF
241248

242249
[esp32]
243250
#platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.2.3/platform-espressif32-2.0.2.3.zip
@@ -251,6 +258,8 @@ build_flags = -g
251258
#use LITTLEFS library by lorol in ESP32 core 1.x.x instead of built-in in 2.x.x
252259
-D LOROL_LITTLEFS
253260
; -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
261+
${esp32_all_variants.build_flags}
262+
254263
tiny_partitions = tools/WLED_ESP32_2MB_noOTA.csv
255264
default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv
256265
extended_partitions = tools/WLED_ESP32_4MB_700k_FS.csv
@@ -259,7 +268,7 @@ large_partitions = tools/WLED_ESP32_8MB.csv
259268
extreme_partitions = tools/WLED_ESP32_16MB_9MB_FS.csv
260269
lib_deps =
261270
https://github.com/lorol/LITTLEFS.git
262-
https://github.com/pbolduc/AsyncTCP.git @ 1.2.0
271+
${esp32_all_variants.lib_deps}
263272
${env.lib_deps}
264273
# additional build flags for audioreactive
265274
AR_build_flags = -D USERMOD_AUDIOREACTIVE
@@ -282,9 +291,10 @@ build_flags = -g
282291
-DARDUINO_ARCH_ESP32 -DESP32
283292
-D CONFIG_ASYNC_TCP_USE_WDT=0
284293
-DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
294+
${esp32_all_variants.build_flags}
285295
-D WLED_ENABLE_DMX_INPUT
286296
lib_deps =
287-
https://github.com/pbolduc/AsyncTCP.git @ 1.2.0
297+
${esp32_all_variants.lib_deps}
288298
https://github.com/someweisguy/esp_dmx.git#47db25d
289299
${env.lib_deps}
290300
board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs
@@ -303,8 +313,9 @@ build_flags = -g
303313
-DARDUINO_USB_MODE=0 ;; this flag is mandatory for ESP32-S2 !
304314
;; please make sure that the following flags are properly set (to 0 or 1) by your board.json, or included in your custom platformio_override.ini entry:
305315
;; ARDUINO_USB_CDC_ON_BOOT
316+
${esp32_all_variants.build_flags}
306317
lib_deps =
307-
https://github.com/pbolduc/AsyncTCP.git @ 1.2.0
318+
${esp32_all_variants.lib_deps}
308319
${env.lib_deps}
309320
board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs
310321

@@ -321,8 +332,9 @@ build_flags = -g
321332
-DARDUINO_USB_MODE=1 ;; this flag is mandatory for ESP32-C3
322333
;; please make sure that the following flags are properly set (to 0 or 1) by your board.json, or included in your custom platformio_override.ini entry:
323334
;; ARDUINO_USB_CDC_ON_BOOT
335+
${esp32_all_variants.build_flags}
324336
lib_deps =
325-
https://github.com/pbolduc/AsyncTCP.git @ 1.2.0
337+
${esp32_all_variants.lib_deps}
326338
${env.lib_deps}
327339
board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs
328340
board_build.flash_mode = qio
@@ -341,8 +353,9 @@ build_flags = -g
341353
-DCO
342354
;; please make sure that the following flags are properly set (to 0 or 1) by your board.json, or included in your custom platformio_override.ini entry:
343355
;; ARDUINO_USB_MODE, ARDUINO_USB_CDC_ON_BOOT
356+
${esp32_all_variants.build_flags}
344357
lib_deps =
345-
https://github.com/pbolduc/AsyncTCP.git @ 1.2.0
358+
${esp32_all_variants.lib_deps}
346359
${env.lib_deps}
347360
board_build.partitions = ${esp32.large_partitions} ;; default partioning for 8MB flash - can be overridden in build envs
348361

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# This file is autogenerated by pip-compile with Python 3.12
2+
# This file is autogenerated by pip-compile with Python 3.11
33
# by the following command:
44
#
55
# pip-compile

wled00/FX.cpp

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4450,6 +4450,24 @@ uint16_t mode_washing_machine(void) {
44504450
static const char _data_FX_MODE_WASHING_MACHINE[] PROGMEM = "Washing Machine@!,!;;!";
44514451

44524452

4453+
/*
4454+
Image effect
4455+
Draws a .gif image from filesystem on the matrix/strip
4456+
*/
4457+
uint16_t mode_image(void) {
4458+
#ifndef WLED_ENABLE_GIF
4459+
return mode_static();
4460+
#else
4461+
renderImageToSegment(SEGMENT);
4462+
return FRAMETIME;
4463+
#endif
4464+
// if (status != 0 && status != 254 && status != 255) {
4465+
// Serial.print("GIF renderer return: ");
4466+
// Serial.println(status);
4467+
// }
4468+
}
4469+
static const char _data_FX_MODE_IMAGE[] PROGMEM = "Image@!,;;;12;sx=128";
4470+
44534471
/*
44544472
Blends random colors across palette
44554473
Modified, originally by Mark Kriegsman https://gist.github.com/kriegsman/1f7ccbbfa492a73c015e
@@ -7734,7 +7752,9 @@ void WS2812FX::setupEffectData() {
77347752
addEffect(FX_MODE_TWO_DOTS, &mode_two_dots, _data_FX_MODE_TWO_DOTS);
77357753
addEffect(FX_MODE_FAIRYTWINKLE, &mode_fairytwinkle, _data_FX_MODE_FAIRYTWINKLE);
77367754
addEffect(FX_MODE_RUNNING_DUAL, &mode_running_dual, _data_FX_MODE_RUNNING_DUAL);
7737-
7755+
#ifdef WLED_ENABLE_GIF
7756+
addEffect(FX_MODE_IMAGE, &mode_image, _data_FX_MODE_IMAGE);
7757+
#endif
77387758
addEffect(FX_MODE_TRICOLOR_CHASE, &mode_tricolor_chase, _data_FX_MODE_TRICOLOR_CHASE);
77397759
addEffect(FX_MODE_TRICOLOR_WIPE, &mode_tricolor_wipe, _data_FX_MODE_TRICOLOR_WIPE);
77407760
addEffect(FX_MODE_TRICOLOR_FADE, &mode_tricolor_fade, _data_FX_MODE_TRICOLOR_FADE);

wled00/FX.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ extern byte realtimeMode; // used in getMappedPixelIndex()
184184
#define FX_MODE_TWO_DOTS 50
185185
#define FX_MODE_FAIRYTWINKLE 51 //was Two Areas prior to 0.13.0-b6 (use "Two Dots" with full intensity)
186186
#define FX_MODE_RUNNING_DUAL 52
187-
// #define FX_MODE_HALLOWEEN 53 // removed in 0.14!
187+
#define FX_MODE_IMAGE 53
188188
#define FX_MODE_TRICOLOR_CHASE 54
189189
#define FX_MODE_TRICOLOR_WIPE 55
190190
#define FX_MODE_TRICOLOR_FADE 56

wled00/FX_fcn.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,9 @@ void Segment::resetIfRequired() {
199199
if (data && _dataLen > 0) memset(data, 0, _dataLen); // prevent heap fragmentation (just erase buffer instead of deallocateData())
200200
next_time = 0; step = 0; call = 0; aux0 = 0; aux1 = 0;
201201
reset = false;
202+
#ifdef WLED_ENABLE_GIF
203+
endImagePlayback(this);
204+
#endif
202205
}
203206

204207
CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) {

wled00/fcn_declare.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,19 @@ void onHueConnect(void* arg, AsyncClient* client);
219219
void sendHuePoll();
220220
void onHueData(void* arg, AsyncClient* client, void *data, size_t len);
221221

222+
#include "FX.h" // must be below colors.cpp declarations (potentially due to duplicate declarations of e.g. color_blend)
223+
224+
//image_loader.cpp
225+
#ifdef WLED_ENABLE_GIF
226+
bool fileSeekCallback(unsigned long position);
227+
unsigned long filePositionCallback(void);
228+
int fileReadCallback(void);
229+
int fileReadBlockCallback(void * buffer, int numberOfBytes);
230+
int fileSizeCallback(void);
231+
byte renderImageToSegment(Segment &seg);
232+
void endImagePlayback(Segment* seg);
233+
#endif
234+
222235
//improv.cpp
223236
enum ImprovRPCType {
224237
Command_Wifi = 0x01,

wled00/image_loader.cpp

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
#include "wled.h"
2+
3+
#ifdef WLED_ENABLE_GIF
4+
5+
#include "GifDecoder.h"
6+
7+
8+
/*
9+
* Functions to render images from filesystem to segments, used by the "Image" effect
10+
*/
11+
12+
File file;
13+
char lastFilename[34] = "/";
14+
GifDecoder<320,320,12,true> decoder;
15+
bool gifDecodeFailed = false;
16+
unsigned long lastFrameDisplayTime = 0, currentFrameDelay = 0;
17+
18+
bool fileSeekCallback(unsigned long position) {
19+
return file.seek(position);
20+
}
21+
22+
unsigned long filePositionCallback(void) {
23+
return file.position();
24+
}
25+
26+
int fileReadCallback(void) {
27+
return file.read();
28+
}
29+
30+
int fileReadBlockCallback(void * buffer, int numberOfBytes) {
31+
return file.read((uint8_t*)buffer, numberOfBytes);
32+
}
33+
34+
int fileSizeCallback(void) {
35+
return file.size();
36+
}
37+
38+
bool openGif(const char *filename) {
39+
file = WLED_FS.open(filename, "r");
40+
41+
if (!file) return false;
42+
return true;
43+
}
44+
45+
Segment* activeSeg;
46+
uint16_t gifWidth, gifHeight;
47+
48+
void screenClearCallback(void) {
49+
activeSeg->fill(0);
50+
}
51+
52+
void updateScreenCallback(void) {}
53+
54+
void drawPixelCallback(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) {
55+
// simple nearest-neighbor scaling
56+
int16_t outY = y * activeSeg->height() / gifHeight;
57+
int16_t outX = x * activeSeg->width() / gifWidth;
58+
// set multiple pixels if upscaling
59+
for (int16_t i = 0; i < (activeSeg->width()+(gifWidth-1)) / gifWidth; i++) {
60+
for (int16_t j = 0; j < (activeSeg->height()+(gifHeight-1)) / gifHeight; j++) {
61+
activeSeg->setPixelColorXY(outX + i, outY + j, gamma8(red), gamma8(green), gamma8(blue));
62+
}
63+
}
64+
}
65+
66+
#define IMAGE_ERROR_NONE 0
67+
#define IMAGE_ERROR_NO_NAME 1
68+
#define IMAGE_ERROR_SEG_LIMIT 2
69+
#define IMAGE_ERROR_UNSUPPORTED_FORMAT 3
70+
#define IMAGE_ERROR_FILE_MISSING 4
71+
#define IMAGE_ERROR_DECODER_ALLOC 5
72+
#define IMAGE_ERROR_GIF_DECODE 6
73+
#define IMAGE_ERROR_FRAME_DECODE 7
74+
#define IMAGE_ERROR_WAITING 254
75+
#define IMAGE_ERROR_PREV 255
76+
77+
// renders an image (.gif only; .bmp and .fseq to be added soon) from FS to a segment
78+
byte renderImageToSegment(Segment &seg) {
79+
if (!seg.name) return IMAGE_ERROR_NO_NAME;
80+
// disable during effect transition, causes flickering, multiple allocations and depending on image, part of old FX remaining
81+
if (seg.mode != seg.currentMode()) return IMAGE_ERROR_WAITING;
82+
if (activeSeg && activeSeg != &seg) return IMAGE_ERROR_SEG_LIMIT; // only one segment at a time
83+
activeSeg = &seg;
84+
85+
if (strncmp(lastFilename +1, seg.name, 32) != 0) { // segment name changed, load new image
86+
strncpy(lastFilename +1, seg.name, 32);
87+
gifDecodeFailed = false;
88+
if (strcmp(lastFilename + strlen(lastFilename) - 4, ".gif") != 0) {
89+
gifDecodeFailed = true;
90+
return IMAGE_ERROR_UNSUPPORTED_FORMAT;
91+
}
92+
if (file) file.close();
93+
openGif(lastFilename);
94+
if (!file) { gifDecodeFailed = true; return IMAGE_ERROR_FILE_MISSING; }
95+
decoder.setScreenClearCallback(screenClearCallback);
96+
decoder.setUpdateScreenCallback(updateScreenCallback);
97+
decoder.setDrawPixelCallback(drawPixelCallback);
98+
decoder.setFileSeekCallback(fileSeekCallback);
99+
decoder.setFilePositionCallback(filePositionCallback);
100+
decoder.setFileReadCallback(fileReadCallback);
101+
decoder.setFileReadBlockCallback(fileReadBlockCallback);
102+
decoder.setFileSizeCallback(fileSizeCallback);
103+
decoder.alloc();
104+
DEBUG_PRINTLN(F("Starting decoding"));
105+
if(decoder.startDecoding() < 0) { gifDecodeFailed = true; return IMAGE_ERROR_GIF_DECODE; }
106+
DEBUG_PRINTLN(F("Decoding started"));
107+
}
108+
109+
if (gifDecodeFailed) return IMAGE_ERROR_PREV;
110+
if (!file) { gifDecodeFailed = true; return IMAGE_ERROR_FILE_MISSING; }
111+
//if (!decoder) { gifDecodeFailed = true; return IMAGE_ERROR_DECODER_ALLOC; }
112+
113+
// speed 0 = half speed, 128 = normal, 255 = full FX FPS
114+
// TODO: 0 = 4x slow, 64 = 2x slow, 128 = normal, 192 = 2x fast, 255 = 4x fast
115+
uint32_t wait = currentFrameDelay * 2 - seg.speed * currentFrameDelay / 128;
116+
117+
// TODO consider handling this on FX level with a different frametime, but that would cause slow gifs to speed up during transitions
118+
if (millis() - lastFrameDisplayTime < wait) return IMAGE_ERROR_WAITING;
119+
120+
decoder.getSize(&gifWidth, &gifHeight);
121+
122+
int result = decoder.decodeFrame(false);
123+
if (result < 0) { gifDecodeFailed = true; return IMAGE_ERROR_FRAME_DECODE; }
124+
125+
currentFrameDelay = decoder.getFrameDelay_ms();
126+
unsigned long tooSlowBy = (millis() - lastFrameDisplayTime) - wait; // if last frame was longer than intended, compensate
127+
currentFrameDelay = tooSlowBy > currentFrameDelay ? 0 : currentFrameDelay - tooSlowBy;
128+
lastFrameDisplayTime = millis();
129+
130+
return IMAGE_ERROR_NONE;
131+
}
132+
133+
void endImagePlayback(Segment *seg) {
134+
DEBUG_PRINTLN(F("Image playback end called"));
135+
if (!activeSeg || activeSeg != seg) return;
136+
if (file) file.close();
137+
decoder.dealloc();
138+
gifDecodeFailed = false;
139+
activeSeg = nullptr;
140+
lastFilename[1] = '\0';
141+
DEBUG_PRINTLN(F("Image playback ended"));
142+
}
143+
144+
#endif

0 commit comments

Comments
 (0)