Turn ESP32-CAM Into a Snapshot Taker, for Selfies and Time Lapse Pictures

by Trevor Lee in Circuits > Arduino

442 Views, 6 Favorites, 0 Comments

Turn ESP32-CAM Into a Snapshot Taker, for Selfies and Time Lapse Pictures

snapper-main.png

This project is an attempt to turn ESP32-CAM (or similar ones like LILYGO T-Camera / T-Camera Plus) microcontroller board into an Android phone-managed snapshot taker, for snapshots like selfies and time-lapse pictures ... maybe ... just for fun!

With your Android phone, connect to the ESP32-CAM, make your post and smile. Then go back to your phone to select the pictures you like best and save to your phone directly.
With your Android phone, connect to the ESP32-CAM, set how frequently a snapshot is to be taken. Then disconnect from the ESP32-CAM and let it do its job of taking time-lapse pictures. Then reconnect to the ESP32-CAM to transfer the taken pictures to your phone.


The microcontroller program (sketch) is developed with Arduino framework using VS Code and PlatformIO, in the similar fashion as described by the post -- A Way to Run Arduino Sketch With VSCode PlatformIO Directly

The remote UI -- control panel -- is realized with the help of the DumbDisplay Android app. You have a choice of using Bluetooth (classic) or WiFi for the connection. For a brief description of DumbDisplay, please refer to the post -- Blink Test With Virtual Display, DumbDisplay

Please note that the UI is driven by the sketch; i.e., the flow of the UI is programmed in the sketch. Other than installing the DumbDisplay Android app, no building of a separate mobile app is necessary.

The UI -- Ways of Taking Snapshots

snapper-ss-00.jpg
snapper-ss-01.jpg
snapper-ss-02.jpg
snapper-ss-03.jpg
dd-settings-menu.jpg
dd-prepare-store.jpg
snapper-folder.gif

There are 3 ways snapshots can be captured and saved to your phone

  1. With your phone connected to the ESP32-CAM -- certainly through the DumbDisplay Android app with Bluetooth or WiFi -- you click the 💾Save button of the UI to save the image being shown. You can turn on / off the auto-save feature by clicking Auto❎ / Auto☑️. With auto-save enabled, whenever a snapshot is captured from the ESP32-CAM, it will be saved to your phone
  2. Assuming you have connected your phone with the ESP32-CAM, clicking on the image canvas will pause showing snapshots, and you will be provided with a slider to slide back in time to select the previously captured snapshots to save to your phone. You will have 20 such snapshots that you can slide back in time to. Once you see the snapshots you like, click 💾Save to save it. When you are done, you can go back by clicking ❌Cancel (or double click on the image canvas)
  3. Select Offline📴 to enable "offline" capturing and saving of snapshots to the ESP32-CAM. With "offline" enabled, when you disconnect (i.e. offline), the ESP32-CAM will start capturing "offline" snapshots saving them to its flash memory (or SD card). Whenever you reconnect, you will be asked if you want to transfer the saved "offline" snapshots to your phone. Better yet, in SD card case, you can physically transfer the "offline" snapshot JPEG files saved the usual way -- insert the SD card to your computer, copy and delete the JPEG files on your SD card. One point about using the SD card slot of ESP32-CAM -- since the SD card module of ESP32-CAM shares the same pin 4 used by the flashlight, whenever the SD card is accessed, the flashlight will light up bright (hence it is not a feature, but it is how it is)

The snapshots will be saved in JPEG format:

  1. Snapshots saved to your phone will be saved to the private folder of DumbDisplay app - /<main-storage>/Android/data/nobody.trevorlee.dumbdisplay/files/Pictures/DumbDisplay/snaps/ -- with file name like 20240904_223249_001.jpg (YYYYMMDD-hhmmss_seq.jpg)
  2. Offline snapshots transferred to your phone will be saved to a subfolder of the above-mentioned private folder of DumbDisplay app. The name of the subfolder depends on the time you do the snapshots transfer, and is like 20240904_231329_OFF.
  3. Offline snapshots are saved to your ESP32-CAM's flash memory / SD card with name like off_001.jpg. Note that the "offline" JPEG files will only be properly time-stamped if there was no reset / reboot of ESP32-CAM after connecting to your phone.

By default, the storage for DumbDisplay app is a private folder, which DumbDisplay app needs to initialize. You can do this By selecting the Settings menu item of DumbDisplay app, and clicking on the Media Storage button

And you can use a folder manager app, like Files by Marc apps & software to browse to that folder

