autoconfig.corz.org text viewer..
[currently viewing: /public/scripts/ESP32/CYD_Slideshow/CYD_SlideShow.ino - raw]
/*
  🖼️

  Simple Slideshow for CYD

  A slideshow appliance for your Cheap Yellow Display.

  http://corz.org/ESP32/
  
  Apache 2.0 License. Do what thou wilt!

  Have fun! ❤

  ;o)

  corz.org 2025

*/


String version = "1.0.38c";



// External libraries..         [URLs and versions used when writing]

// JPEGDEC library - I like how it decodes JPEG images.
#include <JPEGDisplay.h>        /// https://github.com/bitbank2/JPEGDEC  v1.8.1
                                // 1.8.2 works if you change the file extension on JPEGDisplay.inl to .cpp  I have no idea what's going on there, Larry!

// I'm still playing with this and TFT_eSPI, to see which I like best.
#include <bb_spi_lcd.h>         /// https://github.com/bitbank2/bb_spi_lcd v2.9.6


// Standard Libraries..

// SD card access.
#include <SD.h>

// For storing prefs in flash memory, so they are retained after a reboot.
// (NVS == "Non Volatile Storage")
#include "nvs_flash.h"
#include <Preferences.h>
Preferences prefs;



/*
    Prefs..
              */


void START_PREFS() {} // Oh! What a rascal!


/*
  Delay Time.

  Default delay time, in seconds. Tap right corners to change.
  Delay time changes are stored "permanently" in NVS.
                      */

uint16_t delayTime = 15;  // 1 - 65535 (18.2 hours)

// Attempting to set the delay above this (many seconds) sets it back to this number.
uint16_t maxTime = 60; // A safety feature. Set whatever you like here, up to 65535.



/*
  Colours.
            */

uint16_t txtColor = 0x3D25;   // Info text on boot-up. TFT_GREEN is nice but a bit too bright for me.
uint16_t msgColor = txtColor; // You can set a different color for the messages, if you prefer.
uint16_t bgColor = TFT_BLACK; // Background color.
uint16_t fnColor = 0x3A87;    // File names.



/*

Common colors:

Use predefined  or  RGB565 value.

    TFT_BLACK       0x0000
    TFT_GREEN       0x07e0
    TFT_RED         0xf800
    TFT_BLUE        0x001f
    TFT_CYAN        0x07ff
    TFT_YELLOW      0xffe0
    TFT_MAGENTA     0xf81f
    TFT_WHITE       0xffff
    TFT_GREY        0x5AEB
    TFT_ORANGE      0xbbc0

Pick your own here: https://barth-dev.de/online/rgb565-color-picker/

*/



// This sketch should work fine with other displays. Set that here.
#define LCD DISPLAY_CYD_2USB
// See inside the bb_spi_lcd README.md for all the definitions.


// SD Card GPIO pin assignments for the CYD_2USB model. Edit to suit your device.
#define SD_MOSI 23
#define SD_MISO 19
#define SD_SCK 18
#define SD_CS 5

// Pin connected to the display backlight (PWM capable).
const uint8_t backlightPin   = 21;


/*
  Random Image Order

  If you have many thousands of images on your SD card, this will create a few
  seconds delay at boot (around 1s per 3000 images), while we count the files.
  If you don't ever want random images, you can disable this feature altogether.
                              */

bool allowRandomImages = true;


/*
  Random order as the default on boot.

  You can toggle this at any time (if allowRandomImages is true) by touching
  the bottom middle of the screen, or sending 'r' over the Serial connexion.

  This setting is stored to NVS and remembered across reboots, overriding this.
                             */

bool showRandomImage = false;
/*
  How it works:

  We simply count all the files on the card, then pick a random* number, quickly
  iterate the files up to that number and then load the /next/ file.

  You can load images directly by their ID. More on that, elsewhere.

  *   Well, you know, pseudo-random, like computers do.
*/



/*
  Sensor Backlight Control:

  We can use the on-board light sensor to set the back-lighting level.
  You can still set the brightness manually. Whatever *you* set becomes the new
  maximum for mapping the sensor data to the backlight level.
                                   */

bool sensorControlsBackLight = true;

// Pin connected to the light sensor (aka. "LDR", aka "photoresistor")..
const uint8_t lightSensorPin  = 34;


// If you want to test this, don't use your finger to cover the sensor, as your
// finger will allow current to flow across the resistor! To discover your upper
// device's upper limit instead, set debug level 4 and hold a folded black cloth
// tightly over the sensor. Or turn out ALL the lights.

// You can set the top and bottom limits of your sensor here.
uint16_t LDRLo = 250; // Full brightness. 0 - 300-ish, depending on your device's sensor.
uint16_t LDRHi = 1400; // Total darkness. 1300 - 1800-ish, for the devices I have.




/*
  Debug Output

  (prints useful information to the serial console)

  Set to 0 to disable serial output (AND input) altogether.

  1 for basic info, 2 for more info, 3 for most info.
  4: as 3, but adds (lots of) light sensor data.
  5: as 3, but lists all the stored image ID's (in random mode) as each is added.
  6: as 3, but adds complete file list (in random mode) at boot-up.
           Level 6 is NOT recommended if you have a lot of files on your SD card.

  Any higher number will output the same data as level 3.

  You can set this from the serial console: d3

  This setting is saved to NVS (so you can set, e.g. level 6 for next boot-up)

                            */

uint8_t debugLevel = 3;



void END_PREFS() {}




// Setup Classes, etc..

BB_SPI_LCD lcd;
JPEGDisplay jpeg;
SPIClass SD_SPI;
File file, root, previousFile;

// For info..
const char *compile_time  = __TIMESTAMP__;
#include <esp_system.h>    // for chip info


// Internal Variables..

// Initial maximum available backlight brightness. Normally 255. This can be set
// by touch and is remembered across reboots, overriding whatever is set here.
int16_t maxBrightness = 255;
uint16_t iWidth, iHeight;
uint64_t currentTime;
uint64_t lastTouchTime = millis();
uint64_t lastImageTime = 0;
bool slideShowPaused = false;
bool isPrevious = false;
uint64_t lastSensorCheck = 0;
int16_t currentBrightness = maxBrightness;
uint8_t clearLength;
uint64_t SSPausedAt;
uint16_t fileCount = 0;
bool msgAtTop = true;
bool showFileNames = false;
bool namesAtTop = false;
uint16_t currentID;

