CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
SleepController.cpp
Go to the documentation of this file.
1
11
13#include "cdc_hal/IKeypad.h"
14#include "cdc_hal/hw_config.h"
15#include "cdc_log.h"
16#include "esp_sleep.h"
17#include "esp_attr.h"
18#include "esp_system.h"
19#include "esp_task_wdt.h"
20#include "driver/gpio.h"
21#include "nvs_flash.h"
22#include "nvs.h"
23#include "freertos/FreeRTOS.h"
24#include "freertos/task.h"
25#include <cstring>
26
27static const char* TAG = "SLEEP";
28
29namespace cdc::hal {
30
32static constexpr uint32_t DEFAULT_LIGHT_SLEEP_INTERVAL_S = 60;
33
35static constexpr const char* NVS_NAMESPACE = "sleep";
36static constexpr const char* NVS_KEY_INTERVAL = "interval";
37
39static constexpr size_t MAX_CALLBACKS = 8;
40
42RTC_DATA_ATTR static bool g_was_in_deep_sleep = false;
43
44// RTC-retained diagnostic counters. Survive deep-sleep wake and external
45// (EN) reset; cleared only on true power loss. Used to distinguish a
46// spurious-wake/reset loop from a never-firing wake source after long sleeps.
47RTC_DATA_ATTR static uint32_t g_diag_boot_count = 0;
48RTC_DATA_ATTR static uint32_t g_diag_deep_sleep_count = 0;
49
51public:
53
54 // IService implementation
55 bool init() override;
56 bool start() override { state_ = core::ServiceState::STARTED; return true; }
57 void stop() override { state_ = core::ServiceState::STOPPED; }
58 core::ServiceState getState() const override { return state_; }
59 const char* getName() const override { return "sleep"; }
60
61 // ISleepController implementation
62 void enterLightSleep() override;
63 [[noreturn]] void enterDeepSleep() override;
64 WakeupSource getWakeupSource() const override;
65 bool wasInDeepSleep() const override { return g_was_in_deep_sleep; }
66 void clearDeepSleepFlag() override { g_was_in_deep_sleep = false; }
67 void setLightSleepInterval(uint32_t seconds) override;
68 uint32_t getLightSleepInterval() const override { return lightSleepIntervalS_; }
69 void prepareGpioForSleep() override;
70 void stabilizeGpioAfterWakeup() override;
71
72 // Callback registration
73 bool registerPreSleepCallback(const SleepCallbackEntry& entry) override;
74 bool registerWakeupCallback(const SleepCallbackEntry& entry) override;
75 void unregisterCallbacks(const char* moduleName) override;
76
77private:
78 void invokeCallbacks(SleepCallbackEntry* callbacks, size_t count);
79 bool registerCallback(SleepCallbackEntry* callbacks, size_t* count,
80 const SleepCallbackEntry& entry, const char* logLabel);
81 void loadFromNvs();
82 void saveToNvs();
83
85 bool lightSleepConfigured_ = false;
86 uint32_t lightSleepIntervalS_ = DEFAULT_LIGHT_SLEEP_INTERVAL_S;
87
88 // Callback storage
89 SleepCallbackEntry preSleepCallbacks_[MAX_CALLBACKS] = {};
90 SleepCallbackEntry wakeupCallbacks_[MAX_CALLBACKS] = {};
91 size_t preSleepCount_ = 0;
92 size_t wakeupCount_ = 0;
93};
94
100 if (state_ != core::ServiceState::UNINITIALIZED) {
101 return state_ == core::ServiceState::INITIALIZED ||
103 }
104
105 LOG_I(TAG, "Initializing sleep controller");
106
107 // Load saved interval from NVS
108 loadFromNvs();
109
110 // Check if we woke from deep sleep
111 esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
112
113 // Single-line boot diagnostic (no per-loop logging). reset/wake are the
114 // ESP-IDF enum values; boot/deep counters are RTC-retained across wake and
115 // EN reset, so a value jump after an unattended sleep reveals a
116 // spurious-wake/reset loop versus a wake source that never fired.
118 LOG_W(TAG, "DeepSleep-Diag: boot#%lu reset=%d wake=%d deepEntries=%lu wasDeep=%d",
119 (unsigned long)g_diag_boot_count, (int)esp_reset_reason(), (int)cause,
120 (unsigned long)g_diag_deep_sleep_count, (int)g_was_in_deep_sleep);
121
122 if (cause == ESP_SLEEP_WAKEUP_EXT0 || cause == ESP_SLEEP_WAKEUP_EXT1) {
123 // g_was_in_deep_sleep is already set in RTC memory
124 } else if (g_was_in_deep_sleep) {
125 // Reset occurred but not from deep sleep wakeup
126 g_was_in_deep_sleep = false;
127 }
128
129 LOG_I(TAG, "Light sleep interval: %lu seconds", (unsigned long)lightSleepIntervalS_);
130
132 return true;
133}
134
139 if (!lightSleepConfigured_) {
140 // Configure timer wakeup
141 if (lightSleepIntervalS_ > 0) {
142 esp_sleep_enable_timer_wakeup(lightSleepIntervalS_ * 1000000ULL);
143 }
144
145 // Configure GPIO wakeup (keypad interrupt) - level triggered
146 gpio_wakeup_enable(EXP_IRQ_PIN, GPIO_INTR_LOW_LEVEL);
147 esp_sleep_enable_gpio_wakeup();
148
149 lightSleepConfigured_ = true;
150 LOG_I(TAG, "Light sleep configured (GPIO%d + %lus timer)",
151 EXP_IRQ_PIN, (unsigned long)lightSleepIntervalS_);
152 }
153
154 // Invoke pre-sleep callbacks (modules can prepare for sleep)
155 invokeCallbacks(preSleepCallbacks_, preSleepCount_);
156
157 // Prepare GPIO before sleep
159
160 LOG_D(TAG, "Entering light sleep...");
161
162 // Enter light sleep
163 esp_light_sleep_start();
164
165 // Reset the task watchdog as soon as we resume: the sleep window can
166 // be longer than the TWDT timeout, and any subscribed task (notably the
167 // idle task we sit in via WDT_CHECK_IDLE_TASK_CPU0) would otherwise fire
168 // the WDT in the middle of coex cleanup, which can land in the panic
169 // handler with a spinlock already held.
170 if (esp_task_wdt_status(nullptr) == ESP_OK) {
171 esp_task_wdt_reset();
172 }
173
174 // Log wakeup cause
175 esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
176 if (cause == ESP_SLEEP_WAKEUP_GPIO) {
177 LOG_D(TAG, "GPIO wakeup");
178 } else if (cause == ESP_SLEEP_WAKEUP_TIMER) {
179 LOG_D(TAG, "Timer wakeup");
180 }
181
182 // Stabilize GPIO after wakeup
184
185 // Invoke wakeup callbacks (modules can resume operations)
186 invokeCallbacks(wakeupCallbacks_, wakeupCount_);
187}
188
193 LOG_I(TAG, "Entering deep sleep mode...");
194
195 // Mark that we're in deep sleep mode (survives reset)
196 g_was_in_deep_sleep = true;
198
199 // Configure GPIO wakeup only (no timer)
200 // Use EXT1 instead of EXT0 - less RTC GPIO issues on ESP32-S3
201 esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL);
202 esp_sleep_enable_ext1_wakeup_io(1ULL << EXP_IRQ_PIN, ESP_EXT1_WAKEUP_ANY_LOW);
203
204 // Enter deep sleep (causes reset on wake)
205 esp_deep_sleep_start();
206
207 // Never reached, but satisfies [[noreturn]]
208 while (true) { }
209}
210
216 esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
217
218 switch (cause) {
219 case ESP_SLEEP_WAKEUP_TIMER:
220 return WakeupSource::TIMER;
221 case ESP_SLEEP_WAKEUP_GPIO:
222 return WakeupSource::GPIO;
223 case ESP_SLEEP_WAKEUP_TOUCHPAD:
225 case ESP_SLEEP_WAKEUP_EXT0:
226 return WakeupSource::EXT0;
227 case ESP_SLEEP_WAKEUP_EXT1:
228 return WakeupSource::EXT1;
229 default:
231 }
232}
233
239 if (lightSleepIntervalS_ == seconds) return;
240
241 lightSleepIntervalS_ = seconds;
242 lightSleepConfigured_ = false; // Force reconfiguration on next sleep
243
244 // Save to NVS
245 saveToNvs();
246
247 LOG_I(TAG, "Light sleep interval set to %lus", (unsigned long)seconds);
248}
249
254 LOG_D(TAG, "Preparing GPIO for sleep...");
255
256 // Use keypad's comprehensive sleep preparation
257 auto* keypad = getKeypadInstance();
258 if (keypad) {
259 keypad->prepareForSleep();
260 }
261}
262
267 LOG_D(TAG, "Stabilizing GPIO after wakeup...");
268
269 // Use keypad's comprehensive recovery (waits for keys, clears buffer, etc.)
270 auto* keypad = getKeypadInstance();
271 if (keypad) {
272 keypad->recoverFromSleep();
273 }
274}
275
284bool Esp32SleepController::registerCallback(SleepCallbackEntry* callbacks, size_t* count,
285 const SleepCallbackEntry& entry,
286 const char* logLabel) {
287 if (*count >= MAX_CALLBACKS) {
288 LOG_W(TAG, "%s callback limit reached", logLabel);
289 return false;
290 }
291
292 // Insert sorted by priority (lower priority = earlier in array)
293 size_t insertPos = *count;
294 for (size_t i = 0; i < *count; i++) {
295 if (entry.priority < callbacks[i].priority) {
296 insertPos = i;
297 break;
298 }
299 }
300
301 // Shift existing entries
302 for (size_t i = *count; i > insertPos; i--) {
303 callbacks[i] = callbacks[i - 1];
304 }
305
306 callbacks[insertPos] = entry;
307 (*count)++;
308
309 LOG_I(TAG, "Registered %s callback: %s (priority %d)",
310 logLabel, entry.moduleName, entry.priority);
311 return true;
312}
313
320 return registerCallback(preSleepCallbacks_, &preSleepCount_, entry, "pre-sleep");
321}
322
329 return registerCallback(wakeupCallbacks_, &wakeupCount_, entry, "wakeup");
330}
331
336void Esp32SleepController::unregisterCallbacks(const char* moduleName) {
337 if (!moduleName) return;
338
339 // Remove from pre-sleep callbacks
340 for (size_t i = 0; i < preSleepCount_; ) {
341 if (preSleepCallbacks_[i].moduleName &&
342 strcmp(preSleepCallbacks_[i].moduleName, moduleName) == 0) {
343 // Shift remaining entries
344 for (size_t j = i; j < preSleepCount_ - 1; j++) {
345 preSleepCallbacks_[j] = preSleepCallbacks_[j + 1];
346 }
347 preSleepCount_--;
348 } else {
349 i++;
350 }
351 }
352
353 // Remove from wakeup callbacks
354 for (size_t i = 0; i < wakeupCount_; ) {
355 if (wakeupCallbacks_[i].moduleName &&
356 strcmp(wakeupCallbacks_[i].moduleName, moduleName) == 0) {
357 for (size_t j = i; j < wakeupCount_ - 1; j++) {
358 wakeupCallbacks_[j] = wakeupCallbacks_[j + 1];
359 }
360 wakeupCount_--;
361 } else {
362 i++;
363 }
364 }
365
366 LOG_I(TAG, "Unregistered callbacks for: %s", moduleName);
367}
368
374void Esp32SleepController::invokeCallbacks(SleepCallbackEntry* callbacks, size_t count) {
375 for (size_t i = 0; i < count; i++) {
376 if (callbacks[i].callback) {
377 LOG_D(TAG, "Invoking callback: %s", callbacks[i].moduleName);
378 callbacks[i].callback(callbacks[i].context);
379 }
380 }
381}
382
386void Esp32SleepController::loadFromNvs() {
387 static constexpr uint32_t MIN_INTERVAL_S = 10;
388 static constexpr uint32_t MAX_INTERVAL_S = 86400;
389
390 nvs_handle_t handle;
391 esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle);
392 if (err == ESP_OK) {
393 uint32_t interval = 0;
394 if (nvs_get_u32(handle, NVS_KEY_INTERVAL, &interval) == ESP_OK) {
395 if (interval < MIN_INTERVAL_S) {
396 interval = MIN_INTERVAL_S;
397 } else if (interval > MAX_INTERVAL_S) {
398 interval = MAX_INTERVAL_S;
399 }
400 lightSleepIntervalS_ = interval;
401 }
402 nvs_close(handle);
403 }
404}
405
409void Esp32SleepController::saveToNvs() {
410 nvs_handle_t handle;
411 esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle);
412 if (err == ESP_OK) {
413 nvs_set_u32(handle, NVS_KEY_INTERVAL, lightSleepIntervalS_);
414 nvs_commit(handle);
415 nvs_close(handle);
416 }
417}
418
421
429
430} // namespace cdc::hal
static const char * TAG
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_W(tag, fmt,...)
Definition cdc_log.h:146
#define LOG_D(tag, fmt,...)
Definition cdc_log.h:148
#define LOG_I(tag, fmt,...)
Definition cdc_log.h:147
void unregisterCallbacks(const char *moduleName) override
Removes all callbacks belonging to one module.
uint32_t getLightSleepInterval() const override
void enterDeepSleep() override
Enters deep sleep mode and never returns.
void stabilizeGpioAfterWakeup() override
Restores/stabilizes GPIO/keypad state after wake.
void prepareGpioForSleep() override
Prepares GPIO/keypad state for entering sleep.
bool init() override
Initializes sleep controller configuration and wake-state tracking.
core::ServiceState getState() const override
bool registerPreSleepCallback(const SleepCallbackEntry &entry) override
Registers callback invoked before sleep transition.
void setLightSleepInterval(uint32_t seconds) override
Updates light-sleep interval and persists it.
bool registerWakeupCallback(const SleepCallbackEntry &entry) override
Registers callback invoked after wakeup.
WakeupSource getWakeupSource() const override
Returns last wakeup source reported by ESP-IDF.
bool wasInDeepSleep() const override
const char * getName() const override
void enterLightSleep() override
Enters light sleep with configured wake sources.
#define EXP_IRQ_PIN
Definition hw_config.h:19
static constexpr uint32_t DEFAULT_LIGHT_SLEEP_INTERVAL_S
Default light-sleep timer interval in seconds.
static Esp32SleepController g_sleepController
Singleton sleep-controller instance.
static constexpr size_t MAX_CALLBACKS
Maximum number of registered callbacks per callback list.
IKeypad * getKeypadInstance()
Returns the singleton keypad service instance.
static RTC_DATA_ATTR uint32_t g_diag_boot_count
static constexpr const char * NVS_KEY_INTERVAL
static constexpr const char * NVS_NAMESPACE
NVS namespace and keys for display settings.
static RTC_DATA_ATTR bool g_was_in_deep_sleep
RTC-retained flag indicating previous deep-sleep state.
static RTC_DATA_ATTR uint32_t g_diag_deep_sleep_count
ISleepController * getSleepControllerInstance()
Returns the singleton sleep controller service instance.