The UI -- Frequency of Capturing Snapshots

Actually, snapshot capturing is continuous (as fast as possible), but captured snapshot shipments to your phone is not as smooth; in fact there is a setting on how frequently a snapshot is shipped to your phone. Depending on the resolution / quality of the snapshot and the connection method / condition, it can be as frequent as 5 snapshots per second. Treat this frequency control as a feature that allows taking of time-lapse pictures at a desired frequency 😁

There are 4 quick selections of frequency / frame rate -- 5 PS / 2 PS / 1 PS / 30 PM corresponding to 5 frames per second / 2 frames per second / 1 frame per second / 30 frames per minute

And there is a custom per-hour frame rate you can set and select -- 720 PH (720 is the default). Once you click on the 720 PH selection, your phone's virtual keyword will pop up allowing you to enter the value (1 - 3600) you want (an empty value means previously set value).

The UI -- Snapshots Quality Adjustments

There are several camera adjustments that will affect the quality of the captured snapshots:

  1. Snapshot resolution / size selections:
  2. QVGA (320x240)
  3. VGA (640x480)
  4. SVGA (800x600)
  5. XGA (1024x768)
  6. HD (1280x720)
  7. SXGA (1280x1024)
  8. UXGA (1600x1200)
  9. JPEG compression quality slider 🖼️ -- from 5 (high quality) to 60 (low quality)
  10. Brightness slider ☀️ -- from -2 (dim) to 2 (bright)

Note that the higher the snapshots quality / resolution, the more data needs be shipped from the ESP32-CAM to your phone, making the UI less responsive, especially when WiFi (not Bluetooth) since WiFi is not full-duplex.

The UI -- ESP32 Idle Sleep

Another feature is that when "offline" snapshot capturing is enabled, after a while (60 seconds) of being idle (i.e. not connected), the ESP32-CAM will be put to sleep mode in order to save power. Of course, since "offline" capturing is enabled, the ESP32-CAM will wake up in due time to take a snapshot. However if "offline" frequency is too high, higher than 12 frames per minute, the ESP32-CAM will stay on.

In any case, if you want to connect to the ESP32-CAM when it is in sleep mode, you will need to reset / reboot the ESP32-CAM first

Developing and Building

As mentioned previously, the sketch will be developed using VS Code and PlatformIO. Please clone the PlatformIO project ESP32CamSnapper GitHub repository.

The configurations for developing and building of the sketch are basically written down in the platformio.ini file

[env]
monitor_speed = 115200

[env:ESP32CAM]
...
build_flags = -D FOR_ESP32CAM

[env:TCAMERA] ; v7
...
build_flags = -D FOR_TCAMERA

[env:TCAMERAPLUS]
...
build_flags = -D FOR_TCAMERAPLUS

Please make sure you select the correct PlatformIO project environment -- ESP32CAM / TCAMERA / TCAMERAPLUS

The program entry point is src/main.cpp

// *** use Bluetooth with the following device name
#define BLUETOOTH "ESP32CamSnapper"
#include "esp32camsnapper/esp32camsnapper.ino"

The above main.cpp assumes that you prefer to use Bluetooth (classic), and defines the Bluetooth device name to be ESP32CamSnapper

#define BLUETOOTH "ESP32CamSnapper"

Note that, for the purposes of this sketch, Bluetooth (classic) is the suggested way of connecting with DumbDisplay app.

However, if you prefer to use WiFi, you will need to define WIFI_SSID and WIFI_PASSWORD rather than BLUETOOTH

//#define BLUETOOTH "ESP32CamSnapper"
#define WIFI_SSID "your-wifi-ssid"
#define WIFI_PASSWORD "your-wifi-password"

In case of WiFi, you will need to find out the IP of your ESP32-CAM in order to make connect to it. This can be done easily by attaching it to a Serial monitor (set to baud-rate 115200). There, when your ESP32-CAM tries to connect to DumbDisplay app, log lines like below will be printed:

binded WIFI TrevorWireless
listening on 192.168.0.155:10201 ...
listening on 192.168.0.155:10201 ...
listening on 192.168.0.155:10201 ...

That IP address is the IP address of your ESP32-CAM to make connection to.

If you have SD card installed for ESP32-CAM (or LILYGO T-Camera Plus), you can use it to store "offline" snapshots, rather than the limited flash memory of the microcontroller board.

To configure the sketch for such setup, you will need to define the macro OFFLINE_USE_SD, like by uncomment the corresponding line of the sketch

