feat: UWB tag power management — sleep mode (Issue #689) #691
@ -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 <Arduino.h>
|
||||
@ -35,6 +46,8 @@
|
||||
#include <WiFi.h>
|
||||
#include <esp_now.h>
|
||||
#include <esp_wifi.h>
|
||||
#include <esp_sleep.h>
|
||||
#include <driver/gpio.h>
|
||||
#include "DW1000Ranging.h"
|
||||
|
||||
#include <Adafruit_GFX.h>
|
||||
@ -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();
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user