// For color reset..
uint16_t RtxtColor = txtColor;
uint16_t RmsgColor = msgColor;
uint16_t RbgColor = bgColor;
uint16_t RfnColor = fnColor;

// For remembering seen images in random mode. Up to 20,000 images.
// Uses under 48KiB of memory, all-in.
#define MAX_ITEMS 20000
#define BITSET_SIZE (MAX_ITEMS / 8)
uint16_t seenImageIDs[MAX_ITEMS] = {0};
uint8_t seenBitmap[BITSET_SIZE] = {0}; // Each byte keeps track of 8 image IDs.
int16_t seenB4 = 0;
int16_t listCount = 0;

// Long press in the centre of the screen to delete an image from the SD card.
#define LONG_PRESS_MS 810
bool pressInProgress = false;
bool longPressHandled = false;
uint32_t pressStartedAt = 0;
float progress;

// These numbers are about right, I'd say.
uint16_t indicatorRadius = 64;
uint8_t indicatorThickness = 12;

// Couple o' Strings we re-use..
const char strRandom[]     PROGMEM = "Random";
const char strSequential[] PROGMEM = "Sequential";


// General-purpose quick variable debugging, using Strength of Strings.
// Accepts most types, e.g.: DEBUG(varName);  --> varName: Value
#define DEBUG(x) Serial.println(" " #x ": " + String(x))





/*
    Begin Functions
                      */



/*
  Post a message to the top of the screen, centred.

  Hmm.. bb_spi_lcd has no built-in way to centre text.

*/

void doInfoMessage(String msg, uint16_t myColor = msgColor, bool top = msgAtTop, uint8_t myDebugLevel = 0) {
  if (msg.length() > clearLength) clearLength = msg.length() + 2;
  uint16_t myY = 4;
  if (!top) myY = iHeight - 16;
  // Clear away previous message so there are no letters or fragments at the edges..
  lcd.fillRect(iWidth/2 - ((clearLength/2) * 8), myY, (clearLength * 8), 12, bgColor, DRAW_TO_LCD);
  clearLength = msg.length() + 2;
  lcd.setCursor(iWidth/2 - ((msg.length()/2) * 8), myY); // Characters are 8 pixels wide (FONT_8x8).
  lcd.setTextColor(myColor, bgColor);
  lcd.println(msg.c_str());
  lcd.setTextColor(txtColor, bgColor);
  if (debugLevel > myDebugLevel) Serial.println(" " + msg);
}


// One message, two outputs.
// For boot-up report.
void doReport(String myMessage, uint8_t myDebugLevel = 0) {
  lcd.println(myMessage.c_str());
  if (debugLevel > myDebugLevel) Serial.print(myMessage.c_str());
}


// Fire up the SD card..
void mountSD(bool firstNotify) {

    SD_SPI.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);

    if (!SD.begin(SD_CS, SD_SPI, 10000000)) { // 10MHz is about my limit.
        if (firstNotify) {
            doReport(" Card Mount FAILED.\n");
        }
    // A fairly graceful way to deal with users pulling the SD card: reboot.
    // Then come here and wait..
        if(SD.cardType() == CARD_NONE) {
            if (firstNotify) {
                doReport("\n Please insert an SD card.\n");
            }
            // Loop back around every three seconds until an SD card is inserted.
            delay(3000);
            mountSD(false);
        }
    } else {
        doReport(" SD Card Mounted.\n");
    SD.rmdir("System Volume Information");
    }
}


// Guess what this does.
void rebootESP() {
  Serial.flush();
  ESP.restart();
}



/*
  Count the files on the card.
  On a class nothing SD card, this function will scan 10,000 files in under 3s.
                      */

void quickFileCount() {

  File disk = SD.open("/");
  if (!disk || !disk.isDirectory()) {
    doReport(" Problem with root directory. Check your SD Card.\n");
  }
  doReport("\n Scanning files..\n", 1);

  String lookFile = disk.getNextFileName();
  if (debugLevel == 6) Serial.printf(" [1]: %s\n", lookFile.c_str());
  if (lookFile == "") {
    doReport(" Empty Card\n");
    return;
  }

  uint64_t currentTime = millis();

  while (lookFile != "" && fileCount <= MAX_ITEMS) {
    lookFile = disk.getNextFileName(); // We don't open the files, which is why it's fast.
    fileCount++; // 1st file intentionally skipped, for two reasons.
    if (lookFile != "" && debugLevel == 6) Serial.printf(" [%i]: %s\n", fileCount+1, lookFile.c_str());
  }

  if (debugLevel > 1) doReport(" Scan complete in " + (String)((float_t)(millis() - currentTime) / 1000) + "s.\n\n");
  if (debugLevel > 1 && fileCount > 0) {
    doReport(" Total Files: " + (String)fileCount + "\n");
    doReport(" Average Size: " + (String)((float)(SD.usedBytes() / fileCount) / 1024) + "KiB\n");
  }
}



// Check if supplied file name has a valid JPEG file extension.
bool hasJPEGExtension(String fileName) {
  if (debugLevel > 2) Serial.printf(" checking extension of '%s'\n", fileName.c_str());
  String myExt = fileName.substring(fileName.lastIndexOf('.')+1);
  myExt.toLowerCase();
  return (!myExt.compareTo("jpg") || !myExt.compareTo("jpeg")); // exact match returns 0
}


/*
  Display an image.

  Send this function a valid (JPEG) file name.
  That image will be displayed on-screen.
                                */