// *** uncomment the following if offline snaps are to be stored to SD (ESP32CAM / TCameraPlus), rather than LittleFS
#define OFFLINE_USE_SD

or put the #define line in main.cpp like

#define OFFLINE_USE_SD
#define BLUETOOTH "ESP32CamSnapper"
#include "esp32camsnapper/esp32camsnapper.ino"

The Sketch

The sketch of the project is esp32camsnapper/esp32camsnapper.ino. There are several customizations you may want to make:

  1. By default, you have 20 snapshots you can go back in time to. This number is controlled by the macro STREAM_KEEP_IMAGE_COUNT
// *** maximum number of images to keep cached (on app side) when streaming snaps to DD app; this number affects how much you can go back to select which image to save
#define STREAM_KEEP_IMAGE_COUNT 20
  1. By default, you can enable "offline" snapshot capturing. If you don't want "offline" at all, you can comment out the corresponding #define
// *** support offline snap taking; comment out if not desired
#define SUPPORT_OFFLINE
  1. By default, the ESP32-CAM will be put to sleep after 60 seconds being idle (i.e. not connected). You can change it to other value by changing the macro IDLE_SLEEP_SECS. If this 'idle put to sleep' behavior is not desirable, you can comment out that #define to disable such behavior
// *** number of seconds to put ESP to sleep when idle (not connected); if ESP went to sleep, will need to reset it in order to connect; comment out if not desired
#define IDLE_SLEEP_SECS 60
  1. Most settings you change in the UI will be persisted to the EEPROM of the ESP32-CAM. These settings will be effective even you re-upload the program built. In case you want to reset those settings to their defaults, change the HEADER to some other value, build and re-upload
// *** if want to reset settings / offline snap metadata saved in EEPROM, set the following to something else
const int32_t HEADER = 20240910;


Sketch Highlight -- EEPROM

Firstly, in order to use the EEPROM, you will first need to reserve some number of bytes for it, like: void setup() { Serial.begin(115200); EEPROM.begin(32); ... } Here, 32 bytes are reserved for the purpose.

Most settings are persisted to the EEPROM of ESP32-CAM like

EEPROM.writeLong(0, HEADER);
EEPROM.writeChar(4, cameraBrightness);
EEPROM.writeBool(5, cameraVFlip);
EEPROM.writeBool(6, cameraHMirror);
EEPROM.writeChar(7, currentCachePMFrameRateIndex);
EEPROM.writeChar(8, currentFrameSizeIndex);
EEPROM.writeBool(9, enableOffline);
EEPROM.writeShort(10, customCachePHFrameRate);
EEPROM.writeBool(12, autoSave);
EEPROM.writeChar(13, cameraQuality);
EEPROM.commit();
Note that:
  1. Long is 4 bytes in size, and can be used for int32_t
  2. Char is 1 byte in size, and can be used for int8_t
  3. Bool is also 1 byte in size, and can be used for bool
  4. Short is 2 bytes in size, and can be used for int16_t

And these settings are read back when ESP32-CAM starts up like

int32_t header = EEPROM.readLong(0);
if (header != HEADER) {
 dumbdisplay.logToSerial("EEPROM: not the expected header ... " + String(header) + " vs " + String(HEADER));
 return;
}
cameraBrightness = EEPROM.readChar(4);
cameraVFlip = EEPROM.readBool(5);
cameraHMirror = EEPROM.readBool(6);
currentCachePMFrameRateIndex = EEPROM.readChar(7);
currentFrameSizeIndex = EEPROM.readChar(8);
enableOffline = EEPROM.readBool(9);
customCachePHFrameRate = EEPROM.readShort(10);
autoSave = EEPROM.readBool(12);
cameraQuality = EEPROM.readChar(13);

Sketch Highlight -- ESP32 Sleep Mode

ESP32 can be put to sleep like

esp_sleep_enable_timer_wakeup(sleepTimeoutMillis * 1000); // in micro second
esp_deep_sleep_start();

When it wakes up, the sketch / program will be just like freshly started, except for variables like

RTC_DATA_ATTR int32_t tzMins = 0;
RTC_DATA_ATTR int64_t wakeupOfflineSnapMillis = -1;

Note that the variable prefixed with RTC_DATA_ATTR will keep its value through sleep.

Sketch Highlight -- Camera

The ESP32-CAM Camera is initialized with initializeCamera

