autoconfig.corz.org uses cookies to remember that you've seen this notice explaining that autoconfig.corz.org uses cookies, okay!
autoconfig.corz.org text viewer..
[currently viewing: / public/ scripts/ ESP32/ TL-CAM/ TL-CAM.ino - raw]
String version = "1.0.2.0";
bool autoSnap = true;
uint16_t delaySeconds = 0;
uint16_t delayMinutes = 60;
uint16_t captureSize = FRAMESIZE_QSXGA;
String fileNamePrefix = "";
bool showDecimalSize = false;
bool quickListings = true;
uint16_t maxMBytesLimit = 300;
bool displayDetails = true;
uint8_t ledBrightness = 2;
bool eXi = true;
bool overWrite = false;
bool doSleep = false;
bool deepSleep = false;
uint8_t deepSleepDelay = 10;
String addDimensions = "";
bool remControl = true;
const char *timeZone = "GMT+0BST-1,M3.5.0/01:00:00,M10.5.0/02:00:00";
bool useRealTime = true;
bool timeStampFileNames = true;
const char *dateTimeString = "%H.%M.%S~%d-%m-%y";
uint32_t memReserve = 175000;
String streamFileName = "TL-CAM.mjpeg";
uint8_t timeOut = 10;
const char *ssid = "YOUR-SSID";
const char *password = "YOUR-WIFI-PASSWORD";
const char *SGhostName = "espcam";
String softAPSSID = "TL-CAM-AP";
String softAPPass = "JustOpenUpFool";
bool onlyAP = false;
const char *ntpServer = "time1.google.com";
bool realTimeRequired = true;
bool useStoredTime = true;
uint8_t maxNTPRetries = 5;
bool truncateReverse = false;
uint16_t imagesPerPage = 20;
uint16_t perPageMenuStep = 10;
uint16_t thumbWidth = 125;
String imgTitleName = "images";
String imgTallyName = "image";
String loadingMsg = "loading..";
bool loadingIcon = true;
bool loopPoP = false;
uint16_t webMessageTime = 2000;
bool spacesInListView = true;
bool stickyPoP = false;
bool roundedCorners = true;
uint8_t slideTime = 3;
bool upDownRev = false;
bool cacheImages = true;
uint16_t cacheTime = 30;
bool darkMode = false;
bool autoDark = true;
uint8_t dayBegins = 6;
uint8_t nightBegins = 20;
String lightBGColor = ";
String darkBGColor = ";
bool webSnapResetsTimer = false;
bool streamANDRecord = true;
uint8_t zmAcelleration = 10;
bool chromeHack = false;
uint64_t lastSnapTime = millis();
uint64_t delayTime;
const uint64_t MINUTE_MILLIS = 60000;
const uint64_t HOUR_MILLIS = 3600000;
const uint64_t DAY_MILLIS = 86400000;
const uint64_t SECOND_MICROS = 1000000;
bool nightTime = false;
uint64_t levellingBytes;
uint64_t averageSize;
uint32_t mostRecentPic = 0;
String sensorType;
bool supported;
uint64_t storedTime;
struct tm timeinfo;
bool doRecording = false, amStreaming = false, amRecording = false;
WebServer server(80);
const String _HTML_ = "text/html";
const String _TEXT_ = "text/plain; charset=UTF-8";
const String noMoreSlides = " No more slides!";
uint64_t updateTime;
uint8_t retryCount = 1;
bool darkModeINIT = darkMode;
bool autoDarkINIT = autoDark;
bool amDark = false;
uint8_t slideTimeINIT = slideTime;
bool stickyPoPINIT = stickyPoP;
bool loadingIconINIT = loadingIcon;
bool loopPoPINIT = loopPoP;
bool streamANDRecordINIT = streamANDRecord;
uint8_t zmAcellerationINIT = zmAcelleration;
bool spacesInListViewINIT = spacesInListView;
bool cacheImagesINIT = cacheImages;
uint16_t cacheTimeINIT = cacheTime;
bool silentStream = false;
bool thumbView = false, doPoP = false;
uint32_t displayFrom = 1;
uint8_t perPageMax = 200;
String bgColor;
uint16_t INITimagesPerPage;
uint16_t INITthumbWidth;
const char *headerData = "HTTP/1.1 200 OK\r\nAccess-Control-Allow-Origin: *\r\n";
const char *contentType = "Content-Type: multipart/x-mixed-replace; boundary=" PART_BOUNDARY "\r\n";
const char *boundaryMark = "\r\n--" PART_BOUNDARY "\r\n";
const uint8_t bmLen = strlen(boundaryMark);
const char *frameType = "Content-Type: image/jpeg\r\nContent-Length: ";
const uint8_t ctLen = strlen(frameType);
bool sShowPlaying = false;
bool sssPause = false;
int64_t sssSkip = 0;
String currentSlide;
//
Preferences prefs;
String xCommand, mostRecentCapture;
String LastMessage = "foo";
uint32_t totalFileCount;
uint32_t saveNumber;
String fileEXT = ".jpg", imgDimensions;
bool ejected = false;
bool abortUserNotified = false;
String ABORT = "0";
uint32_t _1MB_ = 1048576;
uint32_t SDTestSize = _1MB_;
const uint8_t ledChannel = 1;
const uint16_t ledFreq = 8000;
const uint8_t ledResolution = 8;
int16_t brightness, contrast, saturation, special_effect, awb, awb_gain, wb_mode, dcw;
int16_t aec, aec2, ae_level, aec_value, agc, agc_gain, gainceiling, min_frame_time;
int16_t raw_gma, bpc, wpc, lenc, vflip, hmirror, denoise, sharpness, colorbar, framesize;
float_t max_fps;
int16_t xclk=20, quality=10;
int16_t maxSize = FRAMESIZE_VGA;
int16_t minBrightness = -2, maxBrightness = 2;
int16_t minContrast = -2, maxContrast = 2;
int16_t minSaturation = -2, maxSaturation = 2;
int16_t minAELevel = -2, maxAELevel = 2;
int16_t minSharpness = -2, maxSharpness = 2;
int16_t minDenoise = 0, maxDenoise = 1;
int16_t minGC = 0, maxGC = 6;
String frameSizes[] = { "UNSUPPORTED", "QQVGA", "QCIF", "HQVGA", "240X240", "QVGA", "CIF", "HVGA", "VGA", "SVGA", "XGA", "HD", "SXGA", "UXGA", "FHD", "P_HD", "P_3MP", "QXGA", "QHD", "WQXGA", "P_FHD", "QSXGA" };
uint64_t avgSizes[] = { 1, 2007, 2652, 4413, 6021, 8028, 12380, 16056, 32112, 50176, 82216, 96384, 137031, 200724, 216780, 96384, 138741, 328867, 385392, 428216, 216780, 513853 };
String gatherPrefs(bool init=false, bool final=true) {
char buffer[420] = {'\0'};
sprintf(buffer, " Current Settings:\n\n");
sprintf(buffer + strlen(buffer), " Snap Interval Time:\t%s\n", convertSeconds(delayTime/1000).c_str());
if (init) eXi = prefs.getBool("e", eXi);
sprintf(buffer + strlen(buffer), " Extended Information:\t%s\n", (eXi ? "enabled" : "disabled"));
if (init) remControl = prefs.getBool("r", remControl);
sprintf(buffer + strlen(buffer), " Remote Control:\t%s\n", (remControl ? "enabled" : "disabled"));
if (init) doSleep = prefs.getBool("z", doSleep);
sprintf(buffer + strlen(buffer), " Auto-Sleep: \t%s\n", (doSleep ? "enabled" : "disabled"));
if (init) autoSnap = prefs.getBool("a", autoSnap);
sprintf(buffer + strlen(buffer), " Auto-Snap: \t%s\n", (autoSnap ? "enabled" : "disabled"));
if (init) deepSleep = prefs.getBool("s", deepSleep);
sprintf(buffer + strlen(buffer), " Deep Sleep: \t%s\n", (deepSleep ? "enabled" : "disabled"));
if (init) ledBrightness = prefs.getUChar("b", ledBrightness);
sprintf(buffer + strlen(buffer), " LED Brightness:\t%i\n", ledBrightness);
if (init) overWrite = prefs.getBool("o", overWrite);
sprintf(buffer + strlen(buffer), " Overwrite Mode:\t%s\n", (overWrite ? "enabled" : "disabled"));
if (init) quickListings = prefs.getBool("q", quickListings);
sprintf(buffer + strlen(buffer), " Quick Listing Mode:\t%s\n", (quickListings ? "enabled" : "disabled"));
if (init) timeStampFileNames = prefs.getBool("t", timeStampFileNames);
sprintf(buffer + strlen(buffer), " Timestamp file names:\t%s\n", (timeStampFileNames ? "enabled" : "disabled"));
if (init) webSnapResetsTimer = prefs.getBool("w", webSnapResetsTimer);
sprintf(buffer + strlen(buffer), " Web snap resets timer:\t%s\n", (webSnapResetsTimer ? "true" : "false"));
if (init) displayDetails = prefs.getBool("p", displayDetails);
sprintf(buffer + strlen(buffer), " Display Details:\t%s\n", (displayDetails ? "enabled" : "disabled"));
if (final) sprintf(buffer + strlen(buffer), " %s\n", getFreeEntries().c_str());
return (String)buffer;
}
String gatherSensorPrefs(bool init=false) {
static char buffer[3086] = {'\0'};
char *c = buffer;
c += sprintf(c, "\n Sensor Settings:\n\n");
if (init) captureSize = prefs.getShort("size", captureSize);
c += sprintf(c, " %s size: %i [%s] \tCapture Frame Size (2MP:0-13, 3MP:0-17, 5MP:0-21): \n", prefs.isKey("size") ? "*" : " ", captureSize, frameSizes[captureSize].c_str());
if (init) ae_level = prefs.getShort("ae_level", 0);
c += sprintf(c, " %s ae_level: %i \tAuto Exposure Level (-2 to 2, OV3660/OV5640: -5 to 5)\n", prefs.isKey("ae_level") ? "*" : " ",ae_level);
if (init) aec2 = prefs.getShort("aec2", 1);
c += sprintf(c, " %s aec2: %i \tAutomatic Exposure Correction (0/1): \n", prefs.isKey("aec2") ? "*" : " ",aec2);
if (init) aec = prefs.getShort("aec", 1);
c += sprintf(c, " %s aec: %i \tExposure Control (0/1)\n", prefs.isKey("aec") ? "*" : " ",aec);
if (init) aec_value = prefs.getShort("aec_value", 204);
c += sprintf(c, " %s aec_value: %i \tAuto Exposure Amount (0-1200, OV3660: 0-1536, OV5640: 0-1920)\n", prefs.isKey("aec_value") ? "*" : " ",aec_value);
if (init) agc = prefs.getShort("agc", 1);
c += sprintf(c, " %s agc: %i \tExposure Gain Control (0/1)\n", prefs.isKey("agc") ? "*" : " ",agc);
if (init) agc_gain = prefs.getShort("agc_gain", 0);
c += sprintf(c, " %s agc_gain: %i \tAGC Gain Level (when agc=0) 0-30, OV3660/OV5640: 0-64\n", prefs.isKey("agc_gain") ? "*" : " ",agc_gain);
if (init) awb = prefs.getShort("awb", 1);
c += sprintf(c, " %s awb: %i \tAutomatic White Balance (0/1)\n", prefs.isKey("awb") ? "*" : " ",awb);
if (init) awb_gain = prefs.getShort("awb_gain", 1);
c += sprintf(c, " %s awb_gain: %i \tAutomatic White Balance Gain (0/1)\n", prefs.isKey("awb_gain") ? "*" : " ",awb_gain);
if (init) bpc = prefs.getShort("bpc", 0);
c += sprintf(c, " %s bpc: %i \tBlack Point Control (0/1)\n", prefs.isKey("bpc") ? "*" : " ",bpc);
if (init) brightness = prefs.getShort("brightness", 0);
c += sprintf(c, " %s brightness: %i \tBrightness (-2 to 2, OV3660/OV5640: -3 to 3)\n", prefs.isKey("brightness") ? "*" : " ",brightness);
if (init) colorbar = prefs.getShort("colorbar", 0);
c += sprintf(c, " %s colorbar: %i \tTest Colour Bars (0/1)\n", prefs.isKey("colorbar") ? "*" : " ",colorbar);
if (init) contrast = prefs.getShort("contrast", 0);
c += sprintf(c, " %s contrast: %i \tContrast (-2 to 2, OV3660/OV5640: -3 to 3)\n", prefs.isKey("contrast") ? "*" : " ",contrast);
if (init) denoise = prefs.getShort("denoise", 0);
c += sprintf(c, " %s denoise: %i \tDenoise (0/1, OV5640 = 0:auto to 8)\n", prefs.isKey("denoise") ? "*" : " ",denoise);
if (init) dcw = prefs.getShort("dcw", 1);
c += sprintf(c, " %s dcw: %i \tDownsize image to requested size (0/1)\n", prefs.isKey("dcw") ? "*" : " ",dcw);
if (init) framesize = prefs.getShort("framesize", 0);
c += sprintf(c, " %s framesize: %i [%s]\tLive Stream Frame Size (as 'size' - above): \n", prefs.isKey("framesize") ? "*" : " ",framesize, frameSizes[framesize].c_str());
if (init) gainceiling = (gainceiling_t)prefs.getShort("gainceiling", 3);
c += sprintf(c, " %s gainceiling: %i \tGain Ceiling (0-6, OV3660/OV5640: 511)\n", prefs.isKey("gainceiling") ? "*" : " ",gainceiling);
if (init) hmirror = prefs.getShort("hmirror", 0);
c += sprintf(c, " %s hmirror: %i \tHorizontal Mirror (0/1)\n", prefs.isKey("hmirror") ? "*" : " ",hmirror);
if (init) lenc = prefs.getShort("lenc", 1);
c += sprintf(c, " %s lenc: %i \tLens Correction (0/1)\n", prefs.isKey("lenc") ? "*" : " ",lenc);
if (init) quality = prefs.getShort("quality", 10);
c += sprintf(c, " %s quality: %i \tQuality (4-63, OV3660: 6?-63, OV5640: 8-63)\n", prefs.isKey("quality") ? "*" : " ",quality);
if (init) raw_gma = prefs.getShort("raw_gma", 1);
c += sprintf(c, " %s raw_gma: %i \tRaw Gamma (0/1)\n", prefs.isKey("raw_gma") ? "*" : " ",raw_gma);
if (init) vflip = prefs.getShort("vflip", 0);
c += sprintf(c, " %s vflip: %i \tVertical Mirror (0/1)\n", prefs.isKey("vflip") ? "*" : " ",vflip);
if (init) saturation = prefs.getShort("saturation", 0);
c += sprintf(c, " %s saturation: %i \tSaturation (-2 to 2, OV3660/OV5640: -4 to 4)\n", prefs.isKey("saturation") ? "*" : " ", saturation);
if (init) sharpness = prefs.getShort("sharpness", 0);
c += sprintf(c, " %s sharpness: %i \tSharpness (-2 to 2, OV3660/OV5640: -3 to 3)\n", prefs.isKey("sharpness") ? "*" : " ",sharpness);
if (init) special_effect = prefs.getShort("special_effect", 0);
c += sprintf(c, " %s special_effect: %i\tSpecial Effect", prefs.isKey("special_effect") ? "*" : " ", special_effect);
c += sprintf(c, " (0-6) 0 nothing, 1 Negative, 2 Greyscale, 3 Red Tint, 4 Green Tint, 5 Blue Tint, 6 Sepia\n");
if (init) wb_mode = prefs.getShort("wb_mode", 0);
c += sprintf(c, " %s wb_mode: %i \tWhite Balance Mode (if awb_gain enabled)", prefs.isKey("wb_mode") ? "*" : " ", wb_mode);
c += sprintf(c, " (0-4) 0 - Auto, 1 - Sunny, 2 - Cloudy, 3 - Office, 4 - Home\n");
if (init) wpc = prefs.getShort("wpc", 1);
c += sprintf(c, " %s wpc: %i \tWhite Point Control (0/1)\n", prefs.isKey("wpc") ? "*" : " ",wpc);
if (init) xclk = prefs.getShort("xclk", 12);
c += sprintf(c, " %s xclk: %i \tClock Frequency (4-30)\n", prefs.isKey("xclk") ? "*" : " ",xclk);
if (init) max_fps = prefs.getFloat("max_fps", 24);
c += sprintf(c, " %s max_fps: %.2f \tLive Stream Frame Rate Limit (fps). (0.5 = 1 frame every 2 seconds) 0 to disable limiting.\n", prefs.isKey("max_fps") ? "*" : " ",max_fps);
c += sprintf(c, "\n Settings marked * have been set and saved to NVS. Other settings are defaults.\n");
if (ae_level < minAELevel || ae_level > maxAELevel) { ae_level = 0; prefs.putShort("ae_level", 0); }
if (brightness < minBrightness || brightness > maxBrightness) { brightness = 0; prefs.putShort("brightness", 0); }
if (contrast < minContrast || contrast > maxContrast) { contrast = 0; prefs.putShort("contrast", 0);}
if (denoise < minDenoise || denoise > maxDenoise) { denoise = 0; prefs.putShort("denoise", 0); }
if (gainceiling < minGC || gainceiling > maxGC) { gainceiling = 3; prefs.putShort("gainceiling", 3); }
if (saturation < minSaturation || saturation > maxSaturation) { saturation = 0; prefs.putShort("saturation", 0); }
if (sharpness < minSharpness || sharpness > maxSharpness) { sharpness = 0; prefs.putShort("sharpness", 0); }
c += sprintf(c, " %s\n", getFreeEntries().c_str());
return (String)buffer;
}
String listDir(bool isSerial = false, bool webDisplay = false, bool webList = false, \
bool deleteFiles = false, bool INIT = false, uint64_t delX = 0, uint64_t delZ = UINT64_MAX) {
if (ejected) return " SD Card is Ejected!";
uint32_t fileCount = 0;
uint64_t currentTime = millis();
String webString = "";
if (webDisplay || webList) webString.reserve(memReserve);
String serialString = "";
if (isSerial) serialString.reserve(memReserve);
if ( (isSerial || webList) && !deleteFiles) serialString.concat("\n Listing Images..\n");
File root = SD_MMC.open("/");
if ( !root || !root.isDirectory()) {
ABORT = " Problem with root directory. Check your SD Card.";
return ABORT;
}
File file;
String fileName;
bool qlTMP = quickListings;
if (webDisplay) {
qlTMP = (displayFrom != 1) ? true : false;
}
if (deleteFiles) qlTMP = true;
if (qlTMP) {
fileName = root.getNextFileName();
if (fileName == "") return "";
} else {
file = root.openNextFile();
if (!file) return "";
}
uint64_t dFiles = 0;
String realName;
while (true) {
if (qlTMP) {
realName = fileName.substring(1);
} else {
realName = (String)file.name();
}
if (realName == "") break;
if (realName.indexOf(fileEXT) == -1) {
if (qlTMP) {
fileName = root.getNextFileName();
if (fileName == "") break;
} else {
file.close();
file = root.openNextFile();
if (!file) break;
}
continue;
}
if (deleteFiles) {
dFiles++;
if (dFiles > delZ) break;
if (dFiles < delX) {
fileName = root.getNextFileName();
if (fileName == "") break;
continue;
}
if (SD_MMC.remove("/" + realName) ) {
serialString.concat(" deleted: [" + (String)dFiles + "] " + realName + " OK!\n");
fileCount--;
} else {
serialString.concat(" FAILED to delete: " + realName + " OK!\n");
}
} else {
fileCount++;
char fileSize[12] = {'\0'};
char dateBuffer[32] = {'\0'};
char timeBbuffer[32] = {'\0'};
if (!qlTMP) {
float_t kbSize = file.size()/1024.00;
if (showDecimalSize) {
sprintf(fileSize, "%.2f", kbSize);
} else {
sprintf(fileSize, "%.0f", kbSize);
}
if (useRealTime) {
time_t t= file.getLastWrite();
struct tm * tmstruct = localtime(&t);
sprintf(dateBuffer, "%d-%02d-%02d", (tmstruct->tm_year)+1900, (tmstruct->tm_mon)+1, tmstruct->tm_mday);
sprintf(timeBbuffer, "%02d:%02d:%02d", tmstruct->tm_hour , tmstruct->tm_min, tmstruct->tm_sec);
}
}
if (isSerial) {
serialString.concat(" [" + (String)fileCount + "] " + realName);
if (!qlTMP) {
serialString.concat(" [" + (String)fileSize + "kB]");
if (useRealTime) serialString.concat(" [" + (String)dateBuffer + " @ " + (String)timeBbuffer + "]");
}
serialString.concat("\n");
}
if (webList) {
webString.concat(" [" + (String)fileCount + "]\t" + realName);
if (!qlTMP) {
webString.concat("\t[" + (String)fileSize + "kB]");
if (useRealTime) webString.concat("\t[" + (String)dateBuffer + " @ " + (String)timeBbuffer + "]");
}
webString.concat("\n");
}
if (webDisplay && (fileCount >= displayFrom) && (fileCount < (displayFrom + imagesPerPage) ) ) {
String displayName = realName;
displayName.replace(fileEXT, "");
displayName.replace(fileNamePrefix, "");
String displaySize = (String)fileSize;
uint16_t nLen = displayName.length();
if (thumbView) {
uint8_t kBL = (showDecimalSize) ? 130 : 105;
int16_t cutOFF;
webString.concat("<div class=\"thumbbox\" ");
if (doPoP) {
webString.concat("onpointerenter=\"showPoP('" + realName + "')\" onpointerleave=\"hidePoP()\" ");
}
webString.concat("id=\"" + realName + "\">");
webString.concat("<button class=\"delbutt\" title=\"delete this image\"");
webString.concat(" onclick='deleteImage(\"" + realName + "\")'>x</button>");
webString.concat("<a href=\"" + realName + "\" title=\"[" + (String)fileCount + "] " + realName + " [" + fileSize + "kB]");
webString.concat("\nClick to view in a new tab, shift-click to start a slideshow here - add Ctrl for full-screen\"");
webString.concat("target=\"_blank\"><span class=\"filename\" title=\"no slideshow action here - just a regular link\">");
switch (thumbWidth) {
case 105 ... 215 :
cutOFF = ( thumbWidth / 10 ) - 4;
break;
default :
cutOFF = ( thumbWidth / 10 );
}
if (!truncateReverse) {
displayName = displayName.substring(0, cutOFF);
} else {
displayName = displayName.substring( nLen - ( (cutOFF > nLen) ? nLen : cutOFF ) );
}
webString.concat(displayName); // &
if (!qlTMP && thumbWidth >= kBL) {
webString.concat("<span class=\"size-info\">& + displaySize + "kB]</span>");
}
webString.concat("</span>");
webString.concat("<br><img class=\"thumb\" style=\"height:" + (String)(thumbWidth*0.75) + "px;\" src=\"" + realName);
<br><img class=\"thumb\" style=\"width:" + (String)thumbWidth + "px;\" src=\"" + realName);
webString.concat("\" onclick=\"openSlideShowAt('" + realName + "')\" alt=\"Thumbnail Image: " + realName + "\" >");
if (!qlTMP && useRealTime) {
webString.concat("<span class=\"datefoot\">");
// &
if (thumbWidth >= 130) webString.concat((String)dateBuffer + "&</span></a></div>\n");
} else {
String space = "";
if (spacesInListView) space = " pad-left";
webString.concat("<div class=\"filelist\" ");
if (doPoP) {
webString.concat("onpointerenter=\"showPoP('" + realName + "')\" onpointerleave=\"hidePoP()\" ");
}
webString.concat("id=\"" + realName + "\"><a class=\"file-link\" href=\"" + realName + "\" ");
if (doPoP) webString.concat("onclick=\"openSlideShowAt('" + realName + "')\" ");
webString.concat("title=\"[" + (String)fileCount + "] " + realName);
if (!qlTMP) webString.concat(" [" + displaySize + "kB]");
if (doPoP) webString.concat("\nshift-click to start a slideshow here");
webString.concat("\" target=\"_blank\">[" + (String)fileCount + "] " + realName);
webString.concat("<span class=\"dummy" + space +"\"></span>");
if (!qlTMP) {
webString.concat("<span class=\"list-hide" + space + "\">[" + displaySize + "kB]</span>");
if (useRealTime) {
webString.concat("<span class=\"list-wide-hide" + space + "\">[" + (String)dateBuffer);
webString.concat("<span class=\"dummy" + space +"\"></span>@<span class=\"dummy" + space +"\"></span>");
webString.concat((String)timeBbuffer + "]</span>");
}
}
webString.concat("</a><button class=\"listdelbutt" + space + "\" title=\"delete this image\"");
webString.concat(" onclick='deleteImage(\"" + realName + "\")'>[x]</button></div>");
}
}
if (webDisplay && fileCount > (displayFrom + imagesPerPage) ) break;
}
bool doFile = true, switching = false;
if (qlTMP) {
doFile = false;
if (displayDetails && webDisplay && (fileCount >= (displayFrom-1)) ) {
qlTMP = false;
doFile = true;
switching = true;
------->SWITCHING @ %.2f seconds\n", (float_t)(millis() - currentTime) / 1000);
} else {
fileName = root.getNextFileName();
if (fileName == "") break;
}
}
if (doFile) {
if (!switching) file.close();
file = root.openNextFile();
if (!file) break;
}
}
float_t finishedAt = (float_t)(millis() - currentTime) / 1000;
if (webDisplay) return webString;
String finished = "\n Finished processing in " + (String)finishedAt + " seconds\n";
if (isSerial) serialString.concat(finished);
if (webList) webString.concat(finished);
if (deleteFiles) {
saveNumber = (delX == 0) ? 0 : delX-1;
if (isSerial) serialString.concat("\n Done\n");
} else {
if (fileCount > 0) {
totalFileCount = fileCount;
if ((INIT || isSerial || webList) && fileCount != 0) {
averageSize = ( SD_MMC.usedBytes() - levellingBytes ) / totalFileCount;
}
}
if (INIT && totalFileCount > 0) saveNumber = totalFileCount;
char buff[200] = {'\0'};
\n File system interrogated in %.2f seconds\n", finishedAt);
sprintf(buff + strlen(buff), " Total files: %i\n", fileCount);
uint64_t remainEst = (float_t)(SD_MMC.totalBytes() - SD_MMC.usedBytes() - levellingBytes) / averageSize;
sprintf(buff + strlen(buff), " Space for approx. %llu (avg: %llukB) images", remainEst, (averageSize / 1024));
if (isSerial) serialString.concat(buff);
if (webList) webString.concat("\n " + (String)buff + "\n");
if (INIT) return buff;
}
if (isSerial) return serialString;
if (webList) return webString;
return "";
}
String quickList(bool report = true, bool allFiles = false) {
if (ejected) return " SD Card is Ejected!";
File root = SD_MMC.open("/");
if ( !root || !root.isDirectory()) {
ABORT = " Problem with root directory. Check your SD Card.";
return ABORT;
}
String list = "";
list.reserve(512000);
if (report) list = " Current (quick) file list:\n\n";
uint32_t fileCount = 0;
String file = root.getNextFileName();
if (file == "") return " Empty Card";
uint64_t currentTime = millis();
while (file != "") {
if (file.indexOf(fileEXT) != -1 || allFiles) {
fileCount++;
if (report) list.concat(" [" + (String)fileCount + "] " + file.substring(1) + "\n");
}
file = root.getNextFileName();
}
list.concat("\n Quick List completed in " + (String)((float_t)(millis() -currentTime) / 1000) + " seconds\n");
list.concat(" Total Files: " + (String)fileCount + "\n");
if (fileCount > 0) {
totalFileCount = fileCount;
averageSize = ( SD_MMC.usedBytes() - levellingBytes ) / totalFileCount;
}
return list;
}
String notQuickLst(bool allFiles = false) {
if (ejected) return " SD Card is Ejected!";
File root = SD_MMC.open("/");
if ( !root || !root.isDirectory()) {
ABORT = " Problem with root directory. Check your SD Card.";
return ABORT;
}
uint32_t fileCount = 0;
String thisName;
uint64_t currentTime = millis();
File file = root.openNextFile();
String list;
list.reserve(512000);
list = " Current (not quick) file list:\n\n";
while (file) {
thisName = (String)file.name();
if (allFiles || thisName.indexOf(fileEXT) != -1) {
fileCount++;
list.concat(" [" + (String)fileCount + "] " + thisName.substring(1) + "\n");
}
file.close();
file = root.openNextFile();
}
list.concat("\n NOT Quick List completed in " + (String)((float_t)(millis() -currentTime) / 1000) + " seconds\n");
list.concat(" Total Files: " + (String)fileCount + "\n");
return list;
}
Non Volatile Storage".
You can do this from the serial console, with the command: nvswipe
*/
void WipeNVRAM() {
esp_err_t ret = nvs_flash_init();
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
ESP_ERROR_CHECK(ret);
Serial.println(" NVRAM Erased.");
}
String printMemoryInfo(bool full = false) {
char mbuf[380];
sprintf(mbuf, " Free memory: %d bytes\n", esp_get_free_heap_size());
sprintf(mbuf + strlen(mbuf), " Task High Tide Watermark: %d bytes\n", uxTaskGetStackHighWaterMark(NULL) );
if (full) {
sprintf(mbuf + strlen(mbuf), " Available Internal Heap Size: %d\n", esp_get_free_internal_heap_size());
sprintf(mbuf + strlen(mbuf), " Minimum Free Heap Ever Available Size: %d\n", esp_get_minimum_free_heap_size());
}
sprintf(mbuf + strlen(mbuf), " Total Heap: %d ~", ESP.getHeapSize());
sprintf(mbuf + strlen(mbuf), " Free Heap: %d\n", ESP.getFreeHeap());
return (String)mbuf;
}
bool startMicroSD() {
Serial.print("\n Mounting microSD Card.. ");
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/sd_pullup_requirements.html
pinMode(13, INPUT_PULLUP);
if (SD_MMC.begin("/sdcard", true)) {
Serial.println("OK!");
} else {
Serial.println("FAILED!");
return false;
}
uint8_t cardType = SD_MMC.cardType();
if (cardType == CARD_NONE) {
Serial.println(" No SD Card Attached!");
return false;
}
Serial.print(" SD Card Type: ");
if (cardType == CARD_MMC) {
Serial.println("MMC");
} else if (cardType == CARD_SD) {
Serial.println("SDSC");
} else if (cardType == CARD_SDHC) {
Serial.println("SDHC");
} else {
Serial.println("UNKNOWN");
}
Serial.printf(" SD Card Size: %llu MB\n", SD_MMC.cardSize() / (1024 * 1024));
getWearLevellingBytes();
Serial.printf(" Checking SD Card contents..");
if (quickListings) {
Serial.println();
Serial.println(quickList(false));
} else {
if ( (SD_MMC.usedBytes() / (1024 * 1024) ) < ( maxMBytesLimit + ( levellingBytes / 1024 / 1024) ) ) {
Serial.println(" (this could take a while)");
Serial.println(listDir(false, false, false, false, true));
} else {
Serial.printf(" SD Card used space greater than your %i MB limit. Listing aborted. \n", maxMBytesLimit);
}
}
Serial.println(printSpace());
return true;
}
void getWearLevellingBytes() {
File dir = SD_MMC.open("/");
if (!dir || !dir.isDirectory()) return;
levellingBytes = 4096; empty" FAT
File file, testDir;
String mySize, maybeDir = dir.getNextFileName();
while (maybeDir != "") {
if (maybeDir.indexOf(".") == -1) {
testDir = SD_MMC.open(maybeDir);
if (testDir && testDir.isDirectory()) {
file = testDir.openNextFile();
while (file) {
mySize = (String)(file.size()/1024) + "kB";
if (file.size() > (1024*1024)) mySize = (String)(file.size()/1024/1024) + "MB";
if (eXi) Serial.printf(" + %s wear-levelling: %s/%s\n", mySize.c_str(), maybeDir.c_str(), file.name());
levellingBytes += file.size();
file = testDir.openNextFile();
}
}
testDir.close();
}
maybeDir = dir.getNextFileName();
}
String mjpegFileName = "/" + streamFileName;
if (SD_MMC.exists(mjpegFileName)) {
File videoFile = SD_MMC.open(mjpegFileName);
levellingBytes += videoFile.size();
videoFile.close();
}
}
String printSpace() {
char mbuf[164];
sprintf(mbuf, " Total Space: %llu MB\t", SD_MMC.totalBytes() / (1024 * 1024) );
uint64_t usedUserBytes = SD_MMC.usedBytes()-levellingBytes;
if ( usedUserBytes < (1024 * 1024) ) {
sprintf(mbuf + strlen(mbuf), " Used Space: %llu kB\n", usedUserBytes / 1024);
} else {
sprintf(mbuf + strlen(mbuf), " Used Space: %.2f MB\n", (float_t)(usedUserBytes / (1024 * 1024)) );
}
float_t freeSpace = (SD_MMC.totalBytes() - usedUserBytes);
sprintf(mbuf + strlen(mbuf), " Available Free Space: %.2f MB\n", freeSpace / (1024 * 1024));
sprintf(mbuf + strlen(mbuf), " Space for approx.: %llu (avg: %llukB) images", \
(uint64_t)(freeSpace / averageSize), (averageSize / 1024));
return (String)mbuf;
}
String benchmarkSD() {
static char results[1024];
char *b = results;
b += sprintf(b, "\n");
uint8_t *buf = (uint8_t *)malloc(64 * 1024);
const char *path = "/Speed-Test.bin";
uint64_t start_time = millis();
b += sprintf(b, "\n Writing test file: \"%s\"\n", path);
File file = SD_MMC.open(path, "w");
if (!file) {
b += sprintf(b, " Failed to open file for writing. Speed Test Aborted.\n");
return results;
}
if (!file.write(buf, SDTestSize)) {
Write Failed!");
b += sprintf(b, " Write Failed!");
return results;
}
file.close();
uint32_t runTime = millis() - start_time;
b += sprintf(b, " Wrote %i bytes in %ims == %.2f KB/s\n\n", SDTestSize, runTime, (float_t)(SDTestSize/runTime) );
delay(1000);
b += sprintf(b, " Reading test file \"%s\"\n", path);
start_time = millis();
file = SD_MMC.open(path);
if (!file) {
b += sprintf(b, " Failed to open file for reading!");
return results;
}
if (!file.read(buf, SDTestSize)) {
b += sprintf(b, " Read failed!");
return results;
}
file.close();
runTime = millis() - start_time;
b += sprintf(b, " Read %i bytes in %ims == %.2f KB/s\n\n", SDTestSize, runTime, (float_t)(SDTestSize/runTime) );
b += sprintf(b, " Benchmark complete.\n");
SD_MMC.remove("/Speed-Test.bin");
return results;
}
bool configCamera(bool init=false) {
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sccb_sda = SIOD_GPIO_NUM;
config.pin_sccb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.pixel_format = PIXFORMAT_JPEG;
config.frame_size = FRAMESIZE_UXGA;
config.jpeg_quality = quality;
config.xclk_freq_hz = (xclk * 1000000);
config.fb_count = 2;
config.fb_location = CAMERA_FB_IN_PSRAM;
config.grab_mode = CAMERA_GRAB_LATEST;
if (!psramFound()) {
if (init) Serial.println("\n PSRAM NOT FOUND: Maximum supported resolution is SVGA. Bummer.");
config.fb_location = CAMERA_FB_IN_DRAM;
config.frame_size = FRAMESIZE_SVGA;
config.fb_count = 1;
} else {
if (init) Serial.println("\n PSRAM found: Maximum resolution supported. Yippee!");
}
if (init) Serial.print(" Initialising ESP32-CAM: ");
esp_err_t err = esp_camera_init(&config);
if (err == ESP_OK) {
if (init) Serial.println("OK");
} else {
if (init) Serial.println("FAIL");
return false;
}
return true;
}
bool startCamera(bool init=true) {
Serial.print("\n Starting Camera.. ");
pinMode(FLASH_PIN, OUTPUT);
digitalWrite(FLASH_PIN, LOW);
if (!configCamera(init)) return false;
sensor_t *s = esp_camera_sensor_get();
lamp":0,"autolamp":0,"framesize":11,"quality":12,"xclk":20,"min_frame_time":0,"brightness":0,"contrast":0,"saturation":0,"special_effect":0,"wb_mode":0,"awb":1,"awb_gain":1,"aec":1,"aec2":1,"ae_level":1,"aec_value":204,"agc":1,"agc_gain":0,"gainceiling":1,"bpc":1,"wpc":1,"raw_gma":1,"lenc":1,"vflip":0,"hmirror":0,"dcw":1,"colorbar":0,"rotate":"-90"}
Serial.print(" Detected camera module: ");
switch (s->id.PID) {
case OV2640_PID :
sensorType = "OV2640";
maxSize = FRAMESIZE_UXGA;
supported = true;
break;
case OV3660_PID :
sensorType = "OV3660";
maxSize = FRAMESIZE_QXGA;
minBrightness = -3;
maxBrightness = 3;
minContrast = -3;
maxContrast = 3;
minSaturation = -4;
maxSaturation = 4;
minAELevel = -5;
maxAELevel = 5;
minSharpness = -3;
maxSharpness = 3;
minDenoise = 0;
maxDenoise = 8;
minGC = 0;
maxGC = 511;
supported = true;
break;
case OV5640_PID :
sensorType = "OV5640";
maxSize = FRAMESIZE_QSXGA;
minBrightness = -3;
maxBrightness = 3;
minContrast = -3;
maxContrast = 3;
minSaturation = -4;
maxSaturation = 4;
minAELevel = -5;
maxAELevel = 5;
minSharpness = -3;
maxSharpness = 3;
minDenoise = 0;
maxDenoise = 8;
minGC = 0;
maxGC = 75;
supported = true;
break;
case OV9650_PID :
sensorType = "OV9650";
maxSize = FRAMESIZE_SXGA;
supported = false;
break;
case OV7725_PID :
sensorType = "OV7725";
supported = false;
break;
case OV7670_PID :
sensorType = "OV7670";
supported = false;
break;
case NT99141_PID :
sensorType = "NT99141";
maxSize = FRAMESIZE_HD;
supported = false;
break;
case GC2145_PID :
sensorType = "GC2145";
maxSize = FRAMESIZE_UXGA;
supported = false;
break;
case GC032A_PID :
sensorType = "GC032A";
supported = false;
break;
case GC0308_PID :
sensorType = "GC0308";
supported = false;
break;
case BF3005_PID :
sensorType = "BF3005";
supported = false;
break;
case BF20A6_PID :
sensorType = "BF20A6";
supported = false;
break;
case SC101IOT_PID :
sensorType = "SC101IOT";
maxSize = FRAMESIZE_HD;
supported = false;
break;
case SC030IOT_PID :
sensorType = "SC030IOT";
supported = false;
break;
case SC031GS_PID :
sensorType = "SC031GS";
supported = false;
}
Serial.println(sensorType);
String prefsString = gatherSensorPrefs(true);
if (eXi) Serial.println(prefsString);
average" photo at maximum size on the OV2640 might be around 180kB, or 184320 bytes.
Once real photos exist, we use the size of those.
The more images you capture, the better these estimates get.
The "estimates" are mindfully high.
As soon as you start taking pictures, the estimates get real.
To get the current estimate. use the command: free
NOTE: The list commands re-calculate these figures.
*/
if (prefs.getString("sensor") != sensorType) {
prefs.putString("sensor", sensorType);
Serial.printf(" Switching to NEW Sensor: %s\n", sensorType.c_str());
%s", clearedSettings.c_str());
}
if (!supported) {
Serial.println(" This is an unsupported sensor. It may work fine.");
Serial.println(" PLEASE report your findings, along with any tweaks you applied,");
Serial.println(" if any, to /get/ it working to esp32 @ the usual domain");
Serial.println(" Thank you!");
}
if (framesize == FRAMESIZE_96X96 || framesize < 1 || framesize > maxSize) framesize = maxSize;
if ( captureSize < 1 || captureSize > maxSize ) captureSize = maxSize;
if (eXi) Serial.printf(" Setting Capture frame size to: %i (%s)\n", captureSize, frameSizes[captureSize].c_str());
if (!psramFound()) {
if (framesize != FRAMESIZE_VGA) framesize = FRAMESIZE_SVGA;
if (captureSize != FRAMESIZE_VGA) captureSize = FRAMESIZE_SVGA;
}
if (eXi) Serial.printf(" Setting streaming frame size to: %i (%s)\n", framesize, frameSizes[framesize].c_str());
s->set_framesize(s, (framesize_t)framesize);
if (s->id.PID == OV3660_PID) {
s->set_vflip(s, 1);
}
if (s->id.PID == OV5640_PID) {
s->set_hmirror(s, 1);
}
s->set_xclk(s, LEDC_TIMER_0, xclk);
s->set_quality(s, quality);
s->set_ae_level(s, ae_level);
s->set_aec2(s, aec2);
s->set_aec_value(s, aec_value);
s->set_agc_gain(s, agc_gain);
s->set_awb_gain(s, awb_gain);
s->set_bpc(s, bpc);
if (prefs.isKey("brightness")) s->set_brightness(s, brightness);
s->set_colorbar(s, colorbar);
s->set_contrast(s, contrast);
s->set_dcw(s, dcw);
s->set_denoise(s, denoise);
s->set_exposure_ctrl(s, aec);
s->set_gain_ctrl(s, agc);
s->set_gainceiling(s, (gainceiling_t)gainceiling);
if (prefs.isKey("hmirror")) s->set_hmirror(s, hmirror);
s->set_lenc(s, lenc);
s->set_raw_gma(s, raw_gma);
if (prefs.isKey("saturation")) s->set_saturation(s, saturation);
s->set_sharpness(s, sharpness);
s->set_special_effect(s, special_effect);
if (prefs.isKey("vflip")) s->set_vflip(s, vflip);
s->set_wb_mode(s, wb_mode);
s->set_whitebal(s, awb);
s->set_wpc(s, wpc);
return true;
}
String setSensorParameters(String variable, String value, bool report = true) {
if (variable == "" || value == "") return "INVALID SETTING! Use: c setting=value";
int16_t val = value.toInt();
char buffer[4096] = {'\0'};
sprintf(buffer, " Setting Sensor: %s = %s .. ", variable.c_str(), value.c_str());
sensor_t *s = esp_camera_sensor_get();
int16_t ret = 0;
String xTra = "";
if (variable == "size") {
if ( val < 1 || val > maxSize ) {
xTra = "(size remains " + frameSizes[captureSize] + ")";
ret = -1;
} else {
captureSize = val;
}
} else if (variable == "max_fps") {
float_t fps = value.toFloat();
if (fps < 0 || fps > 100) {
fps = max_fps;
xTra = "(limit remains " + (String)max_fps + "fps)";
ret = -1;
}
max_fps = fps;
} else if (variable == "xclk") {
if (val < 4 || val > 30) val = xclk;
xclk = val;
ret = s->set_xclk(s, LEDC_TIMER_0, xclk);
} else if (variable == "ae_level") {
if (val < minAELevel || val > maxAELevel) val = ae_level;
ae_level = val;
ret = s->set_ae_level(s, ae_level);
} else if (variable == "aec") {
if (val < 0 || val > 1) val = aec;
aec = val;
ret = s->set_exposure_ctrl(s, aec);
} else if (variable == "aec2") {
if (val < 0 || val > 1) val = aec2;
aec2 = val;
ret = s->set_aec2(s, aec2);
} else if (variable == "aec_value") {
if (val < 0 || val > 1200) val = aec_value;
aec_value = val;
ret = s->set_aec_value(s, aec_value);
} else if (variable == "agc") {
if (val < 0 || val > 1) val = awb;
agc = val;
ret = s->set_gain_ctrl(s, agc);
} else if (variable == "agc_gain") {
if (val < 0 || val > 30) val = agc_gain;
agc_gain = val;
ret = s->set_agc_gain(s, agc_gain);
} else if (variable == "awb") {
if (val < 0 || val > 1) val = awb;
colorbar = val;
ret = s->set_whitebal(s, val);
} else if (variable == "awb_gain") {
if (val < 0 || val > 1) val = awb_gain;
awb_gain = val;
ret = s->set_awb_gain(s, awb_gain);
} else if (variable == "bpc") {
if (val < 0 || val > 1) val = bpc;
bpc = val;
ret = s->set_bpc(s, bpc);
} else if (variable == "brightness") {
if (val < minBrightness || val > maxBrightness) val = brightness;
brightness = val;
ret = s->set_brightness(s, brightness);
} else if (variable == "colorbar") {
if (val < 0 || val > 1) val = colorbar;
colorbar = val;
ret = s->set_colorbar(s, colorbar);
} else if (variable == "contrast") {
if (val < minContrast || val > maxContrast) val = contrast;
contrast = val;
ret = s->set_contrast(s, contrast);
} else if (variable == "dcw") {
if (val < 0 || val > 1) val = dcw;
dcw = val;
ret = s->set_dcw(s, dcw);
} else if (variable == "denoise") {
if (val < minDenoise || val > maxDenoise) val = denoise;
denoise = val;
ret = s->set_denoise(s, denoise);
} else if (variable == "framesize") {
if (val < 0 || val > maxSize) val = framesize;
framesize = val;
ret = s->set_framesize(s, (framesize_t)framesize);
x" + t->height;
} else if (variable == "gainceiling") {
if (val < minGC || val > maxGC) val = gainceiling;
gainceiling = val;
ret = s->set_gainceiling(s, (gainceiling_t)gainceiling);
} else if (variable == "hmirror") {
if (val < 0 || val > 1) val = hmirror;
hmirror = val;
ret = s->set_hmirror(s, hmirror);
} else if (variable == "lenc") {
if (val < 0 || val > 1) val = lenc;
lenc = val;
ret = s->set_lenc(s, lenc);
} else if (variable == "quality") {
if (val < 4 || val > 63) val = quality;
quality = val;
ret = s->set_quality(s, quality);
} else if (variable == "raw_gma") {
if (val < 0 || val > 1) val = raw_gma;
raw_gma = val;
ret = s->set_raw_gma(s, raw_gma);
} else if (variable == "saturation") {
if (val < minSaturation || val > maxSaturation) val = saturation;
saturation = val;
ret = s->set_saturation(s, saturation);
} else if (variable == "sharpness") {
if (val < minSharpness || val > maxSharpness) val = sharpness;
sharpness = val;
ret = s->set_sharpness(s, sharpness);
} else if (variable == "special_effect") {
if (val < 0 || val > 6) val = special_effect;
special_effect = val;
ret = s->set_special_effect(s, special_effect);
} else if (variable == "vflip") {
if (val < 0 || val > 1) val = vflip;
vflip = val;
ret = s->set_vflip(s, vflip);
} else if (variable == "wpc") {
if (val < 0 || val > 1) val = wpc;
wpc = val;
ret = s->set_wpc(s, wpc);
} else if (variable == "wb_mode") {
if (val < 0 || val > 4) val = wb_mode;
wb_mode = val;
ret = s->set_wb_mode(s, wb_mode);
} else {
ret = -1;
}
if (ret == 0) {
sprintf(buffer + strlen(buffer), "OK %s\n", xTra.c_str() );
LastMessage = (String)buffer;
if (eXi && report) Serial.print(buffer);
if (variable == "max_fps") {
prefs.putFloat(variable.c_str(), val);
} else {
prefs.putShort(variable.c_str(), val);
}
return "OK! " + xTra;
} else {
sprintf(buffer + strlen(buffer), "Command Failed %s\n", xTra.c_str() );
LastMessage = (String)buffer;
if (eXi && report) Serial.print(buffer);
return "FAIL" + xTra;
}
}
String saveSensorConfig(const char *slotName = "backup") {
bool ret = esp_camera_save_to_nvs(slotName);
if (ret == ESP_OK) return "OK";
return "FAIL";
}
String loadSensorConfig(const char *slotName = "backup") {
bool ret = esp_camera_load_from_nvs(slotName);
saveSensorSettingsToDefaults();
if (ret == ESP_OK) return "OK";
return "FAIL";
}
String saveSensorSettingsToDefaults() {
char buffer[4096] = {'\0'};
sensor_t * s = esp_camera_sensor_get();
ae_level = s->status.ae_level;
prefs.putShort("ae_level", ae_level);
sprintf(buffer + strlen(buffer), " ae_level: %i\n", ae_level);
aec2 = s->status.aec2;
prefs.putShort("aec2", aec2);
sprintf(buffer + strlen(buffer), " aec2: %i\n", aec2);
aec = s->status.aec;
prefs.putShort("aec", aec);
sprintf(buffer + strlen(buffer), " aec: %i\n", aec);
aec_value = s->status.aec_value;
prefs.putShort("aec_value", aec_value);
sprintf(buffer + strlen(buffer), " aec_value: %i\n", aec_value);
agc = s->status.agc;
prefs.putShort("agc", agc);
sprintf(buffer + strlen(buffer), " agc: %i\n", agc);
agc_gain = s->status.agc_gain;
prefs.putShort("agc_gain", agc_gain);
sprintf(buffer + strlen(buffer), " agc_gain: %i\n", agc_gain);
awb = s->status.awb;
prefs.putShort("awb", awb);
sprintf(buffer + strlen(buffer), " awb: %i\n", awb);
awb_gain = s->status.awb_gain;
prefs.putShort("awb_gain", awb_gain);
sprintf(buffer + strlen(buffer), " awb_gain: %i\n", awb_gain);
bpc = s->status.bpc;
prefs.putShort("bpc", bpc);
sprintf(buffer + strlen(buffer), " bpc: %i\n", bpc);
brightness = s->status.brightness;
prefs.putShort("brightness", brightness);
sprintf(buffer + strlen(buffer), " brightness: %i\n", brightness);
colorbar = s->status.colorbar;
prefs.putShort("colorbar", 0);
sprintf(buffer + strlen(buffer), " colorbar: %i\n", colorbar);
contrast = s->status.contrast;
prefs.putShort("contrast", contrast);
sprintf(buffer + strlen(buffer), " contrast: %i\n", contrast);
dcw = s->status.dcw;
prefs.putShort("dcw", dcw);
sprintf(buffer + strlen(buffer), " dcw: %i\n", dcw);
denoise = s->status.denoise;
prefs.putShort("denoise", denoise);
sprintf(buffer + strlen(buffer), " denoise: %i\n", denoise);
framesize = s->status.framesize;
prefs.putShort("framesize", framesize);
sprintf(buffer + strlen(buffer), " framesize: %i [%s]\n", framesize, frameSizes[framesize].c_str());
gainceiling = s->status.gainceiling;
prefs.putShort("gainceiling", gainceiling);
sprintf(buffer + strlen(buffer), " gainceiling: %i\n", gainceiling);
hmirror = s->status.hmirror;
prefs.putShort("hmirror", hmirror);
sprintf(buffer + strlen(buffer), " hmirror: %i \n", hmirror);
lenc = s->status.lenc;
prefs.putShort("lenc", lenc);
sprintf(buffer + strlen(buffer), " lenc: %i\n", lenc);
quality = s->status.quality;
prefs.putShort("quality", quality);
sprintf(buffer + strlen(buffer), " quality: %i\n", quality);
raw_gma = s->status.raw_gma;
prefs.putShort("raw_gma", raw_gma);
sprintf(buffer + strlen(buffer), " raw_gma: %i\n", raw_gma);
saturation = s->status.saturation;
prefs.putShort("saturation", saturation);
sprintf(buffer + strlen(buffer), " saturation: %i\n", saturation);
sharpness = s->status.sharpness;
prefs.putShort("sharpness", sharpness);
sprintf(buffer + strlen(buffer), " sharpness: %i\n", sharpness);
special_effect = s->status.special_effect;
prefs.putShort("special_effect", special_effect);
sprintf(buffer + strlen(buffer), " special_effect: %i\n", special_effect);
vflip = s->status.vflip;
prefs.putShort("vflip", vflip);
sprintf(buffer + strlen(buffer), " vflip: %i\n", vflip);
wb_mode = s->status.wb_mode;
prefs.putShort("wb_mode", wb_mode);
sprintf(buffer + strlen(buffer), " wb_mode: %i\n", wb_mode);
wpc = s->status.wpc;
prefs.putShort("wpc", wpc);
sprintf(buffer + strlen(buffer), " wpc: %i\n", wpc);
return (String)buffer;
}
static int print_reg(char * p, sensor_t * s, uint16_t reg, uint32_t mask, bool json=false) {
if (json) {
return sprintf(p, "\"0x%x\":%u,", reg, s->get_reg(s, reg, mask));
} else {
return sprintf(p, " 0x%x: %u\n", reg, s->get_reg(s, reg, mask));
}
}
String getSensorStatus(bool print=false, bool extra=false, bool json=false) {
static char sensorData[1240] = {'\0'};
sensor_t *s = esp_camera_sensor_get();
char *p = sensorData;
char delim = '\n';
if (json) {
*p++ = '{';
delim = ',';
}
if (extra) {
if (!json) p += sprintf(p, "\n Register Values:\n\n");
if(s->id.PID == OV5640_PID || s->id.PID == OV3660_PID) {
for(int reg = 0x3400; reg < 0x3406; reg+=2) {
p+=print_reg(p, s, reg, 0xFFF, json);
}
p+=print_reg(p, s, 0x3406, 0xFF, json);
p+=print_reg(p, s, 0x3500, 0xFFFF0, json);
p+=print_reg(p, s, 0x3503, 0xFF, json);
p+=print_reg(p, s, 0x350a, 0x3FF, json);
p+=print_reg(p, s, 0x350c, 0xFFFF, json);
for(int reg = 0x5480; reg <= 0x5490; reg++) {
p+=print_reg(p, s, reg, 0xFF, json);
}
for(int reg = 0x5380; reg <= 0x538b; reg++) {
p+=print_reg(p, s, reg, 0xFF, json);
}
for(int reg = 0x5580; reg < 0x558a; reg++) {
p+=print_reg(p, s, reg, 0xFF, json);
}
p+=print_reg(p, s, 0x558a, 0x1FF, json);
} else if(s->id.PID == OV2640_PID) {
p+=print_reg(p, s, 0xd3, 0xFF, json);
p+=print_reg(p, s, 0x111, 0xFF, json);
p+=print_reg(p, s, 0x132, 0xFF, json);
}
}
if (!json) p += sprintf(p, "\n Sensor Status:\n\n");
p += sprintf(p, " \"sensor\": \"%s\"%c" , sensorType.c_str(), delim);
p += sprintf(p, " \"ae_level\": %d%c" , s->status.ae_level, delim);
p += sprintf(p, " \"aec2\": %u%c" , s->status.aec2, delim);
p += sprintf(p, " \"aec\": %u%c" , s->status.aec, delim);
p += sprintf(p, " \"aec_value\": %u%c" , s->status.aec_value, delim);
p += sprintf(p, " \"agc\": %u%c" , s->status.agc, delim);
p += sprintf(p, " \"agc_gain\": %u%c" , s->status.agc_gain, delim);
p += sprintf(p, " \"awb\": %u%c" , s->status.awb, delim);
p += sprintf(p, " \"awb_gain\": %u%c" , s->status.awb_gain, delim);
p += sprintf(p, " \"bpc\": %u%c" , s->status.bpc, delim);
p += sprintf(p, " \"brightness\": %d%c" , s->status.brightness, delim);
p += sprintf(p, " \"colorbar\": %u%c", s->status.colorbar, delim);
p += sprintf(p, " \"contrast\": %d%c" , s->status.contrast, delim);
p += sprintf(p, " \"dcw\": %u%c" , s->status.dcw, delim);
p += sprintf(p, " \"denoise\": %u%c" , s->status.denoise, delim);
p += sprintf(p, " \"framesize\": %u%c" , s->status.framesize, delim);
p += sprintf(p, " \"gainceiling\": %u%c" , s->status.gainceiling, delim);
p += sprintf(p, " \"hmirror\": %u%c" , s->status.hmirror, delim);
p += sprintf(p, " \"lenc\": %u%c" , s->status.lenc, delim);
p += sprintf(p, " \"pixformat\": %u%c" , s->pixformat, delim);
p += sprintf(p, " \"quality\": %u%c" , s->status.quality, delim);
p += sprintf(p, " \"raw_gma\": %u%c" , s->status.raw_gma, delim);
p += sprintf(p, " \"saturation\": %d%c" , s->status.saturation, delim);
p += sprintf(p, " \"sharpness\": %d%c" , s->status.sharpness, delim);
p += sprintf(p, " \"special_effect\": %u%c" , s->status.special_effect, delim);
p += sprintf(p, " \"vflip\": %u%c" , s->status.vflip, delim);
p += sprintf(p, " \"wb_mode\": %u%c" , s->status.wb_mode, delim);
p += sprintf(p, " \"wpc\": %u%c" , s->status.wpc, delim);
if (!json) p += sprintf(p, "\n");
p += sprintf(p, " \"xclk\": %u%c" , s->xclk_freq_hz / 1000000, delim);
p += sprintf(p, " \"max_fps\": %.2f", max_fps);
if (json) {
*p++ = '}';
*p++ = 0;
}
if (eXi || print) Serial.println(sensorData);
return sensorData;
}
bool takePhoto(String filename) {
if (ejected) return " SD Card is Ejected!";
uint8_t setFramesize = 0;
sensor_t *s = esp_camera_sensor_get();
uint16_t currentFrameSize = s->status.framesize;
if (ledBrightness != 0) {
ledcWrite(ledChannel, ledBrightness);
delay(50);
}
if (currentFrameSize != captureSize) {
setFramesize = currentFrameSize;
s->set_framesize(s, (framesize_t)captureSize);
delay(250);
}
camera_fb_t *fb = NULL;
fb = esp_camera_fb_get();
if (!fb) {
Serial.println(" ERROR: Failed to capture an image!");
return false;
}
if (ledBrightness != 0) {
delay(250);
ledcWrite(ledChannel, 0);
}
if (addDimensions != "") {
uint16_t picWidth, picHeight;
picWidth = fb->width;
picHeight = fb->height;
imgDimensions = (String)picWidth + "x" + (String)picHeight;
addDimensions.replace("%x", imgDimensions);
filename += addDimensions;
}
filename += fileEXT;
File file = SD_MMC.open(filename.c_str(), FILE_WRITE);
if (file) {
Serial.println(" Saving: " + filename.substring(1));
file.write(fb->buf, fb->len);
file.flush();
file.close();
mostRecentCapture = filename;
} else {
ABORT = " Unable to write " + filename + "\n Please fix this issue and reboot.";
}
if (setFramesize != 0) {
s->set_framesize(s, (framesize_t)currentFrameSize);
}
if (mostRecentCapture == filename) {
totalFileCount++;
averageSize = ( SD_MMC.usedBytes() - levellingBytes ) / totalFileCount;
}
esp_camera_fb_return(fb);
fb = NULL;
return true;
}
bool instantPic() {
saveNumber++;
String filename = makeFilename(saveNumber);
if ( filename != "" && takePhoto(filename) ) return true;
return false;
}
void flashLED(uint16_t flashTime = 500) {
if (ledBrightness != 0) {
ledcWrite(ledChannel, ledBrightness);
delay(flashTime);
ledcWrite(ledChannel, 0);
}
}
bool getStoredTime() {
storedTime = prefs.getULong64("UTC", 1679897227);
storedTime += millis() / 1000;
setStoredTime(storedTime);
return true;
}
bool setStoredTime(uint64_t newUTC) {
storedTime = newUTC;
struct timeval tv;
tv.tv_sec = storedTime;
settimeofday(&tv, NULL);
prefs.putULong64("UTC", storedTime);
return true;
}
void storeUTC() {
struct timeval tv;
gettimeofday(&tv, NULL);
storedTime = tv.tv_sec;
Serial.printf(" Saving UTC: %llu", storedTime);
prefs.putULong64("UTC", storedTime);
}
Simple" stream recording facility.
Start stream recording from any console with the command: record
Stop stream recording with the command: stop
Simple.
NOTE: If a previous recording exists, it will be overwritten.
If you want to save a recording, use the download facility in the prefs panel.
NOTE: If you load/refresh the web interface after starting a recording from the console, the
recording indicator will be pulsing. Also the record button will be a stop button, which
you can use to stop the recording at any time.
*/
void simpleStreamRecord() {
if (amStreaming) {
Serial.println(" Live stream is already running!");
return;
}
if (ejected) { sendEjected(); return; }
File mjpegFile;
String mjpegFileName = "/" + streamFileName;
if (SD_MMC.exists(mjpegFileName)) SD_MMC.remove(mjpegFileName);
uint8_t failCount = 0;
static int64_t last_frame = 0;
if(!last_frame) last_frame = esp_timer_get_time();
uint64_t frameCounter = 0;
char filename[64];
sprintf(filename, mjpegFileName.c_str());
mjpegFile = SD_MMC.open(filename, FILE_WRITE);
if (!mjpegFile) {
Serial.println(" Error creating video file");
return;
}
amStreaming = true;
Serial.printf(" Recording mjpeg stream to:%s\n", filename);
amRecording = true;
camera_fb_t *fb;
while (true) {
fb = NULL;
fb = esp_camera_fb_get();
if (!fb) {
failCount++;
Serial.println(" ERROR: Failed to capture an image!");
if (failCount == 10) {
Serial.println(" 10 failures. Something is wrong with your image settings.\n Please fix and try again.");
break;
}
}
frameCounter++;
mjpegFile.print("\r\n\r\n");
mjpegFile.write(fb->buf, fb->len);
mjpegFile.print("\r\n\r\n");
esp_camera_fb_return(fb);
loop();
if (eXi && frameCounter % 100 == 0) Serial.printf(" Stream Recording: wrote %llu frames.\n", frameCounter);
if (max_fps != 0) {
int64_t frame_time = (esp_timer_get_time() - last_frame) / 1000;
int32_t userDelay = 1000 / max_fps;
int32_t frame_delay = (userDelay > frame_time) ? userDelay - frame_time : 0;
delay(frame_delay);
last_frame = esp_timer_get_time();
}
if (!doRecording) {
mjpegFile.printf("%llu frames written\r\n", frameCounter);
mjpegFile.close();
Serial.printf(" Stream Recording complete. Wrote %llu frames.\n", frameCounter);
amRecording = false;
break;
}
}
fb = NULL;
amStreaming = false;
}
void printLocalTime() {
Serial.print(" Getting Time..");
bool fallBack = false;
if (!getLocalTime(&timeinfo)) {
if (realTimeRequired) {
if (retryCount > maxNTPRetries) {
if (useStoredTime) {
fallBack = true;
return;
} else {
Serial.println(" Time Server is malfunctioning. Giving up now.\n Check your NTP Settings.");
xCommand = "x";
return;
}
}
retryCount++;
delay(3000);
Serial.println(" Failed to get internet time, but Real Time is REQUIRED. Retrying..");
printLocalTime();
} else {
Serial.println(" Failed to obtain internet time");
if (useStoredTime) fallBack = true;
}
if (fallBack) {
getStoredTime();
setenv("TZ", timeZone, 1);
tzset();
Serial.println("\n Reverting to stored time.");
}
} else {
Serial.print(" Success.\n");
Serial.print(" Setting Internal Clock to: ");
Serial.println(&timeinfo, "%H:%M:%S, %A, %B %d %Y");
}
}
void startOnlineFeatures() {
uint64_t currentTime = millis();
uint64_t startTime = currentTime;
bool gotNet = true;
bool gotAP = true;
INITimagesPerPage = imagesPerPage;
INITthumbWidth = thumbWidth;
Serial.println("\n Going Online..\n");
if (!remControl) {
Serial.println(" Remote Control Features Disabled.");
} else if (softAPSSID == "") {
Serial.println(" Soft Access Point Disabled.");
}
if (remControl && onlyAP && softAPSSID != "") {
WiFi.mode(WIFI_AP);
if (!isEmptyChar(&SGhostName)) WiFi.setHostname(SGhostName);
Serial.println("\n Running in Access Point Only Mode.");
} else {
WiFi.mode(WIFI_AP_STA);
if (!isEmptyChar(&SGhostName)) WiFi.setHostname(SGhostName);
if (!isEmptyChar(&ssid)) {
Serial.printf(" Connecting to %s", ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
currentTime = millis();
if (currentTime > (startTime + (timeOut * 1000) )) {
Serial.printf("\n Connecting to %s FAILED.\n", ssid);
useRealTime = false;
gotNet = false;
break;
}
}
if (WiFi.status() == WL_CONNECTED) {
if (remControl) {
Serial.print(" Success!\n Local WiFi Web Address: http:/");
} else {
Serial.println(" Success!\n Internet Connexion Established.");
}
}
} else { gotNet = false; }
}
if (remControl && softAPSSID != "") {
IPAddress local_IP(192,168,4,1);
IPAddress gateway(192,168,4,1);
IPAddress subnet(255,255,255,0);
WiFi.softAPConfig(local_IP, gateway, subnet);
if (WiFi.softAP(softAPSSID, softAPPass) ) {
Serial.print("\n WiFi Access Point Established.\n Connect your WiFi to ");
Serial.println("\"" + softAPSSID + "\".");
Serial.print(" Once connected to the AP, point your browser to: http:/");
} else { gotAP = false; }
} else { gotAP = false; }
if (remControl && (gotAP || gotNet) && !isEmptyChar(&SGhostName) ) {
Serial.println("\n You can also access your TL-CAM here: http:/", sendRoot);
/console", sendWebConsole);
server.on("/favicon.ico", handleFavicon);
server.onNotFound(universalHandler);
live" stream, in that it won't update automatically or anything like that;
server.on("/recent", handleRecent);
server.on("/capture", handleCapture);
server.on("/mjpeg", handleLiveStream);
server.on("/stream", handleLiveStream);
server.on("/switch", handleSwitchRecording);
server.on("/download", handleDownloadRecording);
server.on("/slides", handleSlides);
server.on("/slideshow", handleStreamingSlideShow);
server.on("/skip", handleSSSSkip);
server.on("/sp", handleSSSPause);
server.on("/currentSlideName", handleReportCurrentSlide);
server.on("/upload", HTTP_POST, [](){}, handleUPLOADFile);
server.on("/delete", handleDeleteFile);
server.on("/snap", handleTakeSnapNow);
server.on("/interval", handleIntervalChange);
server.on("/getint", handleGetInterval);
server.on("/eject", handleEjectSD);
server.on("/insert", handleInsertSD);
server.on("/info", handleSDInfo);
server.on("/L", handleFileList);
server.on("/l", handleFileList);
server.on("/List", handleFullFileList);
server.on("/list", handleFullFileList);
server.on("/QuickList", handleQuickFileList);
server.on("/quicklist", handleQuickFileList);
server.on("/ql", handleQuickFileList);
server.on("/qla", handleQuickAllFileList);
server.on("/help", handleCommandsHelp);
server.on("/sensor", handleSensorStatus);
server.on("/sensorx", handleSensorStatusEx);
server.on("/nvs", handlePrintNVS);
server.on("/memory", handleMemoryInfo);
server.on("/xclk", handleImageSettings);
server.on("/sensorset", handleImageSettings);
server.on("/control", handleESP32ControlWrap);
server.on("/settings", handleCameraSettings);
server.on("/rebooted", handleRebootStatus);
server.on("/backup", handleBackupNVS);
server.on("/restore", handleRestoreNVS);
server.on("/LastMessage", handleLastMessage);
official" API stuff
server.on("/status", handleESPSensorStatus);
server.on("/reg", handleESP32SetReg);
server.on("/greg", handleESP32GetReg);
server.on("/pll", handleESP32pll);
server.on("/resolution", handleESP32WindowRes);
const char *Keys[1] = {"Cookie"};
server.collectHeaders(Keys, 1);
server.begin();
Serial.println("\n Web Server Started. Enjoy!");
} else {
Serial.println("\n\n Connecting to WiFi FAILED.\n Web Controls Disabled.\n");
remControl = false;
}
}
}
void send500(String msg = "fail") {
server.send(500, _TEXT_, msg);
}
void handleSwitchRecording() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
doRecording = doRecording ? false : true;
String msg = doRecording ? "Stream Recording Started" : "Stream Recording Stopped";
if (!amStreaming) {
server.send(200, _TEXT_, "OK " + msg);
silentStream = true;
handleLiveStream();
} else {
server.send(200, _TEXT_, "OK " + msg);
silentStream = false;
}
}
void handleLiveStream() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
if (amStreaming) {
server.send(500, _TEXT_, "Live stream is already running!");
return;
}
WiFiClient client;
if (!silentStream) {
client = server.client();
client.write(headerData, strlen(headerData));
client.write(contentType, strlen(contentType));
client.write(boundaryMark, bmLen);
}
uint8_t failCount = 0;
camera_fb_t *fb = NULL;
char hbuff[32];
uint32_t frame_size;
uint64_t showTime = 0;
amStreaming = true;
File mjpegFile;
uint64_t frameCounter = 0;
while (true) {
if (silentStream && !doRecording) break;
if (!silentStream && !client.connected() && !amRecording) break;
loop();
if (max_fps == 0 || ( max_fps != 0 && ( millis() > ( showTime + (1000 / max_fps) ) ) ) ) {
if (doRecording && !amRecording) {
String mjpegFileName = "/" + streamFileName;
if (SD_MMC.exists(mjpegFileName)) SD_MMC.remove(mjpegFileName);
char filename[64];
sprintf(filename, mjpegFileName.c_str());
mjpegFile = SD_MMC.open(filename, FILE_WRITE);
if (!mjpegFile) {
Serial.println(" Error creating video file");
amStreaming = false;
return;
}
if (eXi) Serial.printf(" Recording mjpeg stream to:%s\n", filename);
amRecording = true;
frameCounter = 0;
}
fb = NULL;
fb = esp_camera_fb_get();
frame_size = fb->len;
if (amRecording) {
mjpegFile.print("\r\n\r\n");
mjpegFile.write(fb->buf, fb->len);
mjpegFile.print("\r\n\r\n");
sample" MJPEG files I tested would play in VLC. This does. Boom!
}
if (!fb) {
failCount++;
Serial.println(" ERROR: Failed to capture an image!");
if (failCount == 10) {
Serial.println(" 10 failures. Something is wrong with your image settings.\n Please fix and try again.");
break;
}
}
if (!amRecording || streamANDRecord) {
client.write(frameType, ctLen);
sprintf(hbuff, "%d\r\n\r\n", frame_size);
client.write(hbuff, strlen(hbuff));
client.write((char *)fb->buf, frame_size);
client.write(boundaryMark, bmLen);
}
frameCounter++;
esp_camera_fb_return(fb);
showTime = millis();
if (amRecording && eXi && frameCounter % 100 == 0) Serial.printf(" Stream Recording: wrote %llu frames.\n", frameCounter);
if (amRecording && !doRecording) {
mjpegFile.printf("%llu frames written\r\n", frameCounter);
mjpegFile.close();
if (eXi) Serial.printf(" Stream Recording complete. Wrote %llu frames.\n", frameCounter);
amRecording = false;
}
}
}
fb = NULL;
amStreaming = false;
}
void handleDownloadRecording() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
if (ejected) { sendEjected(); return; }
String mjpegFileName = "/" + streamFileName;
if ( !SD_MMC.exists(mjpegFileName) ) {
server.send(404);
} else {
File mjpegFile = SD_MMC.open(mjpegFileName);
if (!mjpegFile) return;
server.sendHeader("Content-Disposition", "Attachment; filename=" + streamFileName);
if (eXi) Serial.printf(" Serving MJPEG File: %s\n", mjpegFileName.c_str());
server.streamFile(mjpegFile, "video/mjpeg");
mjpegFile.close();
}
}
fail" in an error response triggers the
JavaScript WARNING color for the message; probably red (in the CSS).
* By "inserted" I mean physically, as well as intra-physically; either with the "sd" or "insert"
commands, or boot-up.
** The cute prefs panel button is a single-click, no-nonsense control. Click it and you will be
presented with a file chooser, to choose a file from your local filesystem. We remain in one-
click territory*** and the instant you select a file it will be uploaded, replacing any
existing file of the same name and; the whole point; available for immediate viewing at..
/your-file-name.html
*** Back in the day I would help local folk with "PeeCees" and invariably I would switch their
desktop to one-click operation in the process of helping. The emails! lol. But always worth
it. Every click costs.
/upload */
void handleUPLOADFile() {
if (ejected) { sendEjected(); return; }
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
String filename = "/" + upload.filename;
if ( SD_MMC.exists(filename) ) SD_MMC.remove(filename);
File file = SD_MMC.open(filename, FILE_WRITE);
if (!file) {
server.send(500, _TEXT_, "Failed to create file");
return;
}
Serial.println(" Uploading file: " + filename);
} else if (upload.status == UPLOAD_FILE_WRITE) {
if (upload.currentSize > 0) {
File file = SD_MMC.open("/" + upload.filename, FILE_APPEND);
if (!file) {
server.send(500, _TEXT_, "Failed to open file");
return;
}
file.write(upload.buf, upload.currentSize);
file.close();
}
} else if (upload.status == UPLOAD_FILE_ABORTED) {
if ( SD_MMC.exists(upload.filename) ) SD_MMC.remove(upload.filename);
server.send(500, _TEXT_, "Upload Failed. User Aborted");
return;
} else if (upload.status == UPLOAD_FILE_END) {
if (upload.totalSize == 0) {
server.send(500, _TEXT_, "UPLOAD FAILED. NO DATA RECEIVED!");
return;
}
server.send(200, _TEXT_, "File uploaded successfully");
Serial.println(" File uploaded successfully");
}
}
special" booleans like "seen", as well as integer values.
Returns a boolean String ("true"/"false").
*/
String cOOkiEChomp(String &Cookies, String cOOkiE, bool defaultValue, String special="", bool isNotBool=false, uint8_t intVal=0) {
if (Cookies.indexOf(cOOkiE) != -1) {
String cookieTest;
cookieTest = Cookies.substring( Cookies.indexOf(cOOkiE)+cOOkiE.length()+1, Cookies.indexOf( ";", Cookies.indexOf(cOOkiE) ) );
cookieTest.trim();
if (cookieTest != "") {
setCookie(cOOkiE, cookieTest);
if (isNotBool) return cookieTest;
return (special != "") ? ((cookieTest == special) ? "true" : "false") : ((cookieTest == "true") ? "true" : "false");
}
} else {
if (isNotBool) return (String)intVal;
return (defaultValue == true) ? "true" : "false";
}
return "";
}
void setCookie(String cOOkiE, String value) {
server.sendHeader("Set-Cookie", cOOkiE + "=" + value + ";Path=/;HttpOnly;samesite=Strict;Expires=Fri, 31 Dec 9999 23:59:59 GMT");
}
bool toBool(String inputString) {
inputString.trim();
inputString.toLowerCase();
if (inputString == "true") return true;
return false;
}
void handleStreamingSlideShow() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
if (ejected) { sendEjected(); return; }
if (sShowPlaying) { sendAlreadyPlaying(); return; }
File root = SD_MMC.open("/");
if ( !root || !root.isDirectory()) {
server.send(200, _TEXT_, " Problem with root directory. Check your SD Card.");
return;
}
sssSkip = 0;
sssPause = false;
sShowPlaying = true;
uint32_t start = 1;
if (server.hasArg("start")) start = server.arg("start").toInt();
WiFiClient client = server.client();
client.write(headerData, strlen(headerData));
client.write(contentType, strlen(contentType));
client.write(boundaryMark, bmLen);
char hbuff[32];
String fileName;
uint32_t fileIndex = 0;
uint64_t showTime = 0;
while (fileIndex+1 < start) {
fileName = root.getNextFileName();
if (fileName.indexOf(fileEXT) != -1) fileIndex++;
}
File file = root.openNextFile();
String realName;
uint32_t imgSize;
while (file) {
realName = (String)file.name();
imgSize = file.size();
if (!client.connected()) {
if (eXi) Serial.println(" Streaming slideshow connexion closed.");
break;
}
if (sssPause) showTime = millis();
while (sssSkip > 0) {
showTime = 0;
sssSkip--;
fileName = root.getNextFileName();
if (fileName == "") break;
fileIndex++;
}
if (sssSkip == -1) {
showTime = 0;
sssSkip = 0;
}
if ( millis() > (showTime + (slideTime * 1000)) ) {
if (realName.indexOf(fileEXT) != -1) {
if (!chromeHack) fileIndex++;
if (eXi && fileIndex > 0) Serial.printf(" Showing Slide: [%i] %s\n", fileIndex, realName.c_str());
if (chromeHack) fileIndex++;
currentSlide = realName;
frame" to the web client..
client.write(frameType, ctLen);
sprintf(hbuff, "%d\r\n\r\n", imgSize);
client.write(hbuff, strlen(hbuff));
client.write(file);
client.write(boundaryMark, bmLen);
showTime = millis();
}
file = root.openNextFile();
}
loop();
}
if (chromeHack && eXi) Serial.printf(" Showing Slide: [%i] %s\n", fileIndex, realName.c_str());
delay(slideTime * 1000);
sShowPlaying = false;
sssPause = false;
sssSkip = 0;
}
skip" is a valid serial command),
like so:
/skip100
or
/skip 100
*/
void handleSSSSkip() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
if (ejected) { sendEjected(); return; }
if (!sShowPlaying) { sendNotPlaying(); return; }
String skip;
if (server.hasArg("skip")) {
skip = server.arg("start");
skip.trim();
uint32_t skipInt = skip.toInt();
sssSkip = skipInt;
}
server.send(200, _TEXT_, "OK\nStreaming SlideShow Skip: " + (String)sssSkip);
}
sp" to toggle the pause state from the console),
but it's nice to get a proper response in the web.
/sp */
void handleSSSPause() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
if (ejected) { sendEjected(); return; }
if (!sShowPlaying) { sendNotPlaying(); return; }
sssPause = sssPause ? false : true;
server.send(200, _TEXT_, "OK\nStreaming SlideShow : " + (String)(sssPause == true) ? "Paused" : "Playing");
}
bool loadImage(String path, bool ignoreCashing = false) {
if (path == "") return false;
File imgFile = SD_MMC.open(path.c_str());
if (!imgFile) {
String ret = "Problem opening " + path;
server.send(500, _TEXT_, ret.c_str());
return false;
}
if (!cacheImages || ignoreCashing) {
server.sendHeader("Cache-Control", "no-store, no-cache, max-age=0, must-revalidate");
} else {
server.sendHeader("Cache-Control", "private, max-age=" + (String)(60 * cacheTime) );
Expires" headers.
}
if (eXi) Serial.printf(" Serving Image File: %s\n", path.c_str());
server.streamFile(imgFile, "image/jpeg");
imgFile.close();
return true;
}
next" image (I know I do).
That's the sort of thing this facility is designed to enable.
/slides */
void handleSlides() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
if (ejected) { sendEjected(); return; }
String slide, fileName;
uint32_t slideInt = 1, fileIndex = 0;
if (server.hasArg("slide")) {
slide = server.arg("slide");
slide.trim();
slideInt = slide.toInt();
if (slideInt == 0) slideInt = 1;
}
File root = SD_MMC.open("/");
if ( !root || !root.isDirectory()) {
server.send(500, _TEXT_, " Problem with root directory. Check your SD Card.");
return;
}
while (fileIndex < slideInt) {
fileName = root.getNextFileName();
if (fileName == "") { handleOutOfBounds(); return; }
if (fileName.indexOf(fileEXT) != -1) fileIndex++;
}
currentSlide = fileName;
if (eXi) Serial.printf(" Serving Slide No: %i (%s)\n", fileIndex, fileName.c_str());
loadImage(fileName, true);
}
void handleOutOfBounds() {
if (eXi) Serial.println(" HTTP: Requested Slide OUT-OF-BOUNDS!");
currentSlide = noMoreSlides;
server.send(500, _TEXT_, currentSlide);
}
void handleReportCurrentSlide() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
if (currentSlide == noMoreSlides) {
handleOutOfBounds();
} else {
server.send(200, _TEXT_, currentSlide);
}
}
void getNightTimeStatus() {
if (!useRealTime) return;
time_t now = time(NULL);
struct tm *tm_struct = localtime(&now);
uint8_t hour = tm_struct->tm_hour;
nightTime = false;
if (hour >= nightBegins || hour < dayBegins) nightTime = true;
}
look" like anything.
When coding for browsers, it's good to develop the ability to quickly flip between human and
spider, depending on your current state; user or coder.
NOTE: If you are editing in here, I definitely recommend you switch your editor over to HTML or
JavaScript syntax highlighting. If your editor can't do that in a couple of clicks or less,
switch editors. (Java highlighting is a good for general-purpose work, too)
I realise that what follows is, for some, a complete head-f**k. I have worked with many folk who
at first, looked at HTML/JavaScript/CSS and their brain just went blank. That's okay. It takes a
minute to wrap your head around the magic. And it is magical.
But in almost every case, I have seen that "Oh Wow!" moment, when they realise that it's all just
a stream of text that they can edit. Simple; when you know how..
A lightning guide for MCU coders..
HTML: The document structure, aka. The Page. Made up of stuff in "Tags". They open, they
close. What what goes between them, folk see. Here is a tag: <strong>I will be
printed in bold</strong>. Simple. Mostly.
There are "block" elements (sections, DIVs, etc.) and there are "inline" elements,
like basic text is. Inline elements can go inside blocks, but not the other way
around. HTML5 (the current standard) is powerful and quite forgiving.
Hit Ctrl+U at any website to see what's going on underneath the hood. Scroll past
the..
JavaScript: Code that your /browser/ runs, so won't tax your ESP32 at all. JavaScript can do
most anything you can imagine a browser-side scripting language could do, and more;
usually changes to the page you see, but not always.
AJAX, which makes all the background communications with the ESP32 server possible,
is all handled with JavaScript. Without JavaScript, the web interface would be
boring and *much* less useful.
The style is C-like, but super-flexible*, and lots of fun to code. It can be found
between HTML <script></script> tags.
* Seriously, while it may frustrate at times, at other times you sketch pseudo-code
and it *just works*. You think, "I wonder if you can do this?". Yup.
CSS: The style of things. The "C" stands for Cascading, and it is beautiful. Later rules
override earlier rules.
An "id" /must/ be unique. Their CSS declarations begin with a "foo", being unique)..
<a href="https:class" can apply to any number of elements (stuff inside HTML tags) and their CSS
declarations begin with ".".
CSS lives inside the <style></style> tags.
On proper websites, CSS and JavaScript usually live in external files, but here we just throw
everything inside the main HTML document, using the above-mentioned tags.
Another important concept to remember when serving up web pages is that a web server has no
concept of history. It has zero memory of your previous visits, unless you GIVE it a memory.
And that's where cookies come in..
*/
void sendRoot() {
if (eXi) Serial.printf(" HTTP Request: Main Page for client @ %s\n", server.client().remoteIP().toString().c_str());
String cookieTest, Cookies = server.header("Cookie") + ";";
darkMode = toBool(cOOkiEChomp(Cookies, "darkmode", darkModeINIT));
bgColor = darkMode ? darkBGColor : lightBGColor;
autoDark = toBool(cOOkiEChomp(Cookies, "autodark", autoDarkINIT));
getNightTimeStatus();
if (autoDark) bgColor = nightTime ? darkBGColor : lightBGColor;
amDark = false;
if (bgColor == darkBGColor) amDark = true;
stickyPoP = toBool(cOOkiEChomp(Cookies, "stickypop", stickyPoPINIT));
loadingIcon = toBool(cOOkiEChomp(Cookies, "rotateicon", loadingIconINIT));
loopPoP = toBool(cOOkiEChomp(Cookies, "looppop", loopPoPINIT));
spacesInListView = toBool(cOOkiEChomp(Cookies, "listspaces", spacesInListViewINIT));
cacheImages = toBool(cOOkiEChomp(Cookies, "cacheimg", cacheImagesINIT));
cookieTest = cOOkiEChomp(Cookies, "cachetime", false, "", true, cacheTimeINIT);
cacheTime = cookieTest.toInt();
streamANDRecord = toBool(cOOkiEChomp(Cookies, "twinstream", streamANDRecordINIT));
cookieTest = cOOkiEChomp(Cookies, "slidetime", false, "", true, slideTimeINIT);
slideTime = cookieTest.toInt();
cookieTest = cOOkiEChomp(Cookies, "zmaccel", false, "", true, zmAcellerationINIT);
zmAcelleration = cookieTest.toInt();
bool seenTWarn = toBool(cOOkiEChomp(Cookies, "tinywarn", false, "seen"));
bool seenPWarn = toBool(cOOkiEChomp(Cookies, "prefwarn", false, "seen"));
&" (?foo=bar&bar=foo&etc.)
displayFrom = 1;
thumbView = false;
doPoP = false;
imagesPerPage = INITimagesPerPage;
thumbWidth = INITthumbWidth;
bool openWithPrefs = false;
bool openWithSlides = false;
bool openWithHelp = false;
bool autoPlaying = false;
bool amStreaming = false;
String floatX = "", floatY = "";
bool floating = false;
bool fitMode = true;
if (eXi) Serial.printf(" URI: %s", server.uri().c_str());
for (uint8_t i = 0; i < server.args(); i++) {
if (server.argName(i) == "start") displayFrom = server.arg(i).toInt();
if (server.argName(i) == "thumbs" && server.arg(i) == "true") thumbView = true;
if (server.argName(i) == "pop" && server.arg(i) == "true") doPoP = true;
if (server.argName(i) == "per-page") imagesPerPage = server.arg(i).toInt();
if (imagesPerPage > perPageMax) imagesPerPage = INITimagesPerPage;
if (server.argName(i) == "width") thumbWidth = server.arg(i).toInt();
states" which are remembered on page refresh/reload..
if (server.argName(i) == "prefs") openWithPrefs = true;
if (server.argName(i) == "help") openWithHelp = true;
if (server.argName(i) == "slideshow") openWithSlides = true;
if (server.argName(i) == "play") autoPlaying = true;
if (server.argName(i) == "live") amStreaming = true;
if (server.argName(i) == "floating") floating = true;
if (server.argName(i) == "floatX") floatX = server.arg(i);
if (server.argName(i) == "floatY") floatY = server.arg(i);
if (server.argName(i) == "fit" && server.arg(i) == "false") fitMode = false;
if (eXi) Serial.printf("%s%s=%s", (i == 0) ? "?" : "&", server.argName(i).c_str(), server.arg(i).c_str());
}
if (eXi) Serial.println();
if (eXi && Cookies.indexOf("=") != -1) Serial.printf(" Cookies: %s\n", Cookies.c_str());
Evaluate selected text in console" to see the *current* value of that variable.
And much, much more.
*/
String webPage;
webPage.reserve(150000 + memReserve);
Reserved %i bytes for webPage\n", 150000 + memReserve); //debug
String author = "https:HTML5(<!DOCTYPE html><html>
<head><title>TL-CAM:</title>
<meta name="author" content=")HTML5");
webPage.concat(author);
webPage.concat(R"HTML5(">
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1">
<style>
:root {
--bg-color:)HTML5");
webPage.concat(bgColor);
webPage.concat(R"HTML5(;
--tl-green:
--br-green:
}
html {
color:
margin: 0;
background-color: var(--bg-color);
width: 100% !important;
font-family: Consolas, ProFontWindows, "Lucida Console", Inconsolata, "Courier New", Courier, monospace;
font-size: 120%;
}
div {
margin: 0;
}
button {
font-family: Consolas, ProFontWindows, "Lucida Console", Inconsolata, "Courier New", Courier, monospace;
}
.clear, .clear-tiny, .clear-small, .clear-med {
clear: both;
}
.clear-tiny {
height: 0.1em;
}
.clear-small {
height: 0.25em;
}
.clear-med {
height: 0.5em;
}
.red {
color:
}
:link, .c-button, .thumbbox, .filelist {
transition: all 125ms ease 0s;
color: var(--tl-green);
text-decoration: none;
}
:visited {
color:
text-decoration: none;
}
a:hover {
color:
text-decoration: underline;
}
a:active {
color: var(--br-green);
text-decoration: none;
}
:focus {
outline: 0;
}
.small {
font-size: 80%;
}
clear: both;
display: block;
padding-bottom: 0.25em;
z-index: 20;
}
.c-button, .delbutt,
border-radius: 0.1em;
border: none;
text-align: center;
font-weight: bold;
}
.c-button,
color: var(--bg-color);
background-color: var(--tl-green);
}
.c-button,
vertical-align: middle;
padding: 0.1em 0.2em;
font-size: 1.5em;
width: 1.33em;
height: 1.33em;
margin: 0 0.25em 0.25em 0;
}
.c-button:hover,
background-color:
}
.c-button:active,
background-color: var(--bg-color);
color:
}
.t-button {
width: 5em;
margin: 0 0.2em 0.2em 0;
font-size: 1em;
}
color: var(--bg-color);
}
color:
}
padding-left: 1em;
}
padding-left: 0;
}
line-height: 94%;
}
display: block;
}
input[type="file"] {
position: fixed;
right: 100%;
bottom: 100%;
}
border: none;
display: inline-block;
border-radius: 0.1em;
padding: 0.5em;
text-align: center;
font-weight: bold;
color: var(--bg-color);
background-color: var(--tl-green);
}
)HTML5");
if (roundedCorners) {
webPage.concat(R"HTML5(
.thumbbox { border-radius: 0.25em; }
.thumb { border-radius: 0.5em; }
.delbutt { border-radius: 0.15em; }
)HTML5");
}
webPage.concat(R"HTML5(
.thumbbox,
position: relative;
float: left;
}
.thumb {
border: 0;
padding: 0.1em 0.5em 0.5em 0.5em;
}
position: fixed;
border: 0.5em solid var(--bg-color);
z-index: 1;
opacity: 0;
visibility: hidden;
}
.filename {
padding-left: 0.75em;
font-size: 0.75em;
}
.delbutt {
position: relative;
top: 0.2em;
width: 1.1em;
height: 1.2em;
margin-top: -0.2em;
padding: 0;
font-size: 1em;
background-color: transparent;
color: var(--tl-green);
}
.delbutt {
float: right;
right: 0.15em;
cursor: crosshair;
}
.datefoot {
position: absolute;
font-size: 0.625em;
bottom: 1.25em;
left: 1em;
color: var(--br-green);
}
.list-wide-hide, .list-hide {
display: none;
margin: 0;
padding: 0;
}
transition: all 125ms ease 0s;
display: block;
opacity: 0;
visibility: hidden;
height: 100vh;
width: 100vw;
background-color: rgba(9,21,10,.98);
z-index: 500;
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0;
}
font-size: 78%;
background-color: rgba(9,21,10,.80);
z-index: 600;
}
z-index: 700;
background-color: rgba(9,21,10,.66);
}
z-index: 900;
}
@keyframes recording {
0%, 100% { opacity: 0.1; }
50% { opacity: 1; }
}
border-radius: 50%;
position: absolute;
background-color: red;
bottom: 0.15em;
right: 0.15em;
width: 1.5em;
height: 1.5em;
animation: recording 1.5s infinite;
z-index: 900;
}
width: 2em;
height: 2em;
position: fixed;
bottom: 0.25em;
right: 0.3em;
z-index: 890;
}
position: fixed;
color: var(--br-green);
font-weight: 700;
top: 0.4em;
left: 1%;
width: 99%;
text-align: center;
white-space: pre;
}
margin: 0;
background: var(--bg-color);
overflow: scroll;
position: absolute;
bottom: 0.5em;
left: 50%;
transform: translateX(-50%);
padding-top: 0.5em;
scrollbar-width: none;
}
height: 87vh;
width: 81vw;
}
height: 90vh;
width: 94vw;
padding: 1em;
white-space: pre;
overflow: overlay;
z-index: 990;
}
display: none;
}
.pref-group {
font-size: 115%;
font-weight: bold;
width: 68vw;
display: inline-block;
margin: 0.5em 0.5em 0 1em;
}
.pref-title {
font-size: 120%;
margin-bottom: 0.25em;
}
.pref-label {
padding-top: 0.1em;
display: block;
}
.pref-label input[type=checkbox], .p-select {
margin-top: -0.1em;
}
.p-select {
padding: 0.05em 0.25em;
width: 3.33em;
float: right;
text-align: left;
font-size: 110%;
}
input[type=checkbox] {
float: right;
margin-right: 0;
border-radius: .3em;
height: 1.5em;
width: 1.5em;
accent-color: var(--tl-green);
}
input[type=range] {
accent-color: var(--tl-green);
margin: 0;
width: 100%
}
height: calc(100vh - 1em);
margin: 0;
position: absolute;
background: transparent;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border: 0.5em solid var(--bg-color);
z-index: 550;
transition: none;
}
position: absolute;
top: 0.5em;
left: 1em;
color: var(--br-green);
z-index: 560;
}
color: var(--bg-color);
background-color: var(--tl-green);
width: 1.25em;
font-size: 600%;
margin: 0;
position: absolute;
top: 50%;
transform: translateY(-50%);
}
left: 0.25em;
}
right: 0.25em;
}
font-weight: bold;
position: fixed;
bottom: 0.1em;
z-index: 999;
}
right: 0.4em;
font-size: 72%;
}
left: 0.5em;
font-size: 90%;
}
display: inline-block;
clear: both;
width: 100%;
margin: 0.25em auto 0.1em;
font-size: 140%;
font-weight: 700;
}
cursor: crosshair;
}
.warning {
color:
}
)HTML5");
webPage.concat(R"HTML5(
.pop-highlight, .filelist:hover a, .thumbbox:hover {
background-color:
}
.pop-highlight, .pop-highlight a, .thumbbox:hover a, .filelist:hover a, .thumbbox:hover {
color:
}
margin-top: 0.2em;
}
margin: 0 auto 0 0.25em;
width: fit-content;
}
.listdelbutt {
background-color: transparent;
border: none;
padding: 0.15em 0 0 0.1em;
color: var(--tl-green);
font-size: 90%;
float: right;
}
line-height: 1.5em;
}
)HTML5");
+") creates a big red border around the
thumbnail image when you hover over the delete buttons. Ugly-ish, but functional as a shovel.
I'll leave this in here for now, in case you don't yet have a level-4 capable web browser.
In List View, only the [x] turns red. See below.
*/
webPage.concat(R"HTML5(
.delbutt:hover + a > img, .listdelbutt:hover {
background:
opacity: 0.80;
}
)HTML5");
webPage.concat(R"HTML5(
.thumbbox:has(> .delbutt:hover), .filelist:has(> .listdelbutt:hover), a:has(~ .listdelbutt:hover) {
background:
opacity: 0.80;
}
)HTML5");
, .listdelbutt:hover ~ a" to the main selector, and then do something like this..
.filelist {
display: flex;
}
.filelist a {
order: -1;
transition: none;
}
Which basically switches around the visual order of the link and delete button. Then in your
markup, make it oppositely so: move the <button> to immediately /before/ the <a> tag. Done.
But note, you will lose the global right-side line-up, for easy gliding over delete buttons, which
is really only an issue where the file names / index numbers change size. Mostly it will work
exactly like the fancy CSS4 version. I guess you could do a similar thing for thumbnails.
*/
webPage.concat(R"HTML5(
.pad-left {
padding-left: 0.2em;
}
)HTML5");
responsive", but there's only so much we can see and do in a small screen.
What follows here are changes to the basic CSS when you use (or resize to) a *bigger* screen..
*/
webPage.concat(R"HTML5(
@media screen and (min-width: 600px) {
.list-wide-hide {
display: inline;
}
}
@media screen and (min-width: 720px) {
.list-hide {
display: inline;
}
.list-wide-hide {
display: inline;
}
}
@media screen and (min-width: 891px) {
html {
font-size: 90%;
}
padding-bottom: 0.9em;
}
font-size: 80%;
}
font-size: 1.5em;
}
.filename {
padding-top: 0;
}
display: inline;
position: relative;
top: 0.25em;
clear: none;
width: auto;
padding: 0 0 0 0.66em;
}
height: 75vh;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.pref-group {
vertical-align: top;
font-size: 150%;
width: 17rem;
}
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 140%;
}
.datefoot {
font-size: 0.77em;
bottom: 1em;
left: 0.9em;
}
}
</style></head><body>
)HTML5");
webPage.concat("<div id=\"controller\">");
String bottomEnd = (String)(displayFrom - imagesPerPage);
if (displayFrom < imagesPerPage) bottomEnd = "1";
String doPrevious = "Previous Page";
if (displayFrom == 1) doPrevious = "THIS PAGE!";
webPage.concat("<button id=\"previousPage\" onclick=\"goPage(" + bottomEnd + ");\" ");
webPage.concat("class=\"c-button\" title=\"" + doPrevious + " (");
webPage.concat(bottomEnd + "-" + (String)( (displayFrom < imagesPerPage) ? imagesPerPage : displayFrom - 1) );
webPage.concat(") HotKey: [\"><</button>");
webPage.concat("<button class=\"c-button\" title=\"home\" id=\"homebutt\" onclick=\"goHome()\">/</button>");
webPage.concat("<button class=\"c-button\" title=\"reload the page\n");
webPage.concat("this icon moonlights as an indicator for background pre-loading images\" id=\"reload\"");
webPage.concat(" onclick=\"location.reload()\"><span id=\"reloading\">&<select onchange=\"setMessage('" + loadingMsg + "'); setPerPage(this.value)\" ");
webPage.concat("title=\"images-per-page chooser\" name=\"per-page\" id=\"per-page\">");
webPage.concat(" <option value=\"" + (String)imagesPerPage + "\">images per page: </option>");
for (uint16_t pp = INITimagesPerPage; pp <= perPageMax; pp += perPageMenuStep) {
webPage.concat("<option value=\"" + (String)pp + "\"");
if (pp == imagesPerPage) webPage.concat(" selected");
webPage.concat(">" + (String)pp + "</option>");
}
webPage.concat("</select>");
String nextImage = (String)(displayFrom + imagesPerPage);
webPage.concat("<button id=\"nextPage\" onclick=\"goPage(" + nextImage + ");\" ");
webPage.concat("class=\"c-button\" title=\"next page (" + nextImage + "-");
webPage.concat((String)( displayFrom + ( imagesPerPage * 2 ) - 1 ) + ") HotKey: ]\">></button>");
webPage.concat("<button class=\"c-button\" id=\"thumbsbutt\" onclick=\"toggleThumbs()\"");
webPage.concat((thumbView ? "title=\"switch to list view (T)\n(yes, it's the Trigram for Heaven) \">&title=\"switch to thumbnail view (T)\">&<select onchange=\"setMessage('" + loadingMsg + "'); setThumbWidth(this.value)\" ");
webPage.concat("title=\"thumbnail width chooser (in actual fact, this is a misnomer as we actually set\n");
webPage.concat("the *height* to 3/4 of whatever you set here, so the thumbnails flow properly)\n");
webPage.concat("regular 4:3 images will scale perfectly - other ratios will scale to fit at whatever width \" ");
webPage.concat("name=\"thumb-width\" id=\"thumb-width\">");
webPage.concat(" <option value=\"" + (String)thumbWidth + "\">Thumbnail width: </option>");
for (uint16_t tw = 50; tw <= 400; tw += 25) {
webPage.concat("<option value=\"" + (String)tw + "\"");
if (tw == thumbWidth) webPage.concat(" selected");
webPage.concat(">" + (String)tw + "</option>");
}
webPage.concat("</select>");
}
webPage.concat("<button class=\"c-button\" id=\"popbutt\" onclick='togglePoP()' ");
webPage.concat((doPoP ? "title=\"disable pop-up previews (P)\">-</button>" : \
"&pop=true'\" title=\"enable pop-up previews (P) \">+</button>") );
webPage.concat("<button class=\"c-button\" title=\"take a snap right now! (.)\" ");
webPage.concat("id=\"snap-butt\" onclick=\"takeSnap()\">&);
webPage.concat("<button class=\"c-button\" title=\"click to toggle live stream preview (L)\" id=\"live-stream\"");
webPage.concat(" onclick=\"streamOpen ? closeLiveStream() : openLiveStream();\">&);
webPage.concat("<button class=\"c-button\" title=\"click to toggle live stream recording (')\" ");
webPage.concat("id=\"stream-record\" onclick=\"toggleRecording();\">");
if (amRecording) { webPage.concat("&); } else { webPage.concat("●"); }
webPage.concat("</button>");
webPage.concat("<button class=\"c-button\" title=\"settings / preferences (,)\" ");
webPage.concat("onclick=\"openPrefs()\">&); // gear/cog
webPage.concat("\n<div id=\"message\"");
if (thumbView || doPoP) {
webPage.concat(" title=\"click me to start a slideshow (ctrl+click to go fullscreen)\n");
webPage.concat("note: spacebar activates an auto-playing slideshow - add Ctrl to go fullscreen. \"");
} else {
webPage.concat("title=\"in thumbnail view, or when Pop-Up previews are enabled, click here to start a slideshow! \"");
}
webPage.concat(">" + loadingMsg + "</div>");
webPage.concat("</div>\n");
if (thumbView) {
webPage.concat("<div id=\"thumbnails\">\n");
} else {
webPage.concat("<div id=\"listing\">\n");
}
webPage.concat( listDir(false, true) );
if (thumbView) webPage.concat("<p class=\"thumbbox\" id=\"end\"></p>\n");
webPage.concat("</div>\n");
if (doPoP) webPage.concat("<img id=\"PoP\" alt=\"Popup Preview Image\" />\n");
if (doPoP || thumbView) {
webPage.concat("<div id=\"slideshow\">");
webPage.concat("<button id=\"previousSlide\" alt=\"Previous\" ");
webPage.concat("title=\"previous slide\" onclick=\"userPreviousSlide()\"><</button>");
webPage.concat("<a id=\"imgSaveLink\" title=\"'s' or <click> to save the image\"><img id=\"slide\" src=\"\" /></a>");
webPage.concat("<button id=\"nextSlide\" alt=\"Next\" title=\"next slide\" onclick=\"userNextSlide()\">></button>");
webPage.concat("<div id=\"slide-name\" title=\"current slide\" ></div>");
webPage.concat("</div>\n");
}
webPage.concat("<div id=\"livestream\"></div>\n");
webPage.concat("<div id=\"dot-holder\"></div>\n");
webPage.concat("<div id=\"preferences\">\n");
webPage.concat("<div id=\"link\"><a href=\"https:
webPage.concat("title=\"A source of ESP32 goodies, and much, much more..\" target=\"_blank\">TL-CAM</a></div>");
webPage.concat("<div id=\"version\"><span style=\"padding-right:0.2em\">v</span>" + version + "</div>");
webPage.concat("<div id=\"prefs-message\"></div>\n");
webPage.concat("<div id=\"pref-box\">\n");
webPage.concat("<div class=\"pref-group\">\n<div class=\"pref-title\">Camera Server:</div>");
webPage.concat("<div class=\"clear-small\"></div>\n");
webPage.concat("<form name=\"firefox-again\" autoComplete=\"off\" onsubmit=\"return false\">\n");
webPage.concat("<label class=\"pref-label\" ");
webPage.concat("title=\"save stress on your ESP32 when browsing and watching slideshows \n");
webPage.concat("note: an image's cache time is set at the moment the image is cached by the browser ");
webPage.concat("this setting has no effect on images already cached (Ctrl+F5 for total refresh)\">Cache Images:");
webPage.concat("<input type=\"checkbox\" onchange=\"boolSetting(this, 'cai')\"");
if (cacheImages) webPage.concat(" checked");
webPage.concat(" /></label>\n<div class=\"clear-tiny\"></div>\n");
webPage.concat("<label class=\"pref-label\" title=\"set the image caching time, in minutes\">Cache Time:");
webPage.concat("<select class=\"p-select\" onchange=\"setTLCAMSettings('cst', this.value)\">");
for (uint16_t si = 1; si <= 30; si++) {
webPage.concat("<option value=\"" + (String)si + "\"");
if ( si == cacheTime ) webPage.concat(" selected");
webPage.concat(">" + (String)si + "</option>");
}
for (uint16_t si = 45; si < 90; si+=15) {
webPage.concat("<option value=\"" + (String)si + "\"");
if ( si == cacheTime ) webPage.concat(" selected");
webPage.concat(">" + (String)si + "</option>");
}
for (uint16_t si = 90; si <= 720; si+=30) {
webPage.concat("<option value=\"" + (String)si + "\"");
if ( si == cacheTime ) webPage.concat(" selected");
webPage.concat(">" + (String)si + "</option>");
}
webPage.concat("</select></label>\n<div class=\"clear-small\"></div>\n");
webPage.concat("<label class=\"pref-label\" title=\"set the LED flash brightness (0-255)\n");
webPage.concat("the extremely powerful 3030 LED will FLASH momentarily at the new level\nWATCH OUT!\">LED Brightness:");
webPage.concat("<select class=\"p-select\" onchange=\"setMessage('TEST!');setTLCAMSettings('ledb', this.value)\">");
for (uint16_t si = 0; si <= 255; si++) {
webPage.concat("<option value=\"" + (String)si + "\"");
if ( si == ledBrightness ) webPage.concat(" selected");
if ( si > 127 ) webPage.concat(" style=\"color: red\"");
webPage.concat(">" + (String)si + "</option>");
}
webPage.concat("</select></label>\n<div class=\"clear-tiny\"></div>\n");
webPage.concat("<label class=\"pref-label\" title=\"use timestamps for the image file names\">Timestamp Files:");
webPage.concat("<input type=\"checkbox\" onchange=\"boolSetting(this, 'tsf')\"");
if (timeStampFileNames) webPage.concat(" checked");
webPage.concat(" /></label>\n<div class=\"clear-tiny\"></div>\n");
webPage.concat("<label class=\"pref-label\" title=\"overwrite old images with new captures\n");
webPage.concat("(only works with numerically-named images, not timestamped images)\">Overwrite Files:");
webPage.concat("<input type=\"checkbox\" onchange=\"boolSetting(this, 'ovw')\"");
if (overWrite) webPage.concat(" checked");
webPage.concat(" /></label>\n<div class=\"clear-tiny\"></div>\n");
webPage.concat("<label class=\"pref-label\" ");
webPage.concat("title=\"TL-CAM defaults to either playing OR recording the live stream, but you can have both.. \n");
webPage.concat("expect a 5%-ish drop in frame-rates when running both simultaneously - not too bad\n");
webPage.concat("note: this setting is saved in a cookie, for each individual browser\">Twin Streaming:");
webPage.concat("<input type=\"checkbox\" onchange=\"streamANDRecord=(this.checked ? true : false);boolSetting(this, 'tws')\"");
if (loopPoP) webPage.concat(" checked");
webPage.concat(" /></label>\n<div class=\"clear-tiny\"></div>\n");
webPage.concat("<label class=\"pref-label\" title=\"set the live stream frame limit, in maximum frames-per-second (0-100)\n");
webPage.concat("set to 0 to disable frame-limiting (this setting applies to stream viewing and recording)\">Frame Limit (FPS):");
webPage.concat("<select class=\"p-select\" onchange=\"setTLCAMSettings('mfps', this.value)\">");
for (uint16_t si = 1; si <= 9; si++) {
webPage.concat("<option value=\"0." + (String)si + "\"");
if ( si/10 == max_fps ) webPage.concat(" selected");
webPage.concat(">0." + (String)si + "</option>");
}
for (uint16_t si = 0; si <= 60; si++) {
webPage.concat("<option value=\"" + (String)si + "\"");
if ( si == max_fps ) webPage.concat(" selected");
webPage.concat(">" + (String)si + "</option>");
}
webPage.concat("</select></label>\n<div class=\"clear-tiny\"></div>\n");
webPage.concat("</div>\n");
webPage.concat("<div class=\"pref-group\">\n<div class=\"pref-title\">Web Settings:</div>");
webPage.concat("<div class=\"clear-small\"></div>\n");
webPage.concat("<label class=\"pref-label\" ");
webPage.concat("title=\"set the slideshow time, in seconds. note: this setting also applies to \n");
webPage.concat("title=\"the Streaming SlideShow (you can alter this value while it is playing)\">SlideShow Time:");
webPage.concat("<select class=\"p-select\" onchange=\"slideTime=this.value;setTLCAMSettings('sst', this.value)\">");
for (uint16_t si = 1; si <= 60; si++) {
webPage.concat("<option value=\"" + (String)si + "\"");
if ( si == slideTime ) webPage.concat(" selected");
webPage.concat(">" + (String)si + "</option>");
}
webPage.concat("</select></label>\n<div class=\"clear-small\"></div>\n");
webPage.concat("<label class=\"pref-label\" ");
webPage.concat("title=\"put extra space between the information elements in list view\">Extra Space in Lists:");
webPage.concat("<input type=\"checkbox\" onchange=\"setMessage('reloading..');boolSetting(this, 'xsp')\"");
if (spacesInListView) webPage.concat(" checked");
webPage.concat(" /></label>\n<div class=\"clear-tiny\"></div>\n");
webPage.concat("<label class=\"pref-label\" title=\"rotating loading icon ");
webPage.concat("let's you know images are pre-loading in the background\">Loading Icon:");
webPage.concat("<input type=\"checkbox\" onchange=\"loadingIcon=(this.checked ? true : false);boolSetting(this, 'aii')\"");
if (loadingIcon) webPage.concat(" checked");
webPage.concat(" /></label>\n<div class=\"clear-tiny\"></div>\n");
webPage.concat("<label class=\"pref-label\" ");
webPage.concat("title=\"sticky pop-up preview image - rather than vanish, PoP will 'stick' in place\n");
webPage.concat("then you can use arrow keys/swipe to navigate and delete/backspace to delete images \">Sticky PoP:");
webPage.concat("<input type=\"checkbox\" onchange=\"stickyPoP=(this.checked ? true : false);boolSetting(this, 'skp')\"");
if (stickyPoP) webPage.concat(" checked");
webPage.concat(" /></label>\n<div class=\"clear-tiny\"></div>\n");
webPage.concat("<label class=\"pref-label\" ");
webPage.concat("title=\"when PoP is 'sticky' you can use arrow keys/swipe to move through your images \n");
webPage.concat("when you reach the end, TL-CAM can either flash to indicate this or loop around to the other end\">PoP Loops:");
webPage.concat("<input type=\"checkbox\" onchange=\"loopPoP=(this.checked ? true : false);boolSetting(this, 'lpp')\"");
if (loopPoP) webPage.concat(" checked");
webPage.concat(" /></label>\n<div class=\"clear-tiny\"></div>\n");
webPage.concat("<label class=\"pref-label\" title=\"toggle dark mode\n");
webPage.concat("like most other interface prefs, this setting is stored in a browser cookie\">Dark Mode:");
webPage.concat("<input type=\"checkbox\" id=\"darkmode\" onchange=\"setMessage('reloading..');boolSetting(this, 'dms')\"");
if (darkMode) webPage.concat(" checked");
if (autoDark) webPage.concat(" disabled");
webPage.concat(" /></label>\n<div class=\"clear-tiny\"></div>\n");
webPage.concat("<label class=\"pref-label\" title=\"Automatic Dark mode switches modes depending on the time of day\n");
webPage.concat("note: if Real (NTP) Time is disabled, dark mode will never occur.\">Automatic Dark Mode:");
webPage.concat("<input type=\"checkbox\" id=\"autodark\" onchange=\"autoDMSetting(this, 'dma')\"");
if (autoDark) webPage.concat(" checked");
webPage.concat(" /></label>\n<div class=\"clear-tiny\"></div>\n");
webPage.concat("<label class=\"pref-label\" ");
webPage.concat("title=\"adjust zoomed-in mouse movement acceleration (1-100: higher is faster)\">Zoomed Mouse Acceleration:");
webPage.concat("<input type=\"range\" id=\"acceleration\" min=\"1\" max=\"100\" value=\"");
webPage.concat(zmAcelleration);
webPage.concat("\" step=\"1\" onchange=\"setAcceleration(this, 'maz')\" oninput=\"updateSliderLiveTitle(this)\" title=\"");
webPage.concat(zmAcelleration);
webPage.concat("\" /></label>\n<div class=\"clear-tiny\"></div>\n");
webPage.concat("</div>\n");
webPage.concat("<div class=\"pref-group\">\n<div onclick=\"getIntervalTime()\" id=\"getInterval\"");
webPage.concat("class=\"pref-title\" title=\"click me to check the current snap interval time\">Snap Interval Time:</div>");
webPage.concat("<div class=\"clear-small\"></div>\n");
webPage.concat("<label class=\"pref-label\" title=\"set the snap interval time, in seconds\" >Seconds:");
webPage.concat("<select class=\"p-select\" onchange=\"setSnapInterval(this, this.value, 'second')\" ");
webPage.concat("id=\"set-snap-seconds\">\n<option value=\"\"></option>");
for (uint16_t si = 1; si < 60; si++) {
webPage.concat("<option value=\"" + (String)si + "\"");
if ( si == (delayTime / 1000) ) webPage.concat(" selected");
webPage.concat(">" + (String)si + "</option>");
}
webPage.concat("</select></label>\n<div class=\"clear-tiny\"></div>\n");
webPage.concat("<label class=\"pref-label\" title=\"set the snap interval time, in minutes\">Minutes:");
webPage.concat("<select class=\"p-select\" onchange=\"setSnapInterval(this, this.value, 'minute')\" ");
webPage.concat("id=\"set-snap-minutes\">\n<option value=\"\"></option>");
for (uint16_t si = 1; si < 60; si++) {
webPage.concat("<option value=\"" + (String)si + "\"");
if ( si == (delayTime / MINUTE_MILLIS) ) webPage.concat(" selected");
webPage.concat(">" + (String)si + "</option>");
}
webPage.concat("</select></label>\n<div class=\"clear-tiny\"></div>\n");
webPage.concat("<label class=\"pref-label\" title=\"set the snap interval time, in hours\">Hours:");
webPage.concat("<select class=\"p-select\" onchange=\"setSnapInterval(this, this.value, 'hour')\" ");
webPage.concat("id=\"set-snap-hours\">\n<option value=\"\"></option>");
for (uint16_t si = 1; si < 24; si++) {
webPage.concat("<option value=\"" + (String)si + "\"");
if ( si == (delayTime / HOUR_MILLIS) ) webPage.concat(" selected");
webPage.concat(">" + (String)si + "</option>");
}
webPage.concat("</select></label>\n<div class=\"clear-tiny\"></div>\n");
webPage.concat("<label class=\"pref-label\" title=\"set the snap interval time, in days\">Days:");
webPage.concat("<select class=\"p-select\" onchange=\"setSnapInterval(this, this.value, 'day')\" ");
webPage.concat("id=\"set-snap-days\">\n<option value=\"\"></option>");
for (uint16_t si = 1; si <= 365; si++) {
webPage.concat("<option value=\"" + (String)si + "\"");
if ( si == (delayTime / DAY_MILLIS) ) webPage.concat(" selected");
webPage.concat(">" + (String)si + "</option>");
}
webPage.concat("</select></label>\n<div class=\"clear-small\"></div>\n");
webPage.concat("<label class=\"pref-label\" ");
webPage.concat("title=\"web 'snap' button resets the main capture timer - or not\n");
webPage.concat("title=\"this setting is saved to NVS\">Web Snap Resets Timer:");
webPage.concat("<input type=\"checkbox\" onchange=\"boolSetting(this, 'wsr')\"");
if (webSnapResetsTimer) webPage.concat(" checked");
webPage.concat(" /></label>\n<div class=\"clear-tiny\"></div>\n");
webPage.concat("</div>\n");
webPage.concat("<div class=\"pref-group\">\n<div class=\"pref-title\">Extras:</div>");
webPage.concat("<div class=\"clear-small\"></div>\n");
webPage.concat("<button class=\"c-button t-button\" title=\"eject the SD card\"");
webPage.concat(" onclick=\"doSDFunctions('eject')\">Eject SD</button>");
webPage.concat("<button class=\"c-button t-button\" title=\"restart the SD card after being ejected\"");
webPage.concat(" onclick=\"doSDFunctions('insert')\">Start SD</button>");
webPage.concat("<button class=\"c-button t-button\" title=\"get SD card space info\"");
webPage.concat(" onclick=\"doGetInfo()\">SD Info</button>");
webPage.concat("<button class=\"c-button t-button\" title=\"ESP32 memory information\"");
webPage.concat(" onclick=\"doGetMem()\">Memory</button>");
webPage.concat("<button class=\"c-button t-button\" title=\"backup your current camera sensor configuration to NVS\"");
webPage.concat(" id=\"backup-butt\" onclick=\"doNVSFunctions('backup')\">Backup</button>");
webPage.concat("<button class=\"c-button t-button\" title=\"restore the backup camera sensor configuration from NVS\"");
webPage.concat(" id=\"restore-butt\" onclick=\"doNVSFunctions('restore')\">Restore</button>");
webPage.concat("<button class=\"c-button t-button\" id=\"DLButt\" title=\"Download recorded video stream (if available)\"");
webPage.concat("\"><a download=\"" + streamFileName + "\" href=\"download\">DL Video</a></button>");
webPage.concat("\n<div class=\"clear-tiny\"></div>\n");
webPage.concat("</div>\n");
webPage.concat("<div class=\"pref-group\">\n");
webPage.concat("<div class=\"pref-title\">File Upload:</div>");
webPage.concat("<div class=\"clear-small\"></div>\n");
webPage.concat("<label for=\"fileUpload\" id=\"fileUploadLabel\">");
webPage.concat("<i class=\"upload\"></i>Upload a File</label>");
webPage.concat("<input id=\"fileUpload\" type=\"file\"/>");
webPage.concat("</div>\n");
webPage.concat("<div class=\"clear-med\"></div>\n");
webPage.concat("</div>\n");
preferences" overlay
webPage.concat("</div>\n");
webPage.concat("</form>\n");
webPage.concat("<div id=\"help\"><div id=\"help-box\">");
webPage.concat("<h2>TL-CAM Web Help</h2>");
webPage.concat("<h3>Main HotKeys</h3>");
webPage.concat(" [ Previous Page\n");
webPage.concat(" ] Next Page\n");
webPage.concat(" , Preferences Panel (comma)\n");
webPage.concat(" T Toggle Thumbnails / List View\n");
webPage.concat(" P Toggle Pop-Up Preview\n");
webPage.concat(" SPACE Open and Play SlideShow\n");
webPage.concat(" L Toggle Live Stream View\n");
webPage.concat(" ' Record Live Stream Start / Stop (apostrophe)\n");
webPage.concat(" R Rotate Live Stream\n");
webPage.concat(" . Capture image immediately (full-stop, aka. 'point')\n");
webPage.concat("\n");
webPage.concat("<h3>SlideShow HotKeys</h3>");
webPage.concat(" UP / LEFT Arrows Previous Slide\n");
webPage.concat(" DOWN / RIGHT Arrows Next Slide (up and down can be reversed in your prefs)\n");
webPage.concat(" S Save current image to browser device\n");
webPage.concat(" SPACE Start / Stop playing SlideShow\n");
webPage.concat(" Enter Exit SlideShow\n");
webPage.concat(" F Toggle Fast As F**k Mode\n");
webPage.concat(" UP / DOWN Arrows During Fast As F**k Mode, increase/decrease delay (by 25ms)\n");
webPage.concat("</div></div>\n");
if (doPoP && !thumbView) webPage.concat("<div id=\"preloaders\" alt=\"SlideShow Preloaded Images\" /></div>\n");
webPage.concat("\n<script>\n");
webPage.concat("const thumbView = " + (String)( (thumbView) ? "true" : "false" ) + ";\n");
webPage.concat("var doPoP = " + (String)((doPoP) ? "true" : "false" ) + ";\n");
webPage.concat("var loopPoP = " + (String)((loopPoP) ? "true" : "false" ) + ";\n");
webPage.concat("var stickyPoP = " + (String)((stickyPoP) ? "true" : "false" ) + ";\n");
webPage.concat("var streamANDRecord = " + (String)((streamANDRecord) ? "true" : "false" ) + ";\n");
webPage.concat("var zmAcelleration = " + (String)zmAcelleration + ";\n");
webPage.concat("var seenTWarn = " + (String)((seenTWarn) ? "true" : "false" ) + ";\n");
webPage.concat("var seenPWarn = " + (String)((seenPWarn) ? "true" : "false" ) + ";\n");
webPage.concat("const upDownRev = " + (String)((upDownRev) ? "true" : "false" ) + ";\n");
webPage.concat("const bgColor = \"" + (String)bgColor + "\";\n");
webPage.concat("const ImgRange = \"" + imgTitleName + " \" + " + (String)(displayFrom) );
webPage.concat(" + \"-\" + " + (String)(displayFrom+imagesPerPage-1) + ";\n");
webPage.concat("var loadingIcon = " + (String)( (loadingIcon) ? "true" : "false" ) + ";\n");
webPage.concat("const loadingMsg = \"" + (String)(loadingMsg) + "\";\n");
webPage.concat("var slideTime = " + (String)slideTime + ";\n");
webPage.concat("const imgTallyName = \"" + imgTallyName + "\";\n");
webPage.concat("const webMessageTime = " + (String)webMessageTime + ";\n");
webPage.concat("const openWithPrefs = " + (String)( (openWithPrefs) ? "true" : "false" ) + ";\n");
webPage.concat("const openWithSlides = " + (String)( (openWithSlides) ? "true" : "false" ) + ";\n");
webPage.concat("const openWithHelp = " + (String)( (openWithHelp) ? "true" : "false" ) + ";\n");
webPage.concat("const autoPlaying = " + (String)( (autoPlaying) ? "true" : "false" ) + ";\n");
webPage.concat("const amStreaming = " + (String)( (amStreaming) ? "true" : "false") + ";\n");
webPage.concat("var fitMode = " + (String)( (fitMode) ? "true" : "false" ) + ";\n");
webPage.concat("var nightTime = " + (String)( (nightTime) ? "true" : "false" ) + ";\n");
webPage.concat("var floating = " + (String)( (floating) ? "true" : "false") + ";\n");
webPage.concat("var floatX = " + (String)( (floatX != "") ? "\"" + floatX + "\"" : "undefined" ) + ";\n");
webPage.concat("var floatY = " + (String)( (floatY != "") ? "\"" + floatY + "\"" : "undefined" ) + ";\n");
preferences"), but ffs! That is a world of future mind-
fuckery-debugging-hell waiting to happen and; for some reason; was implemented to support stupid
non-standard browser behaviour that careless developers thought might be a way to what? "simplify"
their code? No. I won't go there. And if /you/ do, God help you and those who come (to your code)
after you.
There are even online "tutorials" using that method; maybe they fancy they can push some kind of
standard. Fuck off. You have no clue. Remove this stupid shit from the spec and let apps fail,
then get fixed and coded properly! Sheesh!
It's like literally meaning figuratively, literally! (exclamation mark donating "humour")
While I'm here, will so-called "experts" stop putting numbers in quotes! THEY ARE NOT STRINGS!
If you would care to test on more than your one favourite browser, you would realise that your
code is total bollocks.
Google Chrome does not have a 100% share of the market. Yet*.
* While I'm no fan of Google, Chrome is top-notch and all my tests tell me it's the best browser
available at this time, for many reasons. Chromium is probably even better, but I no longer use
it since Google removed the ability to use Sync. ****s!
*/
webPage.concat(R"HTML5(
var theImages;
var messageShowing = false;
const pulseTime = 150;
var msgTimer, prefsMsgTimer, thumbHeight, prefsOpen, helpOpen;
const prefsPane = document.getElementById("preferences");
const helpPane = document.getElementById("help");
const prefsMessage = document.getElementById("prefs-message");
const msgTitle = document.getElementById("message");
const streamHolder = document.getElementById("livestream");
var streamOpen = false, rotationAngle = 0, tinyStream = false, recordingStream = false;
var pollCount = 0;
var touchStarted = false, touchToggleAutoPlay = false;
var PoPLoaded = false;
var fAF = false, fAFStore = slideTime, fAFTime = 0.2;
const centerTrans = "translate(-50%, -50%)"
var uploadInput;
var slideShow, slideShowTimer, slideMsgTimer, slide, slideName, imgSaveLink, sBaseName;
var prevColor, prevBGColor, thisSlide = 0, slideShowOpen = false, slideShowPlaying = false, slidesRev = false;
document.title += " " + ImgRange;
var pointerX, pointerY;
var startT = Date.now();
var buttsHidden = false;
var buttHideTime = 2000;
document.onpointermove = (event) => {
pointerX = event.x;
pointerY = event.y;
if (event.pointerType !== "touch") {
startT = Date.now();
if (slideShowOpen && buttsHidden) showSlideButtons();
}
}
)HTML5");
if (doPoP) {
webPage.concat("var fileEXT = \"" + (String)fileEXT + "\";");
webPage.concat(R"HTML5(
PoPLoaded = true;
var iLinks, bottomOfThumbs, rightOfFileList, xPOS, yPOS, donePoPMSG = false, poPShowing = false;
const PoPIMG = document.getElementById('PoP');
const getScrollbarWidth = () => window.innerWidth - document.documentElement.clientWidth;
function showPoP(popImage, position=true, touch=false, forward=true) {
if (!doPoP) return;
if (position) {
if (thumbView && (thumbHeight > (window.innerHeight / 2) )) {
if (!donePoPMSG) {
setMessage("Thumbnails are bigger than preview!");
donePoPMSG = true;
}
return;
}
sizeUpdate();
}
PoPIMG.src = popImage;
if (position) {
PoPIMG.style.transform = "none"
var overRideX = false, overRideY = false;
PoPIMG.style.height = "calc(50vh - 1em)";
PoPIMG.style.width = "auto";
xPOS = 0;
if ( (window.innerWidth - getScrollbarWidth()) > (PoPIMG.clientWidth * 2) ) {
xPOS = (window.innerWidth / 2) - (getScrollbarWidth() / 2);
}
yPOS = (window.innerHeight / 2);
)HTML5");
Gallery Mode". AI, baby!
Similarly, in list view, we measure the X coordinate of the right-hand side of the list, and if
there's enough available space to the right, we fix the PoP there, as-big-as-can-be. I'm always
thinking.. "If I were me, what I do?". Then I code it.
The methodology is thus: Directly set one CSS property (width or height), using all the CSS
trickery we can muster (stuff like calc(), and so on) then set the /other/ property to "auto",
and let the /browser/ work it out. As SOON as this is done (immediately) we can query the DOM
for information about the /other/ value, and use it right away in our JavaScript calculations.
This mix-and-match approach works well for MCU web serving; dropping C variables right into the
markup as JavaScript variables and using them, along with dynamic CSS and JavaScript processing
of user actions (viewport size, pointer position, etc.) to produce the final calculated result.
It's like this: JavaScript knows nothing of "em". So, these layout calculations need to be done
in CSS which /understands/ "em", and other layout-specific things, being ITS JOB. The CSS calc()
function (which wasn't around when I first created my PHP css-init, which does this sort of
thing and much more) enables us to effortlessly drop JavaScript and CSS into the very same
layout calculation; all three layers working seamlessly to produce our desired result.
Sure, CSS and JavaScript; which these days is near idiot-proof; don't always /precisely/ agree
on what these dynamic values are (in which case, we defer to CSS*), it's close enough for what
we are doing here, and the end results are exactly what I was looking for. Boom!
* In other words, when we are inserting a CSS value in JavaScript, we use CSS's "100vh" over
JavaScript's window.innerHeight, every time. Where CSS fails us (i.e. scrollbar widths) or it's
simply not possible (Syat!**), we use JavaScript to calculate the value. So it goes.
** As I was taught: To the best of my knowledge, at this time.
*/
webPage.concat(R"HTML5(
var narrowPoP = "calc( 100vw - " + getScrollbarWidth() + "px - 1em )";
if (thumbView) {
if (bottomOfThumbs < (window.innerHeight / 2) ) {
PoPIMG.style.height = "calc( 100vh - " + bottomOfThumbs + "px - 1em )";
PoPIMG.style.width = "auto";
if (PoPIMG.clientWidth > window.innerWidth) {
PoPIMG.style.width = narrowPoP;
PoPIMG.style.height = "auto";
}
yPOS = bottomOfThumbs;
PoPIMG.style.transform = "translateX(-50%)";
PoPIMG.style.left = "50%";
overRideX = true;
} else {
if (PoPIMG.clientWidth > window.innerWidth) {
PoPIMG.style.width = narrowPoP;
PoPIMG.style.height = "auto";
}
if (PoPIMG.clientHeight > (window.innerHeight / 2) ) {
PoPIMG.style.height = "calc(50vh - 1em)";
PoPIMG.style.width = "auto";
}
if (pointerX > (window.innerWidth / 2) ) { xPOS = 0; }
if (pointerY > (window.innerHeight / 2) ) { yPOS = 0; }
}
} else {
let availableWidth = window.innerWidth - rightOfFileList - getScrollbarWidth();
if (availableWidth > parseInt(getComputedStyle(PoPIMG).getPropertyValue("width")) ) {
if (PoPIMG.naturalHeight > PoPIMG.naturalWidth) {
PoPIMG.style.height = "calc( " + window.innerHeight + "px - 1em )";
PoPIMG.style.width = "auto";
let middleOfSpace = rightOfFileList + (availableWidth / 2);
PoPIMG.style.left = "calc( (" + middleOfSpace + "px - ( "
+ getComputedStyle(PoPIMG).getPropertyValue("width") + " / 2) ))";
} else {
PoPIMG.style.width = "calc(" + availableWidth + "px - 2em)";
PoPIMG.style.height = "auto";
PoPIMG.style.left = "calc(" + rightOfFileList + "px + 0.5em)";
}
PoPIMG.style.top = "50%";
PoPIMG.style.transform = "translateY(-50%)";
overRideX = true;
overRideY = true;
} else {
if (PoPIMG.clientWidth > window.innerWidth) {
PoPIMG.style.width = narrowPoP;
PoPIMG.style.height = "auto";
PoPIMG.style.left = "50%";
PoPIMG.style.transform = "translateX(-50%)";
overRideX = true;
}
if (pointerY > (window.innerHeight / 2) ) { yPOS = 0; }
}
}
if (!overRideX) PoPIMG.style.left = (xPOS == 0) ? "0.5em" : xPOS + "px";
if (!overRideY) PoPIMG.style.top = (yPOS == 0) ? "0.5em" : yPOS + "px";
}
clearHighlights();
let poPMatch = returnPoPMatch();
iLinks[poPMatch].classList.add("pop-highlight");
let myDimensions = iLinks[poPMatch].getBoundingClientRect();
if ( ( myDimensions.bottom > window.innerHeight ) || ( myDimensions.top < 0 ) ) {
iLinks[poPMatch].scrollIntoView({ behavior: "instant", block: "center", inline: "nearest" });
}
if (touch) {
let pL = PoPIMG.style.left;
if (!forward) {
PoPIMG.animate( [ { left: "0"}, { left: pL } ], { duration: 100, iterations: 1 } );
} else {
PoPIMG.animate( [ { left: "100vw"}, { left: pL } ], { duration: 100, iterations: 1 } );
}
}
PoPIMG.style.opacity = "1";
PoPIMG.style.visibility = "visible";
poPShowing = true;
}
function pulseLink(index) {
let flashCount = 1;
let endersGame = setInterval( function() {
if (flashCount++ === 2) clearInterval(endersGame);
iLinks[index].classList.remove("pop-highlight");
let notFlashy = setTimeout( function() {
iLinks[index].classList.add("pop-highlight");
}, 125);
}, 225);
}
function hidePoP(vanish=false) {
if (stickyPoP && !vanish) return;
PoPIMG.style.visibility = "hidden";
PoPIMG.style.opacity = "0";
clearHighlights();
poPShowing = false;
}
function clearHighlights() {
if (iLinks.length !== undefined) {
for (let i = 0; i < iLinks.length; i++) {
iLinks[i].classList.remove("pop-highlight");
}
}
}
function loadNextPoP(reverse=false, key=false) {
let poPMatch = returnPoPMatch(-1);
var nextPoP;
if (poPMatch == -1) nextPoP = 0;
if (poPShowing) {
if (!reverse) {
nextPoP = poPMatch+1;
if (loopPoP) {
if (nextPoP == iLinks.length) nextPoP = 0;
} else {
if ( nextPoP === theImages.length ) {
pulseLink(nextPoP-1);
return;
}
}
} else {
nextPoP = poPMatch-1;
if (loopPoP) {
if (nextPoP == -1) nextPoP = iLinks.length-1;
} else {
if ( nextPoP === -1 ) {
pulseLink(nextPoP+1);
return;
}
}
}
}
if (thumbView) {
showPoP(theImages[nextPoP].src.split("/").pop(), key);
} else {
showPoP(iLinks[nextPoP].href.split("/").pop(), key);
}
}
function deletePoPImage() {
let poPMatch = returnPoPMatch(false, true);
if (poPMatch) {
let goBack = false
if (returnPoPMatch() == iLinks.length-1) goBack = true;
deleteImage(poPMatch);
loadNextPoP(goBack);
}
}
function returnPoPMatch(falseValue=false, returnName=false, matchFirst=false) {
for (let i = 0; i < iLinks.length; i++) {
var poPMatch = falseValue;
if (thumbView) {
if (PoPIMG.src.indexOf(iLinks[i].getElementsByTagName("img")[0].src) !== -1) poPMatch = true;
} else {
if (iLinks[i].href == PoPIMG.src) poPMatch = true;
}
if (poPMatch !== falseValue || matchFirst) {
poPMatch = i;
if (thumbView) {
if (returnName) poPMatch = theImages[i].src.split("/").pop();
} else {
if (returnName) poPMatch = iLinks[i].href.split("/").pop();
}
break;
}
}
return poPMatch;
}
)HTML5");
}
webPage.concat(R"HTML5(
function takeSnap() {
let AJAX = new XMLHttpRequest();
AJAX.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
if (this.responseText.indexOf("OK") !== -1) {
setMessage("SNAP!");
} else {
setMessage("FAIL!<br>" + this.responseText);
}
}
};
AJAX.open("GET", "snap", true);
AJAX.send();
}
function openPrefs() {
if (prefsOpen) return;
prefsOpen = true;
if (!/prefs/i.test(window.location)) window.history.replaceState("object", "", makeGETParams() + "&prefs=true");
prefsPane.style.opacity = "1";
prefsPane.style.visibility = "visible";
if (!seenPWarn) {
let wMsg = "<span class='warning'>NOTE: <small>";
wMsg += "All settings take effect <strong>immediately</strong>.</small></span>";
setTLCAMSettings('spw', '-', true);
setMessage(wMsg, webMessageTime * 3);
seenPWarn = true;
}
}
function closePrefs() {
if (!prefsOpen) return;
clearMessage(webMessageTime);
prefsOpen = false;
window.history.replaceState("object", "", makeGETParams("prefs"));
prefsPane.style.visibility = "hidden";
prefsPane.style.opacity = "0";
}
function openHelp() {
if (helpOpen) return;
if (!/help/i.test(window.location)) window.history.replaceState("object", "", makeGETParams() + "&help=true");
helpPane.style.opacity = "1";
helpPane.style.visibility = "visible";
helpOpen = true;
}
function closeHelp() {
if (!helpOpen) return;
helpOpen = false;
window.history.replaceState("object", "", makeGETParams("help"));
helpPane.style.visibility = "hidden";
helpPane.style.opacity = "0";
}
function setSnapInterval(thisControl, newInterval, timeFrame) {
let AJAX = new XMLHttpRequest();
AJAX.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
if (this.responseText.indexOf("OK") !== -1) {
setMessage(this.responseText.substring(2));
let snapSelectors = thisControl.parentNode.parentNode.getElementsByTagName("select");
for ( let i = 0 ; i < snapSelectors.length ; i++ ) {
if (snapSelectors[i] !== thisControl) {
snapSelectors[i].getElementsByTagName("option")[0].selected = "selected";
}
}
} else {
setMessage("Failed!");
}
}
};
AJAX.open("GET", "interval?" + timeFrame + "s=" + newInterval, true);
AJAX.send();
}
function getIntervalTime() {
let AJAX = new XMLHttpRequest();
AJAX.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
if (this.responseText.indexOf("OK") !== -1) {
setMessage(this.responseText.substring(2));
} else {
setMessage("FAIL!\n" + this.responseText);
}
}
};
AJAX.open("GET", "getint", true);
AJAX.send();
}
function setTLCAMSettings(sendParam, newValue, silent=false) {
let AJAX = new XMLHttpRequest();
AJAX.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
if (this.responseText.indexOf("OK") !== -1) {
if (/reload/i.test(this.responseText)) {
setMessage(loadingMsg);
location.reload();
} else if (/reboot/i.test(this.responseText)) {
setMessage(this.responseText.substring(2), webMessageTime * 2, "settings");
rebootAndRefresh();
} else {
if (!silent && this.responseText.substring(2) !== "") setMessage(this.responseText.substring(2));
}
} else {
setMessage("Settings FAIL!\n" + this.responseText);
}
}
};
AJAX.open("GET", "settings?" + sendParam + "=" + newValue, true);
AJAX.send();
}
function boolSetting(myCheckBox, sendParam) {
setTLCAMSettings(sendParam, myCheckBox.checked ? "true" : "false");
}
function autoDMSetting(myCheckBox, sendParam) {
boolSetting(myCheckBox, sendParam);
let darkCheck = document.getElementById("darkmode");
darkCheck.disabled = (myCheckBox.checked) ? true : false;
setIntermediateCheckBox(darkCheck);
}
function setIntermediateCheckBox(element) {
element.indeterminate = element.disabled;
}
function setAcceleration(mySlider, rangeCode) {
zmAcelleration = mySlider.value;
setTLCAMSettings(rangeCode, zmAcelleration);
mySlider.title = mySlider.value;
}
function updateSliderLiveTitle(slider) {
setMessage(slider.value);
}
function doNVSFunctions(type) {
let AJAX = new XMLHttpRequest();
AJAX.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
setMessage(this.responseText.substring(2), webMessageTime * 1.5);
if (/reboot/i.test(this.responseText)) rebootAndRefresh();
}
};
AJAX.open("GET", type, true);
AJAX.send();
}
)HTML5");
OK".
webPage.concat(R"HTML5(
function rebootAndRefresh() {
let AJAX = new XMLHttpRequest();
if (pollCount > 22) {
setMessage("No response from TL-CAM. Giving up now.. \nIf you have no physical access, wait a minute and manually refresh this page.\n It may be an NTP server issue.");
pollCount = 0;
return;
}
if (pollCount == 11) setMessage("Still no response.\nA manual reboot may be required.");
if (pollCount == 4) setMessage("Querying TL-CAM for reboot status..");
AJAX.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
setTimeout ( function() {
setMessage("Reboot complete\nRefreshing the page...");
pollCount = 0;
location.reload();
}, 1000 );
}
};
AJAX.open("GET", "rebooted", true);
AJAX.timeout = 1000;
AJAX.ontimeout = () => { pollCount++; rebootAndRefresh(); };
AJAX.onerror = () => { setTimeout ( function() { pollCount++; rebootAndRefresh(); }, 972 ); };
AJAX.send();
}
function doSDFunctions(action) {
let AJAX = new XMLHttpRequest();
AJAX.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
if (this.responseText.indexOf("OK") !== -1) {
setMessage(this.responseText.substring(2));
} else {
setMessage("FAIL!\n" + this.responseText);
}
}
};
AJAX.open("GET", action, true);
AJAX.send();
}
function doGetInfo() {
let AJAX = new XMLHttpRequest();
AJAX.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
if (this.responseText.indexOf("OK") !== -1) {
setMessage(this.responseText.substring(2), webMessageTime * 3);
} else {
setMessage("FAIL!\n" + this.responseText);
}
}
};
AJAX.open("GET", "info", true);
AJAX.send();
}
function doGetMem() {
let AJAX = new XMLHttpRequest();
AJAX.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
if (this.responseText.indexOf("OK") !== -1) {
setMessage(this.responseText.substring(2), webMessageTime * 3);
} else {
setMessage("FAIL!\n" + this.responseText);
}
}
};
AJAX.open("GET", "memory", true);
AJAX.send();
}
function deleteImage(imageFile) {
let AJAX = new XMLHttpRequest();
AJAX.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
if (this.responseText.indexOf("OK") !== -1) {
let myMSG = this.responseText.substring(2);
if (slideShowOpen) myMSG = myMSG.replace("deleted", "<br>\n<strong class='warning'>deleted</strong>");
document.getElementById(imageFile).remove();
if (!thumbView && doPoP) document.getElementById("pl-" + imageFile).remove();
if (slideShowOpen) {
pulseRED();
resetSlideTimer();
showSlide();
setMessage(myMSG);
} else {
setMessage(myMSG);
if (doPoP && !stickyPoP) hidePoP(true);
}
} else {
setMessage("Delete Failed!\n" + this.responseText);
}
}
};
AJAX.open("GET", "delete?file=" + imageFile, true);
AJAX.send();
}
)HTML5");
s" to save the image
to your device. Hit "Delete" to delete an image. Yes, even when the slideshow is /playing/. The
deleted image will display for a moment, then the next image will load.
Spacebar to toggle auto-play, in whatever direction you were previously travelling. This is a
great way to review time-lapse images. NOTE: The slideshow always starts up in regular forward
motion. To switch the direction of the slideshow, simply move in that direction; i.e. click [<][>]
(previous/next image) or hit the left/down
Hit SPACEBAR from the main page to go directly to an auto-play slideshow. Add Ctrl to go full-
screen. SHIFT-Click any thumb/link to open slideshow mode /at/ that image. Add Ctrl key for full-
screen, as usual. No one uses Shift-Click to open an image in a new window, so we are using it.
If you want to change the slideshow time, hit "," (comma) to bring up the prefs panel.
Changes take effect immediately (or rather, at the end of your current slide).
Click anywhere outside the image (or hit <enter>) to exit the slideshow.
*/
webPage.concat(R"HTML5(
function toggleAutoPlay(notify=true) {
if (!slideShowPlaying) {
slideShowPlaying = true;
setMessage("SlideShow Playing");
autoPlay(true, true);
if (notify) pulseGREEN();
} else {
slideShowPlaying = false;
clearTimeout(slideShowTimer);
setMessage("SlideShow Stopped");
if (notify) pulseYELLOW();
}
}
function autoPlay(next = false, delayStart = false) {
if (!slideShowPlaying) return;
if (!/play/i.test(window.location)) window.history.replaceState("object", "", makeGETParams() + "&play=true");
if (!delayStart) {
if (next) {
if (!slidesRev) {
nextSlide();
} else {
previousSlide();
}
} else {
showSlide();
}
} else clearTimeout(slideShowTimer);
slideShowTimer = setTimeout(autoPlay, slideTime * 1000, true);
}
function toggleFastAFMode(report = false) {
fAF = fAF ? false : true;
if (fAF && slideShowOpen && !slideShowPlaying) toggleAutoPlay();
if (fAF) {
if (report) setMessage("fAF Mode Enabled");
fAFStore = slideTime;
slideTime = fAFTime;
} else {
if (report) setMessage("fAF Mode Disabled");
slideTime = fAFStore;
}
}
function resetSlideTimer() {
clearTimeout(slideShowTimer);
autoPlay(false);
}
function openSlideShowAt(thisIMG) {
if (event.shiftKey == true) {
event.preventDefault();
for ( let i = 0 ; i < theImages.length ; i++ ) {
if (theImages[i].src.split("/").pop() == thisIMG) {
openSlideShow(i);
return;
}
}
}
}
function openSlideShow(currentSlide = 0) {
slideShowOpen = true;
slidesRev = false;
if (fAF) {
slideTime = fAFStore;
fAF = false;
}
if (event.ctrlKey == true) {
if (thumbView) {
document.getElementById("thumbnails").style.display = "none";
} else {
document.getElementById("listing").style.display = "none";
}
slide.style.border = "0";
slide.style.height = "100vh";
document.documentElement.requestFullscreen();
}
slideShow.style.opacity = "1";
slideShow.style.visibility = "visible";
thisSlide = currentSlide;
if (!/slideshow/i.test(window.location)) window.history.replaceState("object", "", makeGETParams() + "&slideshow=true");
showSlideButtons();
showSlide();
}
function closeSlideShow(touch=false) {
clearTimeout(slideShowTimer);
if (slideShowPlaying) {
window.history.replaceState("object", "", makeGETParams("play"));
slideShowPlaying = false;
}
slideShowOpen = false;
window.history.replaceState("object", "", makeGETParams("slideshow"));
clearMessage();
if (thumbView) {
document.getElementById("thumbnails").style.display = "block";
} else {
document.getElementById("listing").style.display = "block";
}
if (document.fullscreenElement) {
slide.style.border = "0.5em solid var(--bg-color)";
slide.style.height = "calc(100vh - 1em)";
document.exitFullscreen();
}
let timeOut = 0;
if (touch) {
timeOut = 200;
slideShow.style.transition = "opacity 200ms ease-in 0s";
slide.animate(
[ {transform: "translate(-50%, -50%)" }, { transform: "translate(-50%, -100%)" } ],
{ duration: 200, iterations: 1}
);
}
slideShow.style.opacity = "0";
let slideExit = setTimeout( function() {
slideShow.style.visibility = "hidden";
slideShow.style.transition = "none";
slideShow.style.transform = "none";
if (fAF) toggleFastAFMode();
}, timeOut);
}
function userPreviousSlide(touch=false) {
slidesRev = true;
resetSlideTimer();
previousSlide(touch);
}
function userNextSlide(touch=false) {
slidesRev = false;
resetSlideTimer();
nextSlide(touch);
}
function previousSlide(touch=false) {
thisSlide--;
if (thisSlide < 0) {
thisSlide = theImages.length-1;
setMessage("wrap");
if (!fAF) pulseLIGHTER();
}
showSlide();
if (touch) {
slide.animate(
[ {transform: "translate(-100%, -50%)"}, { transform: centerTrans } ],
{ duration: 200, iterations: 1}
);
}
}
function nextSlide(touch=false) {
thisSlide++;
if (thisSlide == theImages.length) {
thisSlide = 0;
setMessage("wrap");
if (!fAF) pulseLIGHTER();
}
showSlide();
if (touch) {
slide.animate(
[ {transform: "translate(100%, -50%)"}, { transform: centerTrans } ],
{ duration: 200, iterations: 1}
);
}
}
function showSlide() {
slide.style.top = "50%";
if (!theImages[thisSlide]) thisSlide--;
if (theImages.length !== 0) {
if (!theImages[thisSlide].src) thisSlide = 0;
slide.src = theImages[thisSlide].src;
sBaseName = theImages[thisSlide].src.split("/").pop();
imgSaveLink.href = sBaseName;
imgSaveLink.download = sBaseName;
slideName.innerHTML = sBaseName.split(".").slice(0, -1).join(".");
} else {
closeSlideShow();
}
if ( !buttsHidden && (Date.now() - startT) > buttHideTime) {
hideSlideButtons();
}
}
function showSlideButtons() {
let prev = document.getElementById("previousSlide")
let next = document.getElementById("nextSlide")
prev.style.transition = "all 150ms ease-out 0s";
next.style.transition = "all 150ms ease-out 0s";
prev.style.opacity = "1";
next.style.opacity = "1";
buttsHidden = false;
}
function hideSlideButtons() {
let prev = document.getElementById("previousSlide")
let next = document.getElementById("nextSlide")
prev.style.transition = "all 667ms ease-in 0s";
next.style.transition = "all 667ms ease-in 0s";
prev.style.opacity = "0";
next.style.opacity = "0";
buttsHidden = true;
}
function pulseLIGHTER() {
doPulse(", "rgba(40,94,45,.80)"); // #285e2d;
}
function pulseRED() {
doPulse(", "rgba(255,22,14,.90)"); // Same as text
}
function pulseGREEN() {
doPulse(", "rgba(112,255,51,.90)"); // ditto
}
function pulseYELLOW() {
doPulse(", "rgba(255,204,0,.90)"); // If I use a double quote here to ditto the compiler craps-out. WTF!
}
function doPulse(textColor, bgColor) {
slideName.style.color = textColor;
slideShow.style.background = bgColor;
clearPulse();
}
function clearPulse() {
setTimeout( function() {
slideName.style.color = prevColor;
slideShow.style.background = prevBGColor;
}, pulseTime);
}
function openLiveStream() {
let dragging = false, PointerDown = false, dragStarted = false;
let pointerStartX = 0, pointerStartY = 0;
const holderFitStyles = `
background-color: rgba(9,21,10,.66);
position : fixed;
height: 100vh;
width: 100vw;
top: 0;
right: 0;
left: 0;
bottom: 0;
`;
if (!/live/i.test(window.location)) window.history.replaceState("object", "", makeGETParams() + "&live=true");
const frame = document.createElement("div");
const iframe = document.createElement("iframe");
let metaTags = document.createElement("meta");
metaTags.name = "viewport";
metaTags.content = "initial-scale=1";
frame.style.zIndex = "800";
frame.id = "frame";
frame.style.position = "fixed";
frame.style.left = "50%";
frame.style.top = "50%";
frame.style.transform = centerTrans;
iframe.id = "iframe";
iframe.style.width = "100%";
iframe.style.height = "100%";
iframe.style.border = "0.5em solid " + bgColor;
iframe.style.borderRadius = "1em";
streamHolder.appendChild(frame);
frame.appendChild(iframe);
iframe.contentDocument.getElementsByTagName("head")[0].appendChild(metaTags);
const stream = document.createElement("img");
var frameGetFitWidth = () => "calc( ( 100vh * " + (stream.naturalWidth / stream.naturalHeight) + " ) - 1em)";
const frameFillHeight = "calc( 100vh - 1em )";
)HTML5");
webPage.concat(R"HTML5(
var mainLT = setTimeout (function() {
iframe.contentDocument.getElementsByTagName("body")[0].appendChild(stream);
stream.title = "click to toggle auto-fit";
stream.style.touchAction = "none";
stream.id = "stream";
stream.src = "/mjpeg";
let reTries = 0;
stream.onerror = function() {
console.warn("Stream Failure. Retrying..");
if (reTries > 3) {
closeLiveStream();
setMessage("Failure with live stream.\n<small>(perhaps it is already running)</small>");
return;
}
stream.src = null;
stream.src = "/mjpeg";
reTries++;
}
var poll = setInterval(function () {
if (stream.naturalWidth) {
clearInterval(poll);
stream.style.position = "absolute";
stream.style.borderRadius = "0.25em";
stream.style.border = "0";
frame.style.height = frameFillHeight;
frame.style.width = frameGetFitWidth();
stream.style.width = "100%";
stream.style.left = "50%";
stream.style.top = "50%";
stream.style.transform = centerTrans;
var mX, mY, fitFWidth, fitFHeight;
stream.onclick = function (event) {
if (dragging) {
dragging = false;
return;
}
fitFWidth = frame.clientWidth;
fitFHeight = frame.clientHeight;
mX = event.clientX;
mY = event.clientY;
fitMode ? Full() : Fit();
};
fitMode ? Fit() : Full();
streamHolder.style.opacity = "1";
streamHolder.style.visibility = "visible";
streamOpen = true;
function Fit() {
frame.style.left = "50%";
frame.style.top = "50%";
frame.style.height = frameFillHeight;
frame.style.width = frameGetFitWidth();
stream.style.width = "100%";
stream.style.height = "100%";
stream.style.left = "50%";
stream.style.top = "50%";
stream.style.transform = centerTrans;
streamHolder.style.height = frameFillHeight;
streamHolder.style.width = frameGetFitWidth();
streamHolder.style.cssText +=";"+ holderFitStyles;
fitMode = true;
floating = false;
window.history.replaceState("object", "", makeGETParams("fit") + "&fit=true");
}
function Full() {
frame.style.height = frameFillHeight;
frame.style.width = "calc( 100vw - 1em )";
stream.style.width = "auto";
stream.style.top = "auto";
stream.style.left = "auto";
stream.style.transform = "none";
stream.style.height = "auto";
tinyStream = false;
if (stream.width < window.innerWidth || stream.height < window.innerHeight) {
stream.style.left = "50%";
stream.style.top = "50%";
stream.style.transform = centerTrans;
if (stream.width < window.innerWidth) frame.style.width = stream.width + "px";
if (stream.height < window.innerHeight) frame.style.height = stream.height + "px";
if (stream.width < window.innerWidth && stream.height < window.innerHeight) tinyStream = true;
} else {
var posX = ( ( stream.width / ( fitFWidth / mX ) ) - ( window.innerWidth / 2 ) );
var posY = ( ( stream.height / ( fitFHeight / mY ) ) - ( window.innerHeight / 2 ) );
let iFBody = iframe.contentDocument.getElementsByTagName("body");
iFBody[0].scrollLeft = posX;
iFBody[0].scrollTop = posY;
}
if (tinyStream && floatX !== undefined) {
frame.style.left = floatX;
frame.style.top = floatY;
hideFrameHolder();
floating = true;
}
if (floating) {
window.history.replaceState("object", "", makeGETParams("floatingfloatXfloatY")
+ "&floating=true" + "&floatX=" + frame.style.left + "&floatY=" + frame.style.top);
}
fitMode = false;
window.history.replaceState("object", "", makeGETParams("fit") + "&fit=false");
}
}
}, 10);
)HTML5");
webPage.concat(R"HTML5(
stream.addEventListener("pointerdown", function() {
if (fitMode) return true;
event.preventDefault();
pointerStartX = event.clientX;
pointerStartY = event.clientY;
PointerDown = true;
dragStarted = false;
}, true);
stream.addEventListener("pointerup", function() { endDrag(); }, true );
stream.addEventListener("pointerout", function() { if (PointerDown) endDrag(); }, true);
stream.addEventListener("pointermove", function() {
if (!PointerDown) return;
event.preventDefault();
if (!dragStarted) {
dragging = false;
if ( (event.clientX > (pointerStartX + 7) || event.clientX < (pointerStartX - 7) ) ||
(event.clientY > (pointerStartY + 7) || event.clientY < (pointerStartY - 7) ) ) {
dragStarted = true;
dragging = true;
if (tinyStream) hideFrameHolder();
}
if (!dragging) return;
}
)HTML5");
webPage.concat(R"HTML5(
if (tinyStream) {
rotationAngle = parseInt(frame.dataset.rotation || "0");
)HTML5");
webPage.concat(R"HTML5(
if (rotationAngle != 0) {
let deltaX = event.clientX - pointerStartX;
let deltaY = event.clientY - pointerStartY;
let angleAsRads = (-rotationAngle * Math.PI) / 180;
let rotatedPointerX = deltaX * Math.cos(angleAsRads) + deltaY * Math.sin(angleAsRads);
let rotatedPointerY = -deltaX * Math.sin(angleAsRads) + deltaY * Math.cos(angleAsRads);
frame.style.left = "calc( " + getComputedStyle(frame).getPropertyValue("left") + " + " + rotatedPointerX + "px )";
frame.style.top = "calc( " + getComputedStyle(frame).getPropertyValue("top") + " + " + rotatedPointerY + "px )";
} else {
frame.style.left = "calc( " +getComputedStyle(frame).getPropertyValue("left")+" - "+(pointerStartX - event.clientX)+ "px )";
frame.style.top = "calc( " +getComputedStyle(frame).getPropertyValue("top")+" - "+(pointerStartY - event.clientY)+ "px )";
}
floating = true;
} else {
window.history.replaceState("object", "", makeGETParams("floatingfloatXfloatY"));
let nowX = (pointerStartX - event.clientX) / ( 200 / ( zmAcelleration != 0 ? zmAcelleration : 1 ) );
let nowY = (pointerStartY - event.clientY) / ( 100 / ( zmAcelleration != 0 ? zmAcelleration : 1 ) );
let iFBody = iframe.contentDocument.getElementsByTagName("body");
iFBody[0].scrollLeft += nowX;
iFBody[0].scrollTop += nowY;
}
}, true );
function endDrag() {
if (tinyStream && PointerDown) {
floatX = getComputedStyle(frame).getPropertyValue("left");
floatY = getComputedStyle(frame).getPropertyValue("top");
window.history.replaceState("object", "", makeGETParams("floatingfloatXfloatY")
+ "&floating=true" + "&floatX=" + floatX + "&floatY=" + floatY);
if (!seenTWarn) {
let wMsg = "<span class='warning small'>NOTE: <small>";
wMsg += "Some HotKeys will not work until you tap/click outside the stream</small></span>";
setTLCAMSettings('stw', '-', true);
setMessage(wMsg, webMessageTime * 3, "endDrag");
seenTWarn = true;
}
}
PointerDown = false;
}
function hideFrameHolder() {
streamHolder.style.left = frame.style.left;
streamHolder.style.top = frame.style.top;
streamHolder.style.height = "0";
streamHolder.style.width = "0";
}
iframe.contentDocument.body.addEventListener("keydown", (event) => {
switch (event.keyCode) {
case 190:
takeSnap();
break;
case 82:
rotateStream();
break;
case 76:
closeLiveStream();
break;
case 192:
toggleRecording();
break;
case 188:
prefsOpen ? closePrefs() : openPrefs();
break;
case 32:
if (slideShowOpen) {
toggleAutoPlay();
} else {
openSlideShow();
slideShowPlaying = true;
autoPlay(false);
}
break;
case 13:
event.preventDefault();
closeSlideShow();
break;
}
}, true);
}, 100);
}
)HTML5");
stream" is a simple "multipart/x-mixed-replace" type document; basically an HTTP header we
send the browser, telling it to keep the connexion open for more data, and then we just keep
sending jpeg data until the client connexion closes. Chunkie!
*/
webPage.concat(R"HTML5(
function closeLiveStream() {
document.getElementById("frame").remove();
window.history.replaceState("object", "", makeGETParams("live"));
streamHolder.style.opacity = "0";
streamHolder.style.visibility = "hidden";
streamOpen = false;
if (recordingStream) startRecordingNotify();
}
function rotateStream() {
let frame = document.getElementById("frame");
let iframe = document.getElementById("iframe");
let stream = iframe.contentDocument.getElementById("stream");
if (fitMode && frame.dataset.natural != stream.naturalWidth) {
frame.dataset.natural = stream.naturalWidth;
frame.dataset.width = frame.style.width;
frame.dataset.height = frame.style.height;
}
let currentRotation = parseInt(frame.dataset.rotation || "0");
let newRotation = (currentRotation + 90) % 360;
let fitWidth = "calc( ( 100vh * " + ( stream.naturalWidth / stream.naturalHeight ) + " ) - 0.25em )";
let fitHeight = "calc( 100vh - 0.5em )";
let maxWidth = "calc(100vw - 1em)"
let maxHeight = "calc(100vh - 1em)"
if ("width" in frame.dataset) fitWidth = frame.dataset.width;
if ("height" in frame.dataset) fitHeight = frame.dataset.height;
if (fitMode) {
if (newRotation == 90 || newRotation == 270) {
frame.style.width = "calc( 100vh - 0.25em )";
frame.style.height = "calc( ( 100vh * " + ( stream.naturalHeight / stream.naturalWidth ) + " ) - 0.25em )";
} else {
frame.style.width = fitWidth;
frame.style.height = fitHeight;
}
} else if (!tinyStream) {
if (newRotation == 90 || newRotation == 270) {
frame.style.width = maxHeight;
frame.style.height = maxWidth;
} else {
frame.style.width = maxWidth;
frame.style.height = maxHeight;
}
}
frame.style.transform = centerTrans + " rotate(" + newRotation + "deg)";
)HTML5");
webPage.concat(R"HTML5(
frame.dataset.rotation = newRotation.toString();
}
function toggleRecording() {
let AJAX = new XMLHttpRequest();
AJAX.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
setMessage(this.responseText.substring(2));
if (/start/i.test(this.responseText)) {
recordingStream = true;
startRecordingNotify();
} else {
recordingStream = false;
stopRecordingNotify();
}
}
};
AJAX.open("GET", "switch", true);
AJAX.send();
}
function startRecordingNotify() {
stopRecordingNotify();
var dotParent;
let recordButt = document.getElementById("stream-record");
recordButt.innerHTML = "&; // "⏹"
if (streamOpen) {
dotParent = document.getElementById("frame");
} else {
dotParent = document.getElementById("dot-holder");
}
const dot = document.createElement("div");
dot.id = "dot";
dotParent.appendChild(dot);
}
function stopRecordingNotify() {
let dot = document.getElementById("dot");
if (dot !== null) dot.remove();
let recordButt = document.getElementById("stream-record");
recordButt.innerHTML = "&; // "●"
}
function saveImage() {
if (slideShowOpen && !prefsOpen) {
resetSlideTimer();
document.getElementById("slide").click();
}
}
function uploadFile({target}) {
if (target.files.length) {
var file = target.files[0];
uploadInput.value = "";
if (!file) {
setMessage("Please choose a file!");
return;
}
var formData = new FormData();
formData.append("file", file);
let AJAX = new XMLHttpRequest();
AJAX.onreadystatechange = function() {
if (this.readyState == 4) setMessage(this.responseText);
};
AJAX.open("POST", "upload", true);
AJAX.send(formData);
}
}
function goHome() {
let GETS = makeGETParams("", 1);
setMessage(loadingMsg);
goNow(GETS);
}
function goPage(index) {
let GETS = makeGETParams("", index);
setMessage(loadingMsg);
goNow(GETS);
}
function toggleThumbs() {
let GETS = makeGETParams("thumbs");
setMessage(loadingMsg);
goNow(GETS + "&thumbs=" + (thumbView ? false : true).toString());
}
function togglePoP() {
doPoP = doPoP ? false : true;
let GETS = makeGETParams("pop");
if (doPoP && !PoPLoaded) {
setMessage(loadingMsg);
goNow(GETS + "&pop=true");
} else {
let popbutt = document.getElementById("popbutt");
if (doPoP) {
window.history.replaceState("object", "", GETS + "&pop=true");
popbutt.title = "disable pop-up previews";
popbutt.innerHTML = "-";
} else {
window.history.replaceState("object", "", GETS + "&pop=false");
popbutt.title = "enable pop-up previews (P)";
popbutt.innerHTML = "+";
hidePoP(true);
}
}
}
function setPerPage(newVal) {
let GETS = makeGETParams("per-page");
setMessage(loadingMsg);
goNow(GETS + "&per-page=" + newVal);
}
function setThumbWidth(newVal) {
let GETS = makeGETParams("width");
setMessage(loadingMsg);
goNow(GETS + "&width=" + newVal);
}
function goNow(url="/") {
if (streamOpen) document.getElementById("frame").remove();
setTimeout( function() { location.href = url; }, 200);
}
)HTML5");
&prefs=true"
when the state of the interface changes, so we can restore after a refresh/reload.
The URL comes back with the "ignore"d parameter removed.
This beats trying to construct GET strings pre-button-markup (to insert into some
location.href=MONSTER). Here everything happens dynamically, based on current input. Then all we
need are simple wrapper functions for each control (see directly above).
NOTE: This function is designed to accept multiple ignore parameters at-once. Reason: If you were
to instead send three commands, their rapidity would (in the future) trigger a security alert. So
we make one SINGLE change to the location history only.
*/
webPage.concat(R"HTML5(
function makeGETParams(ignore="", start) {
let url = new URL(window.location.href);
var newGET;
if (start != undefined) {
newGET = "?start=" + start;
} else {
newGET = ( url.searchParams.get("start") ) ? "?start=" + url.searchParams.get("start") : "?start=1";
}
if (ignore.indexOf("thumbs") == -1 && url.searchParams.get("thumbs") ) newGET += "&thumbs=" + url.searchParams.get("thumbs");
if (ignore.indexOf("pop") == -1 && url.searchParams.get("pop") ) newGET += "&pop=" + url.searchParams.get("pop");
if (ignore.indexOf("per-page") == -1 && url.searchParams.get("per-page")) newGET += "&per-page=" + url.searchParams.get("per-page");
if (ignore.indexOf("width") == -1 && url.searchParams.get("width") ) newGET += "&width=" + url.searchParams.get("width");
if (ignore.indexOf("prefs") == -1 && url.searchParams.get("prefs") ) newGET += "&prefs=" + url.searchParams.get("prefs");
if (ignore.indexOf("slideshow") == -1
&& url.searchParams.get("slideshow") ) newGET += "&slideshow=" + url.searchParams.get("slideshow");
if (ignore.indexOf("play") == -1 && url.searchParams.get("play") ) newGET += "&play=" + url.searchParams.get("play");
if (ignore.indexOf("live") == -1 && url.searchParams.get("live") ) newGET += "&live=" + url.searchParams.get("live");
if (ignore.indexOf("fit") == -1 && url.searchParams.get("fit") ) newGET += "&fit=" + url.searchParams.get("fit");
if (ignore.indexOf("floating") == -1
&& url.searchParams.get("floating") ) newGET += "&floating=" + url.searchParams.get("floating");
if (ignore.indexOf("floatX") == -1 && url.searchParams.get("floatX") ) newGET += "&floatX=" + url.searchParams.get("floatX");
if (ignore.indexOf("floatY") == -1 && url.searchParams.get("floatY") ) newGET += "&floatY=" + url.searchParams.get("floatY");
return newGET;
}
)HTML5");
webPage.concat(R"HTML5(
function setMessage(newMessage, myTime = webMessageTime) {
messageShowing = true;
if (newMessage.indexOf(loadingMsg) != -1) myTime = 10000;
if (/fail/i.test(newMessage)) newMessage = "<span class='warning'>" + newMessage + "</span>";
if (prefsOpen) {
prefsMessage.innerHTML = newMessage;
clearMessage(myTime);
return;
}
if (slideShowOpen) {
slideName.innerHTML = newMessage;
clearMessage(myTime);
} else {
document.getElementById("controller").scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
msgTitle.innerHTML = newMessage;
clearMessage(myTime);
}
}
)HTML5");
webPage.concat(R"HTML5(
function clearMessage(myTime=0) {
if (prefsOpen) {
clearTimeout(prefsMsgTimer);
prefsMsgTimer = setTimeout( function() {
prefsMessage.innerHTML = "";
messageShowing = false;
}, myTime);
return;
}
if (slideShowOpen) {
clearTimeout(slideMsgTimer);
slideMsgTimer = setTimeout(showSlide, myTime);
messageShowing = false;
} else {
clearTimeout(msgTimer);
msgTimer = setTimeout( function() {
setDefaultMessage();
messageShowing = false;
}, myTime);
}
}
function setDefaultMessage() {
msgTitle.innerHTML = ImgRange + " (" + theImages.length + " " + imgTallyName + ( (theImages.length != 1) ? "s" : "") + ")";
}
)HTML5");
webPage.concat(R"HTML5(
function sizeUpdate() {
donePoPMSG = false;
if (thumbView) {
thumbHeight = (theImages) ? theImages[0].clientHeight : 0;
bottomOfThumbs = document.getElementById("end").getBoundingClientRect().top + thumbHeight;
} else {
rightOfFileList = document.getElementsByClassName("filelist")[0].getBoundingClientRect().right;
}
if (streamOpen) {
let live = document.getElementById("frame");
if (live.getBoundingClientRect().left > window.innerWidth || live.getBoundingClientRect().top > window.innerHeight) {
setMessage("Live Stream is out of view!");
}
}
}
let touchstartX = 0;
let touchmoveX = 0;
let touchendX = 0;
let touchstartY = 0;
let touchmoveY = 0;
let touchendY = 0;
function processSwipe(event) {
let yDelta = touchstartY - touchendY;
if ( slideShowOpen && ( yDelta > (touchendX - touchstartX) && yDelta > (touchstartX - touchendX) ) ) {
closeSlideShow(true);
return;
}
if (slideShowOpen && !prefsOpen) {
if (touchendX < touchstartX) {
if (touchToggleAutoPlay) {
toggleAutoPlay();
touchToggleAutoPlay = false;
} else {
userNextSlide(true);
}
return;
}
if (touchendX > touchstartX) {
if (touchToggleAutoPlay) {
toggleAutoPlay();
touchToggleAutoPlay = false;
} else {
userPreviousSlide(true);
}
return;
}
)HTML5");
webPage.concat(R"HTML5(
} else if (thumbView && doPoP) {
for ( let i = 0 ; i < theImages.length ; i++ ) {
if ( event.srcElement.id !== "PoP" && theImages[i].src == event.srcElement.src ) {
showPoP(theImages[i].src.split("/").pop(), true, true);
return;
}
}
}
if (doPoP) {
if ( event.srcElement.id == "PoP" && PoPIMG.src == event.srcElement.src ) {
for ( let i = 0 ; i < theImages.length ; i++ ) {
if (theImages[i].src == PoPIMG.src) {
if (touchendX < touchstartX) {
if (loopPoP) {
if ( i === iLinks.length-1 ) i=-1;
} else {
if ( i === theImages.length-1 ) {
pulseLink(i);
return;
}
}
showPoP(theImages[i+1].src.split("/").pop(), false, true);
return;
}
if (touchendX > touchstartX) {
if (loopPoP) {
if ( i === 0 ) i = theImages.length;
} else {
if ( i === 0 ) {
pulseLink(i);
return;
}
}
showPoP(theImages[i-1].src.split("/").pop(), false, true, false);
return;
}
}
}
}
}
}
var motion = {
start: function (event) {
touchToggleAutoPlay = false;
touchstartX = event.changedTouches[0].screenX;
touchstartY = event.changedTouches[0].screenY;
if (event.touches.length == 2) touchToggleAutoPlay = true;
if (event.touches.length == 3){
toggleFastAFMode(true);
resetSlideTimer();
return;
}
touchStarted = true;
},
move: function (event) {
if (!touchStarted) return;
touchmoveX = event.changedTouches[0].screenX;
touchmoveY = event.changedTouches[0].screenY;
if (touchmoveY < touchstartY) {
if (slideShowOpen && !prefsOpen) {
slide.style.top = "calc( 50% + " + (touchmoveY - touchstartY) / 2 + "px )";
}
}
if (touchmoveX < touchstartX) {
if (slideShowOpen && !prefsOpen) {
slide.style.left = "calc( 50% - " + (touchstartX - touchmoveX) + "px )";
}
}
if (touchmoveX > touchstartX) {
if (slideShowOpen && !prefsOpen) {
slide.style.left = "calc( 50% + " + (touchmoveX - touchstartX) + "px )";
}
}
},
end: function (event) {
if ((slideShowOpen && !prefsOpen) || thumbView || doPoP) {
touchendX = event.changedTouches[0].screenX;
touchendY = event.changedTouches[0].screenY;
let swipeDistance = window.innerWidth / 50;
if ( ( touchendX > (touchstartX + swipeDistance) || touchendX < (touchstartX - swipeDistance) ) ||
( touchendY > (touchstartY + swipeDistance) || touchendY < (touchstartY - swipeDistance) ) ) {
slide.style.left = "50%";
processSwipe(event);
}
touchStarted = false;
}
}
}
document.addEventListener( "touchstart", motion.start, { capture: true } );
document.addEventListener( "touchmove", motion.move, { capture: true } );
document.addEventListener( "touchend", motion.end, { capture: true } );
)HTML5");
Expert", but even I know
Experts" did, too. The waste! THINK OF THE CHILDREN!!</rant>
webPage.concat(R"HTML5(
document.body.addEventListener("keydown", (event) => {
if (event.keyCode == 188) {
prefsOpen ? closePrefs() : openPrefs();
return;
}
if (event.keyCode == 32) {
if (!doPoP && !thumbView) return;
if (slideShowOpen) {
toggleAutoPlay();
} else {
openSlideShow();
slideShowPlaying = true;
autoPlay(false);
}
return;
}
if (event.keyCode == 46 || event.keyCode == 8) {
if (slideShowOpen) {
deleteImage(sBaseName);
} else {
deletePoPImage();
}
return;
}
if (event.keyCode == 112 || event.keyCode == 72) {
event.preventDefault();
helpOpen ? closeHelp() : openHelp();
return;
}
if (event.keyCode == 80 ) {
togglePoP();
return;
}
if (event.keyCode == 76) {
streamOpen ? closeLiveStream() : openLiveStream();
return;
}
if (event.keyCode == 82) {
if (streamOpen) rotateStream();
return;
}
if (event.keyCode == 190) {
takeSnap();
return;
}
if (event.keyCode == 84) {
document.getElementById("thumbsbutt").click();
return;
}
if (event.keyCode == 192) {
toggleRecording();
return;
}
if (event.keyCode == 219) {
document.getElementById("previousPage").click();
return;
}
if (event.keyCode == 221) {
document.getElementById("nextPage").click();
return;
}
if (fAF) {
if (event.keyCode == 38) {
fAFTime += 0.025;
if (fAFTime > fAFStore) setMessage("Regular slideshow time is faster!");
slideTime = fAFTime;
return;
}
if (event.keyCode == 40) {
fAFTime -= 0.025;
if (fAFTime < 0) fAFTime = 0;
slideTime = fAFTime;
return;
}
}
if (event.keyCode == 38) {
if (slideShowOpen) {
if (upDownRev) {
userNextSlide();
} else {
userPreviousSlide();
}
} else if (doPoP) {
loadNextPoP(true, true);
}
return;
}
if (event.keyCode == 37) {
if (slideShowOpen) {
userPreviousSlide();
slidesRev = true;
} else if (doPoP) {
loadNextPoP(true, true);
}
return;
}
if (event.keyCode == 40) {
if (slideShowOpen) {
if (upDownRev) {
userPreviousSlide();
} else {
userNextSlide();
}
} else if (doPoP) {
loadNextPoP(false, true);
}
return;
}
if (event.keyCode == 39) {
if (slideShowOpen) {
userNextSlide();
} else if (doPoP) {
loadNextPoP(false, true);
}
return;
}
if (!slideShowOpen) return;
if (event.keyCode == 83) {
saveImage();
return;
}
if (event.keyCode == 70) {
event.preventDefault();
toggleFastAFMode(true);
resetSlideTimer();
return;
}
if (event.keyCode == 13) {
event.preventDefault();
closeSlideShow();
return;
}
}, true);
window.onclick = function (event) {
if ( messageShowing && ( event.target.parentNode == prefsMessage ||event.target.parentNode.parentNode == prefsMessage
|| event.target.parentNode == msgTitle || event.target.parentNode.parentNode == msgTitle ) ) {
event.preventDefault();
clearMessage(0);
return;
}
if ( ( thumbView || doPoP ) && event.target == msgTitle) {
openSlideShow();
return;
}
if (slideShowOpen) {
let slideBox = document.getElementById("slide");
if (event.target.contains(slideBox) && event.target !== slideBox) {
closeSlideShow();
return;
}
}
if (prefsOpen) {
let prefsBox = document.getElementById("pref-box");
if (event.target.contains(prefsBox) && event.target !== prefsBox) {
closePrefs();
return;
}
}
if (streamOpen && !floating) {
let live = document.getElementById("frame");
if (event.target.contains(live) && event.target !== live) {
closeLiveStream();
return;
}
}
if (helpOpen) {
let helpBox = document.getElementById("help-box");
if (event.target.contains(helpBox) && event.target !== helpBox) {
closeHelp();
return;
}
}
}
window.onresize = () => { sizeUpdate(); }
document.addEventListener("DOMContentLoaded", function (event) {
if (thumbView || doPoP) {
msgTitle.style.cursor = "zoom-in";
slideShow = document.getElementById("slideshow");
slide = document.getElementById("slide");
slideName = document.getElementById("slide-name");
imgSaveLink = document.getElementById("imgSaveLink");
prevColor = slideName.style.color;
prevBGColor = slideShow.style.background;
}
if (thumbView) {
theImages = document.getElementById("thumbnails").getElementsByTagName("img");
iLinks = document.getElementById("thumbnails").getElementsByTagName("div");
} else if (doPoP) {
iLinks = document.getElementById("listing").getElementsByTagName("a");
let plDiv = document.getElementById("preloaders");
let pl = [];
for (let i = 0 ; i < iLinks.length ; i++) {
if (iLinks[i].href.indexOf(fileEXT) !== -1) {
pl[i] = document.createElement("img");
pl[i].style.display = "none";
pl[i].src = iLinks[i].href;
pl[i].id = "pl-" + iLinks[i].href.split("/").pop();
plDiv.appendChild(pl[i]);
}
}
theImages = plDiv.getElementsByTagName("img");
} else {
theImages = document.getElementById("listing").getElementsByTagName("div");
}
if (loadingIcon && !thumbView && doPoP) {
let reloadIcon = document.getElementById("reloading");
reloadIcon.style.rotate = "100turn";
reloadIcon.style.transition = "150s ease";
let imgs = document.images, len = imgs.length, counter = 0;
function incrementCounter() {
counter++;
if (counter == len) {
reloadIcon.style.transition = "unset";
reloadIcon.style.rotate = "none";
}
}
[].forEach.call( imgs, function(img) {
if (img.complete) {
incrementCounter();
} else {
img.addEventListener("load", incrementCounter, false);
}
} );
}
)HTML5");
if (amRecording) { webPage.concat("startRecordingNotify();"); }
webPage.concat(R"HTML5(
uploadInput = document.getElementById("fileUpload");
uploadInput.addEventListener("change", uploadFile);
setIntermediateCheckBox(document.getElementById("darkmode"));
if (openWithPrefs) openPrefs();
setDefaultMessage();
});
window.onload = () => {
if (openWithSlides) {
openSlideShow();
if (autoPlaying) {
toggleAutoPlay(false);
}
}
if (amStreaming) openLiveStream();
if (openWithHelp) openHelp();
};
</script>
</body>
</html>
)HTML5");
server.send(200, _HTML_, webPage);
}
void handleDeleteFile() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
if (eXi) Serial.printf(" Delete Request:");
if (ejected) {
sendEjected();
return;
} else if (!server.hasArg("file")) {
send500();
return;
} else {
if (server.hasArg("file")) {
String postMessage = deleteFile(server.arg("file"));
if (eXi) Serial.println(postMessage);
server.send(200, _TEXT_, postMessage);
}
}
}
void handleTakeSnapNow() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
if (ejected) {
sendEjected();
return;
} else {
if (webSnapResetsTimer) {
mostRecentPic = 0;
} else {
if (!instantPic()) {
server.send(200, _TEXT_, "FAIL");
}
}
server.send(200, _TEXT_, "OK");
}
}
void handleCapture() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
if (ejected) {
sendEjected();
return;
} else {
if (webSnapResetsTimer) {
mostRecentPic = 0;
} else {
if (!instantPic()) {
server.send(200, _TEXT_, "FAIL");
}
}
loadImage(mostRecentCapture);
}
}
void sendEjected() {
LastMessage = " SD Card is ejected!";
server.send(200, _TEXT_, "FAIL!" + LastMessage);
}
void sendNotPlaying() {
LastMessage = " Streaming SlideShow is not playing!!";
server.send(200, _TEXT_, "FAIL!" + LastMessage);
}
void sendAlreadyPlaying() {
LastMessage = " Streaming SlideShow is already playing!!";
server.send(200, _TEXT_, "FAIL!" + LastMessage);
}
void handleFullFileList() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
bool quickListingsTMP = quickListings;
quickListings = false;
LastMessage = listDir(false, false, true);
server.send(200, _TEXT_, LastMessage);
quickListings = quickListingsTMP;
}
void handleFileList() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
LastMessage = listDir(false, false, true);
server.send(200, _TEXT_, LastMessage);
}
void handleQuickFileList() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
LastMessage = quickList();
server.send(200, _TEXT_, LastMessage);
}
void handleQuickAllFileList() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
LastMessage = quickList(true, true);
server.send(200, _TEXT_, LastMessage);
}
void handleIntervalChange() { if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
String type = "second";
uint64_t multiplier = 1000;
uint16_t newInterval = (delayTime / 1000);
for (uint8_t i = 0; i < server.args(); i++) {
newInterval = server.arg(i).toInt();
if (server.argName(i) == "seconds") {
break;
}
if (server.argName(i) == "minutes") {
multiplier = MINUTE_MILLIS;
type = "minute";
break;
}
if (server.argName(i) == "hours") {
multiplier = HOUR_MILLIS;
type = "hour";
break;
}
if (server.argName(i) == "days") {
multiplier = DAY_MILLIS;
type = "day";
break;
}
}
delayTime = newInterval * multiplier;
prefs.putULong64("d", delayTime);
char plural = '\0';
if (newInterval != 1) plural = 's';
char buffer[64] = {'\0'};
sprintf(buffer, "Snap interval time set to %llu %s%c", (delayTime / multiplier), type.c_str(), plural);
server.send(200, _TEXT_, "OK" + (String)buffer);
if (eXi) Serial.printf(" %s\n", buffer);
}
void handleGetInterval() {
if (eXi) Serial.printf(" HTTP Request: Main Page for client @ %s%s\n", server.client().remoteIP().toString().c_str(), server.uri().c_str());
server.send(200, _TEXT_, "OK Snap interval time: " + convertSeconds(delayTime/1000));
}
void handleCameraSettings() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
String rebootMsg = "\nTL-CAM will now reboot. Please wait..";
String rangeFailMsg = "Fail! Requested setting is out of range!";
String reloadMsg = "\n (reload the page to see the changes)";
String csMsg, bpl = "", newSetting = ""; ";
uint16_t newSettingInt;
for (uint8_t i = 0; i < server.args(); i++) {
newSetting = server.arg(i);
newSetting.trim();
newSettingInt = newSetting.toInt();
if (newSettingInt != 1) bpl = "s";
if (server.argName(i) == "ledb") {
ledBrightness = newSettingInt;
csMsg = "LED brightness set to " + newSetting;
prefs.putUChar("b", ledBrightness);
flashLED(720);
break;
}
if (server.argName(i) == "mfps") {
max_fps = newSettingInt;
csMsg = "Setting Streaming Frame Limit to " + newSetting + " FPS: " + setSensorParameters("max_fps", newSetting, false);
break;
}
if (server.argName(i) == "ovw") {
overWrite = (newSetting == "true") ? true : false;
csMsg = "Overwrite mode " + (String)(overWrite ? "enabled" : "disabled");
prefs.putBool("o", overWrite);
break;
}
if (server.argName(i) == "tsf") {
timeStampFileNames = (newSetting == "true") ? true : false;
csMsg = "timestamped file names " + (String)(timeStampFileNames ? "enabled" : "disabled");
prefs.putBool("t", timeStampFileNames);
break;
}
if (server.argName(i) == "wsr") {
webSnapResetsTimer = (newSetting == "true") ? true : false;
csMsg = "Web snap resets timer: " + (String)(webSnapResetsTimer ? "true" : "false");
prefs.putBool("w", webSnapResetsTimer);
break;
}
Path=/") in the cookie, Firefox et al will reject the cookie.
NOTE HAHA: You can even omit the ';' delimiter and just slam Path=/ onto the *value* and
Firefox will happily accept the cookie.. *ahem* Firefox Devs, again.
Someone please report this bug.
*/
if (server.argName(i) == "dms") {
csMsg = "Dark mode: " + (String)((newSetting == "true") ? "enabled" : "disabled") + reloadMsg;
setCookie("darkmode", newSetting);
break;
}
if (server.argName(i) == "dma") {
csMsg = "Automatic Dark mode: " + (String)((newSetting == "true") ? "enabled" : "disabled");
setCookie("autodark", newSetting);
getNightTimeStatus();
if ( newSetting == "true" ) {
if ( ( !darkMode && nightTime ) || ( darkMode && !nightTime ) ) csMsg += reloadMsg;
} else {
if ( ( amDark && !darkMode ) || ( !amDark && darkMode ) ) csMsg += reloadMsg;
}
break;
}
if (server.argName(i) == "cai") {
cacheImages = (newSetting == "true") ? true : false;
csMsg = "Image caching " + (String)(cacheImages ? "enabled" : "disabled");
setCookie("cacheimg", newSetting);
break;
}
if (server.argName(i) == "cst") {
if (newSettingInt >= 1) {
cacheTime = newSettingInt;
csMsg = "Image cache time set to " + newSetting + " minute" + bpl;
prefs.putUShort("m", cacheTime);
setCookie("cachetime", (String)cacheTime);
} else {
csMsg = rangeFailMsg;
}
break;
}
if (server.argName(i) == "sst") {
csMsg = "SlideShow time set to " + newSetting + " second" + bpl;
setCookie("slidetime", newSetting);
slideTime = newSettingInt;
break;
}
if (server.argName(i) == "aii") {
setCookie("rotateicon", newSetting);
loadingIcon = (newSetting == "true") ? true : false;
csMsg = "Rotating loading icon: " + (String)(loadingIcon ? "enabled" : "disabled");
break;
}
if (server.argName(i) == "skp") {
setCookie("stickypop", newSetting);
stickyPoP = (newSetting == "true") ? true : false;
csMsg = "Sticky PoP-Up preview: " + (String)(stickyPoP ? "enabled" : "disabled");
break;
}
if (server.argName(i) == "lpp") {
setCookie("looppop", newSetting );
loopPoP = (newSetting == "true") ? true : false;
csMsg = "Looping PoP-Up Previews: " + (String)(loopPoP ? "enabled" : "disabled");
break;
}
if (server.argName(i) == "xsp") {
setCookie("listspaces", newSetting);
spacesInListView = (newSetting == "true") ? true : false;
if (thumbView) reloadMsg = "";
csMsg = "Extra space in list views: " + (String)(spacesInListView ? "enabled" : "disabled") + reloadMsg;
break;
}
if (server.argName(i) == "tws") {
setCookie("twinstream", newSetting );
streamANDRecord = (newSetting == "true") ? true : false;
csMsg = "Simultaneous Streaming AND Recording: " + (String)(streamANDRecord ? "enabled" : "disabled");
break;
}
if (server.argName(i) == "maz") {
setCookie("zmaccel", newSetting );
csMsg = "Zoomed Mouse Movement Acceleration set to: " + newSetting;
break;
}
if (server.argName(i) == "stw") {
csMsg = "";
setCookie("tinywarn", "seen" );
break;
}
if (server.argName(i) == "spw") {
csMsg = "";
setCookie("prefwarn", "seen" );
break;
}
}
OK") with JavaScript to create the final displayed message.
server.send(200, _TEXT_, (csMsg != rangeFailMsg) ? "OK " + csMsg : rangeFailMsg);
") xCommand = newCommand; // Not required, for now.
if (eXi) Serial.printf(" %s\n", csMsg.c_str());
}
void handleImageSettings() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
String ret = setSensorParameters(server.argName(0), server.arg(0));
if (ret.substring(0,2) != "OK") {
server.send(500, _TEXT_, "Command Failed");
return;
}
server.send(200, _TEXT_, ret);
}
void handleRebootStatus() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
server.send(200, _TEXT_, "OK");
}
void handleBackupNVS() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
String msg = "OK Configuration Backup: ";
server.send(200, _TEXT_, msg + saveSensorConfig("backup"));
}
void handleRestoreNVS() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
String msg = "OK Configuration Restore: ";
server.send(200, _TEXT_, msg + loadSensorConfig("backup"));
}
// Console.h"
<!--{insert}-->", getCommands()); // Never forget the usefulness of the lowly HTML comment.
known" clients.
HTTP Request: WebConsole for client @ %s\n", myClient.c_str());
void handleLastMessage() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
server.send(200, _TEXT_, (LastMessage == "") ? getCommands() : LastMessage);
LastMessage = "";
}
void handleEjectSD() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
if (ejected) {
LastMessage = "Fail!\n SD Card is already ejected!";
} else {
SD_MMC.end();
LastMessage = "SD Card Stopped.\n Your SD Card can now be safely removed.";
ejected = true;
}
server.send(200, _TEXT_, "OK " + LastMessage);
if (eXi) Serial.printf(" %s\n", LastMessage.c_str());
}
void handleInsertSD() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
if (!ejected) {
LastMessage = "Fail!\n SD Card is not ejected!";
} else {
if (startMicroSD()) {
String stateA = "resumed";
if (!autoSnap) stateA = "enabled (note: auto-snap is currently disabled)";
LastMessage = "SD Card started successfully.\n Image acquisition " + stateA;
ejected = false;
} else {
LastMessage = "SD Card Error. Please try again.";
}
}
server.send(200, _TEXT_, "OK " + LastMessage);
if (eXi) Serial.printf(" %s\n", LastMessage.c_str());
}
void handleSDInfo() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
if (ejected) {
sendEjected();
} else {
LastMessage = printSpace();
server.send(200, _TEXT_, "OK" + LastMessage);
}
}
void handleMemoryInfo() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
server.send(200, _TEXT_, "OK" + printMemoryInfo());
}
void handleCommandsHelp() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
LastMessage = getCommands();
server.send(200, _TEXT_, LastMessage);
}
list", that command will be
intercepted by the web handler and NOT handled here**.
Basically, any URI that doesn't have its own handler, is treated as a command, so if you point
some HTTP-capable device (e.g. web browser) to:
/foo
The command "foo" would be passed to the command interpreter. Of course there is no "foo" command
so nothing would happen. Valid commands will leave their output in "LastMessage", which /console
picks up and displays below the command input.
** However, if required, you can force your command to be handled by the serial command mechanism
by preceding the command with an asterisk, e.g..
*list
*/
void universalHandler() {
String thisURL = urlDecode(server.uri());
if (thisURL[1] == '*') thisURL = (String)thisURL[0] + thisURL.substring(2);
if ( !SD_MMC.exists(thisURL.c_str()) ) {
xCommand = thisURL.substring(1);
server.send(200, _TEXT_, xCommand + ": OK\nSee /LastMessage for any output from your command.");
if (eXi) Serial.printf(" HTTP Request: xCommand: %s\n", xCommand.c_str());
} else if (thisURL.indexOf(fileEXT) != -1) {
loadImage(thisURL);
} else if (thisURL.indexOf(streamFileName) != -1) {
server.sendHeader("Location", "/mjpeg");
server.send(302, _TEXT_, "");
} else {
if (eXi) Serial.printf(" HTTP Request: Serving File: %s\n", thisURL.c_str());
File file = SD_MMC.open(thisURL.c_str());
if (!file) return;
server.streamFile(file, "text/html");
/", SD_MMC, thisURL.c_str()); // only works when set along with server.on() statements @ init
}
}
T", "L" and "C" in the centre.
Rendered with 100% MATHEMATICS!*
* Insert joke about me toiling all night with a protractor.
/favicon.ico */
void handleFavicon() {
String favicon = R"SVGfavicon(<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"><svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="64.000000pt" height="64.000000pt" viewBox="0 0 64.000000 64.000000" preserveAspectRatio="xMidYMid meet"><g transform="translate(0.000000,64.000000) scale(0.050000,-0.050000)" fill=" stroke="none"><path d="M555 1266 c-91 -14 -156 -208 -155 -465 l1 -111 54 99 c66 119 298 256 506 298 l123 25 -77 51 c-122 80 -315 124 -452 103z"/><path d="M380 1225 c-134 -50 -308 -240 -332 -361 -15 -75 66 -171 259 -306 174 -122 188 -122 113 -1 -84 136 -80 449 9 626 29 58 18 68 -49 42z"/><path d="M920 1043 c-60 -20 -164 -64 -230 -99 l-120 -62 96 -1 c148 -2 445 -176 547 -322 l42 -61 12 66 c33 173 -89 501 -189 510 -27 3 -97 -12 -158 -31z"/><path d="M860 723 c84 -137 80 -449 -10 -628 -65 -132 203 20 310 175 124 180 93 256 -187 452 -174 122 -188 122 -113 1z"/><path d="M500 770 c0 -16 14 -30 30 -30 23 0 30 -26 30 -110 l0 -110 120 0 c93 0 120 7 120 30 0 22 -22 30 -80 30 -80 0 -80 0 -80 80 0 58 8 80 30 80 17 0 30 14 30 30 0 23 -24 30 -100 30 -76 0 -100 -7 -100 -30z"/><path d="M13 716 c-27 -145 79 -479 163 -511 42 -16 262 52 404 126 l130 67 -96 1 c-147 2 -450 178 -544 317 l-45 66 -12 -66z"/><path d="M816 480 c-64 -115 -336 -268 -520 -292 -94 -13 -99 -16 -64 -43 197 -147 517 -184 578 -65 38 74 74 291 66 404 l-6 94 -54 -98z"/></g></svg>)SVGfavicon";
server.send(200, "image/svg+xml", favicon);
}
fill" value.
color" is the technical term for web colours. Like "center" is, for the centre of web things.
void handleRecent() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
File imgFile = SD_MMC.open(mostRecentCapture.c_str());
if (imgFile) {
if (eXi) Serial.printf(" Streaming File: %s\n", mostRecentCapture.c_str());
server.streamFile(imgFile, "image/jpeg");
} else server.send(404, _TEXT_, "Image has been deleted.");
}
void handleSensorStatus() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
LastMessage = getSensorStatus();
deJSONify(LastMessage);
server.send(200, _TEXT_, LastMessage);
}
void handleSensorStatusEx() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
LastMessage = getSensorStatus(false, true);
deJSONify(LastMessage);
server.send(200, _TEXT_, LastMessage);
}
void handleESPSensorStatus() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
LastMessage = getSensorStatus(true, true, true);
server.sendHeader("Access-Control-Allow-Origin", "*");
server.send(200, "application/json", LastMessage);
}
void deJSONify(String &string) {
string.replace(":", ": ");
string.replace("\"", "");
}
void handlePrintNVS() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
LastMessage = gatherPrefs(false, false);
LastMessage += gatherSensorPrefs();
server.send(200, _TEXT_, LastMessage);
}
String urlDecode(String text) {
String decoded = "";
char temp[] = "0x00";
uint16_t len = text.length();
uint16_t i = 0;
while (i < len) {
char decodedChar;
char encodedChar = text.charAt(i++);
if ( (encodedChar == '%') && (i + 1 < len) ) {
temp[2] = text.charAt(i++);
temp[3] = text.charAt(i++);
decodedChar = strtol(temp, NULL, 16);
} else {
if (encodedChar == '+') {
decodedChar = ' ';
} else {
decodedChar = encodedChar;
}
}
decoded += decodedChar;
}
return decoded;
}
void handleESP32ControlWrap() {
if (eXi) Serial.printf(" HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
String myVar, myVal;
if (server.hasArg("var")) myVar = server.arg("var");
if (server.hasArg("val")) myVal = server.arg("val");
if (myVar != "" && myVal != "") {
String ret = setSensorParameters(myVar, myVal);
if (ret.substring(0,2) != "OK") {
server.send(500, _TEXT_, "Command Failed");
return;
}
server.send(200, _TEXT_, ret);
} else {
send500();
return;
}
}
void handleESP32SetReg() {
if (eXi) Serial.printf("\n HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
if (server.arg("reg") == "" || server.arg("mask") == "" || server.arg("val") == "" ) { send500(); return; }
int reg = server.arg("reg").toInt();
int mask = server.arg("mask").toInt();
int val = server.arg("val").toInt();
if (eXi) Serial.printf(" Setting Register: reg: 0x%02x, mask: 0x%02x, value: 0x%02x\n", reg, mask, val);
sensor_t *s = esp_camera_sensor_get();
int8_t res = s->set_reg(s, reg, mask, val);
s->set_reg(s, reg, mask, val);
if (res) { send500(); return; }
server.sendHeader("Access-Control-Allow-Origin", "*");
server.send(200, _TEXT_, "OK");
}
void handleESP32GetReg() {
if (eXi) Serial.printf("\n HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
if (server.arg("reg") == "" || server.arg("mask") == "" ) { send500(); return; }
int reg = server.arg("reg").toInt();
int mask = server.arg("mask").toInt();
sensor_t *s = esp_camera_sensor_get();
int8_t res = s->get_reg(s, reg, mask);
if (res < 0) { send500("invalid response"); return; }
char buffer[20];
const char * val = itoa(res, buffer, 10);
if (eXi) Serial.printf(" Getting Register: reg: 0x%02x, mask: 0x%02x, value: %s\n", reg, mask, val);
server.sendHeader("Access-Control-Allow-Origin", "*");
server.send(200, _TEXT_, val);
}
void handleESP32pll() {
if (eXi) Serial.printf("\n HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
if (server.arg("bypass") == "" || server.arg("mul") == "" || server.arg("sys") == "" || \
server.arg("root") == "" || server.arg("pre") == "" || server.arg("seld5") == "" || \
server.arg("pclken") == "" || server.arg("pclk") == "" ) { send500(); return; }
int bypass = server.arg("bypass").toInt();
int mul = server.arg("mul").toInt();
int sys = server.arg("sys").toInt();
int root = server.arg("root").toInt();
int pre = server.arg("pre").toInt();
int seld5 = server.arg("seld5").toInt();
int pclken = server.arg("pclken").toInt();
int pclk = server.arg("pclk").toInt();
if (eXi) Serial.printf("Set Pll: bypass: %d, mul: %d, sys: %d, root: %d, pre: %d, seld5: %d, pclken: %d, pclk: %d\n", bypass, mul, sys, root, pre, seld5, pclken, pclk);
sensor_t *s = esp_camera_sensor_get();
int res = s->set_pll(s, bypass, mul, sys, root, pre, seld5, pclken, pclk);
if (res) { send500(); return; }
server.sendHeader("Access-Control-Allow-Origin", "*");
server.send(200, _TEXT_, "OK");
}
void handleESP32WindowRes() {
if (eXi) Serial.printf("\n HTTP Request: %s from: %s\n", server.uri().c_str(), server.client().remoteIP().toString().c_str());
if (server.arg("sx") == "" || server.arg("sy") == "" || server.arg("ex") == "" || \
server.arg("ey") == "" || server.arg("offx") == "" || server.arg("offy") == "" || \
server.arg("tx") == "" || server.arg("ty") == "" || server.arg("ox") == "" || \
server.arg("oy") == "" || server.arg("scale") == "" || server.arg("binning") == "" ) { send500(); return; }
int startX = server.arg("sx").toInt();
int startY = server.arg("sy").toInt();
int endX = server.arg("ex").toInt();
int endY = server.arg("ey").toInt();
int offsetX = server.arg("offx").toInt();
int offsetY = server.arg("offy").toInt();
int totalX = server.arg("tx").toInt();
int totalY = server.arg("ty").toInt();
int outputX = server.arg("ox").toInt();
int outputY = server.arg("oy").toInt();
bool scale = server.arg("scale").toInt();
bool binning = server.arg("binning").toInt();
if (eXi) Serial.printf("Set Window: Start: %d %d, End: %d %d, Offset: %d %d, Total: %d %d, Output: %d %d, Scale: %u, Binning: %u\n", startX, startY, endX, endY, offsetX, offsetY, totalX, totalY, outputX, outputY, scale, binning);
sensor_t *s = esp_camera_sensor_get();
int res = s->set_res_raw(s, startX, startY, endX, endY, offsetX, offsetY, totalX, totalY, outputX, outputY, scale, binning);
if (res) { send500(); return; }
server.sendHeader("Access-Control-Allow-Origin", "*");
server.send(200, _TEXT_, "OK");
}
String getCommands() {
static char commands[6000];
char *c = commands;
c += sprintf(c, " Commands:\n");
c += sprintf(c, "\n");
c += sprintf(c, " s/m/h/d(int) Set new snap interval time**, in seconds/minutes/hours/days, e.g. s10\n");
c += sprintf(c, "\n");
c += sprintf(c, " list SLOWLY list all image files, with dates and sizes, recalculate space, etc. (web: /list)\n");
c += sprintf(c, " ql Quick list of all image files (no sizes or dates) (web: /ql)\n");
c += sprintf(c, " qla Quick list of all files (including non-image files)\n");
c += sprintf(c, " l List all image files in whatever mode you are currently in. (web: /l)\n");
c += sprintf(c, " tq Toggle Quick Listing mode\n");
c += sprintf(c, "\n");
c += sprintf(c, " NOTE: The 'list' command is SLOW when there are a lot of images\n");
c += sprintf(c, "\n");
c += sprintf(c, " ! Capture an image and reset the timer to NOW\n");
c += sprintf(c, " . Capture an image and DO NOT reset the timer\n");
c += sprintf(c, "\n");
c += sprintf(c, " record Start recording MJPEG Stream (will overwrite any previous recording)\n");
c += sprintf(c, " stop Stop recording MJPEG Stream\n");
c += sprintf(c, "\n");
c += sprintf(c, " i[*] Print out information. With no suffix: current snap interval time\n");
c += sprintf(c, " [a/d/e/o/a/q/r/s/t/z/etc..] information about various current settings, e.g. 'it'\n");
c += sprintf(c, " prefs Print out all current i* settings (also see: 'nvs' command, below)\n");
c += sprintf(c, " t/time Print out the local time / current system time (RTC)\n");
c += sprintf(c, " time * Set system time to *, which is a UNIX UTC time, e.g.: time 1679897227\n");
c += sprintf(c, "\n");
c += sprintf(c, " c Camera Sensor API: \n");
c += sprintf(c, " Use 'c param=value' to set any sensor value **, e.g.\n");
c += sprintf(c, " 'c xclk=12' (to set clock frequency) or 'c framesize=5' (set streaming frame size)\n");
c += sprintf(c, " size (int) Set Image Capture frame size. (2MP:0-13, 3MP:0-17, 5MP:0-21) **\n");
c += sprintf(c, "\n");
c += sprintf(c, " backup Save all camera sensor settings to the backup slot\n");
c += sprintf(c, " restore Restore camera sensor settings from the backup slot\n");
c += sprintf(c, " backup * Save all camera sensor settings to user-specified preset named *\n");
c += sprintf(c, " restore * Restore camera sensor settings from user-specified preset named *\n");
c += sprintf(c, " presets List all currently saved backup presets\n");
c += sprintf(c, "\n");
c += sprintf(c, " b(int) Set LED Flash Brightness (0-255) (LED will flash momentarily at the new brightness). **\n");
c += sprintf(c, "\n");
c += sprintf(c, " free Print out Free Space and images-remaining estimate\n");
c += sprintf(c, " eject / ej Stop SD Card, so it can be ejected safely\n");
c += sprintf(c, " insert / sd Re-Start SD Card after being ejected\n");
c += sprintf(c, " test Run benchmarks on current SD Card (this takes a few seconds)\n");
c += sprintf(c, "\n");
c += sprintf(c, " e Toggle Extended Information in the console. **\n");
c += sprintf(c, " dd Display details in web listings (date and size) **\n");
c += sprintf(c, " to Toggle Overwrite Mode (for numeric sequences, not timestamped file names). **\n");
c += sprintf(c, " tt Toggle Timestamp File Names (see above setting). **\n");
c += sprintf(c, " ta Toggle Auto-Snap (aka. Time-Lapse Capture - la raison d'etre!). **\n");
c += sprintf(c, " ts Toggle Auto-Sleep Mode. **\n");
c += sprintf(c, " td Toggle Deep/Light Sleep. **\n");
c += sprintf(c, " sleep Put TL-CAM to Sleep immediately\n");
c += sprintf(c, " tr Toggle Remote Control Features. **\n");
c += sprintf(c, " web Start WiFi Remote Control Features after sleeping/stopping\n");
c += sprintf(c, "\n");
c += sprintf(c, " sp Pause / Resume the Streaming SlideShow. (web: /sp)\n");
c += sprintf(c, " skip * Skip * number of images in the Streaming SlideShow. (web: /skip?skip=10 OR /skip10\n");
c += sprintf(c, "\n");
c += sprintf(c, " erase Delete all the image files from the SD Card\n");
c += sprintf(c, " wipe[*-[);
c += sprintf(c, "\n");
c += sprintf(c, " wipe 89- (wipe all image files from 89 onwards)\n");
c += sprintf(c, " wipe 89-95 (wipe all image files from 89 to 95, inclusive)\n");
c += sprintf(c, "\n");
c += sprintf(c, " NOTE: Wiping from index * deletes files in the (l) list order. NAMES ARE IGNORED\n");
c += sprintf(c, " Non-Image files also ignored. You will be asked to confirm this command\n");
c += sprintf(c, "\n");
c += sprintf(c, " delete * Delete specific file named * e.g.: delete test.html\n");
c += sprintf(c, " rename * );
c += sprintf(c, "\n");
c += sprintf(c, " uptime Print out ESP32 module uptime\n");
c += sprintf(c, " memory Print out current memory information\n");
c += sprintf(c, " x / reboot Cleanly (i.e. store time, settings, flush buffers, etc.) shutdown and reboot ESP32 device\n");
c += sprintf(c, "\n");
c += sprintf(c, " sensor Print out sensor settings (web: /sensor)\n");
c += sprintf(c, " sensorx Print out extended sensor settings with main register info (web: /sensorx)\n");
c += sprintf(c, " nvs Print out NVS-stored main settings (web: /nvs)\n");
c += sprintf(c, " nvsl Reload and print out NVS-stored settings\n");
c += sprintf(c, "\n");
c += sprintf(c, " reset Reset all NVS-saved settings back to (hard-coded) defaults and reboot\n");
c += sprintf(c, " nvswipe Completely Wipe Entire NVRAM and reboot. (you will be asked to confirm)\n");
c += sprintf(c, "\n");
c += sprintf(c, " help Print out this list of commands\n");
c += sprintf(c, "\n");
c += sprintf(c, " NOTE: Settings marked ** are stored in NVS and recalled after a reboot\n");
return commands;
}
String getUptime() {
char utBuffer[80] = {'\0'};
uint64_t s = millis() / 1000;
uint32_t m = s / 60;
uint16_t h = m / 60;
uint16_t d = h / 24;
String dpl = ""; if (d != 1) dpl = "s";
String hpl = ""; if (h != 1) hpl = "s";
String mpl = ""; if (m % 60 != 1) mpl = "s";
String spl = ""; if (s % 60 != 1) spl = "s";
sprintf(utBuffer, "\n Uptime: ");
if (d > 0) sprintf(utBuffer + strlen(utBuffer), "%d day%s, ", d, dpl.c_str());
if (d > 0 || h > 0) sprintf(utBuffer + strlen(utBuffer), "%d hour%s, ", h, hpl.c_str());
if (d > 0 || h > 0 || m > 0) sprintf(utBuffer + strlen(utBuffer), "%d minute%s and ", m % 60, mpl.c_str());
sprintf(utBuffer + strlen(utBuffer), "%llu second%s", s % 60, spl.c_str());
return (String)utBuffer;
}
String convertSeconds(uint64_t totalSeconds) {
String convertedTime;
uint32_t minutes = totalSeconds / 60;
uint32_t hours = minutes / 60;
uint32_t days = hours / 24;
totalSeconds %= 60;
minutes %= 60;
hours %= 24;
if (days > 0) convertedTime += String(days) + "d ";
if (hours > 0) convertedTime += String(hours) + "h ";
if (minutes > 0) convertedTime += String(minutes) + "m ";
convertedTime += (totalSeconds != 0)? String(totalSeconds) + "s" : "";
return convertedTime;
}
bool isEmptyChar(const char **myChar) {
if (*myChar && !*myChar[0]) {
return true;
}
return false;
}
bool presetExists(String backupName) {
String currentPresets = prefs.getString("presets", "\n");
if (currentPresets.indexOf("\n" + backupName) != -1) return true;
return false;
}
String trimParam(String commandString, int beginAt) {
String trimmedParam = commandString.substring(0, beginAt);
uint8_t startAt = beginAt;
if (commandString.indexOf("=") != -1) startAt = commandString.indexOf("=") + 1;
trimmedParam = commandString.substring(startAt);
trimmedParam.trim();
return trimmedParam;
}
void prefsSwitch(String NVSKey = "TLCam") {
prefs.end();
prefs.begin(NVSKey.c_str());
}
String getFreeEntries() {
char nvbuf[64];
nvs_stats_t nvs_stats;
nvs_get_stats(NULL, &nvs_stats);
sprintf(nvbuf, "\n Used %d of %d total NVS entries (%d free)", nvs_stats.used_entries,
nvs_stats.total_entries, nvs_stats.free_entries);
return (String)nvbuf;
}
void goToSleep() {
uint16_t preWakeTime = 1000;
if (deepSleep) preWakeTime = 3000;
uint64_t thisSleep = ( (delayTime - preWakeTime) / 1000 ) - ( (millis() - lastSnapTime) / 1000);
esp_sleep_enable_timer_wakeup(thisSleep * SECOND_MICROS);
uart_set_wakeup_threshold(UART_NUM_0, 3);
esp_sleep_enable_uart_wakeup(UART_NUM_0);
delay(100);
if (eXi) Serial.println(" Going to sleep for " + convertSeconds(thisSleep) + " ...");
Serial.flush();
if (remControl) server.close();
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
if (deepSleep) {
esp_deep_sleep_start();
} else {
esp_light_sleep_start();
}
}
bool waitForConfirmation(String confirmMsg, uint8_t timeout = 10) {
uint64_t startWait = millis();
LastMessage = confirmMsg + " (y/n)\n Option will timeout in " + (String)timeout + " seconds.";
Serial.println(LastMessage);
String command;
while (true) {
if (remControl) server.handleClient();
if (Serial.peek() > 0 || xCommand != "") {
if (xCommand == "") {
command = Serial.readStringUntil('\n');
} else {
command = xCommand;
}
command.trim();
command.toLowerCase();
if (command == "y") return true;
return false;
}
delay(10);
if ( millis() > ( startWait + ( timeout * 1000 ) ) ) {
Serial.println(" Response Time-Out.");
return false;
}
}
return false;
}
String deleteFile(String file) {
if ( !SD_MMC.exists("/" + file) ) {
return "\n No such file as '" + file + "'";
}
String returnMsg, deletedOKAY = " FAIL";
if ( SD_MMC.remove("/" + file ) ) {
if (file.indexOf(fileEXT) != -1) {
totalFileCount--;
saveNumber--;
}
returnMsg = "\n " + file + " deleted";
deletedOKAY = " OK";
if ( SD_MMC.exists("/" + file) ) {
deletedOKAY += "\n OH OH! FAIL! " + file + " STILL EXISTS!\n SD Error! Please reboot and try again.";
}
} else {
returnMsg = "\n Failed to delete " + file;
}
return returnMsg + deletedOKAY;
}
String renameFile(String origin, String target) {
if ( !SD_MMC.exists("/" + origin) ) {
return " No such file as '" + origin + "'";
}
origin.trim();
target.trim();
String returnMsg, renamedOKAY;
if (SD_MMC.rename("/" + origin, "/" + target)) {
returnMsg = " renamed " + origin + " to " + target;
renamedOKAY = " OK";
} else {
returnMsg = " FAILED to rename " + origin + " to " + target;
}
if ( SD_MMC.exists("/" + origin) ) {
renamedOKAY += "\n OH OH! FAIL! " + origin + " STILL EXISTS!\n SD Error! Please reboot and try again.";
}
return returnMsg + renamedOKAY;
}
String makeFilename(uint32_t startNum) {
String filename;
if (timeStampFileNames) {
if (!getLocalTime(&timeinfo, 0) ) {
Serial.println(" Failed to obtain time. Switching to numerical file names.");
timeStampFileNames = false;
return makeFilename(startNum);
}
char tBuffer[128] = {'\0'};
strftime(tBuffer, sizeof(tBuffer), dateTimeString, &timeinfo);
filename += (String)tBuffer;
} else {
if (startNum < 100000) filename += "0";
if (startNum < 10000) filename += "0";
if (startNum < 1000) filename += "0";
if (startNum < 100) filename += "0";
if (startNum < 10) filename += "0";
filename += startNum;
}
filename.trim();
if (filename == "") return "";
filename = "/" + fileNamePrefix + filename;
if (!overWrite) {
String testName = filename + fileEXT;
const char *test = testName.c_str();
if (SD_MMC.exists(test) ) return makeFilename(saveNumber++);
}
return filename;
}
void loop() {
if (remControl) server.handleClient();
uint64_t currentTime = millis();
String command, confirm;
bool isSerial = false;
if (Serial.peek() > 0 || xCommand != "") {
if (xCommand == "") {
command = Serial.readStringUntil('\n');
isSerial = true;
} else {
command = xCommand;
xCommand = "";
command = urlDecode(command);
}
if (command != "") {
command.trim();
char cmd = command[0];
String value = command.substring(1);
uint64_t intValue = value.toInt();
if (command.substring(0,6) == "delete" || command.substring(0,6) == "Delete") {
value = command.substring(6);
value.trim();
LastMessage = deleteFile(value);
if (isSerial || eXi) Serial.println(LastMessage);
return;
}
if (command.substring(0,6) == "rename" || command.substring(0,6) == "Rename") {
value = command.substring(6);
value.trim();
String origin = value.substring(0, value.indexOf(" "));
String target = value.substring(value.indexOf(" ") + 1);
LastMessage = renameFile(origin, target);
if (isSerial || eXi) Serial.println(LastMessage);
return;
}
command.toLowerCase();
if (eXi) Serial.printf(" command: %s\n", command.c_str());
if (command == "prefs") {
LastMessage = "\n Current Settings: ";
Serial.println(LastMessage);
}
if (command.substring(0,3) == "mem") {
Serial.println();
LastMessage = printMemoryInfo(true);
if (isSerial || eXi) Serial.println("\n" + LastMessage);
return;
}
if (command == "e") {
eXi = eXi ? false : true;
prefs.putBool("e", eXi);
command = "ie";
}
if (command == "ie" || command == "prefs") {
LastMessage = " Extended Info: " + (String)(eXi ? "Enabled" : "Disabled");
if (isSerial || eXi) Serial.println(LastMessage);
if (command != "prefs") return;
}
if (command == "to") {
overWrite = overWrite ? false : true;
prefs.putBool("o", overWrite);
command = "io";
}
if (command == "io" || command == "prefs") {
LastMessage = " Overwrite Mode: " + (String)(overWrite ? "Enabled" : "Disabled");
if (isSerial || eXi) Serial.println(LastMessage);
if (command != "prefs") return;
}
if (command == "ta") {
autoSnap = autoSnap ? false : true;
prefs.putBool("a", autoSnap);
command = "ia";
}
if (command == "ia" || command == "prefs") {
LastMessage = " Auto-Snap: " + (String)(autoSnap ? "Enabled" : "Disabled");
if (isSerial || eXi) Serial.println(LastMessage);
if (command != "prefs") return;
}
if (command == "i" || command == "prefs") {
LastMessage = " Snap Interval: " + convertSeconds(delayTime/1000);
if (isSerial || eXi) Serial.println(LastMessage);
if (command != "prefs") return;
}
if (command == "tt") {
timeStampFileNames = timeStampFileNames ? false : true;
prefs.putBool("t", timeStampFileNames);
command = "it";
}
if (command == "it" || command == "prefs") {
LastMessage = " Timestamp file names: " + (String)(timeStampFileNames ? "Enabled" : "Disabled");
if (isSerial || eXi) Serial.println(LastMessage);
if (command != "prefs") return;
}
if (command == "tr") {
remControl = remControl ? false : true;
prefs.putBool("r", remControl);
if (remControl) {
startOnlineFeatures();
} else {
server.close() ;
}
command = "ir";
}
if (command == "ir" || command == "prefs") {
LastMessage = " Remote Control: " + (String)(remControl ? "Enabled" : "Disabled");
if (isSerial || eXi) Serial.println(LastMessage);
if (command != "prefs") return;
}
if (command == "web") {
startOnlineFeatures();
return;
}
if (command == "?" || command == "help") {
LastMessage = getCommands();
if (isSerial || eXi) Serial.println("\n" + LastMessage);
return;
}
if (command == "sensorx") {
getSensorStatus(true, true);
return;
}
if (command == "sensor") {
getSensorStatus(true);
return;
}
if (command == "nvs") {
LastMessage = gatherPrefs(false, false);
LastMessage += gatherSensorPrefs();
if (isSerial || eXi) Serial.println("\n" + LastMessage);
return;
}
if (command == "nvsl") {
LastMessage = gatherPrefs(true, false);
if (isSerial || eXi) Serial.println("\n" + LastMessage);
return;
}
if (command == "ut" || command == "uptime") {
LastMessage = getUptime();
if (isSerial || eXi) Serial.println("\n" + LastMessage);
return;
}
if (command == "backups" || command == "presets") {
prefsSwitch("presets");
String presetsList = prefs.getString("index");
presetsList.replace("\n", "\n ");
LastMessage = "\n Current Presets:\n" + presetsList;
prefsSwitch();
if (isSerial || eXi) Serial.println("\n" + LastMessage);
return;
}
if (command == "backup" ) {
String backupName = "backup" + sensorType;
LastMessage = " Configuration backup: " + saveSensorConfig(backupName.c_str());
if (isSerial || eXi) Serial.println("\n" + LastMessage);
return;
}
if (command.substring(0,6) == "backup") {
String backupName = trimParam(command, 6);
if (presetExists(backupName)) LastMessage = " Overwriting existing backup\n";
LastMessage = " Saving sensor settings to preset " + backupName + ": " + saveSensorConfig(backupName.c_str());
prefsSwitch("presets");
String currentPresets = prefs.getString("index", "\n");
prefs.putString("index", currentPresets + "\n" + backupName);
prefsSwitch();
if (isSerial || eXi) Serial.println("\n" + LastMessage);
return;
}
if (command == "restore" ) {
String backupName = "backup" + sensorType;
LastMessage = " Restoring sensor settings: " + loadSensorConfig(backupName.c_str());
if (isSerial || eXi) Serial.println("\n" + LastMessage);
return;
}
if (command.substring(0,7) == "restore") {
String backupName = trimParam(command, 7);
if (presetExists(backupName)) {
LastMessage = " Restoring sensor settings from preset '" + backupName + "': " + loadSensorConfig(backupName.c_str());
} else {
LastMessage = " No such preset as '" + backupName + "'.";
}
if (isSerial || eXi) Serial.println("\n" + LastMessage);
return;
}
if (command.substring(0,4) == "size") {
uint16_t newSize = trimParam(command, 4).toInt();
if ( newSize < 1 || newSize > maxSize ) newSize = captureSize;
captureSize = newSize;
prefs.putShort("size", captureSize);
command = "iz";
}
if (command == "iz" || command == "prefs") {
LastMessage = " Capture Size: " + frameSizes[captureSize];
if (isSerial || eXi) Serial.println(LastMessage);
if (command != "prefs") return;
}
String eejitMsg = "\n SD CARD IS EJECTED, IDIOT!\n";
eejitMsg += " First insert an SD Card and then use the 'sd' command to initialise.";
if (command == "erase") {
command = "wipe 1-";
}
if (command.substring(0,4) == "wipe") {
if (ejected) {
LastMessage = eejitMsg;
} else {
uint64_t delIDX = 0, delIDZ = UINT64_MAX;
String tmpVals = command.substring(4);
if (tmpVals.indexOf("-") != -1) {
delIDX = tmpVals.substring(0, tmpVals.indexOf("-") ).toInt();
String tmpZVal = tmpVals.substring(tmpVals.indexOf("-")+1);
tmpZVal.trim();
if (tmpZVal != "") delIDZ =tmpVals.substring(tmpVals.indexOf("-")+1).toInt();
String displayTop = (delIDZ == UINT64_MAX) ? "(last file)" : (String)delIDZ;
confirm = "\n Are you absolutely sure you wish to delete files " + (String)delIDX + "-" + displayTop + "?";
if (waitForConfirmation(confirm, 10)) {
LastMessage = "\n Deleting Image Files..\n\n" + listDir(true, false, false, true, false, delIDX, delIDZ);
mostRecentPic = (delIDX == 0) ? 0 : delIDX - 1;
} else {
LastMessage = " Delete Operations Aborted.";
}
} else {
LastMessage = " Ambiguous Delete Command. Aborting..";
}
}
if (isSerial || eXi) Serial.println("\n" + LastMessage);
return;
}
if (command == "free" || command == "space") {
if (ejected) {
LastMessage = eejitMsg;
} else {
LastMessage = printSpace();
}
if (isSerial || eXi) Serial.println("\n" + LastMessage);
return;
}
if (command == "eject" || command == "ej") {
if (ejected) {
LastMessage = eejitMsg;
} else {
SD_MMC.end();
LastMessage = " SD Card Stopped.\n Your SD Card can now be safely removed.";
ejected = true;
}
if (isSerial || eXi) Serial.println("\n" + LastMessage);
return;
}
if (command == "insert" || command == "sd") {
if (!ejected) {
LastMessage = " SD Card is not ejected!";
if (isSerial || eXi) Serial.println(LastMessage);
} else {
ejected = false;
if (startMicroSD()) {
String stateA = "resumed";
if (!autoSnap) stateA = "enabled (note: auto-snap is currently disabled)";
LastMessage = " SD Card started successfully. Image acquisition " + stateA;
} else {
LastMessage = " SD Card Error. Please try again.";
ejected = true;
}
if (isSerial || eXi) Serial.println("\n" + LastMessage);
}
return;
}
if (command == "test" || command == "benchmark") {
if (ejected) {
LastMessage = eejitMsg;
} else {
LastMessage = benchmarkSD();
}
if (isSerial || eXi) Serial.println(LastMessage);
return;
}
if (command == "!") {
if (ejected) {
LastMessage = eejitMsg;
if (isSerial || eXi) Serial.println(LastMessage);
} else {
LastMessage = "SNAP!";
mostRecentPic = 0;
}
return;
}
if (command == ".") {
if (ejected) {
LastMessage = eejitMsg;
} else {
LastMessage = "SNAP!";
instantPic();
}
return;
}
if (command == "sleep") {
goToSleep();
return;
}
if (command == "ts" || command == "togglesleep") {
doSleep = doSleep ? false : true;
prefs.putBool("z", doSleep);
command = "is";
}
if (command == "is" || command == "prefs") {
LastMessage = " Auto-Sleep: " + (String)(doSleep ? "Enabled" : "Disabled");
if (isSerial || eXi) Serial.println(LastMessage);
if (command != "prefs") return;
}
if (command == "td" || command == "toggledeep" ) {
deepSleep = deepSleep ? false : true;
prefs.putBool("s", deepSleep);
command = "id";
}
if (command == "id" || command == "prefs") {
LastMessage = " Sleep Mode: " + (String)(deepSleep ? "Deep Sleep" : "Light Sleep");
if (isSerial || eXi) Serial.println(LastMessage);
if (command != "prefs") return;
}
if (command == "reset") {
confirm = "\n Are you ABSOLUTELY SURE you wish to WIPE your NVS-saved settings?";
if (waitForConfirmation(confirm)) {
LastMessage = "\n DESTRUCTION OF NVS SETTINGS COMMENCING..\n We will now reboot to hard-coded defaults.\n";
LastMessage += "\n Backup sensor settings are unaffected.";
prefs.clear();
command = "x";
} else {
LastMessage = " Settings Destruction Sequence Aborted.";
return;
}
}
if (command == "nvswipe") {
confirm = "\n Are you ABSOLUTELY SURE you wish to COMPLETELY WIPE your NVS partition?";
if (waitForConfirmation(confirm)) {
LastMessage = "\n DESTRUCTION OF NVS PARTITION AND ALL SETTINGS COMMENCING..\n Enjoy your blank slate!";
if (isSerial || eXi) Serial.println(LastMessage);
WipeNVRAM();
command = "x";
} else {
LastMessage = " NVS Destruction Sequence Aborted.";
return;
}
if (isSerial || eXi) Serial.println(LastMessage);
}
if (command == "x" || command == "reboot") {
storeUTC();
LastMessage += "\n Rebooting..";
if (isSerial || eXi) Serial.println(LastMessage);
server.close();
Serial.flush();
SD_MMC.end();
prefs.end();
ESP.restart();
}
if (command == "l" || command == "list") {
if (ejected) {
LastMessage = eejitMsg;
if (isSerial || eXi) Serial.println(LastMessage);
return;
} else {
bool quickListingsTMP = quickListings;
if (command == "list") {
quickListings = false;
}
LastMessage = listDir(true);
if (isSerial || eXi) Serial.println(LastMessage);
if (command == "list") {
quickListings = quickListingsTMP;
command = "free";
}
}
}
if (command == "ql") {
LastMessage = "\n" + quickList(true);
if (isSerial || eXi) Serial.println(LastMessage);
return;
}
if (command == "qla") {
LastMessage = "\n" + quickList(true, true);
if (isSerial || eXi) Serial.println(LastMessage);
return;
}
if (command == "nq") {
LastMessage = "\n Computing. Please wait...\n" + notQuickLst();
if (isSerial || eXi) Serial.println(LastMessage);
return;
}
if (command == "nqa") {
LastMessage = "\n Computing. Please wait...\n" + notQuickLst(true);
if (isSerial || eXi) Serial.println(LastMessage);
return;
}
if (command == "tq") {
quickListings = quickListings ? false : true;
prefs.putBool("q", quickListings);
command = "iq";
}
if (command == "iq" || command == "prefs") {
LastMessage = " Quick Listing Mode: " + (String)(quickListings ? "Enabled" : "Disabled");
if (isSerial || eXi) Serial.println(LastMessage);
if (command != "prefs") return;
}
if (command == "dd") {
displayDetails = displayDetails ? false : true;
prefs.putBool("p", displayDetails);
command = "ip";
}
if (command == "ip" || command == "prefs") {
LastMessage = " Display Details (date & size): " + (String)(displayDetails ? "Enabled" : "Disabled");
if (isSerial || eXi) Serial.println(LastMessage);
return;
}
if (command.substring(0,4) == "skip") {
if (command.indexOf("=") != -1) {
LastMessage = " You do not need an '=' sign in this command. Simply do: skip50";
} else {
if (sShowPlaying) {
sssSkip = command.substring(4).toInt();
LastMessage = " Streaming SlideShow Skipping " + (String)sssSkip + " images";
} else {
LastMessage = " Streaming SlideShow is not playing!";
}
}
if (isSerial || eXi) Serial.println(LastMessage);
return;
}
if (command == "sp") {
if (sShowPlaying) {
sssPause = sssPause ? false : true;
LastMessage = " Streaming SlideShow " + (String)(sssPause ? "Paused" : "Resumed");
} else {
LastMessage = " Streaming SlideShow is not playing!";
}
if (isSerial || eXi) Serial.println(LastMessage);
return;
}
if (command == "next") {
if (sShowPlaying) {
sssSkip = -1;
LastMessage = " Streaming SlideShow Skipping to next image";
} else {
LastMessage = " Streaming SlideShow is not playing!";
}
if (isSerial || eXi) Serial.println(LastMessage);
return;
}
if (command == "record") {
doRecording = true;
simpleStreamRecord();
return;
}
if (command == "stop") {
doRecording = false;
return;
}
if (command == "wtf") {
if (isSerial || eXi) Serial.println(LastMessage);
return;
}
if (command == "tm") {
time_t now;
struct tm timeDetails;
time(&now);
localtime_r(&now, &timeDetails);
Serial.print("The current date and time is ");
Serial.println(&timeDetails, "%A, %B %d %Y %H:%M:%S");
}
if (command == "t") {
if (getLocalTime(&timeinfo)) {
char buffer[128] = {'\0'};
strftime(buffer, sizeof(buffer), " Time: %H:%M:%S, %A %B %d %Y", &timeinfo);
LastMessage = (String)buffer;
if (isSerial || eXi) Serial.println("\n" + LastMessage);
} else {
LastMessage = " Failed to parse time!";
if (isSerial || eXi) Serial.println(LastMessage);
}
return;
}
if (command == "time") {
struct timeval tv;
time_t t;
struct tm *timeStruct;
char buffer[256] = {'\0'};
gettimeofday(&tv, NULL);
t = tv.tv_sec;
timeStruct = localtime(&t);
strftime(buffer, sizeof(buffer), " Today is %A, %B %d. And what a wonderful day!\n"
" The time is precisely %I:%M:%S %p.\n\n"
" Local Time: %c\n", timeStruct);
LastMessage = (String)buffer;
if (isSerial || eXi) Serial.println("\n" + LastMessage);
return;
}
if (command.substring(0,4) == "time") {
value = command.substring(5);
value.trim();
if (setStoredTime(value.toInt())) {
LastMessage = " Setting time and date..";
if (isSerial || eXi) Serial.println("\n" + LastMessage);
}
xCommand = "t";
return;
}
if (cmd == 'c' ) {
String cameraSetting = command.substring(1);
String setVar = cameraSetting.substring(0, cameraSetting.indexOf("="));
String setVal = cameraSetting.substring(cameraSetting.indexOf("=") + 1);
setVar.trim(); setVal.trim();
setSensorParameters(setVar, setVal);
return;
}
if ( (cmd == 's' || cmd == 'm' || cmd == 'h' || cmd == 'd') && value != "") {
String plural = "";
if (intValue != 1) plural = "s";
String type = "second";
uint64_t multiplier = 1000;
if (cmd == 'm') {
multiplier = MINUTE_MILLIS;
type = "minute";
}
if (cmd == 'h') {
multiplier = HOUR_MILLIS;
type = "hour";
}
if (cmd == 'd') {
multiplier = DAY_MILLIS;
type = "day";
}
if (intValue != 0 || value == "0") {
LastMessage = " Snap Interval changed to " + (String)intValue + " " + type + plural;
if (isSerial || eXi) Serial.println(LastMessage + "\n");
delayTime = (intValue * multiplier);
prefs.putULong64("d", delayTime);
}
return;
}
if (cmd == 'b' && value != "") {
if (command.indexOf("=") != -1) {
LastMessage = " You do not need an '=' sign in this command. Simply do: b2";
} else {
ledBrightness = intValue;
LastMessage = " LED Brightness set to: " + (String)ledBrightness;
prefs.putUChar("b", ledBrightness);
flashLED(600) ;
}
if (isSerial || eXi) Serial.println(LastMessage);
}
if (command == "") {
LastMessage = getCommands();
if (isSerial || eXi) Serial.println("\n" + LastMessage);
}
}
}
if (ABORT != "0") {
if (!abortUserNotified) {
Serial.printf(" ABORT! Reason: %s\n", ABORT.c_str());
abortUserNotified = true;
}
return;
}
if (doSleep && deepSleep && delayTime > 20000 && currentTime > (lastSnapTime + (deepSleepDelay * 1000)) ) goToSleep();
if (!ejected) {
if ( (SD_MMC.totalBytes() - SD_MMC.usedBytes() - levellingBytes ) < _1MB_) {
if (overWrite && !timeStampFileNames) {
saveNumber = 0;
} else {
ABORT = " Out of Space!";
return;
}
}
if (autoSnap) {
if (mostRecentPic < 1) lastSnapTime = currentTime - delayTime - 1000;
if (currentTime < (lastSnapTime + delayTime) ) return;
lastSnapTime = currentTime;
saveNumber++;
mostRecentPic = saveNumber;
String filename = makeFilename(saveNumber);
if (filename != "") {
if (!takePhoto(filename)) {
Serial.println(" Problem capturing image! Please investigate.");
}
} else {
Serial.println(" Could not create a valid file name! Please investigate.");
}
}
}
delay(1);
if (doSleep && !deepSleep && delayTime > 3000) goToSleep();
}
void setup() {
Serial.begin(115200);
Serial.println("\n Welcome to TL-CAM!");
Serial.println("\n Retrieving preferences from NVRAM..\n");
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);
prefs.begin("TLCam");
if (prefs.getULong64("d") != 0) {
delayTime = prefs.getULong64("d");
} else {
if (delaySeconds != 0) {
delayTime = delaySeconds * 1000;
} else {
delayTime = delayMinutes * MINUTE_MILLIS;
}
}
String prefsString = gatherPrefs(true);
if (eXi) Serial.print(prefsString);
if (!startMicroSD()) {
ejected = true;
Serial.println(" SD INIT Failed.\n Insert an SD card and issue an 'sd' command to continue.");
}
if (!startCamera()) ABORT = " Camera INIT Failed.\n Image acquisition will not be possible.";
startOnlineFeatures();
averageSize = avgSizes[captureSize];
Serial.println("\n Send 'help' or '?' for a list of available commands.\n" );
Serial.flush();
ledcSetup(ledChannel, ledFreq, ledResolution);
ledcAttachPin(FLASH_PIN, ledChannel);
}