void showImage(String nowImage) {
  String fullPath = "/" + nowImage;
  if (debugLevel == 13) fullPath += "fooMeBaby"// This causes loadJPEG() to fail, we assume. For debugging purposes.
  if (debugLevel > 1) Serial.printf(" Loading image: %s\n", nowImage.c_str());
  lcd.fillScreen(bgColor);
  jpeg.loadJPEG(&lcd, JPEGDISPLAY_CENTER, JPEGDISPLAY_CENTER, fullPath.c_str());
  if (showFileNames) doInfoMessage(nowImage, fnColor, namesAtTop, 2);
  seenB4 = 0;
}


// Using next() instead of the far more readable nextImage(), is simply UK
// humour.. Next. Next. Next. Other nationalities will get this too, no doubt.

// Load the NEXT file in the file system..
void next() {
  file = root.openNextFile();
}


/*
  Display an image..

  We first check the current "file" is valid. If not, attempt to remedy, try
  again. If SD card has been removed since last we accessed it, initiate reboot.

  IF the file is a valid file and has a valid JPEG extension, we attempt to
  display it as an image, using showImage().
                                                   */


void displayImage(uint64_t nowTime = lastImageTime) {

  lastImageTime = nowTime;
  isPrevious = false;

  while (true) {
    if (!file) {
      root.rewindDirectory();
      next();
    }
    if (!file) {
      if (debugLevel) Serial.println(F("\n\n SD Removed!\n Rebooting..\n\n"));
      rebootESP();
    }
    if (file.isDirectory()) {
      next();
      continue;
    }
    if (hasJPEGExtension(file.name())) {
      showImage(file.name());
      break;
    } else {
      next();
    }
  }
}




/*
  Remember which images we've already seen.

  We keep a simple list (array) of which images we've seen, and a bitmap
  (bitsetting) array keeps track of them with simple division (byte>bit == /8).

  We store the ID, which is the file's position in the file system.

  We can keep track of 20,000 images this way using around 4.7K.

  These two functions do it all..

                            */

bool seenID(uint16_t imgID) {
  return seenBitmap[imgID / 8] & (1 << (imgID % 8)); // 0 or 1. 1 (coerced to true) means we've seen this image.
}

bool storeID(uint16_t imgID) {
  if (debugLevel > 2) Serial.printf(" Storing ID: %i\n", imgID);
  if (seenID(imgID)) return false;
  seenBitmap[imgID / 8] |= (1 << (imgID % 8));
  seenImageIDs[listCount++] = imgID;
  if (debugLevel == 5) printFileIDsReversed();
  return true;
}

// Oh oh!
void resetList() {
  if (debugLevel > 1) Serial.println(F(" Resetting 'seen' images list.\n"));
  listCount = 0;
  memset(seenBitmap, 0, sizeof(seenBitmap));
  memset(seenImageIDs, 0, sizeof(seenImageIDs));
}

/*
  Want to go BACK > BACK > BACK ?

  You noticed how all the IDs get stored in order-of-viewing, right?

  I put this here to show you how trivial it is to access the ID's of previously
  seen images, in reverse order. You could add infinite backwards capability,
  if you really wanted to. Set debugLevel to 5 to see this in action.

                               */

void printFileIDsReversed() {
  Serial.printf(" File ID's: ");
  for (int i = listCount-1; i >= 0; i--) {
    Serial.printf("%i", seenImageIDs[i]);
    Serial.print((i > 0) ? ", " : ".");
  }
  Serial.println("");
}

// If you want to go there, here's something you could use to remove the most
// recently-added ID from the list, after showing your "previous" image.
bool removeLastID() {
  if (listCount == 0) return false;
  uint16_t lastID = seenImageIDs[listCount - 1];
  // Clear the bit in the bitmap..
  seenBitmap[lastID / 8] &= ~(1 << (lastID % 8));
  // Clear the value in the image list array (technically, not actually required).
  seenImageIDs[--listCount] = 0;
  return true;
}



/*
  Select a random or specific JPEG from the file system.

  In sequential mode, we simply use next(). next(). next()..

                                                      */

void selectImage(uint64_t nowTime, uint16_t myID = 0) {

  // Risky (the temp String buffer), but works fine in practice with GCC. Also it's trivial..
  if (debugLevel > 1) Serial.printf("\n Searching for image.. [%s]\n", (myID != 0) ? String(myID).c_str() : "random");

  uint16_t count = 0;
  bool manualSet = false;
  String tFile;
  uint16_t fileID;

  if (myID != 0) {
    if (myID > fileCount) myID = fileCount;
    fileID = myID;
    manualSet = true;
  } else {
    fileID = random(1, fileCount-1); // -1 as we will open the /next/ image.
  }

  // Check if we have seen this image already. If so, try again..
  if (!manualSet && !storeID(fileID)) {
    if (debugLevel > 1) Serial.println(F(" Seen this image already! Trying again.."));
    seenB4++;
    // Darnit! We have run out of "new" images.
    if (seenB4 > 4) { // Three-in-a-row could be a coincidence. Five-in-a-row is not.
      if (debugLevel > 1) Serial.println(F(" Encountered five consecutive ID clashes."));
      resetList(); // Well, at least you made this function momentarily happy.
    }
    selectImage(nowTime);
    return;
  }

  root.rewindDirectory();

  for (uint16_t filez = 1; filez < fileID; filez++ ) {
    tFile = root.getNextFileName();
    if (tFile == "") rebootESP(); // SD card pulled (by pushing).
    count++;
  }
  next(); // See what I did there?
  count++;
  currentID = count;

  if (file) {
    if (debugLevel > 2) Serial.printf(" Using file %u: %s\n", count, file.name());
  } else {
    if (debugLevel > 1) Serial.printf(" Failed to open file %u:\n", count);
  }
}



/*
  Select the next JPEG file.
  When this is done, "file" is set.
                                       */

void showNextImage(uint64_t nowTime) {

  if (file) previousFile = file;

  if (showRandomImage) {
    selectImage(nowTime);
  } else {
    next();
  }

  displayImage(nowTime);
}


/*
  Display the previously shown image.

  Note: we only store one single previous image, for that "missed" image.
  Feel free to hit PAUSE.
                                                     */

void previousImage(uint64_t nowTime = lastImageTime) {
  lastImageTime = nowTime;
  isPrevious = true;
  if (debugLevel > 1 && previousFile) Serial.printf(" Showing Previous Image: %s\n", previousFile.name());
  if (previousFile) showImage(previousFile.name());
}



