fix i2c, add deep sleep

This commit is contained in:
Thaddeus Hughes
2026-04-28 12:43:43 -05:00
parent ef1f3e4e85
commit 666750f710
25 changed files with 2163 additions and 1196 deletions

View File

@@ -165,6 +165,7 @@ esp_err_t comms_handle_post(cJSON *root, cJSON **response_json) {
bool cmd_executed = false;
bool sleep_requested = false;
bool hibernate_requested = false;
bool reboot_requested = false;
bool wifi_params_changed = false;
bool wifi_restart_requested = false;
@@ -235,6 +236,10 @@ esp_err_t comms_handle_post(cJSON *root, cJSON **response_json) {
sleep_requested = true;
cmd_executed = true;
}
else if (strcmp(cmd_str, "hibernate") == 0) {
hibernate_requested = true;
cmd_executed = true;
}
else if (strcmp(cmd_str, "rf_clear_temp") == 0) {
rf_433_clear_temp_keycodes();
cmd_executed = true;
@@ -487,6 +492,14 @@ esp_err_t comms_handle_post(cJSON *root, cJSON **response_json) {
*response_json = response;
return ESP_OK;
}
if (hibernate_requested) {
cJSON_AddStringToObject(response, "status", "ok");
cJSON_AddStringToObject(response, "message", "Hibernating (button to wake)...");
cJSON_AddBoolToObject(response, "hibernate", true);
*response_json = response;
return ESP_OK;
}
if (error_msg != NULL) {
cJSON_AddStringToObject(response, "status", "error");

View File

@@ -46,10 +46,16 @@ esp_err_t i2c_init(void) {
ESP_ERROR_CHECK(i2c_param_config(I2C_PORT, &conf));
ESP_ERROR_CHECK(i2c_driver_install(I2C_PORT, conf.mode, 0, 0, 0));
/* Pre-clear OUTPUT latches BEFORE switching pins to output mode.
* TCA9555 powers up with OUTPUT0/1 = 0xFF, so configuring CONFIG first
* (pins → outputs) would drive every relay + LED on for the few hundred
* µs until i2c_relays_idle() runs. Writing 0 first makes the eventual
* input→output transition drive 0. */
ESP_ERROR_CHECK(tca_write_word_8(TCA_REG_OUTPUT0, 0x00));
ESP_ERROR_CHECK(tca_write_word_8(TCA_REG_OUTPUT1, 0x00));
ESP_ERROR_CHECK(tca_write_word_8(TCA_REG_CONFIG0, 0b00000011));
ESP_ERROR_CHECK(tca_write_word_8(TCA_REG_CONFIG1, 0b00000000));
i2c_initted = true;
//safety_ok = false; // Start with safety not OK
last_relay_request = 0;
@@ -144,6 +150,23 @@ esp_err_t i2c_poll_buttons() {
return ESP_OK;
}
/* One-shot, un-debounced "is button currently pressed?" read.
* Reads NCA9535 INPUT0 directly over I2C; bypasses the polled / debounced
* state machine above. Used in spots where the polled state isn't valid
* yet (cold-boot factory-reset detection runs before the FSM/main loop
* has been polling) or where we deliberately want to check the live wire
* (e.g. waiting for a button release before deep sleep).
*
* Side effect: reading INPUT0 clears the NCA9535 INT line — desirable
* before deep-sleep entry so EXT0 wake doesn't trigger immediately. */
bool i2c_button_held_raw(uint8_t button) {
if (button >= N_BTNS) return false;
uint16_t port_val = 0;
if (tca_read_word(TCA_REG_INPUT0, &port_val) != ESP_OK) return false;
/* Buttons are active-low on P00..P0(N_BTNS-1). */
return ((port_val >> button) & 0x01) == 0;
}
bool i2c_get_button_tripped(uint8_t button) {
return (button < N_BTNS) && debounced_state[button] && !last_known_state[button];
}

View File

@@ -69,6 +69,12 @@ esp_err_t i2c_relays_sleep(void);
esp_err_t i2c_poll_buttons();
/* Live, un-debounced read of a single button bit via NCA9535 INPUT0.
* Use only when the polled debounced state isn't trustworthy — e.g.
* cold-boot factory-reset detection, or before deep sleep when we want
* to clear the NCA9535 INT line as a side effect. */
bool i2c_button_held_raw(uint8_t button);
bool i2c_get_button_tripped(uint8_t button);
bool i2c_get_button_released(uint8_t button);
bool i2c_get_button_state(uint8_t button);

View File

@@ -173,12 +173,15 @@ void app_main(void) {
// LEDs flash while waiting, go solid when triggered
esp_reset_reason_t boot_reset_reason = esp_reset_reason();
if ((boot_reset_reason == ESP_RST_POWERON || boot_reset_reason == ESP_RST_EXT)
&& gpio_get_level(GPIO_NUM_13) == 0) {
&& i2c_button_held_raw(0)) {
ESP_LOGW(TAG, "Button held on cold boot — hold %ds for factory reset", FACTORY_RESET_HOLD_MS / 1000);
// Flash all LEDs while user holds button (100ms on/off cycle)
// Flash all LEDs while user holds button (100ms on/off cycle).
// GPIO13 is the NCA9535 INT line on V5 (not a direct button line),
// so we have to read INPUT0 over I2C to know if the button is
// currently held — the GPIO would only pulse low on edges.
int held_ms = 0;
while (gpio_get_level(GPIO_NUM_13) == 0 && held_ms < FACTORY_RESET_HOLD_MS) {
while (i2c_button_held_raw(0) && held_ms < FACTORY_RESET_HOLD_MS) {
i2c_set_led1((held_ms / 100) % 2 ? 0b111 : 0b000);
vTaskDelay(pdMS_TO_TICKS(100));
held_ms += 100;

View File

@@ -143,6 +143,14 @@ esp_err_t drive_relays(relay_port_t relay_state) {
BRIDGE_TRANSITION_LOGIC(JACK)
BRIDGE_TRANSITION_LOGIC(AUX)
/* In soft idle / hibernate the device is sleeping — don't drive any
* relay outputs, including the sensor rail. soft_idle_enter() already
* pushed the chip into the all-off state via i2c_relays_sleep(); the
* FSM keeps ticking but must not undo that. */
if (soft_idle_is_active()) {
return ESP_OK;
}
/* Sensor rail (P10) is on whenever the device is awake — including
* STATE_IDLE — so the SAFETY input can be observed continuously.
* It is dropped only in soft_idle_enter() (sleep) via i2c_relays_sleep,

View File

@@ -19,6 +19,7 @@
#include "esp_timer.h"
#include "i2c.h"
#include "driver/gpio.h"
#include "driver/rtc_io.h"
#include "rtc_wdt.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
@@ -33,6 +34,9 @@
#define PIN_BTN_INTERRUPT GPIO_NUM_13
#define RTC_NVS_NAMESPACE "hw"
#define RTC_NVS_KEY "rtc_time"
// Return microseconds from the RTC hardware timer.
// Used ONLY in rtc_restore_time() for crash-recovery (survives panics/WDT via RTC domain).
// RC oscillator drift (~150 kHz, ±5%) is negligible over a <30s crash restart (~1.5s worst case).
@@ -104,6 +108,82 @@ void soft_idle_exit(void)
rtc_reset_shutdown_timer();
}
void hibernate_enter(void)
{
ESP_LOGI("RTC", "Entering hibernate (deep sleep, EXT0 button wake, RTC discarded)");
/* Reuse the soft-idle teardown:
* - sets in_soft_idle = true, which gates the main-task LED loop and
* the FSM's drive_relays() so neither overwrites our pre-sleep
* output state during the wait-for-button-release window;
* - stops webserver + BT;
* - drives LEDs to 0 and writes i2c_relays_sleep() (sensor rail off,
* all bridges off). */
soft_idle_enter();
/* Discard saved RTC time so the next boot comes up with rtc_set=false.
* RTC slow memory keeps its contents across deep sleep on ESP32 (the
* RTC clock and slow-mem domain stay alive for EXT0 to work), so we
* also zero the RTC_DATA_ATTR globals here. Together with the NVS
* erase, this guarantees the next boot has no surviving time state. */
nvs_handle_t h;
if (nvs_open(RTC_NVS_NAMESPACE, NVS_READWRITE, &h) == ESP_OK) {
nvs_erase_key(h, RTC_NVS_KEY);
nvs_commit(h);
nvs_close(h);
}
rtc_set = false;
sync_unix_us = 0;
sync_rtc_us = 0;
next_alarm_time_s = -1;
/* If the operator is still pressing the button (web-UI path: they
* shouldn't be; cmd-line path: maybe), wait for release. The button
* is on NCA9535 P00; GPIO13 is the chip's INT line, which only
* pulses low on input changes — so we MUST read the actual button
* state via I2C, not the GPIO level. Capped so we don't loop forever. */
int waited_ms = 0;
while (i2c_button_held_raw(0) && waited_ms < 5000) {
vTaskDelay(pdMS_TO_TICKS(50));
waited_ms += 50;
}
vTaskDelay(pdMS_TO_TICKS(100));
/* Final TCA9555/NCA9535 write right before we halt the CPU — covers
* any stale-state edge cases (e.g. a write that snuck in before the
* FSM gate latched). */
i2c_set_led1(0);
i2c_relays_sleep();
/* Read NCA9535 INPUT0 to clear any pending INT (which would hold
* GPIO13 low and instantly satisfy our EXT0 wake-on-zero condition).
* i2c_button_held_raw() does the read as a side effect. */
(void)i2c_button_held_raw(0);
/* Clear any lingering wake sources from earlier configuration before
* enabling the only one we want. */
esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL);
/* GPIO13 carries the NCA9535 INT line (open-drain, asserts low on any
* input change, clears on INPUT0 read). EXT0 wake on level=0 fires
* the moment the user presses the button — INT pulls low, ESP wakes,
* the post-boot i2c_poll_buttons() read clears INT. */
esp_sleep_enable_ext0_wakeup(PIN_BTN_INTERRUPT, 0);
rtc_gpio_pullup_en(PIN_BTN_INTERRUPT);
rtc_gpio_pulldown_dis(PIN_BTN_INTERRUPT);
/* Note: we deliberately do NOT call esp_sleep_pd_config(... OFF). On
* ESP-IDF v5.3 that path is reference-counted and asserts when the
* counter would go negative; since nothing has previously called ON
* for these domains, OFF would abort. ESP-IDF picks the deepest
* compatible power state automatically given the wake source we set. */
ESP_LOGI("RTC", "esp_deep_sleep_start int_level=%d btn=%d",
gpio_get_level(PIN_BTN_INTERRUPT),
(int)i2c_button_held_raw(0));
esp_deep_sleep_start(); /* never returns */
}
int64_t rtc_get_s(void)
{
if (!rtc_set) return 0;
@@ -132,9 +212,6 @@ void rtc_set_s(int64_t tv_sec)
(long long)(esp_timer_get_time() / 1000000ULL));
}
#define RTC_NVS_NAMESPACE "hw"
#define RTC_NVS_KEY "rtc_time"
void rtc_save_time(void)
{
if (!rtc_set) return;

View File

@@ -37,6 +37,12 @@ void soft_idle_exit(void);
bool soft_idle_is_active(void);
bool soft_idle_button_raw(void); /* direct GPIO read, no I2C */
/* Deepest practical sleep with button wake (EXT0 on PIN_BTN_INTERRUPT).
* Discards the saved RTC time (NVS entry erased) — wake boots cold. RTC
* fast/slow memory are powered down too, so RTC_DATA_ATTR globals are lost.
* Does not return. */
void hibernate_enter(void);
/*void adjust_rtc_hour(char *key, int8_t dir);
void adjust_rtc_min(char *key, int8_t dir);*/

View File

@@ -77,8 +77,10 @@ static void cmd_post(char *json_data) {
// Check for special response flags before deleting
cJSON *reboot_flag = cJSON_GetObjectItem(response, "reboot");
cJSON *sleep_flag = cJSON_GetObjectItem(response, "sleep");
cJSON *hibernate_flag = cJSON_GetObjectItem(response, "hibernate");
bool should_reboot = cJSON_IsTrue(reboot_flag);
bool should_sleep = cJSON_IsTrue(sleep_flag);
bool should_hibernate = cJSON_IsTrue(hibernate_flag);
cJSON_Delete(response);
@@ -104,6 +106,13 @@ static void cmd_post(char *json_data) {
vTaskDelay(pdMS_TO_TICKS(2000));
soft_idle_enter();
}
if (should_hibernate) {
printf("\nHibernating in 2 seconds (button to wake, RTC discarded)...\n");
fflush(stdout);
vTaskDelay(pdMS_TO_TICKS(2000));
hibernate_enter(); /* never returns */
}
}
// Process help command

File diff suppressed because one or more lines are too long

View File

@@ -307,6 +307,12 @@
<button onclick="programRFSequence()">Program All Buttons</button>
</td>
</tr>
<tr>
<td>Deep Sleep</td>
<td>
<button onclick="sendCommand('hibernate')">Enter Deep Sleep</button>
</td>
</tr>
</table>
<button id="cancel_btn" onclick="location.reload();" disabled>Discard</button>
@@ -751,6 +757,10 @@
if (!await modalConfirm("Device will sleep. Are you sure?"))
return;
}
if (cmdName === 'hibernate') {
if (!await modalConfirm("Device will enter deep sleep — clock will be cleared, only the physical button can wake it. Are you sure?"))
return;
}
try {
const response = await fetch('./post', {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -468,6 +468,7 @@ static esp_err_t get_handler(httpd_req_t *req) {
* }
*/
static void soft_idle_enter_cb(void *arg) { soft_idle_enter(); }
static void hibernate_enter_cb(void *arg) { hibernate_enter(); }
static void webserver_restart_wifi_cb(void *arg) { webserver_restart_wifi(); }
/**
@@ -529,9 +530,11 @@ static esp_err_t post_handler_locked(httpd_req_t *req) {
// Check for special response flags
cJSON *reboot_flag = cJSON_GetObjectItem(response, "reboot");
cJSON *sleep_flag = cJSON_GetObjectItem(response, "sleep");
cJSON *hibernate_flag = cJSON_GetObjectItem(response, "hibernate");
cJSON *wifi_restart_flag = cJSON_GetObjectItem(response, "wifi_restart");
bool should_reboot = cJSON_IsTrue(reboot_flag);
bool should_sleep = cJSON_IsTrue(sleep_flag);
bool should_hibernate = cJSON_IsTrue(hibernate_flag);
bool should_restart_wifi = cJSON_IsTrue(wifi_restart_flag);
// Convert response to string
@@ -600,6 +603,24 @@ static esp_err_t post_handler_locked(httpd_req_t *req) {
}
return ESP_OK;
}
if (should_hibernate) {
ESP_LOGI(TAG, "Hibernating in 2 seconds...");
/* Same deadlock concern as soft_idle: webserver_stop() inside
* hibernate_enter() blocks on the httpd task; defer via timer. */
static esp_timer_handle_t s_hibernate_timer = NULL;
if (s_hibernate_timer == NULL) {
esp_timer_create_args_t ta = {
.callback = hibernate_enter_cb,
.name = "hibernate",
};
esp_timer_create(&ta, &s_hibernate_timer);
}
if (s_hibernate_timer != NULL) {
esp_timer_start_once(s_hibernate_timer, 2000 * 1000); /* 2 s in µs */
}
return ESP_OK;
}
err = httpd_resp_set_hdr(req, "Connection", "close");
if (err != ESP_OK) {