Boron404x constantly wakes up from ULP mode via network

This setup allows for the Boron to go into ULP sleep mode and wake every 900 seconds (15 minutes) to read and publish data. It worked fine for about a week, but now it is waking up from the network at random intervals (15 s < t < 2 min). I could simply remove the .network(NETWORK_INTERFACE_CELLULAR) from the code, but I would like to be able to wake it up from my function call to change the sleep duration.

Here is the code that it has been running:

#include "Particle.h"

//Instances
SystemSleepConfiguration config; //Sleep Config instance to change sleep mode/wake options

// PINS
#define GROUND_MOISTURE_PIN A0 //Delcare analog pins for moisture readings
#define SURFACE_MOISTURE_PIN A1 

#define HIGH_FREQ_BUTTON_PIN 5 //Declare digital pin for high frequency button pin
#define LOW_FREQ_BUTTON_PIN 6 //Declare digital pin for low frequency button pin

// VARIABLES 
double ground_moisture, surface_moisture, ground_percent, surface_percent; //Declare variables to store moisture readings

int read_delay = 900; //Delay time variable (seconds) is passed to 'delayTimeSeconds()' or the 'config.mode(SLEEP_MODE).duration(ms)'
const unsigned long wait_time = 1800; //Time (seconds) until system goes to old reading state if user forgets to reset 
unsigned long start_time, current_time; //Time variables to hold start time and current time after user initiates faster polling rate

const unsigned long high_freq_button_time = 300; //Set high_freq button time delay (seconds) (Faster)
const unsigned long low_freq_button_time = 900; //Set low_freq button time delay (seconds) (Slower)

int high_freq_button_state; //Variable to hold current button state
int last_high_freq_button_state = 0; //Variable to hold last button state (0 or 1)

int low_freq_button_state; 
int last_low_freq_button_state = 0; 

unsigned long last_high_freq_debounce_time = 0; 
unsigned long last_low_freq_debounce_time = 0;

unsigned long debounce_delay = 50;

unsigned long time_pressed = 0; //Variable to store the time when the high_freq button is pressed
bool high_freq_button_is_pressed = false; //Boolean to check if the high_freq button is the last button pressed

int batteryLevel = 0; //Variable to store battery level

int32_t temp; //Variables to get and hold the die temperature
uint32_t res;
float tempC; 

bool dataSent = false; //Boolean to check if data has been sent

//  FUNCTIONS 
int changeDelaySeconds(String seconds); //Function to change the read_delay
void delayTimeSeconds(int time_seconds); //Function to delay the read time and break if there is any change (NOT TO USE WITH ULP mode)
const char* getBatteryState(); //Function to get battery state (see documentation)
const char* getWakeupReason(); //Function to get wakeup readon from sleep (see documentation)
void checkFreqButtons(); //Function to check if the high_freq and low_freq buttons have been pressed and perform respective operations
void publishData(); //Function to publish all data to the particle cloud
void checkLastPressTime(); //Function to check how long the high_freq button has been pressed and to turn it off
void getDieTemp(); //Function to get the temperature of the die (nRF52840) on the Boron in degrees Celsius
void getMoisture(); //Function to read/calculate the ground and surface moistures
void firmwareUpdateHandler(system_event_t event, unsigned int param); //Function to handle if the firmware is being updated

// These are the states in the finite state machine, handled in loop()
enum State {
    STATE_WAIT_CONNECTED = 0,
    STATE_PUBLISH,
    STATE_PRE_SLEEP,
    STATE_SLEEP,
    STATE_FIRMWARE_UPDATE
};

State state = STATE_WAIT_CONNECTED;
unsigned long stateTime;
bool firmwareUpdateInProgress = false;

const std::chrono::milliseconds publishMaxTime = 3min; //Time to wait for publish to the particle cloud
const std::chrono::milliseconds firmwareUpdateMaxTime = 5min; //Time to wait for firmware update
const std::chrono::milliseconds connectMaxTime = 6min; //Wait time for connection
const std::chrono::milliseconds cloudMinTime = 10s; //Time to wait for any particle cloud activity, this adds a delay of time specified

SYSTEM_THREAD(ENABLED);
SYSTEM_MODE(SEMI_AUTOMATIC);

