Argon - protect device from low battery voltage

I’ve implemented logic to put Electrons and Borons to sleep and eventually disconnect the battery when the battery state of charge gets low. I’m now wondering if I should do the same for Argons.

Although I cannot read the battery’s state of charge, I can read the battery voltage via gBattV = analogRead(BATT) * 0.0011224; and I’m wondering if I should simply put the device to deep sleep if the battery voltage is below a “safe” voltage in order to protect the device.

I’d like to disconnect the battery if the battery voltage gets low enough, but I don’t believe the I have access to that functionality as I do on the Electrons and Borons.

I let an Argon run on battery until it shutdown at 3.2 V as read above. I’m fine with this as long as I’m not putting the Argon at risk. I will plan on sending an alert that power is no longer being applied to VIN (detected via an external relay connected to a DI).

Our use case involves outdoor devices powered via GFCI outlets. By attaching a battery to the device, I can continue to read inputs for a period of time, hopefully, longer than it takes to get someone to the site to reset the GFCI. As stated above, one of the inputs will be the indicator that external power has been lost. With Borons and Electrons, I put the device to sleep for increasing periods of time based upon the battery’s state of charge. I’m guessing I can do something similar with the Argon, albeit based upon battery voltage since I can’t read the state of charge.

You can find many different charts/tables that are published on the internet (for 1S Li-Po) that express Resting Voltage as SOC %.
They are all slightly different but surprising the first time you pay attention to the numbers, for me anyway.

Li-Po’s are called 3.7V “nominal” simply because that’s the midpoint in the specified typical operating range for voltage.
But it is far from the midpoint of SOC % or capacity.

Here’s a common table:
image

I think you’re right on track with applying the same progressive sleep schedule for the Argon as you mentioned you perform for Electrons and Borons.
I’d suggest starting the battery saving sleep procedures pretty early in the discharge cycle.
I use 3.7V in many applications similar to what you describe, even when a PMIC is onboard.
I prefer voltage anyway as it’s an empirical value and directly measured.

1 Like

@Rftop, thank you for sharing the above. It appears I wasn’t be quite aggressive enough, but will reconsider my “sleep” points based upon the chart you provided. Do you have any idea if running an Argon until “out of gas” can damage the device, as it supposedly can when an Electron is involved? If it’s a risky proposition, then I’m in a bit of a quandary as to what to do if the voltage gets extremely low. As far as I know, I don’t have access to disable the battery FET, as I do with Electrons and Borons. Perhaps, I could implement a very long (Rip Van Winkle) sleep interval.

it could be that for some scenarios 20 years would be too long to sleep :sleeping:

You all know the story of Rip Van Winkle, who slept for 20 years. But how many of you know the story of Rip Van Twinkle, who stayed awake for 100 years?
source

I’m trying to implement code to put the device to sleep if the voltage gets sufficiently low. The lower the voltage, the longer it will sleep.

Upon testing, the voltage dropped to 3.8, at which point in time, I want to put the device to sleep for 2 hours. When the code was executed, I got an SOS w/ 10 blinks. Am I doing something wrong with the sleep configuration?

Here’s a snippet of my code:

SystemSleepConfiguration config; //used to define sleep parameters

and the code when I determine low voltage:

config.mode(SystemSleepMode::ULTRA_LOW_POWER)
  .duration(2h); //sleep for 2 hours
System.sleep(config);

I’m a bit surprised/disappointed I have received no feedback concerning this issue. Am I doing something wrong regarding this code?

Hey Tom,

I used this code recently on a Boron and it looks similar to what you have:

  SystemSleepConfiguration config;
  config.mode(SystemSleepMode::ULTRA_LOW_POWER)
      .duration(NORMAL_SLEEP_CYCLE * MILLISECONDS_TO_MINUTES)
      .gpio(ADXL343_INPUT_PIN_INT1, RISING)
      .flag(SystemSleepFlag::WAIT_CLOUD);
  result = System.sleep(config);

I ran that code with Device OS 2.0.1.

@gusgonnet , thanks for the feedback! Still no luck. I changed the 2h to 2*60*60*1000 to see if it was related to the chrono literal, but without success.

