Boron sleeping in ULP mode cannot be woken up from sleep to flash new code

I am wondering how I can use Ultra Low Power sleeping on a Boron unit while allowing the unit to wake up from its sleep cycle if I need to flash new code to the unit. I have a unit flashed with the code (listed below) and it works except that once this code is flashed to the Boron, whenever I try to flash new code to it via the Cloud, the flash starts, fails and responds with unresponsive. The unit then carries on running the code, but I cannot get it to accept a new flash.

Thank you to anyone taking the time to look at this.

// This #include statement was automatically added by the Particle IDE.
#include "SparkFun_TMP117_Registers.h"

// This #include statement was automatically added by the Particle IDE.
#include "SparkFun_TMP117.h"

// Used to establish serial communication on the I2C bus
#include <Wire.h>

// This #include statement was automatically added by the Particle IDE.
#include "OneWire.h"

// This #include statement was automatically added by the Particle IDE.
#include "DS18.h"

// Program code starts here
SYSTEM_THREAD(ENABLED);
SYSTEM_MODE(SEMI_AUTOMATIC);

// This is the maximum amount of time to wait for the cloud to be connected in
// milliseconds. This should be at least 5 minutes. If you set this limit shorter,
// on Gen 2 devices the modem may not get power cycled which may help with reconnection.
const std::chrono::milliseconds connectMaxTime = 6min;

// This is the minimum amount of time to stay connected to the cloud. You can set this
// to zero and the device will sleep as fast as possible, however you may not get
// firmware updates and device diagnostics won't go out all of the time. Setting this
// to 10 seconds is typically a good value to use for getting updates.
const std::chrono::milliseconds cloudMinTime = 10s;

// How long to sleep
const std::chrono::milliseconds sleepTime = 10min;

// Maximum time to wait for publish to complete. It normally takes 20 seconds for Particle.publish
// to succeed or time out, but if cellular needs to reconnect, it could take longer, typically
// 80 seconds. This timeout should be longer than that and is just a safety net in case something
// goes wrong.
const std::chrono::milliseconds publishMaxTime = 3min;

// Maximum amount of time to wait for a user firmware download in milliseconds
// before giving up and just going back to sleep
const std::chrono::milliseconds firmwareUpdateMaxTime = 5min;

// 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;

// TMP117 Setup
// The default address of the device is 0x48 = (GND)
TMP117 water_sensor; // Initalize sensor

// DS18B20 Setup
DS18 air_sensor(D8);

void readSensorAndPublish(); // forward declaration
void firmwareUpdateHandler(system_event_t event, int param); // forward declaration
void turnOnCharging(); // forward declaration

// setup() runs once, when the device is first turned on.
void setup() {
  System.on(firmware_update, firmwareUpdateHandler);

  // It's only necessary to turn cellular on and connect to the cloud. Stepping up
  // one layer at a time with Cellular.connect() and wait for Cellular.ready() can
  // be done but there's little advantage to doing so.
  Cellular.on();
  Particle.connect();
  stateTime = millis();

  // In some cases errant settings or misconfigurations can cause undesired behavior.
  // The single line of code below will reset the PMIC to its default values.
  System.setPowerConfiguration(SystemPowerConfiguration()); 

  // The following code applies a custom power configuration to the PMIC
  SystemPowerConfiguration powerConf;
  powerConf.powerSourceMaxCurrent(900)
           .powerSourceMinVoltage(5080)
           .batteryChargeCurrent(900)
           .batteryChargeVoltage(4210);
  System.setPowerConfiguration(powerConf);

  // Ensure that charging is enabled on the PMIC
  turnOnCharging();

  Wire.setSpeed(400000); // Set clock speed to be the fastest for better communication (fast mode)
  Wire.begin();
  water_sensor.begin();
}

void loop() {
  switch(state) {
    case STATE_WAIT_CONNECTED:
      // Wait for the connection to the Particle cloud to complete
      if (Particle.connected()) {
        state = STATE_PUBLISH;
        stateTime = millis();
      }
      else
      if (millis() - stateTime >= connectMaxTime.count()) {
        state = STATE_SLEEP;
      }
      break;

    case STATE_PUBLISH:
      readSensorAndPublish();

      if (millis() - stateTime < cloudMinTime.count()) {
        state = STATE_PRE_SLEEP;
      }
      else {
        state = STATE_SLEEP;
      }
      break;

    case STATE_PRE_SLEEP:
      // This delay is used to make sure firmware updates can start and diagnostics go out
      // It can be eliminated by setting cloudMinTime to 0 and sleep will occur as quickly
      // as possible.
      if (millis() - stateTime >= cloudMinTime.count()) {
        state = STATE_SLEEP;
      }
      break;

    case STATE_SLEEP:
      if (firmwareUpdateInProgress) {
        state = STATE_FIRMWARE_UPDATE;
        stateTime = millis();
        break;
      }         

      {
        SystemSleepConfiguration config;
        // Gen 3 (nRF52840) does not suppport HIBERNATE with a time duration
        // to wake up. This code uses ULP sleep instead.
        config.mode(SystemSleepMode::ULTRA_LOW_POWER)
          .duration(sleepTime)
          .network(NETWORK_INTERFACE_CELLULAR);
        System.sleep(config);

        // One difference is that ULP continues execution. For simplicity,
        // we just match the HIBERNATE behavior by resetting here.
        System.reset();
      }
      break;

    case STATE_FIRMWARE_UPDATE:
      if (!firmwareUpdateInProgress) {
        state = STATE_SLEEP;
      }
      else
      if (millis() - stateTime >= firmwareUpdateMaxTime.count()) {
        state = STATE_SLEEP;
      }
      break;
  }
}

