From 509f9b6e2bfeb63c6a0c7865270ae0a4713f3ef7 Mon Sep 17 00:00:00 2001 From: geeksville Date: Sat, 22 Feb 2020 12:01:59 -0800 Subject: [PATCH] WIP state machine builds --- TODO.md | 1 + docs/sw-design.md | 28 +++++++----- src/PowerFSM.cpp | 106 ++++++++++++++++++++++++++++++++++++++++++++ src/PowerFSM.h | 14 ++++++ src/configuration.h | 4 -- src/main.ino | 24 ++-------- src/screen.cpp | 32 +++++++------ src/screen.h | 11 ++++- src/sleep.cpp | 7 ++- src/sleep.h | 2 + 10 files changed, 174 insertions(+), 55 deletions(-) create mode 100644 src/PowerFSM.cpp create mode 100644 src/PowerFSM.h diff --git a/TODO.md b/TODO.md index 9f000ce10..7e6aa3ef8 100644 --- a/TODO.md +++ b/TODO.md @@ -72,6 +72,7 @@ until the phone pulls those packets. Ever so often power on bluetooth just so w Items after the first final candidate release. +* read the PMU battery fault indicators and blink/led/warn user on screen * make a no bluetooth configured yet screen - include this screen in the loop if the user hasn't yet paired * the AXP debug output says it is trying to charge at 700mA, but the max I've seen is 180mA, so AXP registers probably need to be set to tell them the circuit can only provide 300mAish max. So that the low charge rate kicks in faster and we don't wear out batteries. * increase the max charging rate a bit for 18650s, currently it limits to 180mA (at 4V). Work backwards from the 500mA USB limit (at 5V) and let the AXP charge at that rate. diff --git a/docs/sw-design.md b/docs/sw-design.md index 5c200fba1..e204f394e 100644 --- a/docs/sw-design.md +++ b/docs/sw-design.md @@ -7,44 +7,50 @@ This is a mini design doc for various core behaviors... From lower to higher power consumption. * Super-deep-sleep (SDS) - everything is off, CPU, radio, bluetooth, GPS. Only wakes due to timer or button press - -* deep-sleep (DS) - CPU is off, radio is on, bluetooth and GPS is off. Note: This mode is never used currently, because it only saves 1.5mA vs light-sleep onEntry: setBluetoothOn(false), call doDeepSleep onExit: (standard bootup code, starts in DARK) +* deep-sleep (DS) - CPU is off, radio is on, bluetooth and GPS is off. Note: This mode is never used currently, because it only saves 1.5mA vs light-sleep + (Not currently used) + * light-sleep (LS) - CPU is suspended (RAM stays alive), radio is on, bluetooth is off, GPS is off. Note: currently GPS is not turned off during light sleep, but there is a TODO item to fix this. - onEntry: setBluetoothOn(false), setGPSPower(false), call doLightSleep - onExit: start trying to get gps lock: gps.startLock(), once lock arrives service.sendPosition(BROADCAST) + onEntry: setBluetoothOn(false), setGPSPower(false) - happens inside doLightSleep() + onIdle: (if we wake because our led blink timer has expired) blink the led then go back to sleep until we sleep for ls_secs + onExit: setGPSPower(true), start trying to get gps lock: gps.startLock(), once lock arrives service.sendPosition(BROADCAST) * No bluetooth (NB) - CPU is running, radio is on, GPS is on but bluetooth is off, screen is off. - onEntry: setGPSPower(true), setBluetoothOn(false) + onEntry: setBluetoothOn(false) onExit: * running dark (DARK) - Everything is on except screen onEntry: setBluetoothOn(true) - onExit: + onExit: * full on (ON) - Everything is on onEntry: setBluetoothOn(true), screen.setOn(true) onExit: screen.setOn(false) - + ## Behavior ### events that increase CPU activity +* At cold boot: The initial state (after setup() has run) is DARK +* While in DARK: if we receive EVENT_BOOT, transition to ON (and show the bootscreen). This event will be sent if we detect we woke due to reset (as opposed to deep sleep) * While in LS: Once every position_broadcast_secs (default 15 mins) - the unit will wake into DARK mode and broadcast a "networkPing" (our position) and stay alive for wait_bluetooth_secs (default 30 seconds). This allows other nodes to have a record of our last known position if we go away and allows a paired phone to hear from us and download messages. * While in LS: Every send_owner_interval (defaults to 4, i.e. one hour), when we wake to send our position we _also_ broadcast our owner. This lets new nodes on the network find out about us or correct duplicate node number assignments. -* While in LS/NB/DARK: If the user presses a button we go to full ON mode for screen_on_secs (default 30 seconds). Multiple presses keeps resetting this timeout -* While in LS/NB/DARK: If we receive text messages, we go to full ON mode for screen_on_secs (same as if user pressed a button) -* While in LS: if we receive packets on the radio we will wake and handle them and stay awake in NB mode for min_wake_secs (default 10 seconds) - if we don't have packets we need to deliver to our phone. If we do have packets the phone would want we instead stay in DARK mode for wait_bluetooth secs. +* While in LS/NB/DARK: If the user presses a button (EVENT_PRESS) we go to full ON mode for screen_on_secs (default 30 seconds). Multiple presses keeps resetting this timeout +* While in LS/NB/DARK: If we receive new text messages (EVENT_RECEIVED_TEXT_MSG), we go to full ON mode for screen_on_secs (same as if user pressed a button) +* While in LS: if we receive packets on the radio (EVENT_RECEIVED_PACKET) we will wake and handle them and stay awake in NB mode for min_wake_secs (default 10 seconds) +* While in NB: If we do have packets the phone (EVENT_PACKETS_FOR_PHONE) would want we transition to DARK mode for wait_bluetooth secs. ### events that decrease cpu activity +* While in ON: If PRESS event occurs, reset screen_on_secs timer and tell the screen to handle the pess * While in ON: If it has been more than screen_on_secs since a press, lower to DARK * While in DARK: If time since last contact by our phone exceeds phone_timeout_secs (15 minutes), we transition down into NB mode * While in DARK or NB: If nothing above is forcing us to stay in a higher mode (wait_bluetooth_secs, min_wake_secs) we will lower down -into either LS or SDS levels. If either phone_sds_timeout_secs (default 1 hr) or mesh_sds_timeout_secs (default 1 hr) are exceeded we will lower into SDS mode for sds_secs (or a button press). Otherwise we will lower into LS mode for ls_secs (default 1 hr) (or until an interrupt, button press) +into either LS or SDS levels. If either phone_sds_timeout_secs (default 1 hr) or mesh_sds_timeout_secs (default 1 hr) are exceeded we will lower into SDS mode for sds_secs (default 1 hr) (or a button press). Otherwise we will lower into LS mode for ls_secs (default 1 hr) (or until an interrupt, button press) TODO: Eventually these scheduled intervals should be synchronized to the GPS clock, so that we can consider leaving the lora receiver off to save even more power. diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp new file mode 100644 index 000000000..014900ef9 --- /dev/null +++ b/src/PowerFSM.cpp @@ -0,0 +1,106 @@ + +#include "sleep.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "configuration.h" +#include "screen.h" +#include "PowerFSM.h" + +static void sdsEnter() +{ + /* + + // Don't deepsleep if we have USB power or if the user as pressed a button recently + // !isUSBPowered <- doesn't work yet because the axp192 isn't letting the battery fully charge when we are awake - FIXME + if (millis() - lastPressMs > radioConfig.preferences.mesh_sds_timeout_secs) + { + doDeepSleep(radioConfig.preferences.sds_secs); + } +*/ + + doDeepSleep(radioConfig.preferences.sds_secs * 1000LL); +} + +static void lsEnter() +{ +/* + // while we have bluetooth on, we can't do light sleep, but once off stay in light_sleep all the time + // we will wake from light sleep on button press or interrupt from the RF95 radio + if (!bluetoothOn && !is_screen_on() && service.radio.rf95.canSleep() && gps.canSleep()) + doLightSleep(radioConfig.preferences.ls_secs); + else + { + delay(msecstosleep); + } */ + + doLightSleep(radioConfig.preferences.ls_secs * 1000LL); +} + +static void lsIdle() +{ + // FIXME - blink led when we occasionally wake from timer, then go back to light sleep +} + +static void lsExit() +{ + // nothing +} + +static void nbEnter() +{ + setBluetoothEnable(false); +} + +static void darkEnter() +{ + DEBUG_MSG("screen timeout, turn it off for now...\n"); + screen.setOn(true); +} + +static void onEnter() +{ + screen.setOn(true); + setBluetoothEnable(true); +} + +static void onExit() +{ + screen.setOn(false); +} +static void wakeForPing() +{ +} + +static void screenPress() +{ + screen.onPress(); +} + +State stateSDS(sdsEnter, NULL, NULL); +State stateLS(lsEnter, lsIdle, lsExit); +State stateNB(nbEnter, NULL, NULL); +State stateDARK(darkEnter, NULL, NULL); +State stateON(onEnter, NULL, onExit); +Fsm powerFSM(&stateDARK); + +void PowerFSM_setup() +{ + powerFSM.add_transition(&stateDARK, &stateON, EVENT_BOOT, NULL); + powerFSM.add_transition(&stateLS, &stateDARK, EVENT_WAKE_TIMER, wakeForPing); + powerFSM.add_transition(&stateLS, &stateNB, EVENT_RECEIVED_PACKET, NULL); + + powerFSM.add_transition(&stateLS, &stateON, EVENT_PRESS, NULL); + powerFSM.add_transition(&stateNB, &stateON, EVENT_PRESS, NULL); + powerFSM.add_transition(&stateDARK, &stateON, EVENT_PRESS, NULL); + powerFSM.add_transition(&stateON, &stateON, EVENT_PRESS, screenPress); // reenter On to restart our timers + + powerFSM.add_transition(&stateLS, &stateON, EVENT_RECEIVED_TEXT_MSG, NULL); + powerFSM.add_transition(&stateNB, &stateON, EVENT_RECEIVED_TEXT_MSG, NULL); + powerFSM.add_transition(&stateDARK, &stateON, EVENT_RECEIVED_TEXT_MSG, NULL); + + powerFSM.add_transition(&stateNB, &stateDARK, EVENT_PACKET_FOR_PHONE, NULL); + + powerFSM.add_timed_transition(&stateON, &stateDARK, radioConfig.preferences.screen_on_secs, NULL); + + powerFSM.add_timed_transition(&stateDARK, &stateNB, radioConfig.preferences.phone_timeout_secs, NULL); +} \ No newline at end of file diff --git a/src/PowerFSM.h b/src/PowerFSM.h new file mode 100644 index 000000000..7a72d4d7a --- /dev/null +++ b/src/PowerFSM.h @@ -0,0 +1,14 @@ +#pragma once + +#include "Fsm.h" + +// See sw-design.md for documentation + +#define EVENT_PRESS 1 +#define EVENT_WAKE_TIMER 2 +#define EVENT_RECEIVED_PACKET 3 +#define EVENT_PACKET_FOR_PHONE 4 +#define EVENT_RECEIVED_TEXT_MSG 5 +#define EVENT_BOOT 6 + +extern Fsm powerFSM; \ No newline at end of file diff --git a/src/configuration.h b/src/configuration.h index 7bf206db8..a0180a68e 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -60,10 +60,6 @@ along with this program. If not, see . #define REQUIRE_RADIO true // If true, we will fail to start if the radio is not found -// If not defined, we will wait for lock forever - -#define MINWAKE_MSECS (60 * 60 * 1000) // stay awake a long time (30 mins) for debugging - FIXME, change per the TBD sleep rules doc -// #define MINWAKE_MSECS (30 * 1000) // Wait after every boot for GPS lock (may need longer than 5s because we turned the gps off during deep sleep) // ----------------------------------------------------------------------------- // DEBUG diff --git a/src/main.ino b/src/main.ino index ad7790799..69315a554 100644 --- a/src/main.ino +++ b/src/main.ino @@ -37,6 +37,7 @@ #include "esp_pm.h" #include "MeshRadio.h" #include "sleep.h" +#include "PowerFSM.h" #ifdef T_BEAM_V10 #include "axp20x.h" @@ -337,7 +338,7 @@ void loop() // if user presses button for more than 3 secs, discard our network prefs and reboot (FIXME, use a debounce lib instead of this boilerplate) static bool wasPressed = false; static uint32_t minPressMs; // what tick should we call this press long enough - static uint32_t lastPingMs, lastPressMs; + static uint32_t lastPingMs; if (!digitalRead(BUTTON_PIN)) { if (!wasPressed) @@ -349,7 +350,6 @@ void loop() wasPressed = true; uint32_t now = millis(); - lastPressMs = now; minPressMs = now + 3000; if (now - lastPingMs > 60 * 1000) @@ -358,7 +358,7 @@ void loop() lastPingMs = now; } - screen_press(); + powerFSM.trigger(EVENT_PRESS); } } else if (wasPressed) @@ -375,15 +375,6 @@ void loop() } #endif -#ifdef MINWAKE_MSECS - // Don't deepsleep if we have USB power or if the user as pressed a button recently - // !isUSBPowered <- doesn't work yet because the axp192 isn't letting the battery fully charge when we are awake - FIXME - if (millis() - lastPressMs > MINWAKE_MSECS) - { - doDeepSleep(radioConfig.preferences.sds_secs); - } -#endif - // No GPS lock yet, let the OS put the main CPU in low power mode for 100ms (or until another interrupt comes in) // i.e. don't just keep spinning in loop as fast as we can. //DEBUG_MSG("msecs %d\n", msecstosleep); @@ -391,12 +382,5 @@ void loop() // FIXME - until button press handling is done by interrupt (see polling above) we can't sleep very long at all or buttons feel slow msecstosleep = 10; - // while we have bluetooth on, we can't do light sleep, but once off stay in light_sleep all the time - // we will wake from light sleep on button press or interrupt from the RF95 radio - if (!bluetoothOn && !is_screen_on() && service.radio.rf95.canSleep() && gps.canSleep()) - doLightSleep(60 * 1000); // FIXME, wake up to briefly flash led, then go back to sleep (without repowering bluetooth) - else - { - delay(msecstosleep); - } + delay(msecstosleep); } \ No newline at end of file diff --git a/src/screen.cpp b/src/screen.cpp index bbb7fde58..b0920da45 100644 --- a/src/screen.cpp +++ b/src/screen.cpp @@ -60,7 +60,7 @@ static bool showingBootScreen = true; // start by showing the bootscreen uint32_t lastPressMs; -bool is_screen_on() { return screenOn; } +bool Screen::isOn() { return screenOn; } void msOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) { @@ -491,22 +491,19 @@ void _screen_header() } #endif -void screen_off() -{ - if (!disp) - return; - - dispdev.displayOff(); - screenOn = false; -} - -void screen_on() + +void Screen::setOn(bool on) { if (!disp) return; + if(on != screenOn) { + if(on) dispdev.displayOn(); - screenOn = true; + else + dispdev.displayOff(); + screenOn = on; + } } static void screen_print(const char *text, uint8_t x, uint8_t y, uint8_t alignment) @@ -579,7 +576,7 @@ void Screen::setup() // Scroll buffer dispdev.setLogBuffer(3, 32); - screen_on(); // update our screenOn bool + setOn(true); // update our screenOn bool #ifdef BICOLOR_DISPLAY dispdev.flipScreenVertically(); // looks better without this on lora32 @@ -612,7 +609,7 @@ void Screen::doTask() if (wakeScreen) // If a new text message arrived, turn the screen on immedately { lastPressMs = millis(); // if we were told to wake the screen, reset the press timeout - screen_on(); // make sure the screen is not asleep + screen.setOn(true); // make sure the screen is not asleep wakeScreen = false; } @@ -655,11 +652,12 @@ void Screen::doTask() nodeDB.updateTextMessage = false; } +/* if (millis() - lastPressMs > SCREEN_SLEEP_MS) { DEBUG_MSG("screen timeout, turn it off for now...\n"); - screen_off(); - } + screen.setOn(false); + } */ } } @@ -716,7 +714,7 @@ void screen_set_frames() } /// handle press of the button -void screen_press() +void Screen::onPress() { // screen_start_bluetooth(123456); diff --git a/src/screen.h b/src/screen.h index c23702d1e..18918bf18 100644 --- a/src/screen.h +++ b/src/screen.h @@ -4,7 +4,6 @@ void screen_print(const char * text); -void screen_on(), screen_off(), screen_press(); // Show the bluetooth PIN screen void screen_start_bluetooth(uint32_t pin); @@ -12,7 +11,6 @@ void screen_start_bluetooth(uint32_t pin); // restore our regular frame list void screen_set_frames(); -bool is_screen_on(); /** * Slowly I'm moving screen crap into this class @@ -27,6 +25,15 @@ public: /// Turn on the screen asap void doWakeScreen(); + + /// Is the screen currently on + bool isOn(); + + /// Turn the screen on/off + void setOn(bool on); + + /// Handle a button press + void onPress(); }; extern Screen screen; \ No newline at end of file diff --git a/src/sleep.cpp b/src/sleep.cpp index f07a986bd..02641a509 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -65,6 +65,8 @@ void setLed(bool ledOn) void setGPSPower(bool on) { + DEBUG_MSG("Setting GPS power=%d\n", on); + #ifdef T_BEAM_V10 if (axp192_found) axp.setPowerOutPut(AXP192_LDO3, on ? AXP202_ON : AXP202_OFF); // GPS main power @@ -97,7 +99,7 @@ void doDeepSleep(uint64_t msecToWake) BLEDevice::deinit(false); // We are required to shutdown bluetooth before deep or light sleep - screen_off(); // datasheet says this will draw only 10ua + screen.setOn(false); // datasheet says this will draw only 10ua // Put radio in sleep mode (will still draw power but only 0.2uA) service.radio.rf95.sleep(); @@ -212,6 +214,7 @@ void doLightSleep(uint64_t sleepMsec) // FIXME, use a more reasonable default setBluetoothEnable(false); // has to be off before calling light sleep gps.prepareSleep(); // abandon in-process parsing + setGPSPower(false); // kill GPS power setLed(false); // Never leave led on while in light sleep // NOTE! ESP docs say we must disable bluetooth and wifi before light sleep @@ -228,6 +231,8 @@ void doLightSleep(uint64_t sleepMsec) // FIXME, use a more reasonable default esp_sleep_enable_timer_wakeup(sleepUsec); esp_light_sleep_start(); DEBUG_MSG("Exit light sleep\n"); + + setGPSPower(true); // restore GPS power } #if 0 diff --git a/src/sleep.h b/src/sleep.h index ae3d09f42..9e3aadd3d 100644 --- a/src/sleep.h +++ b/src/sleep.h @@ -1,5 +1,7 @@ #pragma once +#include "Arduino.h" + void doDeepSleep(uint64_t msecToWake); void doLightSleep(uint64_t msecToWake); void setBluetoothEnable(bool on);