The other thing…when I include the .flag(SystemSleepFlag::WAIT_CLOUD) attribute, I get a compile error:

/Users/tom/Documents/Particle/Projects-HVACSP/U6T8SAr/src/U6T8SAr.ino:306:30: error: 'WAIT_CLOUD' is not a member of 'particle::SystemSleepMode'
  306 |       .flag(SystemSleepMode::WAIT_CLOUD);

I had that error in the past and so I removed setting that attribute. Does that provide any clue?

This is for an Argon and I’m compiling under 2.0.1.

oh, that’s REALLY weird.
it does not provide a clue to me, but perhaps to someone else.
What happens if you clean your env and build again? maybe re-set the device os by setting it to another version and setting it back to 2.0.1?

No change. Still cannot compile with WAIT_CLOUD specified.

OK. Truly weird. I must have had some funky invisible character somewhere. I have code for two durations of sleep and I noticed I wasn’t getting the compile error on the second instance of WAIT_CLOUD so I copy-pasted the other code, changed the duration appropriately and it compiled.

That being said, I’m still getting SOS and 10 blinks when the code is executed. Perhaps I’ll simply submit a support ticket to see if I can get Particle’s attention via that route. I had hoped to resolve it here in case it helped others.

By the way, here’s the relevant code:

/*
 * checks to see if there's incoming power on VIN
 * @return 'true' if there's incoming power or 'false' if there is not
 * the assumption is that the power sensing relay has been installed and connected to S8!
 */
bool incoming_power() {
    return (gS8==1 ? 1 : 0);
}

/*
 * @param capacity The value to compare current battery to.
 * @return If battery is lower than `capacity`, return `true`.
 */
bool battery_lower_than(float volts)
{
    return (gBattV < volts) ? 1 : 0;
}

void qualify_battery_and_hibernate() {
  //Let's make sure any cloud communications are completed
  for (int i=0;i<5;i++) {
    Particle.process();
    delay(1000);
  }
  //PHASE 2 TEST
  if (!incoming_power() && battery_lower_than(PHASE2_BATTERY_LEVEL)) {
    Particle.publish("PHASE_2",PRIVATE);
    delay(1000);//wait for the publish to complete
    Log.info("No incoming power, but sufficient battery -- Phase 1 sleep");
    //System.sleep(SLEEP_MODE_DEEP, PHASE1_SLEEP_DURATION, SLEEP_DISABLE_WKP_PIN);
    config.mode(SystemSleepMode::ULTRA_LOW_POWER)
      .duration(8* 60 * 60 * 1000) //8 hours * 60min/hr * 60sec/min * 1000 millis/sec
      .flag(SystemSleepFlag::WAIT_CLOUD);
    System.sleep(config);
  }
  //PHASE 1 TEST
  else if (!incoming_power() && battery_lower_than(PHASE1_BATTERY_LEVEL)) {
    Particle.publish("PHASE_1",PRIVATE);
    delay(1000);//wait for the publish to complete
    Log.info("No incoming power, but sufficient battery -- Phase 1 sleep");
    //System.sleep(SLEEP_MODE_DEEP, PHASE1_SLEEP_DURATION, SLEEP_DISABLE_WKP_PIN);
    config.mode(SystemSleepMode::ULTRA_LOW_POWER)
      .duration(2* 60 * 60 * 1000) //2 hours * 60min/hr * 60sec/min * 1000 millis/sec
      .flag(SystemSleepFlag::WAIT_CLOUD);
    System.sleep(config);
  }

}

@ctmorrison can you put together a minimum failure example code you can share? It’s hard to advise when the entire code is not visible.

@peekay123 , I think the only relevant thing missing from he above code would be:

#define PHASE1_BATTERY_LEVEL 3.8 //~40% capacity
#define PHASE2_BATTERY_LEVEL 3.7 //~10% capacity

gS8 is a dry contact (relay) on D8 that’s closed when power is applied to VIN.

The above code gets called roughly every minute after I push data to ubidots and is running on a bunch of other systems. I guess I’m not too reluctant to post all of the code. I simply thought I’d focus on the new code I added to this application to handle low battery situations.