bool initializeCamera(framesize_t frameSize, int jpegQuality) {
 esp_camera_deinit(); // just in case, disable camera first
 delay(50);
 camera_config_t config;
 config.ledc_channel = LEDC_CHANNEL_0;
 config.ledc_timer = LEDC_TIMER_0;
 ...
 config.pixel_format = PIXFORMAT_JPEG;
 config.frame_size = frameSize;
 config.jpeg_quality = jpegQuality;
 config.fb_count = 1;
 ...
 esp_err_t camerr = esp_camera_init(&config); // initialize the camera
 if (camerr != ESP_OK) {
  dumbdisplay.logToSerial("ERROR: Camera init failed with error -- " + String(camerr));
 }
 resetCameraImageSettings();
 return (camerr == ESP_OK);
}

It is important that `pixel_format` be set to `PIXFORMAT_JPEG`. Also notice that at the end of initializeCamera, resetCameraImageSettings is called to set the various settings of the Camera, which is also called when you change camera settings with the UI

bool resetCameraImageSettings() {
 sensor_t *s = esp_camera_sensor_get();
 if (s == NULL) {
  dumbdisplay.logToSerial("Error: problem reading camera sensor settings");
 return 0;
 }
 ...
 s->set_brightness(s, cameraBrightness); // (-2 to 2) - set brightness
 s->set_quality(s, cameraQuality); // set JPEG quality (0 to 63)
 ...
 return 1;
}

Concerning capturing snapshots, here is how an "offline" snapshot is taken when ESP32-CAM wakes up from sleep

void setup() {
 Serial.begin(115200);
 EEPROM.begin(32);
 initRestoreSettings();
 initializeStorage();
 ...
 if (wakeupOfflineSnapMillis != -1) { // wakeupOfflineSnapMillis will be -1 if ESP is reset
  String localTime;
  long now = getTimeNow(&localTime);
  Serial.println("*** woke up for offline snap ... millis=" + localTime + " ***");
  framesize_t frameSize = cameraFrameSizes[currentFrameSizeIndex];
  if (initializeCamera(frameSize, cameraQuality)) {
   delay(SLEEP_WAKEUP_TAKE_SNAP_DELAY_MS);
   if (true) {
    // it appears that the snap taken will look better if the following "empty" steps are taken
    camera_fb_t* camera_fb = esp_camera_fb_get();
    esp_camera_fb_return(camera_fb);
   }
   camera_fb_t* camera_fb = esp_camera_fb_get();
   saveOfflineSnap(camera_fb->buf, camera_fb->len);
   esp_camera_fb_return(camera_fb);
  } else {
   Serial.println("failed to initialize camera for offline snapping");
  }
  Serial.println("*** going back to sleep ... timeout=" + String(wakeupOfflineSnapMillis /  1000.0) + "s ***");
  Serial.flush();
  esp_sleep_enable_timer_wakeup(wakeupOfflineSnapMillis * 1000); // in micro second
  esp_deep_sleep_start();
  // !!! the above call will not return !!!
 }
 ...
}

Notice:

  1. the value of wakeupOfflineSnapMillis is used to determine whether the program is started due to normal boot up of wake up; it is set to the value of "how many milliseconds to sleep for next "offline" snap
  2. certain, initializeCamera will be called to initialize the camera
  3. after initializing the camera, the camera is given some time (2 seconds) to be ready
  4. then the camera's buffer is retrieved by calling esp_camera_fb_get, which contains the bytes (JPEG bytes) to be saved by calling saveOfflineSnap
  5. after saving the bytes, the buffered is returned by calling esp_camera_fb_return
  6. at last, it goes back to sleep again

You will find that the tutorial Change ESP32-CAM OV2640 Camera Settings: Brightness, Resolution, Quality, Contrast, and More by Random Nerd Tutorials gives more details on ESP32-CAM.

Sketch Highlight -- DumbDisplay

Like all other use cases of using DumbDisplay, you first declare a global DumbDisplay object dumbdisplay

#if defined(BLUETOOTH)
 #include "esp32dumbdisplay.h"
 DumbDisplay dumbdisplay(new DDBluetoothSerialIO(BLUETOOTH));
#elif defined(WIFI_SSID)
 #include "wifidumbdisplay.h"
 DumbDisplay dumbdisplay(new DDWiFiServerIO(WIFI_SSID, WIFI_PASSWORD),  DD_DEF_SEND_BUFFER_SIZE, 2 * DD_DEF_IDLE_TIMEOUT);
#else
 #include "dumbdisplay.h"
 DumbDisplay dumbdisplay(new DDInputOutput());
