First place I’d look is at those “3000mAh 18650 lico cells”. A 3000mAh 18650 should be heavy with a fill of lithium and other things.
I found that when I bought those miracle mAh batteries at some incredible price… Buyer be ware.
Next, the ESP32 has 3 cores. All the modules of the ESP32 can be shut down, leaving just the RTC and ULP processor with power. The RTC can be set to wake up the ULP, the ULP do its thing, and either go back to sleep or wake up the rest of the ESP32 module to do the thing.
This is a link to the ESP32 API, https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/index.html that will have info on the ULP.
Deep Sleep Wake Stubs https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/deep-sleep-stub.html
ULP coprocessor (Legacy GNU Make) https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/ulp-legacy.html
The current Arduino IDE ESP32 core uses the Legacy GNU make format.
Here is an example to program the ULP to blink the on-board LED:
#include "esp32/ulp.h"
#include "driver/rtc_io.h"
void ULP_BLINK_RUN(uint32_t us);
setup()
{
ULP_BLINK_RUN(100000);
}
void ULP_BLINK_RUN(uint32_t us)
{
int memPortState = 8000; // memory address outside of program space to use
int memCounts = 8001; // memory address to hold a count
size_t load_addr = 0;
RTC_SLOW_MEM[memPortState] = 0;
RTC_SLOW_MEM[memCounts] = 0;
ulp_set_wakeup_period(0, us);
const ulp_insn_t ulp_blink[] =
{
I_MOVI( R2, memCounts ), // get info from memCounts address
I_LD( R1, R2, 0 ), // put contents of memCounts into R1
I_ADDI( R1, R1, 1 ), // Add 1 to R1 holding result into R1
I_ST( R1, R2, 0 ), // Put R1 into mem address pointed to by R2
I_MOVI(R3, memPortState), // memPortState -> R3
I_LD(R0, R3, 0), // R0 = RTC_SLOW_MEM[R3(memPortState)]
M_BL(1, 1), // GOTO M_LABEL(1) IF R0 < 1
I_WR_REG(RTC_GPIO_OUT_REG, 26, 27, 1), // RTC_GPIO2 = 1
I_SUBI(R0, R0, 1), // R0 = R0 - 1, R0 = 1, R0 = 0
I_ST(R0, R3, 0), // RTC_SLOW_MEM[R3(memPortState)] = R0
M_BX(2), // GOTO M_LABEL(2)
M_LABEL(1), // M_LABEL(1)
I_WR_REG(RTC_GPIO_OUT_REG, 26, 27, 0), // RTC_GPIO2 = 0
I_ADDI(R0, R0, 1), // R0 = R0 + 1, R0 = 0, R0 = 1
I_ST(R0, R3, 0), // RTC_SLOW_MEM[R3(memPortState)] = R0
M_LABEL(2), // M_LABEL(2)
I_HALT() // HALT COPROCESSOR
};
rtc_gpio_init( GPIO_NUM_2 ); // GPIO2 built in led
rtc_gpio_set_direction( GPIO_NUM_2, RTC_GPIO_MODE_INPUT_OUTPUT );
rtc_gpio_set_level( GPIO_NUM_2, 0);
size_t size = sizeof(ulp_blink) / sizeof(ulp_insn_t);
ulp_process_macros_and_load( load_addr, ulp_blink, &size);
ulp_run( load_addr );
} // void ULP_BLINK_RUN(uint32_t us)
It is up to you, the programmer to keep track of RTC memory used. To help keep track of memory used keep this in mind
/*
Each I_XXX preprocessor define translates into a single 32-bit instruction. So you can count instructions to learn which memory address are used and where the free mem space starts.
To generate branch instructions, special M_ preprocessor defines are used. M_LABEL define can be used to define a branch target.
Implementation note: these M_ preprocessor defines will be translated into two ulp_insn_t values: one is a token value which contains label number, and the other is the actual instruction.
*/
RTC memory is 8K in size. Thus the memory location for the variable storage location can be 8000, which should give room for about 100 variables.
With that, I think the issue you are experiencing starts with the batteries and getting good quality, weighty, batteries.
You can put a FET from the bottom resistor of the V divider to ground and have the ULP enable/disable the FET during power up/down cycles.
Also, note how this project uses a LDO Power ESP32/ESP8266 with Solar Panels and Battery | Random Nerd Tutorials to keep the ESP32 powered for as long as possible as batt voltage drops.