I get the PHASE_1 publish immediately prior to the device blinking SOS. The batter voltage is just under 3.8, as expected.

If you really want the WHOLE program, I’ll post it.

@ctmorrison do you see the log.info() text prior to the SOS? Where do you call qualify_battery_and_hibernate() from?

I don’t see the log info, as I don’t have a serial connection that would be charging the battery and put it above the threshold. Is there a means to see the log.info() when I don’ have a serial connection?

I call qualify_battery_and_hibernate() from my procedure that’s called periodically from loop(). Essentially, every minute I check to see if any inputs have changed sufficiently. If they have, I pack up the readings and send the changed ones to Ubidots. Every 15 minutes, I send all readings to Ubidots. It’s after the Ubidots send that I call qualify_battery_and_hibernate(). It only does anything if there’s no incoming power and the battery voltage is sufficiently low.

As I said above, I do see the PHASE_1 publish in the console just prior to SOS

Here’s the entire app please don’t chuckle too much! :wink:

//HVAC StarPlus
//StarGazer - Electron - sending analog (temperature) and digital (dry contact closure) inputs to Ubidots
//20191026 - Tom Morrison
// - modified:
//
//

#define dReadPeriod 10000     //read inputs every 10 seconds by default
#define dPublishPeriod 60000  //publish to Ubidots every 60 seconds by default
#define __FILENAME__ (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__)
#define dMaxDelta 0.5 //number of tenths of a degree that forces an Ubidots update

#include "thermistorLU.h"
#include "SparkJson.h"
#include "string.h"
#include "math.h"
#include "Particle.h"
#include "Ubidots.h"

SerialLogHandler logHandler;

const char* WEBHOOK_NAME = "ubidots";
Ubidots ubidots("webhook", UBI_PARTICLE);

//used for timing loops
unsigned long gNow = 0;

double gMaxDelta = dMaxDelta;
char gPublishString[200];
char gDeviceID[25];
char gS[100];
char gT[200];
char gB[50];
char gSlastSent[100];
unsigned long gLastPublish   = 0; //timestamp of last push to Grovestreams
unsigned long gLastRead      = 0; //timestamp of last input sensor reading
int  gPublishPeriod         = dPublishPeriod;
char gDeviceInfo [300] = "~";
bool gHasCredentials = false;
bool gSendValues = true; // indicates if values should be sent to Grovestreams
char gFred[200]; //used for debugging
////////////Battery declarations
#define PHASE1_BATTERY_LEVEL 3.8 //~40% capacity
#define PHASE2_BATTERY_LEVEL 3.7 //~10% capacity
//used to estimate amount of battery charge left
double gBattV;
double gBattVLastSent=0;
char* lBattV = "battV";
////////////Battery declarations
SystemSleepConfiguration config; //used to define sleep parameters

//The following are labels for the parameters sent to Ubidots
char* lT1 = "T1";char* lT2 = "T2";char* lT3 = "T3";char* lT4 = "T4";char* lT5 = "T5";char* lT6 = "T6";
char* lS1 = "S1";char* lS2 = "S2";char* lS3 = "S3";char* lS4 = "S4";char* lS5 = "S5";char* lS6 = "S6";char* lS7 = "S7";char* lS8 = "S8";

//define input analog and digital ports
//the numbering is the same as on the StarGazer PCB (1-12)
thermistorLU gT1(A5);
thermistorLU gT2(A4);
thermistorLU gT3(A3);
thermistorLU gT4(A2);
thermistorLU gT5(A1);
thermistorLU gT6(A0);

int S1 = D0;
int S2 = D1;
int S3 = D2;
int S4 = D3;
int S5 = D4;
int S6 = D5;
int S7 = D6;
int S8 = D8;
int led7 = D7;

//used to hold the most recent digital inputs
int gS1, gS2, gS3, gS4, gS5, gS6, gS7, gS8;

//used to hold the most recent reading instead of redeclaring in the read function or forcing excessive AI reads
double gT1f, gT2f, gT3f, gT4f, gT5f, gT6f;

