fix i2c, add deep sleep
This commit is contained in:
13
main/comms.c
13
main/comms.c
@@ -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");
|
||||
|
||||
27
main/i2c.c
27
main/i2c.c
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
83
main/rtc.c
83
main/rtc.c
@@ -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;
|
||||
|
||||
@@ -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);*/
|
||||
|
||||
|
||||
@@ -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
@@ -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
1187
main/webpage_gzip.h
1187
main/webpage_gzip.h
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user