#endif
In WiFi case, normally, you will use the defaults for sendBufferSize (2nd parameter) and idleTimeout (3rd parameter). However for this program, since a large amount of data might be shipped to DumbDisplay app, the idleTimeout better be longer than the default.

Then, several global helper objects / pointers are declared

DDMasterResetPassiveConnectionHelper pdd(dumbdisplay, true);
GraphicalDDLayer* imageLayer;
JoystickDDLayer* frameSliderLayer;
SelectionDDLayer* frameSizeSelectionLayer;
...
BasicDDTunnel* generalTunnel;
The construction of DDMasterResetPassiveConnectionHelper accepts two parameters
  1. The DumbDisplay object
  2. Whether to call DumbDisplay::recordLayerCommands() / DumbDisplay::playbackLayerCommands() before and after calling "initialize" lambda expression. The effect of calling DumbDisplay::recordLayerCommands() / DumbDisplay::playbackLayerCommands() is so the UI construction of the various layers is more smooth.

The life-cycle of the above DumbDisplay layers and "tunnels" are managed by the global pdd object, which will monitor connection and disconnection of DumbDisplay app, calling appropriate C++ lambda expressions in the appropriate time. It is cooperatively given "time slices" in the loop() block like

void loop() {
 pdd.loop([]() {
  initializeDD();
 }, []() {
  if (updateDD(!pdd.firstUpdated())) {
   saveSettings();
  }
 }, []() {
  deinitializeDD();
 });
 if (pdd.isIdle()) {
  handleIdle(pdd.justBecameIdle());
 }
}
  1. pdd.loop() accepts 3 C++ lambda expressions -- []() { ... }
  2. the 1st lambda expression is called when DumbDisplay app is connected, and DumbDisplay components need be initialized. Here initializeDD() is called for the purpose.
  3. the 2nd lambda expression is called during DumbDisplay app is connected to update the DumbDisplay components. Here, updatedDD() for the purpose. Note that pdd.firstUpdated() tells if this 2nd lambda expression was previously called after DumbDisplay component initialization (hence not of that means first call)
  4. the 3rd optional lambda expression is called when DumbDisplay app is disconnected. Here, deinitializeDD() is called for the purpose.
  5. After pdd.loop() is the code that will be run whether DumbDisplay app is connected or not.
  6. pdd.isIdle() tells if DumbDisplay is not connected (i.e. idle). In idle case, the function handleIdle() is called. Note that pdd.justBecameIdle() tells if DumbDisplay just disconnected making it idle.

The first time when the ESP32-CAM is connected, ESP32-CAM's clock is synchronized with that of your phone via generalTunnel -- a "general tunnel"

void initializeDD() {
 ...
 // *** please refer to the source in the repository ***
 ...
}
...
bool updateDD(bool isFirstUpdate) {
 ...
 // *** please refer to the source in the repository ***
 ...
}
Note that syncing and setting of the ESP32-CAM's clock is only done once when the ESP32-CAM connects with your phone. Also note that the ESP32-CAM's clock will keep running during sleep.

Other than the main scene / state, the UI has other scenes, like for selecting snapshots to save, and for transferring "offline" snapshots. When switching between the different scenes, the UI layers will be re-auto-pined by calling pinLayers

void pinLayers(State forState) {
if (forState == STREAMING) {
 dumbdisplay.configAutoPin(DDAutoPinConfig('V')
 .beginGroup('H')
 .addLayer(frameSizeSelectionLayer)
 ...
 .addLayer(saveButtonLayer)
 .endGroup()
 .build(), true);
} else if (forState == CONFIRM_SAVE_SNAP) {
 dumbdisplay.configAutoPin(DDAutoPinConfig('V')
 .beginGroup('H')
 .addLayer(imageLayer)
 ...
 .endGroup()
 .build(), true);
} else if (forState == TRANSFER_OFFLINE_SNAPS) {
 dumbdisplay.configAutoPin(DDAutoPinConfig('V')
 .addLayer(imageLayer)
 .addLayer(progressLayer)
 .addLayer(cancelButtonLayer)
 .build(), true);
}
}
The last parameter of configAutoPin is autoShowHideLayers. Which if true, auto-pinning will automatically hide all other not-involved layers. This relieves the need to explicitly hide all layers not involved in different scenes of the UI.

Selection and prompting for custom frame rate might be worth highlighting here. It is accomplished with the customFrameRateSelectionLayer layer

