Particle Software Timers not functioning

NOTE: I currently have this posted on StackOverflow as well.

I have a Particle.IO application I wrote (that worked), and I am currently refactoring it to use C++ classes and objects.

The target device is a Particle Boron BRN404X with DeviceOS and firmware version 6.2.1.

The application has four software timers that in the original version worked (note that one class has two timers in it that's not obvious from the examples below), but now that I am using class member callbacks, the timers do not seem to be running after the respective timer start() functions are called.

I have searched and have tried a couple of ways to initialize the timers. Currently each timer is being initialized in the constructor of each object, and a public function is called from setup() to start them. Callback functions are currently marked as private in the class header files.

The following is the main file and where my objects are instantiated and the timers are started.

#include "Particle.h"
#include "spark_wiring.h"
#include "../lib/local_includes/digitalIO.h"
#include "../lib/local_includes/DataTransmit.h"
#include "../lib/local_includes/CellularCheck.h"
#include "../lib/local_includes/Power.h"
#include "../lib/local_includes/Battery.h"
#include "../lib/local_includes/Shutdown.h"
#include "../lib/local_includes/Weekly.h"

// Let Device OS manage the connection to the Particle Cloud
SYSTEM_MODE(AUTOMATIC);

// Run the application and system concurrently in separate threads
SYSTEM_THREAD(ENABLED);

// Objects
PMIC pmic;
BoronDigitalOutputs digitalOutputs;
DataTransmit dataTransmission;
CellularCheck cellularCheck(digitalOutputs);
Power powerMonitor(digitalOutputs, &cellularCheck, &dataTransmission);
Battery batteryMonitor(digitalOutputs, &powerMonitor, &dataTransmission);
Shutdown shutdown(&dataTransmission, &pmic);
Weekly weeklyAction(&dataTransmission);

/*
 *  @brief  Initialization routine called automatically by Particle Boron SoM when system is booted up.
 *  @param  void
 *  @return void
 */
void setup() 
{
	pmic.enableBATFET();
 
	digitalOutputs = initDigitalOutputs();
    
    if (!Cellular.ready() && !Particle.connected()) 
    {
        cellularCheck.setSystemConnected(false);
        digitalOutputWrite(digitalOutputs, CELLSEARCH, HIGH);
        digitalOutputWrite(digitalOutputs, ONCELL, LOW);
    }

    waitUntil(Cellular.ready);
    waitUntil(Particle.connected);

    if (Cellular.ready() && Particle.connected()) 
    {
        cellularCheck.setSystemConnected(true);
        digitalOutputWrite(digitalOutputs, CELLSEARCH, LOW);
        digitalOutputWrite(digitalOutputs, ONCELL, HIGH);
    }

    //EST
    Time.zone(-5);
    Particle.syncTime();

    int powerSource = System.powerSource();

    if (powerSource == POWER_SOURCE_BATTERY) 
    {
        powerMonitor.setIsOnBattery(true);
        powerMonitor.setOnAc(false);
        digitalOutputWrite(digitalOutputs, ONBATPWR, HIGH);
        digitalOutputWrite(digitalOutputs, ONACPWR, LOW);
    }
    else 
    {
        powerMonitor.setIsOnBattery(false);
        powerMonitor.setOnAc(true);
        digitalOutputWrite(digitalOutputs, ONBATPWR, LOW);
        digitalOutputWrite(digitalOutputs, ONACPWR, HIGH);
    }
    
    if(powerMonitor.getIsOnBattery()) 
    {
         dataTransmission.setStringOne("Application booting...");
         dataTransmission.setStringTwo("NO AC POWER");
         dataTransmission.sendData();
    }
    else 
    {
         dataTransmission.setStringOne("Application booting...");
         dataTransmission.setStringTwo("AC POWER GOOD");
         dataTransmission.sendData();        
    }
    
    //start timers
    cellularCheck.startTimer();
    powerMonitor.startTimer();
    batteryMonitor.startTimer();
}

/*
 *  @brief  Main routine continuous loop.
 *  @param  void
 *  @return void
 */
void loop() 
{
    //service data transmission for timer interrupts.
	if (dataTransmission.getPublishFlag()) 
    {
		dataTransmission.sendData();
		dataTransmission.setPublishFlag(false);
	}

    //send weekly test messages.
    if (weeklyAction.checkTime())
    {
        weeklyAction.performWeeklyAction();
    }
    
	shutdown.lowBatteryLife();
}

Here is one class header file and matching cpp file to show the timer initialization and callback functions. All other classes with timers and callback functions follow the same pattern.

Power.h

#ifndef POWER_H
#define POWER_H

#include "Particle.h"
#include "../lib/local_includes/digitalIO.h"
#include "../lib/local_includes/DataTransmit.h"
#include "../lib/local_includes/CellularCheck.h"

using namespace std;

class Power
{
    public:
        //constructor
		Power(BoronDigitalIOStruct * BoronDigitalOutputs, CellularCheck * c, DataTransmit * d);
        //default constructor
        Power();
        
        //destructor
        ~Power();

        

        //public member functions
        void startTimer(void);

        //accessors
        bool getIsOnAc() const;
        bool getIsOnBattery() const;

        //mutators
        void setOnAc(bool isOnAC);
        void setIsOnBattery(bool isOnBattery);

    private:
        BoronDigitalOutputs digitalOutputs;
        CellularCheck cellularCheck;
        DataTransmit transmitData;
        Timer *powerCheckTimer;

        //timer callback
        void checkACLineVoltage(void);

        bool onAC;
        bool onBattery;
};

#endif

Power.cpp

#include "Particle.h"
#include "../lib/local_includes/Power.h"
#include "../lib/local_includes/digitalIO.h"
#include "../lib/local_includes/CellularCheck.h"
#include "../lib/local_includes/DataTransmit.h"

using namespace std;

//constructors & destructor
Power::Power(BoronDigitalIOStruct * BoronDigitalOutputs, CellularCheck * c,  DataTransmit * d)
{
    digitalOutputs = BoronDigitalOutputs;
	cellularCheck = *c;
	transmitData = *d;
	
	onAC = false;
	onBattery = false;

	powerCheckTimer = new Timer(1000, &Power::checkACLineVoltage, *this);
	
}

Power::Power()
{
    //intentionally left empty.
}

Power::~Power()
{
    //intentionally left empty.
}

void Power::startTimer(void)
{
	powerCheckTimer->start();
}

/*
 *  @brief  Monitor AC line voltage if connected to cellular and Particle Cloud.
 *  @param  void
 *  @return void
 */
void Power::checkACLineVoltage(void) 
{
	if(cellularCheck.getIsSystemConnected()) 
	{
		int powerSource = System.powerSource();
		
		if (powerSource == POWER_SOURCE_BATTERY) 
		{
			if(!onBattery && onAC)
			{
				onBattery = true;
				onAC = false;

				digitalOutputWrite(digitalOutputs, ONBATPWR, HIGH);
				digitalOutputWrite(digitalOutputs, ONACPWR, LOW);

				transmitData.setStringOne("Power Monitor");
				transmitData.setStringTwo("AC POWER LOST");
				transmitData.setPublishFlag(true);
			}
		}   
		else if (onBattery && !onAC) 
		{
			onBattery = false;
			onAC = true;

			digitalOutputWrite(digitalOutputs, ONBATPWR, LOW);
			digitalOutputWrite(digitalOutputs, ONACPWR, HIGH);

			transmitData.setStringOne("Power Monitor");
			transmitData.setStringTwo("AC POWER IS ON");
			transmitData.setPublishFlag(true);
		}
	}
}

bool Power::getIsOnAc() const
{
    return onAC;
}

bool Power::getIsOnBattery() const
{
    return onBattery;
}

void Power::setOnAc(bool isOnAC)
{
    onAC = isOnAC;
}

void Power::setIsOnBattery(bool isOnBattery)
{
    onBattery = isOnBattery;
}

My setup() function appears to be working as the device LED’s light up based on the logic in this function. I also receive my push notifications based on this logic. The only logic that seems to work in the loop() is the Weekly functions. Everything else is based on timer callbacks that do not work.

What am I missing or doing wrong?

That won't work, though you figured that part out already. The reason is that you have your objects defined as global variables, and you are doing setup in the constructor. The problem is that global object construction occurs very early in startup, and you cannot start a timer that early.

Two most common solutions are:

  • Do a two-phase setup, typically with a setup() method in your class, that you call from the global setup().
  • Use the singleton pattern which delays object construction until later, typically during global setup() instead of using a global variable.

I have tried the separate set up procedure previously with no success. I tried again though as shown here:

void Battery::setup(void)
{
    batteryCheckTimer = new Timer(1000, &Battery::batteryStatus, *this);
    batteryCheckTimer->start();
}

and called this setup() function at the end of the global setup() as shown here:

void setup() 
{
	pmic.enableBATFET();
 
	digitalOutputs = initDigitalOutputs();
    
    if (!Cellular.ready() && !Particle.connected()) 
    {
        cellularCheck.setSystemConnected(false);
        digitalOutputWrite(digitalOutputs, CELLSEARCH, HIGH);
        digitalOutputWrite(digitalOutputs, ONCELL, LOW);
    }

    waitUntil(Cellular.ready);
    waitUntil(Particle.connected);

    if (Cellular.ready() && Particle.connected()) 
    {
        cellularCheck.setSystemConnected(true);
        digitalOutputWrite(digitalOutputs, CELLSEARCH, LOW);
        digitalOutputWrite(digitalOutputs, ONCELL, HIGH);
    }

    //EST
    Time.zone(-5);
    Particle.syncTime();

    int powerSource = System.powerSource();

    if (powerSource == POWER_SOURCE_BATTERY) 
    {
        powerMonitor.setIsOnBattery(true);
        powerMonitor.setOnAc(false);
        digitalOutputWrite(digitalOutputs, ONBATPWR, HIGH);
        digitalOutputWrite(digitalOutputs, ONACPWR, LOW);
    }
    else 
    {
        powerMonitor.setIsOnBattery(false);
        powerMonitor.setOnAc(true);
        digitalOutputWrite(digitalOutputs, ONBATPWR, LOW);
        digitalOutputWrite(digitalOutputs, ONACPWR, HIGH);
    }
    
    if(powerMonitor.getIsOnBattery()) 
    {
         dataTransmission.setStringOne("Application booting...");
         dataTransmission.setStringTwo("NO AC POWER");
         dataTransmission.sendData();
    }
    else 
    {
         dataTransmission.setStringOne("Application booting...");
         dataTransmission.setStringTwo("AC POWER GOOD");
         dataTransmission.sendData();        
    }
    
    //start timers
    cellularCheck.setup();
    powerMonitor.setup();
    batteryMonitor.setup();
}

This compiles, but my timers still do not appear to function.

How will having my classes as Singleton's benefit the situation?

I'd add

SerialLogHander logHandler(LOG_LEVEL_TRACE);

and add Log.info() statements to see what's being executed via USB serial debug.

Since you have the setup calls at the end of global setup() and you have a waitFor(Particle.isConnected in it, you won't start the threads until connected.

Also beware when using software timers. Since all timers run out of a single thread with a small stack, doing things like publishing that can block can cause issues with all timers stopping.

Seeing marginal improvements. Need to walk away from this for the day but will get back soon. Thank you

Changing the classes/objects to singletons worked form me.

I still need to clean up a lot of code, but I am in a better pace now with this application.

Thanks for the suggestion.