void setup() {
    
    System.on(firmware_update, firmwareUpdateHandler);
    
    Cellular.on();
    Particle.connect();
    
    pinMode(GROUND_MOISTURE_PIN, INPUT); //Analog input pin for moisture readings
    pinMode(SURFACE_MOISTURE_PIN, INPUT); 
    
    pinMode(HIGH_FREQ_BUTTON_PIN, INPUT); //High_Freq Button
    pinMode(LOW_FREQ_BUTTON_PIN, INPUT); //Low_Freq Button
    
    //Particle functions
    Particle.function("Delay (s)", changeDelaySeconds); //Register delay function to the cloud to call using the cloud
    
    //Particle variables
    Particle.variable("Data Sent", dataSent);
    Particle.variable("HF BTN", high_freq_button_is_pressed);
    
    attachInterrupt(HIGH_FREQ_BUTTON_PIN, checkFreqButtons, CHANGE); //Interrupt for high_freq button when GPIO pin changes states
    attachInterrupt(LOW_FREQ_BUTTON_PIN, checkFreqButtons, CHANGE); //Interrupt for low_freq button when GPIO pin changes states
      
    Time.now(); //Initialize RTC time
    stateTime = millis(); //Initialize stateTime start
}

void loop(){
    checkLastPressTime();
    switch(state){
        case STATE_WAIT_CONNECTED: //Wait for connection
            if(Particle.connected()){
                state = STATE_PUBLISH; //Once connected chnage state to PUBLISH
                stateTime = millis(); 
            }
            else if(millis() - stateTime >= connectMaxTime.count()){ //If it takes too long to connect, go to sleep
                state = STATE_SLEEP;
            }
            break;
            
        case STATE_PUBLISH:
            getDieTemp();
            getMoisture();
            publishData();
            
            if(millis() - stateTime < cloudMinTime.count()){ //Give it time to publish the data
                state = STATE_PRE_SLEEP;
            }
            else{
                state = STATE_SLEEP;
            }
            break;
            
        case STATE_PRE_SLEEP:
            if(millis() - stateTime >= cloudMinTime.count()){ //After buffer time and data published, change to sleep state
                state = STATE_SLEEP;
            }
            break;
            
        case STATE_SLEEP:
            if(firmwareUpdateInProgress){ //Wait for any firware to be updated 
                state = STATE_FIRMWARE_UPDATE;
                stateTime = millis();
                break;
            }
            else{ //If no firware update in progress, configure sleep mode and go to sleep
                config.mode(SystemSleepMode::ULTRA_LOW_POWER) //Set sleep mode to ULP to save power
                    .duration(read_delay * 1000) //Wake after t time in milliseconds (23 minutes is the maximum before losing connection)
                    .network(NETWORK_INTERFACE_CELLULAR) //Wake when we talk to the device through the particle cloud
                    .gpio(HIGH_FREQ_BUTTON_PIN, CHANGE) //Wake when one of the buttons is pressed
                    .gpio(LOW_FREQ_BUTTON_PIN, CHANGE)
                    
                Particle.publish("WAKEUP", getWakeupReason(), PRIVATE); //This is here for debugging, move 'System.sleep(config)' here found in 'getWakeUpReason()' if a wakeup reason isn't needed
                state = STATE_WAIT_CONNECTED;
            }
            break;
            
        case STATE_FIRMWARE_UPDATE:
            if(!firmwareUpdateInProgress){ //If no firmware update change to sleep state
                state = STATE_SLEEP;
            }
            else if (millis() - stateTime >= firmwareUpdateMaxTime.count()) { //If firmware update takes too long, go to sleep
                state = STATE_SLEEP;
            }
            break;
    }
}


// FUNCTION DEFINITIONS
int changeDelaySeconds(String seconds){
    read_delay = seconds.toInt(); //Change delay based on cloud function input
    return seconds.toInt(); //Particle function must return an integer to view from the cloud
}

void delayTimeSeconds(int time_seconds){
    for(int i = 0; i < time_seconds; i++){
        if(time_seconds != read_delay) //If delay ever changes due to button press or function call
            break; //This way we can change the delay immediately instead of waiting for the delay() function
        else
            delay(1000); //Delay for one second
    }
}

const char* getBatteryState() {
    switch (System.batteryState()) {
        case 0:
            return "Unknown";
        case 1:
            return "Not Charging";
        case 2:
            return "Charging";
        case 3:
            return "Charged";
        case 4:
            return "Discharging";
        case 5:
            return "Fault";
        case 6:
            return "Disconnected";
        default:
            return "Error";
    }
}

const char* getWakeupReason(){
    SystemSleepResult result = System.sleep(config); //Go to sleep using the passed configuration
    switch (result.wakeupReason()) {
        case SystemSleepWakeupReason::UNKNOWN: 
            return "UNKNOWN";
        case SystemSleepWakeupReason::BY_GPIO: 
            return "BUTTON";
        case SystemSleepWakeupReason::BY_RTC: 
            return "RTC";
        case SystemSleepWakeupReason::BY_NETWORK:
            return "NETWORK";
        default:
            return "Error/DID NOT WAKE";
    }
}