/*
  Reload current image.

  This clears messages, sets new colors, etc..
  Hardware timers causing issues with bb_spi, so..
               */

void reLoad() {
  if (isPrevious) {
    previousImage();
  } else {
    displayImage();
  }
}


/*
  Backlight
             */


// Brightness (backlight) is actually set here..
void setBrightness(uint8_t brightnessLevel) {
  analogWrite(backlightPin, brightnessLevel);
}

void brightnessSET() {
  if (debugLevel > 1) Serial.printf("\n Setting Brightness to: %lu%%\n", map(maxBrightness, 1, 255, 1, 100));
  setBrightness(maxBrightness);
  prefs.putShort("brightness", maxBrightness);
}

// You pressed top-left corner area of screen (3 ascending zones - as you slide your pointer towards the centre, the pace of change increases)
void brightnessUP(uint8_t up) {
  maxBrightness += up;
  if (maxBrightness > 255) maxBrightness = 255;
  brightnessSET();
}

// You pressed bottom-left corner area of screen (another 3 zones, but downwards)
void brightnessDOWN(uint8_t down) {
  maxBrightness -= down;
  if (maxBrightness < 1) maxBrightness = 1;
  brightnessSET();
}



/*
  LDR-Controlled backlight

  Read the on-board light sensor and adjusts the LED backlight accordingly.
  Never sets the backlight higher than the user-set brightness level.

  Only kicks-in in a very dark environment.
  Modding your LDR circuit can improve its range and performance.

                            */

void autoAdjustBackLight() {
  int16_t sensorDATUM = analogRead(lightSensorPin); // 0 (okay, 250-ish) - 1800+ and not linear.
  if (debugLevel == 4) Serial.printf("\n sensorDATUM: %i\n", sensorDATUM);
  int16_t brightness = map(sensorDATUM, LDRLo, LDRHi, maxBrightness, 5);
  if (debugLevel == 4) Serial.printf(" Mapped Brightness: %i\n", brightness);
  brightness = constrain(brightness, 5, maxBrightness);
  if (debugLevel == 4) Serial.printf(" Constraining Brightness: %i\n", brightness);
  setBrightness(brightness);
}




// You pressed the top or bottom right-hand corner of the screen
// We don't clear this message, so we can get rapid changes on touch hold.
void delayChanged( bool doMSG = true) {
  delayTime = constrain(delayTime, 1, maxTime);
  prefs.putUShort("dTime", delayTime); // Save to NVS
  if (doMSG) doInfoMessage("Delay: " + (String)delayTime + "s");
  if (debugLevel > 1) Serial.printf(" Delay Time changed to: %is.\n", delayTime);
}


// Under normal circumstances, this never fires.
void WipeNVRAM() {
    if (debugLevel) Serial.println(F("\n Wiping NVS\n"));
    esp_err_t ret = nvs_flash_init();
    ESP_ERROR_CHECK(nvs_flash_erase()); // The actual wipe
    ret = nvs_flash_init();
    ESP_ERROR_CHECK(ret);
}


void pausePlaySlideshow(bool forcePause = false) {

  if (forcePause) {
    slideShowPaused = true;
  } else {
    slideShowPaused = !slideShowPaused;
  }

  if (slideShowPaused) {
    if (!forcePause) doInfoMessage(F("PAUSE"));
    SSPausedAt = millis();
  } else {
    doInfoMessage(F("PLAY"));
    lastImageTime += millis() - SSPausedAt;
  }

  if (debugLevel > 1) Serial.printf(" Slideshow %s \n", (slideShowPaused) ? "PAUSED" : "PLAYING");
}


void toggleRandomImages() {
  if (allowRandomImages) {
    showRandomImage = !showRandomImage;
    prefs.putBool("random", showRandomImage);
    doInfoMessage(showRandomImage ? strRandom : strSequential);
  } else {
    doInfoMessage(F("Random Feature DISABLED"));
  }
  if (debugLevel) {
    char buffer[12];
    strcpy_P(buffer, showRandomImage ? strRandom : strSequential);
    Serial.printf(" Setting image order to %s.\n", buffer);
  }
}


void toggleFileNames() {
  showFileNames = !showFileNames;
  prefs.putBool("showNames", showFileNames);
  (showFileNames) ? doInfoMessage(F("File names enabled")) : doInfoMessage(F("File names disabled"));
  delay(250);
}


// Print uptime to the serial console..
//
void printUptime() {
  uint32_t seconds = millis() / 1000;
  uint16_t days = seconds / 86400;
  seconds %= 86400;
  uint8_t hours = seconds / 3600;
  seconds %= 3600;
  uint8_t minutes = seconds / 60;
  seconds %= 60;
  Serial.printf(" Uptime: %ud %uh %um %lus\n", days, hours, minutes, seconds);
}