//last sent values intialized to force a send on the first loop
double gT1LastSent=0, gT2LastSent=0, gT3LastSent=0, gT4LastSent=0, gT5LastSent=0, gT6LastSent=0; //last T value sent to Grovestreams
int gS1LastSent=-1, gS2LastSent=-1, gS3LastSent=-1, gS4LastSent=-1, gS5LastSent=-1, gS6LastSent=-1, gS7LastSent=-1, gS8LastSent=-1;//last S value sent to Ubidots

void printMAC();

void setup() {
  delay(100);//brief pause to let serial interface start
  //The following lines are to ensure the system clock is set accurately
  printMAC();
  Log.info("we're in setup");
  Log.info("we're trying to sync the device's time");
  while(Time.year() <= 1970) Particle.process();
  Log.info("Time is sync'd");
  Particle.variable("gPubPeriod",gPublishPeriod);
  Particle.variable("gS",gS);
  Particle.variable("gT",gT);
  Particle.variable("Battery",gBattV);
  Particle.function("pReset",pReset);
  Particle.function("setPeriod",setPeriod);
  Particle.subscribe("particle/device/name",updateDeviceInfo);
  //Publishes info about the device
  Particle.variable("deviceInfo",gDeviceInfo);
  //
  pinMode(led7, OUTPUT);
  for(int i=0;i<5;i++) {
    Log.info("waiting " + String(5-i) + " seconds before we publish");
    digitalWrite(led7, HIGH);
    delay(100);
    digitalWrite(led7, LOW);
    delay(100);
  }
  pinMode(S1,INPUT);
  pinMode(S2,INPUT);
  pinMode(S3,INPUT);
  pinMode(S4,INPUT);
  pinMode(S5,INPUT);
  pinMode(S6,INPUT);
  pinMode(S7,INPUT);
  pinMode(S8,INPUT);
  gLastRead = (60-Time.second())*1000;//need to contemplate this to get readings on the minute
}

void loop() {
  //The loop involves periodically reading the sensors to update published variables
  //and periodically pushing updates to Grovestreams. These are two different periods
  gNow = millis();
  //check to see if inputs should be read
  if (gNow < gLastRead) { //handle roll over
    gLastRead = 0;
  }
  if ((gNow - gLastRead) >= dReadPeriod){
    gLastRead = gNow;
    //Particle.publish("read the damn sensors");
    readSensors();
    if (gDeviceInfo[0]=='~') {
      Particle.publish("particle/device/name",PRIVATE);
    }
  }
  //check to see if readings should be sent to Grovestreams
  if (gNow < gLastPublish) { //handle roll over
    gLastPublish = 0;
  }
  if ((gNow - gLastPublish) >= gPublishPeriod) {
      sendToUbidots();
  }
}

void updateDeviceInfo(const char *topic,const char *data) {
  char deviceName [40] = "";
  byte mac[6];
  WiFi.macAddress(mac);
  strncpy(deviceName,data,sizeof(deviceName)-1);
  snprintf(gDeviceInfo, sizeof(gDeviceInfo)
        ,"Device: %s, Application: %s, Date: %s, Time: %s, System firmware: %s, SSID: %s,mac: %02x:%02x:%02x:%02x:%02x:%02x"
        ,(const char*)deviceName
        ,__FILENAME__
        ,__DATE__
        ,__TIME__
        ,(const char*)System.version()  // cast required for String
        ,(const char*)WiFi.SSID()
        ,mac[0],mac[1],mac[2],mac[3],mac[4],mac[5]
        );
  Log.info("mac: %02x:%02x:%02x:%02x:%02x:%02x",mac[0],mac[1],mac[2],mac[3],mac[4],mac[5]);
}

void printMAC() {
  byte mac[6];
  WiFi.macAddress(mac);
  Log.info("mac: %02x:%02x:%02x:%02x:%02x:%02x",mac[0],mac[1],mac[2],mac[3],mac[4],mac[5]);
}

