├── SD_Content ├── LICENSE.txt └── ReadMe.txt ├── LICENSE.txt ├── ReadMe.md ├── gifdownloader.h └── ESP32-GifPlayer.ino /SD_Content/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Animated GIFs by Cyriak Harris is licensed under CC BY-NC-SA 4.0. To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0 2 | -------------------------------------------------------------------------------- /SD_Content/ReadMe.txt: -------------------------------------------------------------------------------- 1 | This is a collection of 207 GIF files made by Cyriak Harris. 2 | 3 | Animated GIFs by Cyriak Harris is licensed under CC BY-NC-SA 4.0 4 | 5 | http://www.cyriak.co.uk 6 | http://www.cyriak.co.uk/animation/contact 7 | info@cyriak.co.uk 8 | 9 | Usage: 10 | 11 | - Pick archive from one of those URLs: 12 | - (25MB, zip) https://github.com/bitbank2/AnimatedGIF/releases/download/1.0.1/Gif_Animations_By_Cyriak_Harris.zip 13 | - (25MB tar.gz) https://github.com/bitbank2/AnimatedGIF/releases/download/1.0.1/Gif_Animations_By_Cyriak_Harris.tar.gz 14 | - (1.5MB lite) https://github.com/bitbank2/AnimatedGIF/releases/download/1.0.1/Gif_Animations_By_Cyriak_Harris_lite.tar.gz 15 | - Unzip it to your SD Card 16 | - Insert the SD Card in your ESP32 17 | - Flash the sketch 18 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 tobozo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | # ESP32-GifPlayer 2 | 3 | GIF player Demo for M5Stack, Odroid-GO, ESP32-Wrover-Kit, LoLinD32-Pro, D-Duino32-XS, and more... 4 | 5 | This sketch will open the SD card and queue all files found in the `/gif/` folder then play them in an endless loop. 6 | 7 | # Depends on the following libraries (all of them available from the Arduino Library Manager): 8 | - [AnimatedGIF](https://github.com/bitbank2/AnimatedGIF) 9 | - [ESP32-Chimera-core](https://github.com/tobozo/ESP32-Chimera-core) 10 | - [LovyanGFX](https://github.com/lovyan03/LovyanGFX) 11 | - [ESP32-Targz](https://github.com/tobozo/ESP32-targz) 12 | - [M5StackUpdater](https://github.com/tobozo/M5Stack-SD-Updater) 13 | 14 | # Deploying GIFs manually: 15 | 16 | - Create a "gif" folder on the root of your SD Card 17 | - Copy your GIF files in the /gif folder 18 | - Run the sketch 19 | 20 | # Deploying GIFs automatically: 21 | 22 | - Run any WiFi example sketch to connect your ESP32 to your WiFi router (make sure the connection is successful) 23 | - Edit the value of `GIF_ARCHIVE_URL` in `gifdownloader.h' to match the URL to your .tar.gz GIFs archive (must contain a /gif/ folder too) 24 | - Run the sketch 25 | - Wait for the download 26 | - Wait for the unpacking 27 | - Wait for the unzipping 28 | 29 | 30 | loosely coded by @tobozo for @bitbank2 31 | -------------------------------------------------------------------------------- /gifdownloader.h: -------------------------------------------------------------------------------- 1 | /*\ 2 | * 3 | * ESP32-GifPlayer 4 | * **************** 5 | * 6 | * https://github.com/tobozo/ESP32-GifPlayer 7 | * 8 | * GIF player Demo for M5Stack, Odroid-GO, ESP32-Wrover-Kit, LoLinD32-Pro, 9 | * D-Duino32-XS, and more... 10 | * 11 | * MIT License 12 | * 13 | * Copyright (c) 2020 tobozo 14 | * 15 | * Permission is hereby granted, free of charge, to any person obtaining a copy 16 | * of this software and associated documentation files (the "Software"), to deal 17 | * in the Software without restriction, including without limitation the rights 18 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | * copies of the Software, and to permit persons to whom the Software is 20 | * furnished to do so, subject to the following conditions: 21 | * 22 | * The above copyright notice and this permission notice shall be included in all 23 | * copies or substantial portions of the Software. 24 | * 25 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | * SOFTWARE. 32 | * 33 | * 34 | * 35 | * 36 | * 37 | \*/ 38 | 39 | #include 40 | #include 41 | #include 42 | #include 43 | 44 | HTTPClient http; 45 | 46 | // must be a tar.gz 47 | //#define GIF_ARCHIVE_URL "https://github.com/bitbank2/AnimatedGIF/releases/download/1.0.1/Gif_Animations_By_Cyriak_Harris.tar.gz" // 25MB ~=20mn download 48 | #define GIF_ARCHIVE_URL "https://github.com/bitbank2/AnimatedGIF/releases/download/1.0.1/Gif_Animations_By_Cyriak_Harris_lite.tar.gz" // 1.5MB ~= 2mn download 49 | #define GIF_ARCHIVE_PATH "/gifs.tar.gz" 50 | #define GIF_DIR_PATH "/gif" 51 | #define TMP_DIR_PATH "/tmp" 52 | #define TMP_TAR_PATH TMP_DIR_PATH "/data.tar" 53 | 54 | static bool /*yolo*/wget( const char* url, fs::FS &fs, const char* path ); 55 | static void runWifiDownloader( void * param ); 56 | static void startWifi( void * param = NULL ); 57 | 58 | 59 | static void progressBar( void * param ); 60 | static void activityIndicator( void * param ); 61 | 62 | 63 | static void runWifiDownloader( fs::FS &destFs ) { 64 | bool downloadfile = false; 65 | 66 | if( !destFs.exists( GIF_DIR_PATH ) ) { 67 | log_w("Folder %s does not exists, creating", GIF_DIR_PATH ); 68 | destFs.mkdir( GIF_DIR_PATH ); 69 | } 70 | if( !destFs.exists( TMP_DIR_PATH ) ) { 71 | log_w("Folder %s does not exists, creating", TMP_DIR_PATH ); 72 | destFs.mkdir( TMP_DIR_PATH ); 73 | } 74 | 75 | if( destFs.exists( GIF_ARCHIVE_PATH ) ) { 76 | File f = destFs.open( GIF_ARCHIVE_PATH ); 77 | size_t fileSize = f.size(); 78 | log_w("File %s exists on filesystem [%d bytes]", GIF_ARCHIVE_PATH, fileSize ); 79 | f.close(); 80 | if( fileSize != 26448701 ) { 81 | destFs.remove( GIF_ARCHIVE_PATH ); 82 | downloadfile = true; 83 | } 84 | } else { 85 | downloadfile = true; 86 | } 87 | if( downloadfile ) { 88 | startWifi(); 89 | if( ! wget( GIF_ARCHIVE_URL, destFs, GIF_ARCHIVE_PATH ) ) { 90 | log_e("Failed to download %s", GIF_ARCHIVE_URL ); 91 | return; 92 | } 93 | log_w("Successfully saved file as %s, now unpacking", GIF_ARCHIVE_PATH ); 94 | } else { 95 | log_w("Unpacking to %s", TMP_TAR_PATH); 96 | } 97 | 98 | tft.setTextColor( TFT_WHITE, TFT_BLACK); 99 | 100 | tft.fillScreen( TFT_BLACK ); 101 | tft.drawString( " Unpacking ... ", tft.width()/2, tft.height()/2 ); 102 | 103 | gzExpander(destFs, GIF_ARCHIVE_PATH, destFs, TMP_TAR_PATH); 104 | 105 | tft.fillScreen( TFT_BLACK ); 106 | tft.drawString( " Unzipping ... ", tft.width()/2, tft.height()/2 ); 107 | 108 | tarExpander(destFs, TMP_TAR_PATH, destFs, "/"); 109 | 110 | // finished ! 111 | ESP.restart(); 112 | 113 | } 114 | 115 | 116 | static void startWifi( void * param ) { 117 | WiFi.mode(WIFI_STA); 118 | Serial.println(WiFi.macAddress()); 119 | 120 | if( String( WiFi_SSID ) !="" && String( WiFi_PASS ) !="" ) { 121 | WiFi.begin( WiFi_SSID, WiFi_PASS ); 122 | } else { 123 | WiFi.begin(); 124 | } 125 | while(WiFi.status() != WL_CONNECTED) { 126 | log_e("Not connected"); 127 | delay(1000); 128 | } 129 | log_w("Connected!"); 130 | if( String( WiFi_SSID ) !="" ) { 131 | Serial.print("Connected to "); 132 | Serial.println(WiFi_SSID); 133 | } 134 | Serial.print("IP address: "); 135 | Serial.println(WiFi.localIP()); 136 | Serial.println(""); 137 | 138 | } 139 | 140 | 141 | static bool /*yolo*/wget( const char* url, fs::FS &fs, const char* path ) { 142 | 143 | WiFiClientSecure *client = new WiFiClientSecure; 144 | client->setCACert( NULL ); // yolo security 145 | 146 | const char* UserAgent = "GifPlayerHTTPClient"; 147 | 148 | http.setUserAgent( UserAgent ); 149 | http.setConnectTimeout( 10000 ); // 10s timeout = 10000 150 | 151 | if( ! http.begin(*client, url ) ) { 152 | log_e("Can't open url %s", url ); 153 | return false; 154 | } 155 | 156 | const char * headerKeys[] = {"location", "redirect"}; 157 | const size_t numberOfHeaders = 2; 158 | http.collectHeaders(headerKeys, numberOfHeaders); 159 | 160 | log_w("URL = %s", url); 161 | 162 | int httpCode = http.GET(); 163 | 164 | // file found at server 165 | if (httpCode == HTTP_CODE_FOUND || httpCode == HTTP_CODE_MOVED_PERMANENTLY) { 166 | String newlocation = ""; 167 | for(int i = 0; i< http.headers(); i++) { 168 | String headerContent = http.header(i); 169 | if( headerContent !="" ) { 170 | newlocation = headerContent; 171 | Serial.printf("%s: %s\n", headerKeys[i], headerContent.c_str()); 172 | } 173 | } 174 | 175 | http.end(); 176 | if( newlocation != "" ) { 177 | log_w("Found 302/301 location header: %s", newlocation.c_str() ); 178 | return wget( newlocation.c_str(), fs, path ); 179 | } else { 180 | log_e("Empty redirect !!"); 181 | return false; 182 | } 183 | } 184 | 185 | WiFiClient *stream = http.getStreamPtr(); 186 | 187 | if( stream == nullptr ) { 188 | http.end(); 189 | log_e("Connection failed!"); 190 | return false; 191 | } 192 | 193 | File outFile = fs.open( path, FILE_WRITE ); 194 | if( ! outFile ) { 195 | log_e("Can't open %s file to save url %s", path, url ); 196 | return false; 197 | } 198 | 199 | uint8_t *wgetBuff = getGzBufferUint8(); 200 | size_t sizeOfBuff = sizeof(wgetBuff); 201 | 202 | if( sizeOfBuff == 0 ) { 203 | log_e("bad buffer"); 204 | while(1); 205 | } else { 206 | log_e("Buffer size : %d", sizeOfBuff ); 207 | } 208 | 209 | int len = http.getSize(); 210 | int bytesLeftToDownload = len; 211 | int bytesDownloaded = 0; 212 | float lastprogress = 0; 213 | bool progresstoggle = false; 214 | char *progressStr = new char[32]; 215 | 216 | while(http.connected() && (len > 0 || len == -1)) { 217 | size_t size = stream->available(); 218 | if(size) { 219 | // read up to 1024 bytes 220 | int c = stream->readBytes(wgetBuff, ((size > sizeOfBuff) ? sizeOfBuff : size)); 221 | outFile.write( wgetBuff, c ); 222 | bytesLeftToDownload -= c; 223 | bytesDownloaded += c; 224 | float progress = (((float)bytesDownloaded / (float)len) * 100.00); 225 | float roundedprogress = int(progress*10) / 10.0; 226 | float roundedactivity = int(progress*10) / 10.0; 227 | //Serial.printf("%.2f / %.2f / %d\n", progress, roundedprogress, bytesLeftToDownload ); 228 | if( lastprogress != roundedprogress ) { 229 | sprintf( progressStr, " %.1f%s ", progress, "%" ); 230 | Serial.printf("Progress: %s - %d bytes left\n", progressStr, bytesLeftToDownload ); 231 | tft.setTextColor( TFT_BLACK, TFT_WHITE ); 232 | tft.drawString( progressStr, tft.width()/2, tft.height()/2 + 20 ); 233 | progresstoggle = !progresstoggle; 234 | if( progresstoggle ) { 235 | tft.setTextColor( TFT_BLACK, TFT_WHITE ); 236 | } else { 237 | tft.setTextColor( TFT_WHITE, TFT_BLACK ); 238 | } 239 | tft.drawString( " - - - - ", tft.width()/2, tft.height()/2 + 40 ); 240 | lastprogress = roundedprogress; 241 | } 242 | } 243 | } 244 | outFile.close(); 245 | return fs.exists( path ); 246 | } 247 | -------------------------------------------------------------------------------- /ESP32-GifPlayer.ino: -------------------------------------------------------------------------------- 1 | /*\ 2 | * 3 | * ESP32-GifPlayer 4 | * **************** 5 | * 6 | * https://github.com/tobozo/ESP32-GifPlayer 7 | * 8 | * GIF player Demo for M5Stack, Odroid-GO, ESP32-Wrover-Kit, LoLinD32-Pro, 9 | * D-Duino32-XS, and more... 10 | * 11 | * This sketch will open the SD card and queue all files found in the /gif 12 | * folder then play them in an endless loop. 13 | * 14 | * If no GIF files are found on the SD Card, it will download and unpack 15 | * a tar.gz archive of GIFs from a provided URL (default github) 16 | * 17 | * This demo is based on Larry Bank's AnimatedGIF library: 18 | * 19 | * https://github.com/bitbank2/ 20 | * 21 | * Library dependencies (available from the Arduino Library Manager): 22 | * 23 | * - AnimatedGIF https://github.com/bitbank2/AnimatedGIF 24 | * - ESP32-Chimera-core https://github.com/tobozo/ESP32-Chimera-core 25 | * - LovyanGFX https://github.com/lovyan03/LovyanGFX 26 | * - ESP32-Targz https://github.com/tobozo/ESP32-targz 27 | * - M5StackUpdater https://github.com/tobozo/M5Stack-SD-Updater 28 | * 29 | * 30 | * MIT License 31 | * 32 | * Copyright (c) 2020 tobozo 33 | * 34 | * Permission is hereby granted, free of charge, to any person obtaining a copy 35 | * of this software and associated documentation files (the "Software"), to deal 36 | * in the Software without restriction, including without limitation the rights 37 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 38 | * copies of the Software, and to permit persons to whom the Software is 39 | * furnished to do so, subject to the following conditions: 40 | * 41 | * The above copyright notice and this permission notice shall be included in all 42 | * copies or substantial portions of the Software. 43 | * 44 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 45 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 46 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 47 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 48 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 49 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 50 | * SOFTWARE. 51 | * 52 | * 53 | * 54 | * 55 | * 56 | \*/ 57 | 58 | //#define LGFX_ONLY // enable this for custom board profiles 59 | //#define USE_SPIFFS // requires "Large SPIFFS" partition !! 60 | 61 | #if defined LGFX_ONLY 62 | #include 63 | #include 64 | #define TFCARD_CS_PIN 4 // CS pin for SD Card 65 | //static lgfx::Touch_XPT2046 touch; 66 | //static lgfx::Panel_ILI9341 panel; 67 | //static lgfx::Panel_ILI9342 panel; 68 | static LGFX tft; 69 | //#define HAS_TOUCH 70 | #else 71 | #include // https://github.com/tobozo/ESP32-Chimera-Core or regular M5Stack Core 72 | #define tft M5.Lcd // syntax sugar 73 | #endif 74 | 75 | #if defined USE_SPIFFS 76 | #define GIFPLAYER_FS SPIFFS 77 | #define GIFPLAYER_FS_Begin() SPIFFS.begin(true) 78 | #elif defined M5STACK_SD 79 | #define GIFPLAYER_FS M5STACK_SD 80 | #define GIFPLAYER_FS_Begin() M5.sd_begin() 81 | #else 82 | #define GIFPLAYER_FS SD 83 | #define GIFPLAYER_FS_Begin() GIFPLAYER_FS.begin( TFCARD_CS_PIN ) 84 | #endif 85 | 86 | #include 87 | 88 | // leave empty if your ESP32 had a previous successful WiFi connection 89 | char WiFi_SSID[32] = ""; 90 | char WiFi_PASS[32] = ""; 91 | 92 | #include "gifdownloader.h" 93 | #include 94 | 95 | AnimatedGIF gif; 96 | 97 | // rule: loop GIF at least during 3s, maximum 5 times, and don't loop/animate longer than 30s per GIF 98 | const int maxLoopIterations = 5; // stop after this amount of loops 99 | const int maxLoopsDuration = 3000; // ms, max cumulated time after the GIF will break loop 100 | const int maxGifDuration = 30000; // ms, max GIF duration 101 | 102 | // used to center image based on GIF dimensions 103 | static int xOffset = 0; 104 | static int yOffset = 0; 105 | 106 | static int totalFiles = 0; // GIF files count 107 | static int currentFile = 0; 108 | static int lastFile = -1; 109 | 110 | char GifComment[256]; 111 | 112 | static File FSGifFile; // temp gif file holder 113 | static File GifRootFolder; // directory listing 114 | 115 | std::vector GifFiles; // GIF files path 116 | 117 | 118 | static void MyCustomDelay( unsigned long ms ) { 119 | delay( ms ); 120 | //log_d("delay %d\n", ms); 121 | } 122 | 123 | 124 | static void * GIFOpenFile(const char *fname, int32_t *pSize) 125 | { 126 | //log_d("GIFOpenFile( %s )\n", fname ); 127 | FSGifFile = GIFPLAYER_FS.open(fname); 128 | if (FSGifFile) { 129 | *pSize = FSGifFile.size(); 130 | return (void *)&FSGifFile; 131 | } 132 | return NULL; 133 | } 134 | 135 | 136 | static void GIFCloseFile(void *pHandle) 137 | { 138 | File *f = static_cast(pHandle); 139 | if (f != NULL) 140 | f->close(); 141 | } 142 | 143 | 144 | static int32_t GIFReadFile(GIFFILE *pFile, uint8_t *pBuf, int32_t iLen) 145 | { 146 | int32_t iBytesRead; 147 | iBytesRead = iLen; 148 | File *f = static_cast(pFile->fHandle); 149 | // Note: If you read a file all the way to the last byte, seek() stops working 150 | if ((pFile->iSize - pFile->iPos) < iLen) 151 | iBytesRead = pFile->iSize - pFile->iPos - 1; // <-- ugly work-around 152 | if (iBytesRead <= 0) 153 | return 0; 154 | iBytesRead = (int32_t)f->read(pBuf, iBytesRead); 155 | pFile->iPos = f->position(); 156 | return iBytesRead; 157 | } 158 | 159 | 160 | static int32_t GIFSeekFile(GIFFILE *pFile, int32_t iPosition) 161 | { 162 | int i = micros(); 163 | File *f = static_cast(pFile->fHandle); 164 | f->seek(iPosition); 165 | pFile->iPos = (int32_t)f->position(); 166 | i = micros() - i; 167 | //log_d("Seek time = %d us\n", i); 168 | return pFile->iPos; 169 | } 170 | 171 | 172 | static void TFTDraw(int x, int y, int w, int h, uint16_t* lBuf ) 173 | { 174 | tft.pushRect( x+xOffset, y+yOffset, w, h, lBuf ); 175 | } 176 | 177 | 178 | // Draw a line of image directly on the LCD 179 | void GIFDraw(GIFDRAW *pDraw) 180 | { 181 | uint8_t *s; 182 | uint16_t *d, *usPalette, usTemp[320]; 183 | int x, y, iWidth; 184 | 185 | iWidth = pDraw->iWidth; 186 | if (iWidth > tft.width() ) 187 | iWidth = tft.width() ; 188 | usPalette = pDraw->pPalette; 189 | y = pDraw->iY + pDraw->y; // current line 190 | 191 | s = pDraw->pPixels; 192 | if (pDraw->ucDisposalMethod == 2) {// restore to background color 193 | for (x=0; xucTransparent) 195 | s[x] = pDraw->ucBackground; 196 | } 197 | pDraw->ucHasTransparency = 0; 198 | } 199 | // Apply the new pixels to the main image 200 | if (pDraw->ucHasTransparency) { // if transparency used 201 | uint8_t *pEnd, c, ucTransparent = pDraw->ucTransparent; 202 | int x, iCount; 203 | pEnd = s + iWidth; 204 | x = 0; 205 | iCount = 0; // count non-transparent pixels 206 | while(x < iWidth) { 207 | c = ucTransparent-1; 208 | d = usTemp; 209 | while (c != ucTransparent && s < pEnd) { 210 | c = *s++; 211 | if (c == ucTransparent) { // done, stop 212 | s--; // back up to treat it like transparent 213 | } else { // opaque 214 | *d++ = usPalette[c]; 215 | iCount++; 216 | } 217 | } // while looking for opaque pixels 218 | if (iCount) { // any opaque pixels? 219 | TFTDraw( pDraw->iX+x, y, iCount, 1, (uint16_t*)usTemp ); 220 | x += iCount; 221 | iCount = 0; 222 | } 223 | // no, look for a run of transparent pixels 224 | c = ucTransparent; 225 | while (c == ucTransparent && s < pEnd) { 226 | c = *s++; 227 | if (c == ucTransparent) 228 | iCount++; 229 | else 230 | s--; 231 | } 232 | if (iCount) { 233 | x += iCount; // skip these 234 | iCount = 0; 235 | } 236 | } 237 | } else { 238 | s = pDraw->pPixels; 239 | // Translate the 8-bit pixels through the RGB565 palette (already byte reversed) 240 | for (x=0; xiX, y, iWidth, 1, (uint16_t*)usTemp ); 243 | } 244 | } /* GIFDraw() */ 245 | 246 | 247 | int gifPlay( char* gifPath ) 248 | { // 0=infinite 249 | 250 | gif.begin(BIG_ENDIAN_PIXELS); 251 | 252 | if( ! gif.open( gifPath, GIFOpenFile, GIFCloseFile, GIFReadFile, GIFSeekFile, GIFDraw ) ) { 253 | log_n("Could not open gif %s", gifPath ); 254 | return maxLoopsDuration; 255 | } 256 | 257 | int frameDelay = 0; // store delay for the last frame 258 | int then = 0; // store overall delay 259 | bool showcomment = false; 260 | 261 | // center the GIF !! 262 | int w = gif.getCanvasWidth(); 263 | int h = gif.getCanvasHeight(); 264 | xOffset = ( tft.width() - w ) /2; 265 | yOffset = ( tft.height() - h ) /2; 266 | 267 | if( lastFile != currentFile ) { 268 | log_n("Playing %s [%d,%d] with offset [%d,%d]", gifPath, w, h, xOffset, yOffset ); 269 | lastFile = currentFile; 270 | showcomment = true; 271 | } 272 | 273 | while (gif.playFrame(true, &frameDelay)) { 274 | if( showcomment ) 275 | if (gif.getComment(GifComment)) 276 | log_n("GIF Comment: %s", GifComment); 277 | 278 | then += frameDelay; 279 | if( then > maxGifDuration ) { // avoid being trapped in infinite GIF's 280 | //log_w("Broke the GIF loop, max duration exceeded"); 281 | break; 282 | } 283 | } 284 | 285 | gif.close(); 286 | 287 | return then; 288 | } 289 | 290 | 291 | int getGifInventory( const char* basePath ) 292 | { 293 | int amount = 0; 294 | GifRootFolder = GIFPLAYER_FS.open(basePath); 295 | if(!GifRootFolder){ 296 | log_n("Failed to open directory"); 297 | return 0; 298 | } 299 | 300 | if(!GifRootFolder.isDirectory()){ 301 | log_n("Not a directory"); 302 | return 0; 303 | } 304 | 305 | File file = GifRootFolder.openNextFile(); 306 | 307 | tft.setTextColor( TFT_WHITE, TFT_BLACK ); 308 | tft.setTextSize( 2 ); 309 | 310 | int textPosX = tft.width()/2 - 16; 311 | int textPosY = tft.height()/2 - 10; 312 | 313 | tft.drawString("GIF Files:", textPosX-40, textPosY-20 ); 314 | 315 | while( file ) { 316 | if(!file.isDirectory()) { 317 | GifFiles.push_back( file.name() ); 318 | amount++; 319 | tft.drawString(String(amount), textPosX, textPosY ); 320 | file.close(); 321 | } 322 | file = GifRootFolder.openNextFile(); 323 | } 324 | GifRootFolder.close(); 325 | log_n("Found %d GIF files", amount); 326 | return amount; 327 | } 328 | 329 | 330 | 331 | 332 | void setup() 333 | { 334 | #if defined LGFX_ONLY 335 | Serial.begin(115200); 336 | /* 337 | auto p = new lgfx::Panel_ILI9341(); 338 | p->spi_3wire = false; 339 | p->spi_cs = TFT_CS; // 14 340 | p->spi_dc = TFT_DC; // 27 341 | p->gpio_rst = TFT_RST; // 33 342 | p->gpio_bl = TFT_LED; // 32 343 | p->pwm_ch_bl = 7; 344 | 345 | tft.setPanel(p); 346 | 347 | auto t = new lgfx::Touch_XPT2046(); // sharing SPI with TFT 348 | t->spi_mosi = MOSI; // 23 349 | t->spi_miso = MISO; // 19 350 | t->spi_sclk = SCK; // 18 351 | t->spi_cs = TOUCH_CS; // TOUCH_CS may be custom build spefific ? 352 | t->spi_host = VSPI_HOST; 353 | t->bus_shared = true; 354 | t->freq = 1600000; 355 | 356 | tft.touch(t); 357 | */ 358 | 359 | auto p = new lgfx::Panel_ILI9342; 360 | p->reverse_invert = true; 361 | p->spi_3wire = true; 362 | p->spi_cs = 5; 363 | p->spi_dc = 15; 364 | p->rotation = 1; 365 | p->offset_rotation = 3; 366 | 367 | tft.setPanel(p); 368 | tft.init(); 369 | 370 | 371 | //tft.begin(); 372 | 373 | #else 374 | 375 | M5.begin(); 376 | 377 | #endif 378 | 379 | checkSDUpdater( GIFPLAYER_FS, MENU_BIN, 1500 ); 380 | 381 | int attempts = 0; 382 | int maxAttempts = 50; 383 | int delayBetweenAttempts = 300; 384 | bool isblinked = false; 385 | 386 | tft.setTextDatum( MC_DATUM ); 387 | 388 | while(! GIFPLAYER_FS_Begin() ) { 389 | log_n("SD Card mount failed! (attempt %d of %d)", attempts, maxAttempts ); 390 | isblinked = !isblinked; 391 | attempts++; 392 | if( isblinked ) { 393 | tft.setTextColor( TFT_WHITE, TFT_BLACK ); 394 | } else { 395 | tft.setTextColor( TFT_BLACK, TFT_WHITE ); 396 | } 397 | tft.drawString( "INSERT SD", tft.width()/2, tft.height()/2 ); 398 | 399 | if( attempts > maxAttempts ) { 400 | log_n("Giving up"); 401 | tft.setBrightness(0); 402 | #if defined( ARDUINO_M5Stack_Core_ESP32 ) || defined( ARDUINO_M5STACK_FIRE ) || defined( ARDUINO_ODROID_ESP32 )// || defined( ARDUINO_M5STACK_Core2 ) 403 | #ifndef LGFX_ONLY 404 | M5.setWakeupButton( BUTTON_B_PIN ); 405 | M5.powerOFF(); 406 | #else 407 | // TODO: turn TFT off 408 | esp_deep_sleep_start(); 409 | #endif 410 | #else 411 | // TODO: turn TFT off 412 | esp_deep_sleep_start(); 413 | #endif 414 | } 415 | delay( delayBetweenAttempts ); 416 | 417 | GIFPLAYER_FS_Begin(); 418 | } 419 | 420 | log_n("SD Card mounted!"); 421 | 422 | tft.begin(); 423 | tft.fillScreen(TFT_BLACK); 424 | 425 | totalFiles = getGifInventory( "/gif" ); // scan the SD card GIF folder 426 | 427 | if( totalFiles == 0 ) { 428 | tft.fillScreen( TFT_BLACK ); 429 | tft.drawString( "Downloading ...", tft.width()/2, tft.height()/2 ); 430 | runWifiDownloader( GIFPLAYER_FS ); 431 | // TODO: download and unzip https://github.com/bitbank2/AnimatedGIF/releases/download/1.0.1/Gif_Animations_By_Cyriak_Harris.tar.gz 432 | while(1); 433 | } 434 | 435 | tft.setTextDatum( TL_DATUM ); 436 | 437 | } 438 | 439 | 440 | 441 | void loop() 442 | { 443 | 444 | tft.clear(); 445 | 446 | const char * fileName = GifFiles[currentFile++%totalFiles].c_str(); 447 | 448 | int loops = maxLoopIterations; // max loops 449 | int durationControl = maxLoopsDuration; // force break loop after xxx ms 450 | 451 | while(loops-->0 && durationControl > 0 ) { 452 | durationControl -= gifPlay( (char*)fileName ); 453 | gif.reset(); 454 | } 455 | 456 | } 457 | 458 | --------------------------------------------------------------------------------