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"
15#include "cdc_hal/hw_config.h"
16#include "cdc_log.h"
17#include "esp_sleep.h"
18#include "esp_attr.h"
19#include "esp_system.h"
20#include "esp_task_wdt.h"
21#include "driver/gpio.h"
22#include "nvs_flash.h"
23#include "nvs.h"
24#include "freertos/FreeRTOS.h"
25#include "freertos/task.h"
26#include <cstring>
27
28static const char* TAG = "SLEEP";
29
30namespace cdc::hal {
31
33static constexpr uint32_t DEFAULT_LIGHT_SLEEP_INTERVAL_S = 60;
34
36static constexpr const char* NVS_NAMESPACE = "sleep";
37static constexpr const char* NVS_KEY_INTERVAL = "interval";
38
40static constexpr size_t MAX_CALLBACKS = 8;
41
43RTC_DATA_ATTR static bool g_was_in_deep_sleep = false;
44
45// RTC-retained diagnostic counters. Survive deep-sleep wake and external
46// (EN) reset; cleared only on true power loss. Used to distinguish a
47// spurious-wake/reset loop from a never-firing wake source after long sleeps.
48RTC_DATA_ATTR static uint32_t g_diag_boot_count = 0;
49RTC_DATA_ATTR static uint32_t g_diag_deep_sleep_count = 0;
50
52public:
54
55 // IService implementation
56 bool init() override;
57 bool start() override { state_ = core::ServiceState::STARTED; return true; }
58 void stop() override { state_ = core::ServiceState::STOPPED; }
59 core::ServiceState getState() const override { return state_; }
60 const char* getName() const override { return "sleep"; }
61
62 // ISleepController implementation
63 void enterLightSleep() override;
64 [[noreturn]] void enterDeepSleep() override;
65 WakeupSource getWakeupSource() const override;
66 bool wasKeypadWakeup() const override { return lastKeypadWake_; }
67 bool wasInDeepSleep() const override { return g_was_in_deep_sleep; }
68 void clearDeepSleepFlag() override { g_was_in_deep_sleep = false; }
69 void setLightSleepInterval(uint32_t seconds) override;
70 uint32_t getLightSleepInterval() const override { return lightSleepIntervalS_; }
71 void prepareGpioForSleep() override;
72 void stabilizeGpioAfterWakeup() override;
73
74 // Callback registration
75 bool registerPreSleepCallback(const SleepCallbackEntry& entry) override;
76 bool registerWakeupCallback(const SleepCallbackEntry& entry) override;
77 void unregisterCallbacks(const char* moduleName) override;
78
79private:
80 void invokeCallbacks(SleepCallbackEntry* callbacks, size_t count);
81 bool registerCallback(SleepCallbackEntry* callbacks, size_t* count,
82 const SleepCallbackEntry& entry, const char* logLabel);
83 void loadFromNvs();
84 void saveToNvs();
85
87 bool lightSleepConfigured_ = false;
88 bool lastKeypadWake_ = false;
89 uint32_t lightSleepIntervalS_ = DEFAULT_LIGHT_SLEEP_INTERVAL_S;
90
91 // Callback storage
92 SleepCallbackEntry preSleepCallbacks_[MAX_CALLBACKS] = {};
93 SleepCallbackEntry wakeupCallbacks_[MAX_CALLBACKS] = {};
94 size_t preSleepCount_ = 0;
95 size_t wakeupCount_ = 0;
96};
97
103 if (state_ != core::ServiceState::UNINITIALIZED) {
104 return state_ == core::ServiceState::INITIALIZED ||
106 }
107
108 LOG_I(TAG, "Initializing sleep controller");
109
110 // Load saved interval from NVS
111 loadFromNvs();
112
113 // Check if we woke from deep sleep
114 esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
115
116 // Single-line boot diagnostic (no per-loop logging). reset/wake are the
117 // ESP-IDF enum values; boot/deep counters are RTC-retained across wake and
118 // EN reset, so a value jump after an unattended sleep reveals a
119 // spurious-wake/reset loop versus a wake source that never fired.
121 LOG_W(TAG, "DeepSleep-Diag: boot#%lu reset=%d wake=%d deepEntries=%lu wasDeep=%d",
122 (unsigned long)g_diag_boot_count, (int)esp_reset_reason(), (int)cause,
123 (unsigned long)g_diag_deep_sleep_count, (int)g_was_in_deep_sleep);
124
125 if (cause == ESP_SLEEP_WAKEUP_EXT0 || cause == ESP_SLEEP_WAKEUP_EXT1) {
126 // g_was_in_deep_sleep is already set in RTC memory
127 } else if (g_was_in_deep_sleep) {
128 // Reset occurred but not from deep sleep wakeup
129 g_was_in_deep_sleep = false;
130 }
131
132 LOG_I(TAG, "Light sleep interval: %lu seconds", (unsigned long)lightSleepIntervalS_);
133
135 return true;
136}
137
142 if (!lightSleepConfigured_) {
143 // Configure timer wakeup
144 if (lightSleepIntervalS_ > 0) {
145 esp_sleep_enable_timer_wakeup(lightSleepIntervalS_ * 1000000ULL);
146 }
147
148 // Configure GPIO wakeup (keypad + charger interrupt) - level triggered
149 gpio_wakeup_enable(EXP_IRQ_PIN, GPIO_INTR_LOW_LEVEL);
150 gpio_wakeup_enable(CHG_IRQ_PIN, GPIO_INTR_LOW_LEVEL);
151 esp_sleep_enable_gpio_wakeup();
152
153 lightSleepConfigured_ = true;
154 LOG_I(TAG, "Light sleep configured (GPIO%d+%d + %lus timer)",
155 EXP_IRQ_PIN, CHG_IRQ_PIN, (unsigned long)lightSleepIntervalS_);
156 }
157
158 // Invoke pre-sleep callbacks (modules can prepare for sleep)
159 invokeCallbacks(preSleepCallbacks_, preSleepCount_);
160
161 // Prepare GPIO before sleep
163
164 LOG_D(TAG, "Entering light sleep...");
165
166 // Enter light sleep
167 esp_light_sleep_start();
168
169 // Capture the keypad IRQ level before recovery reads/clears the expander.
170 // The TCA9535 holds its IRQ line low after a key event until the inputs are
171 // read, so a low level here reliably identifies a keypad wakeup versus a
172 // timer or charger-only interrupt.
173 lastKeypadWake_ = (gpio_get_level(EXP_IRQ_PIN) == 0);
174
175 // Reset the task watchdog as soon as we resume: the sleep window can
176 // be longer than the TWDT timeout, and any subscribed task (notably the
177 // idle task we sit in via WDT_CHECK_IDLE_TASK_CPU0) would otherwise fire
178 // the WDT in the middle of coex cleanup, which can land in the panic
179 // handler with a spinlock already held.
180 if (esp_task_wdt_status(nullptr) == ESP_OK) {
181 esp_task_wdt_reset();
182 }
183
184 // Log wakeup cause
185 esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
186 if (cause == ESP_SLEEP_WAKEUP_GPIO) {
187 LOG_D(TAG, "GPIO wakeup");
188 } else if (cause == ESP_SLEEP_WAKEUP_TIMER) {
189 LOG_D(TAG, "Timer wakeup");
190 }
191
192 // Stabilize GPIO after wakeup
194
195 // Invoke wakeup callbacks (modules can resume operations)
196 invokeCallbacks(wakeupCallbacks_, wakeupCount_);
197}
198
203 LOG_I(TAG, "Entering deep sleep mode...");
204
205 // Mark that we're in deep sleep mode (survives reset)
206 g_was_in_deep_sleep = true;
208
209 // Configure GPIO wakeup only (no timer)
210 // Use EXT1 instead of EXT0 - less RTC GPIO issues on ESP32-S3
211 esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL);
212 esp_sleep_enable_ext1_wakeup_io(1ULL << EXP_IRQ_PIN, ESP_EXT1_WAKEUP_ANY_LOW);
213
214 // Enter deep sleep (causes reset on wake)
215 esp_deep_sleep_start();
216
217 // Never reached, but satisfies [[noreturn]]
218 while (true) { }
219}
220
226 esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
227
228 switch (cause) {
229 case ESP_SLEEP_WAKEUP_TIMER:
230 return WakeupSource::TIMER;
231 case ESP_SLEEP_WAKEUP_GPIO:
232 return WakeupSource::GPIO;
233 case ESP_SLEEP_WAKEUP_TOUCHPAD:
235 case ESP_SLEEP_WAKEUP_EXT0:
236 return WakeupSource::EXT0;
237 case ESP_SLEEP_WAKEUP_EXT1:
238 return WakeupSource::EXT1;
239 default:
241 }
242}
243
249 if (lightSleepIntervalS_ == seconds) return;
250
251 lightSleepIntervalS_ = seconds;
252 lightSleepConfigured_ = false; // Force reconfiguration on next sleep
253
254 // Save to NVS
255 saveToNvs();
256
257 LOG_I(TAG, "Light sleep interval set to %lus", (unsigned long)seconds);
258}
259
264 LOG_D(TAG, "Preparing GPIO for sleep...");
265
266 // Use keypad's comprehensive sleep preparation
267 auto* keypad = getKeypadInstance();
268 if (keypad) {
269 keypad->prepareForSleep();
270 }
271
272 // Arm the charger IRQ line for the level-triggered USB-plug wakeup
273 auto* power = getPowerManagerInstance();
274 if (power) {
275 power->prepareForSleep();
276 }
277}
278
283 LOG_D(TAG, "Stabilizing GPIO after wakeup...");
284
285 // Use keypad's comprehensive recovery (waits for keys, clears buffer, etc.)
286 auto* keypad = getKeypadInstance();
287 if (keypad) {
288 keypad->recoverFromSleep();
289 }
290
291 // Refresh charger status (USB presence) and re-arm the charger IRQ
292 auto* power = getPowerManagerInstance();
293 if (power) {
294 power->recoverFromSleep();
295 }
296}
297
306bool Esp32SleepController::registerCallback(SleepCallbackEntry* callbacks, size_t* count,
307 const SleepCallbackEntry& entry,
308 const char* logLabel) {
309 if (*count >= MAX_CALLBACKS) {
310 LOG_W(TAG, "%s callback limit reached", logLabel);
311 return false;
312 }
313
314 // Insert sorted by priority (lower priority = earlier in array)
315 size_t insertPos = *count;
316 for (size_t i = 0; i < *count; i++) {
317 if (entry.priority < callbacks[i].priority) {
318 insertPos = i;
319 break;
320 }
321 }
322
323 // Shift existing entries
324 for (size_t i = *count; i > insertPos; i--) {
325 callbacks[i] = callbacks[i - 1];
326 }
327
328 callbacks[insertPos] = entry;
329 (*count)++;
330
331 LOG_I(TAG, "Registered %s callback: %s (priority %d)",
332 logLabel, entry.moduleName, entry.priority);
333 return true;
334}
335
342 return registerCallback(preSleepCallbacks_, &preSleepCount_, entry, "pre-sleep");
343}
344
351 return registerCallback(wakeupCallbacks_, &wakeupCount_, entry, "wakeup");
352}
353
358void Esp32SleepController::unregisterCallbacks(const char* moduleName) {
359 if (!moduleName) return;
360
361 // Remove from pre-sleep callbacks
362 for (size_t i = 0; i < preSleepCount_; ) {
363 if (preSleepCallbacks_[i].moduleName &&
364 strcmp(preSleepCallbacks_[i].moduleName, moduleName) == 0) {
365 // Shift remaining entries
366 for (size_t j = i; j < preSleepCount_ - 1; j++) {
367 preSleepCallbacks_[j] = preSleepCallbacks_[j + 1];
368 }
369 preSleepCount_--;
370 } else {
371 i++;
372 }
373 }
374
375 // Remove from wakeup callbacks
376 for (size_t i = 0; i < wakeupCount_; ) {
377 if (wakeupCallbacks_[i].moduleName &&
378 strcmp(wakeupCallbacks_[i].moduleName, moduleName) == 0) {
379 for (size_t j = i; j < wakeupCount_ - 1; j++) {
380 wakeupCallbacks_[j] = wakeupCallbacks_[j + 1];
381 }
382 wakeupCount_--;
383 } else {
384 i++;
385 }
386 }
387
388 LOG_I(TAG, "Unregistered callbacks for: %s", moduleName);
389}
390
396void Esp32SleepController::invokeCallbacks(SleepCallbackEntry* callbacks, size_t count) {
397 for (size_t i = 0; i < count; i++) {
398 if (callbacks[i].callback) {
399 LOG_D(TAG, "Invoking callback: %s", callbacks[i].moduleName);
400 callbacks[i].callback(callbacks[i].context);
401 }
402 }
403}
404
408void Esp32SleepController::loadFromNvs() {
409 static constexpr uint32_t MIN_INTERVAL_S = 10;
410 static constexpr uint32_t MAX_INTERVAL_S = 86400;
411
412 nvs_handle_t handle;
413 esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle);
414 if (err == ESP_OK) {
415 uint32_t interval = 0;
416 if (nvs_get_u32(handle, NVS_KEY_INTERVAL, &interval) == ESP_OK) {
417 if (interval < MIN_INTERVAL_S) {
418 interval = MIN_INTERVAL_S;
419 } else if (interval > MAX_INTERVAL_S) {
420 interval = MAX_INTERVAL_S;
421 }
422 lightSleepIntervalS_ = interval;
423 }
424 nvs_close(handle);
425 }
426}
427
431void Esp32SleepController::saveToNvs() {
432 nvs_handle_t handle;
433 esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle);
434 if (err == ESP_OK) {
435 nvs_set_u32(handle, NVS_KEY_INTERVAL, lightSleepIntervalS_);
436 nvs_commit(handle);
437 nvs_close(handle);
438 }
439}
440
443
451
452} // 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
bool wasKeypadWakeup() const override
const char * getName() const override
void enterLightSleep() override
Enters light sleep with configured wake sources.
#define CHG_IRQ_PIN
Definition hw_config.h:24
#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.
IPowerManager * getPowerManagerInstance()
Returns the singleton power manager instance.
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.