// System info..
// If the used space / average file size is wrong, try reformatting your card.
void printInfo() {

  Serial.println(F("\n System Information: \n"));

  Serial.printf("   CPU frequency: %lu MHz\n", ESP.getCpuFreqMHz());
  Serial.printf("   Chip revision: %u\n", ESP.getChipRevision());
  Serial.printf("   SDK version: %s\n", ESP.getSdkVersion());

  // Use lambda function to neatly convert bytes to KiB or MiB on-the-fly.. (save us repeating this code again and again)
  auto printSize = [](const char* label, uint32_t bytes) {
    if (bytes < (1024 * 1024)) {
      Serial.printf("   %s: %.2f KiB\n", label, bytes / 1024.0); // Add decimal to create float result
    } else {
      Serial.printf("   %s: %.2f MiB\n", label, bytes / 1048576.0);
    }
  };

  printSize("Flash chip size",     ESP.getFlashChipSize());
  printSize("Heap size",           ESP.getHeapSize());
  printSize("Free heap",           ESP.getFreeHeap());
  printSize("Min free heap",       ESP.getMinFreeHeap());
  printSize("Max alloc heap",      ESP.getMaxAllocHeap());

  Serial.print("  "); printUptime();

  Serial.println(F("\n Sketch Information: \n"));

  Serial.printf("   Sketch compiled: %s\n", compile_time);
  Serial.printf("   Sketch MD5: %s\n", ESP.getSketchMD5().c_str());
  printSize("Sketch size",         ESP.getSketchSize());
  printSize("Sketch free space",   ESP.getFreeSketchSpace());
  Serial.printf("   Used space: %.1f%%\n", (float)(100 * ESP.getSketchSize() / ESP.getFreeSketchSpace()));

  Serial.println(F("\n SD Card: \n"));

  printSize("SD Card Size", SD.cardSize());
  printSize("Total space", SD.totalBytes());
  printSize("Used space", SD.usedBytes());
  if (fileCount) {
    Serial.printf("   File count: %i\n", fileCount);
    Serial.printf("   Average size: %.2f KiB\n", (float)(SD.usedBytes() / fileCount) / 1024);
  }

  Serial.println(F("\n Simple Slideshow: \n"));
  Serial.printf("   Brightness: %lu%%\n", map(maxBrightness, 1, 255, 1, 100));
  Serial.printf("   Image delay: %is\n", delayTime);
  Serial.printf("   Image Order: %s\n", (showRandomImage) ? strRandom : strSequential);
  Serial.printf("   Debug level: %i\n", debugLevel);
  Serial.println();

}



// Also *my* command reference..