void readSensors() {
  gLastRead = gNow;
  //read input sensors and store in global variables
  gT1f = gT1.temperature();
  gT2f = gT2.temperature();
  gT3f = gT3.temperature();
  gT4f = gT4.temperature();
  gT5f = gT5.temperature();
  gT6f = gT6.temperature();
  snprintf(gT, sizeof(gT),"%4.2f,%4.2f,%4.2f,%4.2f,%4.2f,%4.2f",gT1f,gT2f,gT3f,gT4f,gT5f,gT6f);
  Log.info("temperatures: %s",gT);
  //read the digital inputs and format output string
  gS1 = digitalFilter(S1);
  gS3 = digitalFilter(S3);
  gS2 = digitalFilter(S2);
  gS4 = digitalFilter(S4);
  gS5 = digitalFilter(S5);
  gS6 = digitalFilter(S6);
  gS7 = digitalFilter(S7);
  gS8 = digitalFilter(S8);
  snprintf(gS,sizeof(gS),"%u%u%u%u%u%u%u%u",gS1,gS2,gS3,gS4,gS5,gS6,gS7,gS8);
  Log.info("switch inputs: %s",gS);
  //gBatt = gBattVoltage;
  gBattV = analogRead(BATT) * 0.0011224; //this is uniquely how we read battery voltage on an Argon
  Log.info("Battery voltage: %2.1f",gBattV);
  ///Battery logic
}

void sendToUbidots() {
  //send readings to Ubidots if they have changed sufficiently
  gLastPublish = gNow; //set timestamp
  if (Time.second()!=0){ //align the time sent with even minutes as much as possible
    gLastPublish = gLastPublish - Time.second()*1000;
  }
  if (Time.minute() % 15 == 0) {//we'll send all values every at hh:00, hh:15, hh: 30 and hh:45
    gS1LastSent=gS2LastSent=gS3LastSent=gS4LastSent=gS5LastSent=gS6LastSent=gS7LastSent=gS8LastSent=-1;
    gT1LastSent=gT2LastSent=gT3LastSent=gT4LastSent=gT5LastSent=gT6LastSent=0;
    gBattVLastSent = -1;
  }
  gSendValues = false;
  //Let's check and send any digital inputs that have changed in group 1
  if (gS1 != gS1LastSent)   {gS1LastSent = gS1; ubidots.add(lS1,gS1); gSendValues = true;}
  if (gS2 != gS2LastSent)   {gS2LastSent = gS2; ubidots.add(lS2,gS2); gSendValues = true;}
  if (gS3 != gS3LastSent)   {gS3LastSent = gS3; ubidots.add(lS3,gS3); gSendValues = true;}
  if (gS4 != gS4LastSent)   {gS4LastSent = gS4; ubidots.add(lS4,gS4); gSendValues = true;}
  if (gS5 != gS5LastSent)   {gS5LastSent = gS5; ubidots.add(lS5,gS5); gSendValues = true;}
  if (gS6 != gS6LastSent)   {gS6LastSent = gS6; ubidots.add(lS6,gS6); gSendValues = true;}
  if (gS7 != gS7LastSent)   {gS7LastSent = gS7; ubidots.add(lS7,gS7); gSendValues = true;}
  if (gS8 != gS8LastSent)   {gS8LastSent = gS8; ubidots.add(lS8,gS8); gSendValues = true;}
  if (abs(gT1f-gT1LastSent)>gMaxDelta) {gT1LastSent = gT1f; ubidots.add(lT1,gT1f); gSendValues = true;} 
  if (abs(gT2f-gT2LastSent)>gMaxDelta) {gT2LastSent = gT2f; ubidots.add(lT2,gT2f); gSendValues = true;}
  if (abs(gT3f-gT3LastSent)>gMaxDelta) {gT3LastSent = gT3f; ubidots.add(lT3,gT3f); gSendValues = true;} 
  if (abs(gT4f-gT4LastSent)>gMaxDelta) {gT4LastSent = gT4f; ubidots.add(lT4,gT4f); gSendValues = true;} 
  if (abs(gT5f-gT5LastSent)>gMaxDelta) {gT5LastSent = gT5f; ubidots.add(lT5,gT5f); gSendValues = true;} 
  if (abs(gT6f-gT6LastSent)>gMaxDelta) {gT6LastSent = gT6f; ubidots.add(lT6,gT6f); gSendValues = true;}
  if (abs(gBattV-gBattVLastSent)>gMaxDelta) {gBattVLastSent = gBattV; ubidots.add(lBattV,gBattV); gSendValues = true;}
  if(gSendValues){
    bool bufferSent = false;
    bufferSent = ubidots.send(WEBHOOK_NAME,PUBLIC); //will use Particle webhook to send
    if (bufferSent) {
      Log.info("Values sent to Ubidots successfully");
    }
  }
  qualify_battery_and_hibernate();
}