void checkFreqButtons(){ // Atached to the GPIO interrupts for immediate button recognition
    //Debounce for high_freq button
    if((Time.now() - last_high_freq_debounce_time/1000) > debounce_delay){
        int high_freq_reading = digitalRead(HIGH_FREQ_BUTTON_PIN);
        if(high_freq_reading != last_high_freq_button_state){
            last_high_freq_debounce_time = Time.now();
            last_high_freq_button_state = high_freq_reading;
            if(high_freq_button_state == 0){
                read_delay = high_freq_button_time; //Set to a faster polling rate
                time_pressed = Time.now(); //Get time pressed to begin timing
                high_freq_button_is_pressed = true; //High_freq button has been pressed
            }
        }
    }


    
    //Debounce for low_freq button
    if((Time.now() - last_low_freq_debounce_time/1000) > debounce_delay){
        int low_freq_reading = digitalRead(LOW_FREQ_BUTTON_PIN);
        if(low_freq_reading != last_low_freq_button_state){
            last_low_freq_debounce_time = Time.now();
            last_low_freq_button_state = low_freq_reading;
            if(low_freq_button_state == 0){
                read_delay = low_freq_button_time; //Set to a slower polling rate
                high_freq_button_is_pressed = true; //"Unpress" high_freq button
            }
        }
    }
}

void publishData(){
    Particle.publish("Europa", String(int(ground_percent)) + "," + String(int(surface_percent)) + "," + String(int(System.batteryCharge())), PRIVATE); //Publish data to the particle cloud
    Particle.publish("Battery State", getBatteryState(), PRIVATE); //Publish battery state to the particle cloud 
    Particle.publish("Die Temp", String(tempC), PRIVATE); //Publish the temperature of the die to the particle cloud
    dataSent = true;
}

void checkLastPressTime(){
    //Check if it has been long enough to automatically go back to a slower polling rate
    if( (Time.now() - time_pressed) >= wait_time & high_freq_button_is_pressed ){ // This will reset the frequency if the user forgets after pressing the high_freq button
        read_delay = low_freq_button_time; //Go back to slow polling rate
        high_freq_button_is_pressed = false; //Reset the high_freq button to not pressed
    }
}

void getDieTemp(){
    //Get die temperature in 0.25 degrees Celsius.
    res = sd_temp_get(&temp);
    if (res == NRF_SUCCESS) {
		tempC = (float)temp / 4;
	}
}

void getMoisture(){
    //Get ground moisture
    ground_moisture = analogRead(GROUND_MOISTURE_PIN); //Get raw moisture readings
    surface_moisture = analogRead(SURFACE_MOISTURE_PIN); 
    
    ground_percent = (1.0-(ground_moisture/4095.0)) * 100; //Calculate readable moisture readings
    surface_percent = (1.0-(surface_moisture/4095.0)) * 100;
}

void firmwareUpdateHandler(system_event_t event, unsigned int param) {
  switch(param) {
    case firmware_update_begin:
        firmwareUpdateInProgress = true;
        break;
    case firmware_update_complete:
        break;
    case firmware_update_failed:
        firmwareUpdateInProgress = false;
        break;
  }
}

Just to be sure, you don't have anything querying the variables, as that will wake up the device as well. Temporarily commenting out the calls to register the function and variable handlers is a good test for that.

Also how are your HIGH_FREQ_BUTTON_PIN and LOW_FREQ_BUTTON_PIN implemented? A change interrupt on a button is unusual.

It is still waking up after commenting out the calls to register the function and variable handlers. My HIGH_FREQ_BUTTON_PIN and LOW_FREQ_BUTTON_PIN sit at low until pressed, so I have changed the interrupt to rising.

Are they normal momentary buttons? What is the size of the pull-down resistor and what is the length of the wires?

Yes, they are normal momentary buttons. The pull down resistors are 4.7k and the wires are roughly 6 inches.

@rickkas7 I will say the network waking has stopped. This makes me wonder if there was a queue with many particles function/variable calls that were waiting or if there was noise on the cellular that was causing the network wake to trigger.

Here is an image of the random bursts of network waking. This burst lasted for about 20 minutes before returning to its normal operation of only waking from the RTC.

This will be hard to troubleshoot. The best option would be if you could see this happen with USB serial debugging enabled using

SerialLogHandler logHandler(LOG_LEVEL_TRACE);

This should show the CoAP message that triggered the network wake. However in some cases, the serial monitor may not be able to reconnect fast enough after wake, in which case you might need to use Serial1LogHandler and a USB to UART converter.

Unfortunately there's no good way to see what network activity caused the wake other than the on-device debug logs.

Got it :+1: I will give it a shot and see what I can find! Thank you @rickkas7