const char HELP_TEXT[] PROGMEM = R"HELP(
 CYD Simple Slideshow accepts the following commands:

    t<*>              Set Delay Time. Either an absolute value (t25 or t=25) or nudge up/down a second with t+ or t-
    b<*>              Set Screen Brightness. Either an absolute % value (b50 or b=100) or nudge up/down approx. 4% with b+ or b-
    .|*               Next Image. That's '.' or '*'. Numeric keypad operation is in mind.
    0|/               Previous Image. That's '0' or '/'.
    -                 Pause / Resume Slideshow
    r                 Toggle Random / Sequential viewing order.
    l<int>            Load specific image ID.
                      (In random mode, IDs are printed to the serial console. Use debug level 6 for a list of all IDs at boot-up)
    f                 Toggle show file names.
    m                 Toggle message position (top / bottom).
    n                 Toggle file name position (top / bottom).
    sensor            Toggle Sensor-controlled backlight ("
ldr" also works).

    tcolor=<*>        Change color of boot-up text using RGB565 value, like so: tcolor=0x3D25
    ncolor=<*>        Change color of file names.       (Note: You can also use web hex format for colors, e.g. color=#0x3A87)
    bcolor=<*>        Change the background color.      (Short format web hex is fine, too, e.g. color=#0F0, the "#" is optional)
    mcolor=<*>        Change the message color.         (All color controls use the same formats, converted internally to RGB565)
                      NOTES: You MUST use the '=' format to assign colors, if you want it to work.
                             You can send "
reset" as your color value, to reset back to your hard-coded color.

    d<int>            Set the debug level.
    z<int>            TEMPORARILY set the debug level. Reverts on reboot.
    reset             Reset the "
seen" list (in random mode).
    uptime            Print out ESP32 system uptime.
    info              Print out lots of useful information about the system and software. 'i' also works.
    wipenvs           Wipe NVS (permanent storage) memory. There is NO WARNING. Only use this if you want the NVS totally wiped!
    help              Print out this help screen. '?' also works.
    reboot            Reboots the ESP32 device. 'x' also works.

 Single character commands with numeric values can optionally use the '=' format; d3 and d=3 both work.
 "
Word" commands with values *must* use the '=' format; color0x03E0 will not work.

)HELP"
;

void printHelp() {
 Serial.println(FPSTR(HELP_TEXT));
}




// Input an RGB888 web color, get back a usable RGB565 value..
//
uint16_t webToRGB565(const String& webColorInput, uint16_t defColor) {

  String webColor = webColorInput;

  if (webColor.startsWith("#")) webColor.remove(0, 1);

  if (webColor.length() == 3) {
    // User supplied shorthand #RGB. We convert it to #RRGGBB..
    webColor = String(webColor.charAt(0)) + webColor.charAt(0) +
               webColor.charAt(1) + webColor.charAt(1) +
               webColor.charAt(2) + webColor.charAt(2);
  }

  if (webColor.length() != 6) {
    return defColor;  // Invalid. Use default.
  }

  for (uint8_t i = 0; i < 6; i++) {
    if (!isHexadecimalDigit(webColor.charAt(i))) {
      return defColor; // and again
    }
  }

  uint8_t r = strtoul(webColor.substring(0, 2).c_str(), nullptr, 16);
  uint8_t g = strtoul(webColor.substring(2, 4).c_str(), nullptr, 16);
  uint8_t b = strtoul(webColor.substring(4, 6).c_str(), nullptr, 16);

  //return lcd.color565(r, g, b); // <-- built-in conversion function, does this..

  return ((r & 0xF8) << 8) |   // top 5 bits of red
         ((g & 0xFC) << 3) |   // top 6 bits of green
         (b >> 3);             // top 5 bits of blue
}                              /// Scroll here: https://barth-dev.de/online/rgb565-color-picker/ for how this works.





// Delete the current image from the SD Card..
//
String deleteImage() {

  String filename;

  if (isPrevious) {
    filename = previousFile.name();
  } else {
    filename = file.name();
    file.close();
  }

  if (filename != "") {
    if (SD.remove("/" + filename)) {
      return "Deleted: " + filename;
    } else {
      return "Failed to delete: " + filename;
    }
  } else {
    return "Error! No file to delete!";
  }
}



// Release touch before the end of the red zone, or Poof! It's gone.
// Not purely cosmetic; we want progress to be seen against *any* backdrop.
uint16_t getGradientColor(float progress) {

  uint8_t r, g, b = 0;

  if (progress <= 0.33f) { // Green (0,255,0) -> Yellow (255,255,0)
    float t = progress / 0.33f;
    r = (uint8_t)(255 * t); // 0 -> 255
    g = 255;
    b = 0;
  }
  else if (progress <= 0.66f) { // Yellow (255,255,0) -> Orange (255,128,0)
    float t = (progress - 0.33f) / 0.33f;
    r = 255;
    g = (uint8_t)(255 - 127 * t); // 255 -> 128
    b = 0;
  }
  else { // Orange (255,128,0) -> Red (255,0,0)
    float t = (progress - 0.66f) / 0.34f;
    r = 255;
    g = (uint8_t)(128 * (1.0f - t)); // 128 -> 0
    b = 0;
  }

  return lcd.color565(r, g, b);
}



// Indicate long press on centre of screen..
void doLongPressFeedback(float progress, int16_t cx, int16_t cy, uint8_t radius = 64, uint8_t thickness = 12) {

  static uint8_t lastSeg = 0; // We don't re-draw the entire progress; only *this* progress.

  const uint8_t segments = 45;
  const uint8_t pipWidth = 3; // Fatter == slower. Fat enough you get solid (*segments >= 360).

  uint8_t endSeg = (uint8_t)(progress * segments);
  if (endSeg > segments) endSeg = segments;

  for (uint8_t t = 0; t < thickness; t++) {

    int16_t innerRad = radius - t;
    int16_t outerRad = innerRad - 1;

    for (uint8_t i = lastSeg; i < endSeg; i++) {

      float angle = 2.0f * PI * (float)i / segments - PI / 2; // Start at 12 o'clock.

      int16_t x1 = cx + (int16_t)(innerRad * cos(angle));
      int16_t y1 = cy + (int16_t)(innerRad * sin(angle));
      int16_t x2 = cx + (int16_t)(outerRad * cos(angle));
      int16_t y2 = cy + (int16_t)(outerRad * sin(angle));

      uint16_t discColor = getGradientColor((float)i / segments);

      // Draw offset lines for fatter pips..
      for (int8_t dx = -pipWidth / 2; dx <= pipWidth / 2; dx++) {
        for (int8_t dy = -pipWidth / 2; dy <= pipWidth / 2; dy++) {
          lcd.drawLine(x1 + dx, y1 + dy, x2 + dx, y2 + dy, discColor);
        }
      }
    }
  }

  lastSeg = endSeg;
}



// End functions










/*
   This space left intentionally blank.
                                        */








void loop() {

  currentTime = millis();

  // Check light sensor (5 times a second)..
  if (sensorControlsBackLight && currentTime > (lastSensorCheck + 200)) {
    lastSensorCheck = currentTime;
    autoAdjustBackLight();
  }

  bool doClear = false;
  bool delayChange = false;
  TOUCHINFO ti;


  /*
     A Touch Event..
                      */


  if (lcd.rtReadTouch(&ti) && ti.count >= 1) {

    // Eliminates touch echoes (aka. de-bounce) and provides nice fast step for touch-and-hold..
    if (currentTime > (lastTouchTime + (120))) { // Increase this if you still get echoes

      lastTouchTime = currentTime;

      uint16_t touchX = ti.x[0];
      uint16_t touchY = ti.y[0];
      uint16_t touchZ = ti.pressure[0];

      // Print Touchscreen info to serial monitor..
      if (debugLevel > 2) Serial.printf(" Touch: X = %i | Y = %i | Pressure = %i\n", touchX, touchY, touchZ);


      // Middle strip (half screen height)..

      if ( (touchY >= (iHeight/4)) && (touchY <= (iHeight*3/4)) ) {

        // Pause / Resume / [long press = delete]
        if (touchX > iWidth/4 && touchX < iWidth*3/4) {

          // Begin press / long press..
          if (!pressInProgress) {
            pressInProgress = true;
            longPressHandled = false;
            pressStartedAt = currentTime;
          }

          // Hold to delete current image.
          if (!longPressHandled) {

            progress = (float)(currentTime - pressStartedAt) / (float)LONG_PRESS_MS;
            if (progress > 1.0) progress = 1.0;

            // Draw progress indicator..
            doLongPressFeedback(progress, iWidth/2, iHeight/2, indicatorRadius, indicatorThickness);

            if (progress >= 1.0) {
              doInfoMessage(deleteImage());
              delay(333);
              longPressHandled = true;
              showNextImage(currentTime);
            }
          }

        // Previous Image
        } else if (touchX < iWidth/4) {

          if (debugLevel > 2) Serial.println(F(" LEFT BUTTON: Previous image"));
          previousImage(currentTime);

        // NEXT Image
        } else if (touchX > iWidth*3/4) {
          if (debugLevel > 2) Serial.println(F(" RIGHT BUTTON: Next image"));
          if (showRandomImage) doInfoMessage(F("Working.."));
          showNextImage(currentTime);
        }


      // Finished with middle buttons.


      /*
        Top & Bottom edge buttons..

        Left: Brightness +/-
        Right: Delay Time +/-

        Middle Top: Toggle show file names
        Middle Bottom: Toggle random order

                        */

      } else {

        // Top 1/4 of display..
        if (touchY <= (iHeight/4)) {

          // Left side: Brightness. (three buttons, increasing in speed)
          if (touchX <= iWidth/9) {
            brightnessUP(5);
          } else if (touchX <= iWidth/6) {
            brightnessUP(10);
          } else if (touchX <= iWidth/3) {
            brightnessUP(20);

          // Right side: Delay time.
          } else if (touchX >= iWidth*2/3) {
            delayTime += 1;
            delayChange = true;

          // Middle
          } else {
            toggleFileNames();
            doClear = true;
          }
        }

        // Bottom 1/4 of display..
        if (touchY >= (iHeight*3/4)) {

          // Left side: Brightness. (another three buttons, same story)
          if (touchX <= iWidth/9) {
            brightnessDOWN(5);
          } else if (touchX <= iWidth/6) {
            brightnessDOWN(10);
          } else if (touchX <= iWidth/3) {
            brightnessDOWN(20);

          // Right side: Delay time.
          } else if (touchX >= iWidth*2/3) {
            delayTime -= 1;
            delayChange = true;

          // Middle
          } else {
            doClear = true;
            toggleRandomImages();
          }
        }
      }

      // Delay time is going up or down..
      if (delayChange) delayChanged();

    } // lastTouch timer had elapsed.

  } else { // No touch on this loop()

    // Maybe finger lifted..
    if (pressInProgress) {
      if (!longPressHandled) {
        if (progress > 0.1) {
          doInfoMessage(F("Delete cancelled"));
        } else {
          if (debugLevel > 2) Serial.println(F(" MIDDLE BUTTON: Play/Pause"));
          pausePlaySlideshow();
        }
        doClear = true;
      }
    }
    pressInProgress = false;
  }

  if (pressInProgress) return;


  /*
      End touches.
                    */




  /*
     Serial commands..
                              */


  if (Serial.available() > 0) {

    String rawCommand, longParams;

    if (Serial.peek() > 0) rawCommand = Serial.readStringUntil('\n');
    rawCommand.trim();

    // Capture first character of the command as a char..
    char cmd = rawCommand[0];
    cmd = tolower(cmd);

    // And everything after it..
    String cmdData = rawCommand.substring(1);
    cmdData.trim();

    // Using x=y format, remove the '='..
    if (cmdData[0] == '=') cmdData = cmdData.substring(1);

    // Word command with parameters, e.g. time=HH:MM:SS
    if (cmdData.indexOf('=') != -1) {
      longParams = rawCommand.substring(rawCommand.indexOf('=') + 1);
      rawCommand = rawCommand.substring(0, rawCommand.indexOf('='));
    }

    // Commands are case-insensitive. Parameters are not.
    rawCommand.toLowerCase();


    /*
        Long commands without parameters..
                                              */


    if (rawCommand == "reset") {
      resetList();
      return;
    }

    if (rawCommand == "reboot" || rawCommand == "x") {
      rebootESP();
    }

    if (rawCommand == "info" || rawCommand == "i") {
      printInfo();
      return;
    }

    if (rawCommand == "uptime") {
      printUptime();
      return;
    }

    if (rawCommand == "help" || rawCommand == "?") {
      printHelp();
      return;
    }

    if (rawCommand == "wipenvs") {
      WipeNVRAM();
      Serial.println(F(" NVS Wiped."));
      return;
    }

    if (rawCommand == "sensor" || rawCommand == "ldr") {
      sensorControlsBackLight = !sensorControlsBackLight;
      prefs.putBool("sensor", sensorControlsBackLight);
      Serial.printf(" Sensor controls backlight: %s\n", (sensorControlsBackLight) ? "ENABLED" : "DISABLED");
      return;
    }


    /*
        Long commands with parameters..
                                          */


    // Boot-up text color..
    if (rawCommand == "tcolor" && longParams != "") {
      if (longParams.substring(0, 2) != "0x" && longParams != "reset" && longParams.charAt(0) != '#') {
        Serial.println(F("\n Incorrect color format. Use RGB565 hex value, e.g. tcolor=0x3D25\n You can also use web hex #RGB or #RRGGBB format, e.g. tcolor=#7f7f7f"));
      } else {
        if (longParams.charAt(0) == '#') {
          txtColor = webToRGB565(longParams, RtxtColor);
        } else {
          txtColor = (longParams == "reset") ? RtxtColor : (uint16_t)strtol(longParams.c_str(), nullptr, 0);
        }
        prefs.putShort("txtColor", txtColor);
        Serial.printf("\n Boot-up text color %sset to: 0x%04X \n", (longParams != "reset" && txtColor != RtxtColor) ? "" : "re", txtColor);
      }
      return;
    }

    // File Names color..
    if (rawCommand == "ncolor" && longParams != "") {
      if (longParams.substring(0, 2) != "0x" && longParams != "reset" && longParams.charAt(0) != '#') {
        Serial.println(F("\n Incorrect color format. Use RGB565 hex value, e.g. ncolor=0x0180\n You can also use web hex #RGB or #RRGGBB format, e.g. ncolor=#0d4d00"));
      } else {
        if (longParams.charAt(0) == '#') {
          fnColor = webToRGB565(longParams, RfnColor);
        } else {
          fnColor = (longParams == "reset") ? RfnColor : (uint16_t)strtol(longParams.c_str(), nullptr, 0);
        }
        prefs.putShort("fnColor", fnColor);
        Serial.printf("\n Filename color %sset to: 0x%04X \n", (longParams != "reset" && fnColor != RfnColor) ? "" : "re", fnColor);
      }
      return;
    }


    // Message color..
    if (rawCommand == "mcolor" && longParams != "") {
      if (longParams.substring(0, 2) != "0x" && longParams != "reset" && longParams.charAt(0) != '#') {
        Serial.println(F("\n Incorrect color format. Use RGB565 hex value, e.g. mcolor=0x3D25\n You can also use web hex #RGB or #RRGGBB format, e.g. mcolor=#46a739"));
      } else {
        if (longParams.charAt(0) == '#') {
          msgColor = webToRGB565(longParams, RmsgColor);
        } else {
          msgColor = (longParams == "reset") ? RmsgColor : (uint16_t)strtol(longParams.c_str(), nullptr, 0);
        }
        prefs.putShort("msgColor", msgColor);
        Serial.printf("\n Message color %sset to: 0x%04X \n", (longParams != "reset" && msgColor != RmsgColor) ? "" : "re", msgColor);
      }
      return;
    }


    // Background color..
    if (rawCommand == "bcolor" && longParams != "") {
      if (longParams.substring(0, 2) != "0x" && longParams != "reset" && longParams.charAt(0) != '#') {
        Serial.println(F("\n Incorrect color format. Use RGB565 hex value, e.g. bcolor=0x0000\n You can also use web hex #RGB or #RRGGBB format, e.g. bcolor=#000"));
      } else {
        if (longParams.charAt(0) == '#') {
          bgColor = webToRGB565(longParams, RbgColor);
        } else {
          bgColor = (longParams == "reset") ? RbgColor : (uint16_t)strtol(longParams.c_str(), nullptr, 0);
        }
        doClear = true;
        prefs.putShort("bgColor", bgColor);
        Serial.printf("\n Background color %sset to: 0x%04X \n", (longParams != "reset" && bgColor != RbgColor) ? "" : "re", bgColor);
      }
      reLoad();
      return;
    }


    /*
       Single char commands with no parameters.

                      */

    if (cmdData == "") {

      // Next Image..
      if (cmd == '.' || cmd == '*') showNextImage(currentTime);

      // Previous Image..
      if (cmd == '0' || cmd == '/') previousImage(currentTime);


      // Toggle File Names..
      if (cmd == 'f') {
        toggleFileNames();
        doClear = true;
      }

      // Pause / Resume Slideshow..
      if (cmd == '-') {
        doClear = true;
        pausePlaySlideshow();
      }

      // Toggle random image order..
      if (cmd == 'r') {
        doClear = true;
        toggleRandomImages();
        prefs.putBool("random", showRandomImage);
      }

      // Toggle file names position..
      if (cmd == 'n' ) {
        namesAtTop = !namesAtTop;
        prefs.putBool("namesAtTop", namesAtTop);
        Serial.printf(" File names at: %s\n", (namesAtTop) ? "Top" : "Bottom");
      }

      // Toggle message position..
      if (cmd == 'm') {
        msgAtTop = !msgAtTop;
        prefs.putBool("msgAtTop", msgAtTop);
        Serial.printf(" Messages at: %s\n", (msgAtTop) ? "Top" : "Bottom");
      }

    }

    /*
        Char commands with parameters
        e.g. d3 (or d=3, or d = 3)
                                      */


    // Set debug level..
    if ((cmd == 'z' || cmd == 'd') && isDigit(cmdData[0])) {
      uint8_t oldLevel = debugLevel;
      debugLevel = cmdData.toInt();
      Serial.printf("\n Debug level%s %s: %i\n", (cmd == 'd') ? "" : " TEMPORARILY", (debugLevel == oldLevel) ? "remains" : "set to", debugLevel);
      if (cmd == 'd') {
        prefs.putShort("debug", debugLevel);
        if (debugLevel == 0) Serial.println(F(" NOTE: The serial connexion will be DISABLED on your next reboot.\n If you don't want this, set debug level to 1 or more.\n"));
        if (debugLevel == 0) Serial.println(F(" A safer option is to use 'z0', which sets a TEMPORARY debug level of 0.\n On reboot it will revert to the previously set level.\n"));
      } else {
        Serial.printf(F(" Debug level will revert to previously (d)set value on reboot.\n"));
      }
      return;
    }

    // Set Delay Time
    if (cmd == 't' && cmdData) {
      switch (cmdData[0]) {
        case '+':
          delayTime += 1;
          break;
        case '-':
          delayTime -= 1;
          break;
        default:
          delayTime = cmdData.toInt();
      }
      delayChanged(false);
      return;
    }

    // Brightness
    if (cmd == 'b' && cmdData) {
      switch (cmdData[0]) {
        case '+':
          brightnessUP(10);
          break;
        case '-':
          brightnessDOWN(10);
          break;
        default:
          maxBrightness = map(constrain(cmdData.toInt(), 1, 100), 0, 100, 0, 255);
          brightnessSET();
      }
    }

    // Load a specific image..
    if (cmd == 'l' && cmdData) {
      selectImage(currentTime, cmdData.toInt());
      displayImage(currentTime);
      pausePlaySlideshow(true); // Force the slideshow to pause.
    }
  }

  /*
      End Serial Commands
                           */



  if (doClear) {
    delay(250);
    reLoad();
    return;
  }

  if (slideShowPaused) return;

  if (lastImageTime == 0 || currentTime > (lastImageTime + (delayTime * 1000))) {
    showNextImage(currentTime);
  }

  delay(5);

// loop()




void setup(void) { // <- Folk who do this are messing with you. It's exactly the same as setup(). They want you to stare into the void, literally.

  pinMode(lightSensorPin, INPUT);
  pinMode(backlightPin, OUTPUT); // not actually required.

    prefs.begin("CYDSS");
    esp_err_t err = nvs_flash_init();
    if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) WipeNVRAM();
    ESP_ERROR_CHECK(err);
    debugLevel = prefs.getShort("debug", debugLevel);

    if (debugLevel) Serial.begin(115200);

    lcd.begin(LCD);
    lcd.rtInit(); // Resistive Touchscreen init
    iWidth = lcd.width();
    iHeight = lcd.height();

    lcd.setFont(FONT_8x8);
    lcd.fillScreen(bgColor);
    lcd.setTextColor(txtColor, bgColor);

  doReport("\n Simple Slideshow v" + version + "\n\n");

  // Check for prefs stored in NVS..
    maxBrightness = prefs.getShort("brightness", maxBrightness);
    delayTime = prefs.getUShort("dTime", delayTime);
  showRandomImage = prefs.getBool("random", showRandomImage);
  sensorControlsBackLight = prefs.getBool("sensor", sensorControlsBackLight);
  showFileNames = prefs.getBool("showNames", showFileNames);
  msgAtTop = prefs.getBool("msgAtTop", msgAtTop);
  namesAtTop = prefs.getBool("namesAtTop", namesAtTop);

  setBrightness(maxBrightness);

  doReport(" Debug Level: " + (String)debugLevel + "\n\n");
  doReport(" Brightness: " + (String)map(maxBrightness, 1, 255, 1, 100) + "%\n");
  doReport(" Image Delay: " + (String)delayTime + "s\n");
  doReport(" Image Order: " + (String)((showRandomImage) ? strRandom : strSequential) + "\n\n");

    mountSD(true);
    delay(250);

  if (allowRandomImages) {
    randomSeed(esp_random() ^ micros());
    quickFileCount();
    delay(1250);
  } else {
    showRandomImage = false;
  }

  Serial.print(" Playing Slideshow..\n");
  root = SD.open("/");

  if (!root || !root.isDirectory()) {
    if (debugLevel) Serial.println(F(" Failed to open root directory. Reformat your card and try again."));
  }

}

Welcome to autoconfig.corz.org!

I'm always messing around with the back-end.. See a bug? Wait a minute and try again. Still see a bug? Mail Me!