int digitalFilter(int port) {
  //an input needs to be on for 3 successive checks every 10 msec
  int holdInput;
  holdInput = digitalRead(port);
  delay(10);
  holdInput += digitalRead(port);
  delay(10);
  holdInput += digitalRead(port);
  if (holdInput==3){
    return 1;
  }
  else {
    return 0;
  }
}

int pReset(String command) {
  //function to call to remotely reset device
  System.reset();
  return 1;
}

int setPeriod(String command) {
  //change the Grovestreams update period
  Log.info("Setting publish period");
  int _command;
  _command = command.toInt();
  if (_command>=60 && _command<=900) {//in seconds - between 1 and 15 minutes)
    gPublishPeriod = _command * 1000;
    return 1;
  }
  else {
    return -1;
  }
}

/*
 * checks to see if there's incoming power on VIN
 * @return 'true' if there's incoming power or 'false' if there is not
 * the assumption is that the power sensing relay has been installed and connected to S8!
 */
bool incoming_power() {
    return (gS8==1 ? 1 : 0);
}

/*
 * @param capacity The value to compare current battery to.
 * @return If battery is lower than `capacity`, return `true`.
 */
bool battery_lower_than(float volts)
{
    return (gBattV < volts) ? 1 : 0;
}

void qualify_battery_and_hibernate() {
  //Let's make sure any cloud communications are completed
  for (int i=0;i<5;i++) {
    Particle.process();
    delay(1000);
  }
  //PHASE 2 TEST
  if (!incoming_power() && battery_lower_than(PHASE2_BATTERY_LEVEL)) {
    Particle.publish("PHASE_2",PRIVATE);
    delay(1000);//wait for the publish to complete
    Log.info("No incoming power, but sufficient battery -- Phase 1 sleep");
    //System.sleep(SLEEP_MODE_DEEP, PHASE1_SLEEP_DURATION, SLEEP_DISABLE_WKP_PIN);
    config.mode(SystemSleepMode::ULTRA_LOW_POWER)
      .duration(8* 60 * 60 * 1000) //8 hours * 60min/hr * 60sec/min * 1000 millis/sec
      .flag(SystemSleepFlag::WAIT_CLOUD);
    System.sleep(config);
  }
  //PHASE 1 TEST
  else if (!incoming_power() && battery_lower_than(PHASE1_BATTERY_LEVEL)) {
    Particle.publish("PHASE_1",PRIVATE);
    delay(1000);//wait for the publish to complete
    Log.info("No incoming power, but sufficient battery -- Phase 1 sleep");
    //System.sleep(SLEEP_MODE_DEEP, PHASE1_SLEEP_DURATION, SLEEP_DISABLE_WKP_PIN);
    config.mode(SystemSleepMode::ULTRA_LOW_POWER)
      .duration(2* 60 * 60 * 1000) //2 hours * 60min/hr * 60sec/min * 1000 millis/sec
      .flag(SystemSleepFlag::WAIT_CLOUD);
    System.sleep(config);
  }

}

maybe you can still connect your serial cable while conveniently modifying (temporarily) the threshold value in the code?

Or use a USB cable modified so it doesn’t provide power.

1 Like

Perhaps I’m missing something, but I’m seeing the results of the Particle.publish("PHASE_1",PRIVATE); in the console, why is it important to also see the Log.info() via serial?

To see how far the code runs. I’m looking at your code now and it’s not obvious what might cause an assertion error.