void readSensorAndPublish() {
  String air_temperature = "Air Temperature not read";
  String water_temperature = "Water Temperature not read";
  String battery_charge = "Battery not read";   

  // water_sensor.dataReady() is a function to make sure that there is data ready
  // only reads and publishes water temperature when data from TMP117 is ready
  if (water_sensor.dataReady() == true)
  {
    // Read the water temperature in degrees C
    float tmp117_reading = water_sensor.readTempC();
    // Convert temperature reading to a string with 2 decimal places
    water_temperature = String(tmp117_reading , 2);
    // Trigger the webhook integration
    Particle.publish("tmp117_water_temperature", water_temperature, PRIVATE | WITH_ACK);  
  }

  // 1Wire Air Temperature reading
  if (air_sensor.read()) {
    // Read the air temperature in degrees C
    float ds18b20_reading = air_sensor.celsius();
    // Convert temperature reading to a string with 2 decimal places
    air_temperature = String(ds18b20_reading, 2);
    // Trigger the webhook integration
    Particle.publish("ds18b20_air_temperature", air_temperature, PRIVATE | WITH_ACK);
  }

  // Read the battery charge status
  float battery_soc = System.batteryCharge();
  // Convert battery charge reading to a string with 0 decimal places
  battery_charge = String(battery_soc, 0);
  // Trigger the webhook integration
  Particle.publish("battery_charge", battery_charge, PRIVATE | WITH_ACK);
}

void firmwareUpdateHandler(system_event_t event, int param) {
  switch(param) {
    case firmware_update_begin:
      firmwareUpdateInProgress = true;
      break;

    case firmware_update_complete:
    case firmware_update_failed:
      firmwareUpdateInProgress = false;
      break;
  }
}

/* Currently (as of DeviceOS 2.0.1) the Power manager cannot enable or disable charging.
   To do so one needs to call the old PMIC API.
   To call the PMIC API it ideally needs to be wrapped inside a function to avoid
   interference from the Power Manager.
*/ 
void turnOnCharging()
{
  PMIC pmic(true); //Calling it with true locks it to the user code
  pmic.enableCharging();
}

When a device is asleep, the cellular modem is off, so you cannot access it from the cloud, because it’s disconnected. The options are:

  • If the device is in front of you, you can put it in safe mode by holding down MODE and tapping RESET. Continue to hold down MODE until the status LED blinks magenta (red and blue at the same time) then release MODE. The LED will cycle through blinking green and blinking cyan. When it gets to breathing cyan you can OTA flash code.
  • If your device is in a product, you can have configure it to receive an update when it connects to the cloud. See the wake publish sleep application note for more information on handling this correctly. Your code already includes handling of this.
  • You can make your firmware stay awake longer at certain times of the day so you can flash it during that window.
  • You can use cellular standby with cloud connected sleep mode. This will significantly increase the amount of power the device uses in sleep, but it allows the device to wake up from sleep for network activity, including OTA, because the cellular modem is left on.

My objective, if possible, is to use the ULP sleep mode in order to maximize power savings while (1) not disconnecting from the cellular network and (2) to be able to wake the device up and flash it with new code from the cloud. Reading the information in the link you sent, will the following sleep configuration allow for (1) and (2) above?

SystemSleepConfiguration config;
config.mode(SystemSleepMode::ULTRA_LOW_POWER)
.duration(sleepTime) // sleepTime variable is declared and set earlier in the code
.network(NETWORK_INTERFACE_CELLULAR);
System.sleep(config);

I also saw the note → If you are waking on network activity, be sure to wait for Particle.connected() to be true before entering sleep mode. If you device has not completely connected to the cloud, it will not be possible to wake from sleep by network activity.

In the state machine code I am using, in which state should I check for Particle.connected() == true?

Is my code messing something up by issuing a system reset → System.reset(); right after the System.sleep(config); line?

Thank you for you reply and for any further tips you can offer on this topic.
Cheers!

That sleep statement looks correct.

  • The test in STATE_WAIT_CONNECTED solves the warning in documentation about waiting for being cloud connected before sleeping.
  • Remove the System.reset() call and instead set state to STATE_WAIT_CONNECTED.

Thank you for your time and help with my project. This solves my problem.

Cheers!