Last active
November 8, 2025 17:50
-
-
Save jstockdale/b84ea838266bfe3279a60e7150566673 to your computer and use it in GitHub Desktop.
First version of a deep sleep watchdog sketch for xiao-esp32-s3 configured to wake up once every 10 minutes for up to 60 seconds
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| Basic deep sleep sketch based on examples provided with xiao-esp32-s3 | |
| *** PRE-ALPHA *** | |
| version rc-0; updated to use Serial1 UNTESTED | |
| Copyright 2024 @jstockdale/Off by One, Inc. | |
| Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: | |
| 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. | |
| 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. | |
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| by @jstockdale | |
| */ | |
| #define LED_PIN LED_BUILTIN // built in LED solid briefly on wakeup/boot, flashes dim while sensing, flashes bright if it detects t-beam is off or unresponsive | |
| #define SENSE_PIN A9 // one method we use to determine state is via an analog voltage threshold on GPIO0 | |
| #define POWER_PIN D8 // connect floating leg of t-beam power button to D8; used to power on the t-beam normally | |
| #define RESET_PIN D10 // connect RST on t-beam to D10; used to hard boot if the t-beam has power but no uart activity | |
| #define UART_TX_PIN D7 // connect TX on t-beam to D7; we read the tx line to see if the t-beam is alive or "stuck" | |
| #define UART_RX_PIN D6 // this can be a no connect; not currently used | |
| #define UART_BAUD 38400 // Any baudrate from 300 to 115200 | |
| #define MIN_LOOPS 100 | |
| #define MAX_LOOPS 600 | |
| #define WATCH_LOOPS 100 | |
| int sensorValue = 0; // variable to store the analog value from SENSE_PIN | |
| bool detectBool = false; | |
| int detectCount = 0; | |
| int okCount = 0; | |
| volatile int loopCount = 0; // used to track our total number of elapsed loops during boot and each wake cycle | |
| volatile int targetLoops = MIN_LOOPS; // current target number of loops; we add WATCH_LOOPS to the current loop count each time we reset the device to extend the runtime of the watchdog | |
| #define uS_TO_S_FACTOR 1000000ULL /* Conversion factor for micro seconds to seconds */ | |
| #define TIME_TO_SLEEP 600 /* Time ESP32 will go to sleep (in seconds) */ | |
| RTC_DATA_ATTR int bootCount = 0; | |
| /* | |
| Method to print the reason by which ESP32 | |
| has been awaken from sleep | |
| */ | |
| void print_wakeup_reason() { | |
| esp_sleep_wakeup_cause_t wakeup_reason; | |
| wakeup_reason = esp_sleep_get_wakeup_cause(); | |
| switch (wakeup_reason) { | |
| case ESP_SLEEP_WAKEUP_EXT0: Serial.println("Wakeup caused by external signal using RTC_IO"); break; | |
| case ESP_SLEEP_WAKEUP_EXT1: Serial.println("Wakeup caused by external signal using RTC_CNTL"); break; | |
| case ESP_SLEEP_WAKEUP_TIMER: Serial.println("Wakeup caused by timer"); break; | |
| case ESP_SLEEP_WAKEUP_TOUCHPAD: Serial.println("Wakeup caused by touchpad"); break; | |
| case ESP_SLEEP_WAKEUP_ULP: Serial.println("Wakeup caused by ULP program"); break; | |
| default: Serial.printf("Wakeup was not caused by deep sleep: %d\n", wakeup_reason); break; | |
| } | |
| } | |
| // volatile declaration will avoid any compiler optimization when reading variable values | |
| volatile size_t uart_received_bytes = 0; | |
| void onReceiveFunction(void) { | |
| // This is a callback function that will be activated on UART RX events | |
| size_t available = Serial1.available(); | |
| uart_received_bytes = uart_received_bytes + available; | |
| Serial.printf("onReceive Callback:: There are %d bytes available: ", available); | |
| while (available--) { | |
| Serial.print((char)Serial1.read()); | |
| } | |
| Serial.println(); | |
| } | |
| void setup() { | |
| // set up POWER_PIN and RESET_PIN as INPUT so that they are high impedence | |
| // we will set them to OUTPUT only when powering or resetting the device | |
| pinMode(POWER_PIN, INPUT); | |
| pinMode(RESET_PIN, INPUT); | |
| // GPIO0 (SENSE_PIN) sense pin is always INPUT | |
| pinMode(SENSE_PIN, INPUT); | |
| // built-in led is always OUTPUT | |
| pinMode(LED_PIN, OUTPUT); | |
| Serial.begin(115200); // setup serial | |
| Serial1.begin(UART_BAUD, SERIAL_8N1, UART_RX_PIN, UART_TX_PIN); | |
| delay(250); // wait a bit to let serial startup | |
| //Increment wake number and print it every wake cycle | |
| ++bootCount; | |
| Serial.println("Sleep cycle count: " + String(bootCount)); | |
| //Print the wakeup reason for ESP32 | |
| print_wakeup_reason(); | |
| /* | |
| here we configure the wake up source | |
| we set our ESP32 to wake up every TIME_TO_SLEEP seconds | |
| */ | |
| esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR); | |
| Serial.println("Setup ESP32 to sleep for every " + String(TIME_TO_SLEEP) + " Seconds"); | |
| } | |
| // loop until we think we should go to sleep | |
| void loop() { | |
| // turn on the built-in LED | |
| digitalWrite(LED_PIN, LOW); | |
| // bitbang read the uartTxPin for uartTxReadCycles to hopefully see a HIGH (1) | |
| // for(int i = 0; i < uartTxReadCycles; i++) { | |
| // uartTxValue = digitalRead(uartTxPin); | |
| // if(uartTxValue == HIGH) { | |
| // // we only care that the UART went high while we were looking | |
| // // UART is high between transmissions if the device is on and responding | |
| // // UART floats around 0V and reads LOW when the t-beam is powered but "stuck" (requires hard boot) | |
| // // since we think the device is okay, we won't reset after pressing power | |
| // resetAfterPower = false; | |
| // break; | |
| // } | |
| // } | |
| sensorValue = analogRead(SENSE_PIN); | |
| Serial.println("State: " + String(sensorValue) + ", " + String(uart_received_bytes)); | |
| digitalWrite(LED_PIN, HIGH); | |
| if(sensorValue > 2000 && uart_received_bytes > 0) { | |
| // if GPIO0 is above 2000 when reading the ADC and the UART has received data | |
| // we think the t-beam is turned on and most likely functioning properly | |
| // turn off the led, count the loop, and zero the detect count | |
| digitalWrite(LED_PIN, HIGH); | |
| detectCount = 0; | |
| ++okCount; | |
| } else { | |
| // if sensorValue is 2000 or less, the device is probably off | |
| // if sensorvalue is 4095 and uart_received_bytes == 0, the device is probably "stuck" | |
| digitalWrite(LED_PIN, LOW); | |
| delay(5); | |
| digitalWrite(LED_PIN, HIGH); | |
| okCount = 0; | |
| ++detectCount; | |
| detectBool = true; | |
| Serial.println(detectCount); | |
| // we add one to targetLoops (up to a max of maxLoops) if the device is off | |
| // this behaves well since the UART tx detection isn't perfect but is a weird hack | |
| // TODO(jstockdale): fix the way the various loop limits interact | |
| if(targetLoops < MAX_LOOPS) { | |
| ++targetLoops; | |
| Serial.println("++targetLoops"); | |
| } | |
| } | |
| if(detectCount >= WATCH_LOOPS) { | |
| // we have observed the t-beam to be off for WATCH_LOOPS consecutive sense-loops | |
| // we will power on the device by pulling the POWER_PIN to ground for 500ms | |
| Serial.println("Powering ESP32-S3 ON"); | |
| digitalWrite(POWER_PIN, HIGH); | |
| pinMode(POWER_PIN, OUTPUT); | |
| digitalWrite(POWER_PIN, LOW); | |
| digitalWrite(LED_PIN, LOW); | |
| delay(500); | |
| digitalWrite(POWER_PIN, HIGH); | |
| digitalWrite(LED_PIN, HIGH); | |
| pinMode(POWER_PIN, INPUT); | |
| delay(100); | |
| if(uart_received_bytes == 0) { | |
| // since we didn't see any serial data tx from the t-beam, we think it is "stuck" | |
| // hard boot / reset the device by pulling RESET_PIN to ground for 500ms | |
| Serial.println("Resetting ESP32-S3 by pulling RST to GND"); | |
| digitalWrite(RESET_PIN, HIGH); | |
| pinMode(RESET_PIN, OUTPUT); | |
| digitalWrite(RESET_PIN, LOW); | |
| delay(500); | |
| digitalWrite(RESET_PIN, HIGH); | |
| pinMode(RESET_PIN, INPUT); | |
| delay(100); | |
| } | |
| // zero counters | |
| okCount = 0; | |
| detectCount = 0; | |
| // increase targetLoops by WATCH_LOOPS so we can see if we're successful or try until MAX_LOOPS | |
| if (targetLoops < MAX_LOOPS) { | |
| Serial.println("Current loop count: " + String(loopCount)); | |
| Serial.println("Updating target loops from: " + String(targetLoops)); | |
| targetLoops = (loopCount + 1 + WATCH_LOOPS > MAX_LOOPS) ? MAX_LOOPS : loopCount + 1 + WATCH_LOOPS; | |
| Serial.println("Updated target loops to: " + String(targetLoops)); | |
| } | |
| } | |
| // deep sleep to save power if we've seen WATCH_LOOPS consecutive ok loops | |
| // or performed MIN_LOOPS sense-loops where the t-beam is always-on | |
| // TODO(jstockdale): improve this logic | |
| if(okCount > WATCH_LOOPS || (detectBool == false && loopCount > MIN_LOOPS) || loopCount > targetLoops) { | |
| Serial.println("Going to sleep now"); | |
| Serial.flush(); | |
| esp_deep_sleep_start(); | |
| } | |
| ++loopCount; | |
| delay(100); | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Please credit @jstockdale if you end up using this. Otherwise consider it under a BSD license. I’ll try to move it to a repo and write it up properly.
Anyway. You need a few components (a couple resistors across the 5v to split the voltage for the analog sense pin, a mosfet from 5v to ground to short the circuit when needed (wire the gate to the control gpio, add a small resistor like idk 20 ohms if you want to be safe about not blowing the mosfet, probably $3 cost), and a xiao esp32-s3 ($6))
This is the end result. Green wire is reset, red wire triggers power button. Yellow is 5v (split to 2.5v for the sense gpio with two resistors). White is gate to control the mosfet.
This unit was one of my first and has been out in the sun about 2 years which is why it looks a little banged up and the wires look old. 😂