fb = customFrameRateSelectionLayer->getFeedback();
if (fb != NULL) {
 if (fb->type == CUSTOM) {
  int phFrameRate = fb->text.isEmpty() ? customCachePHFrameRate : fb->text.toInt();
  if (phFrameRate >= 1 && phFrameRate <= 3600) {
   customCachePHFrameRate = phFrameRate;
   ...
  } else {
   dumbdisplay.log("invalid custom frame rate!", true);
  }
 } else {
  customFrameRateSelectionLayer->explicitFeedback(0, 0, "'per-hour' frame rate; e.g. " +   String(customCachePHFrameRate) , CUSTOM, "numkeys");
 }
}
  1. normally the type of "feedback" (fb->type) from the UI will not be CUSTOM
  2. in such a non-CUSTOM case (i.e. "feedback" is because of clicking), call customFrameRateSelectionLayer->explicitFeedback like above
  3. explicitFeedback will explicitly fake a "feedback" to the layer
  4. but this time, the type of "feedback" is CUSTOM -- the 4th parameter
  5. the first 3 parameters are for x, y, and text of the "feedback"
  6. the last parameters -- option to explicitFeedback numkeys -- means that during the "feedback" routing, you will be prompted for numeric value with your phone's virtual keyboard, which will then replace the text of the "feedback"
  7. note that the initial text passed in to explicitFeedback is used as the "hint" on the virtual keyboard
  8. in CUSTOM case, fb->text will be the value you entered

Another opportunity of prompting is confirmation for transferring "offline" snapshots to your phone. It is accomplished via generalTunnel like

bool updateDD(bool isFirstUpdate) {
 ...
 // *** please refer to the source in the repository ***
 ...
}
  1. Right after gotten the "sync time" from DumbDisplay app, and when need to prompt for "yes/no" confirmation for transferring "offline" snaps, generalTunnel->reconnectTo is called to handle the "confirm" request.
  2. The "Yes" or "No" response will come back through generalTunnel again (the same way as DD_CONNECT_FOR_GET_DATE_TIME)

After the "offline" transfer, an "alert" is popped up indicating the transfer is done. This is done by calling dumbdispaly.alert

bool updateDD(bool isFirstUpdate) {
 ...
 // *** please refer to the source in the repository ***
 ...
}

Build and Upload

Build, upload the sketch and try it out!

If it interests you, you may use a Serial monitor (set to baud-rate 115200) to observe when things are happening

Initialize offline snap storage ...
... offlineSnapCount=3 ...
... existing STORAGE ...
$ total: 896 KB
$ used: 180 KB
$ free: 716 KB (79.91%)
... offline snap storage ...
- startOfflineSnapIdx=0
- offlineSnapCount=3
... offline snap storage initialized
bluetooth address: 84:0D:8E:D2:90:EE
*** just became idle ... millis=1067 ***
bluetooth address: 84:0D:8E:D2:90:EE
bluetooth address: 84:0D:8E:D2:90:EE
bluetooth address: 84:0D:8E:D2:90:EE
...
**********
* _SendBufferSize=128
**********
*** just connected ... millis=7133 ***
- got sync time 2024-09-07-10-17-41-+0800
. tz_mins=480
Initializing camera ...
... initialized camera!
...
*** just became idle ... millis=93898(Saturday, September 07 2024 10:19:07+0800) ***
bluetooth address: 84:0D:8E:D2:90:EE
! written offline snap to [/off_000.jpg] ... size=48.39KB
bluetooth address: 84:0D:8E:D2:90:EE
bluetooth address: 84:0D:8E:D2:90:EE
...
*** going to sleep ... millis=153898(Saturday, September 07 2024 10:20:07+0800) ***
...
Initialize offline snap storage ...
... offlineSnapCount=2 ...
... existing STORAGE ...
$ total: 896 KB
$ used: 112 KB
$ free: 784 KB (87.50%)
... offline snap storage ...
- startOfflineSnapIdx=0
- offlineSnapCount=2
... offline snap storage initialized
*** woke up for offline snap ... millis=542(Saturday, September 07 2024 10:20:33+0800) ***
! written offline snap to [/off_002.jpg] ... size=78.91KB
*** going back to sleep ... timeout=27.20s ***
...


Demo

Demo: Turn ESP32-CAM into a Snapshot Taker, for Selfies and Time Lapse Pictures

Enjoy!

Hope that you will have fun with it! Enjoy!

Peace be with you! May God bless you! Jesus loves you! Amazing Grace!