diff --git a/esp32/uwb_tag/src/main.cpp b/esp32/uwb_tag/src/main.cpp index d83bbac..b6c7ca2 100644 --- a/esp32/uwb_tag/src/main.cpp +++ b/esp32/uwb_tag/src/main.cpp @@ -27,6 +27,17 @@ * I2C: SDA=4 SCL=5 OLED @0x3C, MPU6050 @0x68 * E-Stop: GPIO 0 (BOOT), active LOW — disabled (floats LOW) * LED: GPIO 2 + * + * Power management (Issue #689): + * OLED auto-off after 30s inactivity → saves ~25mA + * DW1000 deep sleep after 5min idle → saves ~155mA (160mA→3.5μA) + * Periodic 10s wake window to reacquire anchors + * ESP32 deep sleep on GPIO0 hold 3s → saves ~240mA total + * Wake on GPIO0 button press + * Active: ~250mA Sleep: <5mA (50x reduction target) + * + * NOTE: GPIO0 requires hardware pull-up (10kΩ to 3.3V) for reliable + * button detection. Internal pullup is marginal on some boards. */ #include @@ -35,6 +46,8 @@ #include #include #include +#include +#include #include "DW1000Ranging.h" #include @@ -373,11 +386,150 @@ static void estop_check(void) { } } +/* ── Power Management ──────────────────────────────────────────── + * + * Power states (cascading): + * ACTIVE → display on, DW1000 ranging (~250mA) + * DISPLAY_OFF → display off, DW1000 ranging (~225mA) + * DW_SLEEP → display off, DW1000 deep sleep (~10mA) + * DW1000 wakes for a 5s scan window every 30s + * DEEP_SLEEP → ESP32 deep sleep (<0.5mA) + * Triggered by GPIO0 held 3s + * Wake by GPIO0 press (EXT0) + * + * Activity resets the idle timer and wakes display + DW1000. + * ─────────────────────────────────────────────────────────────── */ + +#define PM_DISPLAY_TIMEOUT_MS (30UL * 1000) /* 30s → display off */ +#define PM_DW1000_SLEEP_MS (300UL * 1000) /* 5min → DW1000 deep sleep */ +#define PM_DW1000_WAKE_PERIOD_MS (30UL * 1000) /* wake DW1000 every 30s */ +#define PM_DW1000_WAKE_WINDOW_MS 5000 /* stay awake 5s to range */ +#define PM_DEEP_SLEEP_HOLD_MS 3000 /* GPIO0 hold → deep sleep */ + +static uint32_t g_pm_last_activity_ms = 0; +static bool g_pm_display_on = true; + +/* DW1000 deep sleep state */ +static bool g_dw1000_sleeping = false; +static uint32_t g_dw1000_sleep_ms = 0; /* when DW1000 entered sleep */ +static uint32_t g_dw1000_wake_ms = 0; /* when current wake started */ +static bool g_dw1000_in_window = false; + +/* GPIO0 hold tracking */ +static uint32_t g_btn_held_ms = 0; + +/* Forward declaration */ +static void dw1000_ranging_init(void); + +/* Called on any ranging activity — resets idle timer, wakes hw */ +static void pm_activity(void) { + g_pm_last_activity_ms = millis(); + + if (!g_pm_display_on) { + display.ssd1306_command(SSD1306_DISPLAYON); + g_pm_display_on = true; + Serial.println("[pm] Display on (activity)"); + } +} + +static void pm_dw1000_sleep(void) { + Serial.println("[pm] DW1000 deep sleep"); + DW1000.deepSleep(); + g_dw1000_sleeping = true; + g_dw1000_sleep_ms = millis(); + g_dw1000_in_window = false; +} + +/* Wake DW1000 and reinitialize ranging */ +static void pm_dw1000_wake(void) { + Serial.println("[pm] DW1000 waking"); + DW1000.spiWakeup(); + delay(5); + dw1000_ranging_init(); + g_dw1000_sleeping = false; + g_dw1000_in_window = true; + g_dw1000_wake_ms = millis(); +} + +static void pm_enter_deep_sleep(void) { + Serial.println("[pm] ESP32 deep sleep — GPIO0 to wake"); + Serial.flush(); + + display.ssd1306_command(SSD1306_DISPLAYON); + display.clearDisplay(); + display.setTextSize(2); + display.setTextColor(SSD1306_WHITE); + display.setCursor(10, 20); + display.println(F("Sleeping...")); + display.setTextSize(1); + display.setCursor(20, 48); + display.println(F("Press to wake")); + display.display(); + delay(800); + display.ssd1306_command(SSD1306_DISPLAYOFF); + + /* DW1000 deep sleep before ESP32 sleeps */ + if (!g_dw1000_sleeping) { + DW1000.deepSleep(); + } + + /* Wake on GPIO0 low (button press) */ + esp_sleep_enable_ext0_wakeup(GPIO_NUM_0, 0); + esp_deep_sleep_start(); + /* never returns */ +} + +static void pm_update(void) { + uint32_t now = millis(); + uint32_t idle_ms = now - g_pm_last_activity_ms; + + /* ── OLED auto-off ─────────────────────────────────────────── */ + if (g_pm_display_on && idle_ms >= PM_DISPLAY_TIMEOUT_MS) { + display.ssd1306_command(SSD1306_DISPLAYOFF); + g_pm_display_on = false; + Serial.println("[pm] Display off (30s idle)"); + } + + /* ── DW1000 sleep ──────────────────────────────────────────── */ + if (!g_dw1000_sleeping) { + if (idle_ms >= PM_DW1000_SLEEP_MS) { + pm_dw1000_sleep(); + } + } else { + /* Periodic scan window */ + bool window_expired = g_dw1000_in_window && + (now - g_dw1000_wake_ms) >= PM_DW1000_WAKE_WINDOW_MS; + bool period_elapsed = !g_dw1000_in_window && + (now - g_dw1000_sleep_ms) >= PM_DW1000_WAKE_PERIOD_MS; + + if (window_expired) { + /* End of scan window with no activity → back to sleep */ + Serial.println("[pm] DW1000 scan window done — back to sleep"); + pm_dw1000_sleep(); + } else if (period_elapsed) { + pm_dw1000_wake(); + } + } + + /* ── GPIO0 hold → deep sleep ───────────────────────────────── */ + if (digitalRead(PIN_ESTOP) == LOW) { + if (g_btn_held_ms == 0) { + g_btn_held_ms = now; + } else if ((now - g_btn_held_ms) >= PM_DEEP_SLEEP_HOLD_MS) { + pm_enter_deep_sleep(); + /* unreachable */ + } + } else { + g_btn_held_ms = 0; + } +} + /* ── OLED display (5 Hz) ────────────────────────────────────────── */ static uint32_t g_disp_last = 0; static void display_update(void) { + if (!g_pm_display_on) return; if (millis() - g_disp_last < 200) return; g_disp_last = millis(); @@ -400,6 +552,24 @@ static void display_update(void) { return; } + /* Show sleep status when DW1000 is sleeping */ + if (g_dw1000_sleeping && !g_dw1000_in_window) { + uint32_t next_wake_ms = PM_DW1000_WAKE_PERIOD_MS - + (millis() - g_dw1000_sleep_ms); + display.setTextSize(1); + display.setTextColor(SSD1306_WHITE); + display.setCursor(0, 0); + display.println(F("UWB SLEEP")); + display.setCursor(0, 16); + display.printf("Wake in %ds", (int)(next_wake_ms / 1000)); + display.setCursor(0, 32); + display.printf("Idle: %ds", (int)((millis() - g_pm_last_activity_ms) / 1000)); + display.setCursor(0, 48); + display.println(F("Hold BTN 3s: off")); + display.display(); + return; + } + uint32_t now = millis(); /* Find closest anchor */ @@ -499,6 +669,14 @@ static void newRange(void) { digitalWrite(PIN_LED, HIGH); delay(1); digitalWrite(PIN_LED, LOW); + + /* Range received → mark activity, wake display/DW1000 if needed */ + pm_activity(); + + /* If DW1000 was in a scan window, it's awake — keep it up */ + if (g_dw1000_in_window) { + g_dw1000_in_window = false; /* anchor found, stay fully active */ + } } static void newDevice(DW1000Device *device) { @@ -509,6 +687,16 @@ static void inactiveDevice(DW1000Device *device) { Serial.printf("+GONE:%04X\r\n", device->getShortAddress()); } +/* ── DW1000 init helper (used at startup and after wakeup) ─────── */ + +static void dw1000_ranging_init(void) { + DW1000Ranging.initCommunication(PIN_RST, PIN_CS, PIN_IRQ); + DW1000Ranging.attachNewRange(newRange); + DW1000Ranging.attachNewDevice(newDevice); + DW1000Ranging.attachInactiveDevice(inactiveDevice); + DW1000Ranging.startAsTag(TAG_ADDR, DW1000.MODE_LONGDATA_RANGE_LOWPOWER); +} + /* ── Setup ──────────────────────────────────────────────────────── */ void setup(void) { @@ -519,6 +707,12 @@ void setup(void) { pinMode(PIN_LED, OUTPUT); digitalWrite(PIN_LED, LOW); + /* Log wakeup cause after deep sleep */ + esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause(); + if (cause == ESP_SLEEP_WAKEUP_EXT0) { + Serial.println("[pm] Woke from deep sleep via GPIO0"); + } + Serial.printf("\r\n[uwb_tag] tag_id=0x%02X starting\r\n", TAG_ID); /* I2C bus (shared: OLED @0x3C + MPU6050 @0x68) */ @@ -575,13 +769,7 @@ void setup(void) { /* DW1000 */ SPI.begin(PIN_SCK, PIN_MISO, PIN_MOSI); - DW1000Ranging.initCommunication(PIN_RST, PIN_CS, PIN_IRQ); - - DW1000Ranging.attachNewRange(newRange); - DW1000Ranging.attachNewDevice(newDevice); - DW1000Ranging.attachInactiveDevice(inactiveDevice); - - DW1000Ranging.startAsTag(TAG_ADDR, DW1000.MODE_LONGDATA_RANGE_LOWPOWER); + dw1000_ranging_init(); /* Init state */ for (int i = 0; i < NUM_ANCHORS; i++) { @@ -590,8 +778,14 @@ void setup(void) { g_anchor_last_ok[i] = 0; } + g_pm_last_activity_ms = millis(); + Serial.println("[uwb_tag] DW1000 ready MODE_LONGDATA_RANGE_LOWPOWER"); Serial.println("[uwb_tag] Ranging + display + ESP-NOW active"); + Serial.printf("[pm] Timeouts: display=%ds DW1000=%ds deep_sleep=hold_%ds\r\n", + PM_DISPLAY_TIMEOUT_MS / 1000, + PM_DW1000_SLEEP_MS / 1000, + PM_DEEP_SLEEP_HOLD_MS / 1000); } /* ── Loop ───────────────────────────────────────────────────────── */ @@ -630,11 +824,14 @@ void loop(void) { s_last_imu_tx = millis(); } - /* DW1000 ranging */ - if (!g_estop_active) { + /* DW1000 ranging — skip while DW1000 is in deep sleep */ + if (!g_dw1000_sleeping && !g_estop_active) { DW1000Ranging.loop(); } + /* Power management (display timeout, DW1000 sleep, GPIO0 deep sleep) */ + pm_update(); + /* Display at 5 Hz */ display_update();