How to debug Boron GW that frequently reboots

boron
Tags: #<Tag:0x00007fe21f4ba4c0>

#1

Hi,

I’ve recently migrated my setup from two Photons on my wifi network to a 3G Boron gateway with a Xenon in mesh. The Boron reboots frequently, previously after several days, then several hours, this last time after 20 minutes.

The Xenon is a temperature probe, it publishes a value every 5 mintues. The Boron listens to the Xenon and if needed will turn the heating on or off by turning a servo. During the 20 minutes from flashing new code to the time of the reboot it didn’t actuate the servo so I don’t think I have a power issue. Moreover, the servo and Boron have seperate power supplies.

I’m looking for advice on how to debug this issue. I include my code below for reference. I’m using current firmware and all the reported vitals are healthy.

Boron code (DeviceOS 1.4.2)

Servo servoDial;      // create servo object to control a servo

int actualTemp = 120; // variable to store actual temp
int targetTemp = 70;  // variable to store target temp
int targetDelta = 10;

int pos = 10;         // variable to store the servo position
int Heating = 0;
int servoPositionOn = 175;
int servoPositionOff = 10;

char HeatingStr[8] = "OFF";
char statusMsg[64] = "Empty";

// set to 1 to conserve bandwidth
int ConserveBW = 1;

void setup()
{
    // initialise the servo
    servoDial.attach(D2);  // attaches the servo on the correct pin to the servo object

    // This subscribed function allows the Particle to get the temperature
    Particle.subscribe("io_temp", gotSalonTemp, MY_DEVICES);

    // Expose variables to allow information gathering    
    Particle.variable("Position", &pos, INT);
    Particle.variable("TargetTemp", &targetTemp, INT);
    Particle.variable("ActualTemp", &actualTemp, INT);
    Particle.variable("Heating", &Heating, INT);
    Particle.variable("ConserveBW", &ConserveBW, INT);

    // Functions to allow control
    Particle.function("ioTarget", setTargetTemp);
    Particle.function("ioPosition", setPosition);
    Particle.function("ioSaveBandwidth", setBandwidth);


    // default to off
    servoDial.write(servoPositionOff);
 
    snprintf(statusMsg,64,"%s/%d/%d/Setup !",HeatingStr,actualTemp,targetTemp);
    //statusMsg = HeatingStr + "/" + actualTemp + "/" + targetTemp + "/" + "Setup !";
    Particle.publish("io_boiler_setup",statusMsg,PRIVATE);

    // End of setup
}

void loop()
{
   
    //nothing to do here
}

// Set Bandwidth function will make Particle less busy on the wire
int setBandwidth(String BWValue) {

    if (BWValue.toInt() == 1) {
        ConserveBW = BWValue.toInt();
    } else {
        ConserveBW = 0;
    }

    snprintf(statusMsg,64,"%s/%d/%d/Set Bandwidth %d",HeatingStr,actualTemp,targetTemp,ConserveBW);
    Particle.publish("io_boiler_bandwidth",statusMsg,PRIVATE);

    return 0;
}

// Set position function will allow for testing and calibrating the servo motor
int setPosition(String posValue) {

    pos = posValue.toInt();

    snprintf(statusMsg,64,"%s/%d/%d/Move to position %d",HeatingStr,actualTemp,targetTemp,pos);
    Particle.publish("io_boiler_position",statusMsg,PRIVATE);

    servoDial.write(pos);
    return 0;
}


// Set target Temperature

int setTargetTemp(String tempValue) {

//    Trying to make foolproof
//    Particle.publish("io_target_temp",tempValue);

    targetTemp = tempValue.toInt();

    if (targetTemp < 101)
    {
        // In antifreeze mode it is better to heat longer that to continue switching between states, there is a risk of the servo popping out
        targetDelta = 10;
    } else 
    {
        targetDelta = 3;
    }
    
    snprintf(statusMsg,64,"%s/%d/%d/Set new temp !%d",HeatingStr,actualTemp,targetTemp,targetTemp);

    Particle.publish("io_boiler_target",statusMsg,PRIVATE);

    boilerControlLogic();
    return 0;
}

// Subscribed function which receives fetched temperature
void gotSalonTemp(const char *name, const char *data) {
    // Important note!  -- Right now the response comes in 512 byte chunks.  
    //  This code assumes we're getting the response in large chunks, and this
    //  assumption breaks down if a line happens to be split across response chunks.
    //

    actualTemp = String(data).toInt();
    
    if (ConserveBW == 0) {
        snprintf(statusMsg,64,"%s/%d/%d/Received:%s",HeatingStr,actualTemp,targetTemp,data);

        Particle.publish("io_boiler",statusMsg,PRIVATE);
    }
 
    boilerControlLogic();
}

// Turn on heating
void turnOnHeating() {

    snprintf(statusMsg,64,"%s/%d/%d/Turning on",HeatingStr,actualTemp,targetTemp);
//    statusMsg = HeatingStr + "/" + actualTemp + "/" + targetTemp + "/" + "Turning on";
    Particle.publish("io_boiler_on",statusMsg,PRIVATE);

    servoDial.write(servoPositionOn);

    //Record new state
    pos = servoPositionOn;
    snprintf(HeatingStr,8,"ON");
    Heating = 1;
}

// Turn off heating
void turnOffHeating() {

    snprintf(statusMsg,64,"%s/%d/%d/Turning off",HeatingStr,actualTemp,targetTemp);
//    statusMsg = HeatingStr + "/" + actualTemp + "/" + targetTemp + "/" + "Turning off";
    Particle.publish("io_boiler_off",statusMsg,PRIVATE);
    
    servoDial.write(servoPositionOff);

    //Record new state
    pos = servoPositionOff;
    snprintf(HeatingStr,8,"OFF");
    Heating =0;

}

// Logic for turning on and off
// Compares input temperature to targets
int boilerControlLogic() {
    if (actualTemp > targetTemp + targetDelta)
    {
       if(Heating==1) {turnOffHeating();}
    } else
    {
        if (actualTemp < targetTemp)
        {
            // Too cool
            if(Heating==0) {turnOnHeating();}
        } 
    }

    return 0;
}





Photon code (DeviceOS 1.4.2)

#include <math.h>

// the value of the 'other' resistor
#define SERIESRESISTOR 10000    
 
// which analog pin to connect
#define THERMISTORPIN A0         
// resistance at 25 degrees C
#define THERMISTORNOMINAL 10000      
// temp. for nominal resistance (almost always 25 C)
#define TEMPERATURENOMINAL 25   
// how many samples to take and average, more takes longer
// but is more 'smooth'
#define NUMSAMPLES 5
// The beta coefficient of the thermistor (usually 3000-4000)
#define BCOEFFICIENT 3977
// the value of the 'other' resistor
#define SERIESRESISTOR 10000    
 
int samples[NUMSAMPLES];
int tempindecimals0 = TEMPERATURENOMINAL;
char publishString[40];

// set to 1 to conserve bandwidth
int ConserveBW = 1;

void setup() {
    // register API variable
    Particle.variable("temperature", &tempindecimals0, INT);
    Particle.variable("ConserveBW", &ConserveBW, INT);

    Particle.function("ioSaveBandwidth", setBandwidth);

    pinMode(THERMISTORPIN, INPUT);
}

void loop() {

    uint8_t i;
    float average;
    
    // take N samples in a row, with a slight delay
    for (i=0; i< NUMSAMPLES; i++) {
        samples[i] = analogRead(THERMISTORPIN);
        delay(10);
    }
    
    // average all the samples out
    average = 0;
    for (i=0; i< NUMSAMPLES; i++) {
         average += samples[i];
    }
    average /= NUMSAMPLES;
    
    // convert the value to resistance
    average = 4095 / average - 1;
    average = SERIESRESISTOR / average;
    
    float steinhart;
    steinhart = average / THERMISTORNOMINAL;     // (R/Ro)
    steinhart = log(steinhart);                  // ln(R/Ro)
    steinhart /= BCOEFFICIENT;                   // 1/B * ln(R/Ro)
    steinhart += 1.0 / (TEMPERATURENOMINAL + 273.15); // + (1/To)
    steinhart = 1.0 / steinhart;                 // Invert
    steinhart -= 273.15;                         // convert to C
    
    tempindecimals0 = int(steinhart*10.0);
    snprintf(publishString,40,"%d", tempindecimals0);
    Particle.publish("io_temp",publishString, PRIVATE);
    
    if (ConserveBW == 1) {
        // Delay 5 minutes
        delay(300000);
    } else {
        // Delay 1 minute
        delay(60000);
    }

}

// Set Bandwidth function will make Particle less busy on the wire
int setBandwidth(String BWValue) {

    if (BWValue.toInt() == 1) {
        ConserveBW = BWValue.toInt();
    } else {
        ConserveBW = 0;
    }
    return 0;
}








#2

Most of the following may not directly address your issue, but will at least improve the code.

This is outdated syntax.
You may also want to consider using SYSTEM_THREAD(ENABLED).
Before calling Particle.publish() you should make sure Particle.connected() == true and also guard against hitting the rate limit.

Instead of creating a dummy String object with this call

  actualTemp = String(data).toInt();

just use the immediate C string like this

  actualTemp = atoi(data);

I also prefer using the undocumented int fn(const char*) format for Particle.function() handlers and also avoid using anything String related therein wherever possible.
e.g. here

    if (BWValue.toInt() == 1) {
        ConserveBW = BWValue.toInt();
    } else {
        ConserveBW = 0;
    }

Additionally, since you already know BWValue.toInt() == 1 there is absolutely no need to do the conversion again :wink:

For constant strings strcpy() would be the way to go.

And I’d rather use a single function int controlHeating(bool mode) instead of two dedicated functions (turnOffHeating() and turnOnHeating()) which virtually do the same thing.

For more useful feedback of your functions you should return a more useful value that actually tells you about the result of the call.
Unconditionally calling return 0; makes no sense. If you do that you may just as well define the functions as void.

In order to preserve power and reduce noise and wear on the servo you could also consider attaching the servo only when actually doing something with it, allow for some time to reach the position and then detach again.


#3

Thank you for the advice, lets hope refactored code will bring some stability to the system.
I’ve not yet implemented proper return parameters but have been meaning to so shall do it soon.

I’ll report back if I notice a difference in behaviour.

The new Boron code:

Servo servoDial;      // create servo object to control a servo

int actualTemp = 120; // variable to store actual temp
int targetTemp = 70;  // variable to store target temp
int targetDelta = 10;

int pos = 10;         // variable to store the servo position
int Heating = 0;
int servoPositionOn = 175;
int servoPositionOff = 10;

char HeatingStr[8] = "OFF";
char statusMsg[64] = "Empty";

// set to 1 to conserve bandwidth
int ConserveBW = 1;

SYSTEM_THREAD(ENABLED);
SYSTEM_MODE(SEMI_AUTOMATIC);

void setup()
{

    // This subscribed function allows the Particle to get the temperature
    Particle.subscribe("io_temp", gotSalonTemp, MY_DEVICES);

    // Expose variables to allow information gathering    
    Particle.variable("Position", pos);
    Particle.variable("TargetTemp", targetTemp);
    Particle.variable("ActualTemp", actualTemp);
    Particle.variable("Heating", Heating);
    Particle.variable("ConserveBW", ConserveBW);

    // Functions to allow control
    Particle.function("ioTarget", setTargetTemp);
    Particle.function("ioPosition", setPosition);
    Particle.function("ioSaveBandwidth", setBandwidth);

    Particle.connect();
    
    // default to off
    controlHeating(false);
    
    snprintf(statusMsg,64,"%s/%d/%d/Setup !",HeatingStr,actualTemp,targetTemp);
    // wait up to 60 seconds for the cloud connection to be connected.
    if (waitFor(Particle.connected, 60000)) {
        Particle.publish("io_boiler_setup",statusMsg,PRIVATE);
    }

    // End of setup
}

void loop()
{
   
    //nothing to do here
}

// Set Bandwidth function will make Particle less busy on the wire
int setBandwidth(const char * BWValue) {

    if (atoi(BWValue) == 1) {
        ConserveBW = 1;
    } else {
        ConserveBW = 0;
    }

    snprintf(statusMsg,64,"%s/%d/%d/Set Bandwidth %d",HeatingStr,actualTemp,targetTemp,ConserveBW);
    // wait up to 10 seconds for the cloud connection to be connected.
    if (waitFor(Particle.connected, 10000)) {
        Particle.publish("io_boiler_bandwidth",statusMsg,PRIVATE);
    }
    return 0;
}

// Set position function will allow for testing and calibrating the servo motor
int setPosition(const char * posValue) {

    pos = atoi(posValue);

    snprintf(statusMsg,64,"%s/%d/%d/Move to position %d",HeatingStr,actualTemp,targetTemp,pos);
    // wait up to 10 seconds for the cloud connection to be connected.
    if (waitFor(Particle.connected, 10000)) {
        Particle.publish("io_boiler_position",statusMsg,PRIVATE);
    }

    servoDial.write(pos);
    return 0;
}


// Set target Temperature

int setTargetTemp(const char * tempValue) {

//    Trying to make foolproof
//    Particle.publish("io_target_temp",tempValue);

    targetTemp = atoi(tempValue);

    if (targetTemp < 101)
    {
        // In antifreeze mode it is better to heat longer that to continue switching between states, there is a risk of the servo popping out
        targetDelta = 10;
    } else 
    {
        targetDelta = 3;
    }
    
    snprintf(statusMsg,64,"%s/%d/%d/Set new temp !%d",HeatingStr,actualTemp,targetTemp,targetTemp);

    // wait up to 10 seconds for the cloud connection to be connected.
    if (waitFor(Particle.connected, 10000)) {
        Particle.publish("io_boiler_target",statusMsg,PRIVATE);
    }
    
    boilerControlLogic();
    return 0;
}

// Subscribed function which receives fetched temperature
void gotSalonTemp(const char *name, const char *data) {
    // Important note!  -- Right now the response comes in 512 byte chunks.  
    //  This code assumes we're getting the response in large chunks, and this
    //  assumption breaks down if a line happens to be split across response chunks.
    //

    actualTemp = atoi(data);
    
    if (ConserveBW == 0) {
        snprintf(statusMsg,64,"%s/%d/%d/Received:%s",HeatingStr,actualTemp,targetTemp,data);

        // wait up to 10 seconds for the cloud connection to be connected.
        if (waitFor(Particle.connected, 10000)) {
            Particle.publish("io_boiler",statusMsg,PRIVATE);
        }
    }
 
    boilerControlLogic();
}

void controlHeating(bool TurnOn) {
    // initialise the servo
    servoDial.attach(D2);  // attaches the servo on the correct pin to the servo object

    if(TurnOn) {
        snprintf(statusMsg,64,"%s/%d/%d/Turning on",HeatingStr,actualTemp,targetTemp);
        // wait up to 10 seconds for the cloud connection to be connected.
        if (waitFor(Particle.connected, 10000)) {
            Particle.publish("io_boiler_on",statusMsg,PRIVATE);
        }

       //Record new state
        pos = servoPositionOn;
        strcpy(HeatingStr,"ON");
        Heating = 1;
    } else {
        snprintf(statusMsg,64,"%s/%d/%d/Turning off",HeatingStr,actualTemp,targetTemp);
        // wait up to 10 seconds for the cloud connection to be connected.
        if (waitFor(Particle.connected, 10000)) {
            Particle.publish("io_boiler_off",statusMsg,PRIVATE);
        }

        pos = servoPositionOff;
        strcpy(HeatingStr,"OFF");
        Heating =0;
    } 

    servoDial.write(pos);
    delay(50000);

    servoDial.detach();
}


// Logic for turning on and off
// Compares input temperature to targets
void boilerControlLogic() {
    if (actualTemp > targetTemp + targetDelta)
    {
       if(Heating==1) {controlHeating(false);}
    } else
    {
        if (actualTemp < targetTemp)
        {
            // Too cool
            if(Heating==0) {controlHeating(true);}
        } 
    }

}

The new Xenon code:

#include <math.h>

// the value of the 'other' resistor
#define SERIESRESISTOR 10000    
 
// which analog pin to connect
#define THERMISTORPIN A0         
// resistance at 25 degrees C
#define THERMISTORNOMINAL 10000      
// temp. for nominal resistance (almost always 25 C)
#define TEMPERATURENOMINAL 25   
// how many samples to take and average, more takes longer
// but is more 'smooth'
#define NUMSAMPLES 5
// The beta coefficient of the thermistor (usually 3000-4000)
#define BCOEFFICIENT 3977
// the value of the 'other' resistor
#define SERIESRESISTOR 10000    

int samples[NUMSAMPLES];
int tempindecimals0 = TEMPERATURENOMINAL;
char publishString[40];

// set to 1 to conserve bandwidth
int ConserveBW = 1;

SYSTEM_THREAD(ENABLED);
SYSTEM_MODE(SEMI_AUTOMATIC);

void setup() {
    pinMode(THERMISTORPIN, INPUT);

    // register API variable
    Particle.variable("temperature", tempindecimals0);
    Particle.variable("ConserveBW", ConserveBW);

    Particle.function("ioSaveBandwidth", setBandwidth);
    Particle.connect();
    
}

void loop() {

    uint8_t i;
    float average;
    
    // take N samples in a row, with a slight delay
    for (i=0; i< NUMSAMPLES; i++) {
        samples[i] = analogRead(THERMISTORPIN);
        delay(10);
    }
    
    // average all the samples out
    average = 0;
    for (i=0; i< NUMSAMPLES; i++) {
         average += samples[i];
    }
    average /= NUMSAMPLES;
    
    // convert the value to resistance
    average = 4095 / average - 1;
    average = SERIESRESISTOR / average;
    
    float steinhart;
    steinhart = average / THERMISTORNOMINAL;     // (R/Ro)
    steinhart = log(steinhart);                  // ln(R/Ro)
    steinhart /= BCOEFFICIENT;                   // 1/B * ln(R/Ro)
    steinhart += 1.0 / (TEMPERATURENOMINAL + 273.15); // + (1/To)
    steinhart = 1.0 / steinhart;                 // Invert
    steinhart -= 273.15;                         // convert to C
    
    tempindecimals0 = int(steinhart*10.0);
    snprintf(publishString,40,"%d", tempindecimals0);
    if (waitFor(Particle.connected, 10000)) {
        Particle.publish("io_temp",publishString, PRIVATE);
    }
    
    if (ConserveBW == 1) {
        // Delay 5 minutes
        delay(300000);
    } else {
        // Delay 1 minute
        delay(60000);
    }

}

// Set Bandwidth function will make Particle less busy on the wire
int setBandwidth(const char* BWValue) {

    if (atoi(BWValue) == 1) {
        ConserveBW = 1;
    } else {
        ConserveBW = 0;
    }
    return ConserveBW;
}


#4

Unfortunately I’ve just observed several reboots.
My only idea to fix the problem is to turn off the Xenon for a while to see if that has an impact on stability.

I can plug my laptop into the Boron, is there a way to trace what’s triggering the reset? Some kind of debug mode.


#5

@mayhew1955, my recommendation for any controller design like yours is to use a non-blocking FSM in loop() and control the state changes via the Particle.function() callbacks. This allows clear state management including exception handling.

As it stands, you have waitFor() and large delay() calls in the callbacks via boilerControlLogic(). This should all be moved to the FSM. Also, any connection recovery code (if Cellular or Cloud connectivity is lost) can go in loop() as well, as a second FSM for example.


#6

is there a way to trace what’s triggering the reset? Some kind of debug mode.

Yes several.

  1. Easiest is to put Serial.begin(); in setup and then write out to Serial e.g. Serial.printlnf(“value is %i”, integervalue); One technique is to put a line trace or just lots of output to narrow done on the problem.
  2. Enable SerialLogHandler - look in the documentation under Logging for examples. Log.error(“not OK”);
  3. Enable Reset Reason and then use either 1 or 2 methods to display this at startup - likely it will be Panic (for resetReason enumerator values do a search on the forum).
  4. Use Debugging in Workbench together with a Particle debugger plugged in to your Boron.

I know you have had a lot of advice from the best on the Forum - I’ll add this: the aim must always be to keep the loop() function cadence as high as possible, as @peekay123 mentioned implementing a Finite State Machine is key to achieving this, I would never call a Particle.publish() from within a Particle function handler. The best model is set a flag (global variable) and then test in the loop() for the flag set - then if set clear the flag and Particle.publish. Calling waitFor() and long delay() in setup() is OK


#7

Thanks for all the input.
Turning off the Xenon has not made an appreciable difference. There was at least one reset during the night, it seemed to take longer than before but the delay is random so thats not a definitive observation.

I’ve implemented Reset Reason to gather some more data short term:

I’ve also switched back to the old logic board I made for the Photon, I may have damaged the new logic board and the old one has stood the test of time.

I’ve updated my code to use Finite State Machine logic, it’s below, although I don’t know how to remove the delay for the motor swing from loop().

Fingers crossed !

Servo servoDial;      // create servo object to control a servo

int actualTemp = 120; // variable to store actual temp
int targetTemp = 70;  // variable to store target temp
int targetDelta = 10;

int pos = 10;         // variable to store the servo position
int Heating = 0;
int servoPositionOn = 175;
int servoPositionOff = 10;

char HeatingStr[8] = "OFF";
char statusMsg[64] = "Empty";
char rstReason[8] = "Empty";
bool sendNextMsg = false;

// set to 1 to conserve bandwidth
int ConserveBW = 1;

enum MyStates {
  IDLE_STATE,
  SET_TEMPERATURE,
  TOO_HOT,
  TOO_COOL,
  GOT_TEMPERATURE,
};

MyStates mainState = IDLE_STATE;

SYSTEM_THREAD(ENABLED);
SYSTEM_MODE(SEMI_AUTOMATIC);

STARTUP(System.enableFeature(FEATURE_RESET_INFO));

void setup()
{

    switch(System.resetReason()){
        case RESET_REASON_PIN_RESET:
            strcpy(rstReason,"PIN");
            break;
        case RESET_REASON_POWER_MANAGEMENT:
            strcpy(rstReason,"POWERMGT");
            break;
        case RESET_REASON_POWER_DOWN:
            strcpy(rstReason,"POWERDWN");
            break;
        case RESET_REASON_POWER_BROWNOUT:
            strcpy(rstReason,"POWERBRN");
            break;
        case RESET_REASON_WATCHDOG:
            strcpy(rstReason,"WDOG");
            break;
        case RESET_REASON_UPDATE:
            strcpy(rstReason,"UPD");
            break;
        case RESET_REASON_UPDATE_TIMEOUT:
            strcpy(rstReason,"UPDTO");
            break;
        case RESET_REASON_FACTORY_RESET:
            strcpy(rstReason,"FACTORY");
            break;
        case RESET_REASON_SAFE_MODE:
            strcpy(rstReason,"SAFE");
            break;
        case RESET_REASON_DFU_MODE:
            strcpy(rstReason,"DFU");
            break;
        case RESET_REASON_PANIC:
            strcpy(rstReason,"PANIC");
            break;
        case RESET_REASON_USER:
            strcpy(rstReason,"USER");
            break;
        case RESET_REASON_UNKNOWN:
            strcpy(rstReason,"UNKNOWN");
            break;
        case RESET_REASON_NONE:
            strcpy(rstReason,"NONE");
            break;
    }
    // This subscribed function allows the Particle to get the temperature
    Particle.subscribe("io_temp", gotioTemp, MY_DEVICES);

    // Expose variables to allow information gathering    
    Particle.variable("Position", pos);
    Particle.variable("TargetTemp", targetTemp);
    Particle.variable("ActualTemp", actualTemp);
    Particle.variable("Heating", Heating);
    Particle.variable("ConserveBW", ConserveBW);

    // Functions to allow control
    Particle.function("ioTarget", setTargetTemp);
    Particle.function("ioPosition", setPosition);
    Particle.function("ioSaveBandwidth", setBandwidth);

    Particle.connect();
    
    // default to off
    controlHeating(false);
    
    snprintf(statusMsg,64,"%s/%d/%d/Setup !/%s",HeatingStr,actualTemp,targetTemp,rstReason);
    // wait up to 60 seconds for the cloud connection to be connected.
    if (waitFor(Particle.connected, 60000)) {
        Particle.publish("io_boiler_setup",statusMsg,PRIVATE);
    }

    // End of setup
}

void loop()
{
  static MyStates lastState = mainState;

    // react to state changes...
    //
    if (mainState != lastState) {
        switch (mainState) {
            case IDLE_STATE:
                // perform functions to return to IDLE_STATE
                break;
            case SET_TEMPERATURE:
                snprintf(statusMsg,64,"%s/%d/%d/Set new temp/%s",HeatingStr,actualTemp,targetTemp,rstReason);
                sendNextMsg = true;
                break;
            case TOO_HOT:
                snprintf(statusMsg,64,"%s/%d/%d/Turning off/%s",HeatingStr,actualTemp,targetTemp,rstReason);
                sendNextMsg = true;
                break;
            case TOO_COOL:
                snprintf(statusMsg,64,"%s/%d/%d/Turning on/%s",HeatingStr,actualTemp,targetTemp,rstReason);
                sendNextMsg = true;
                break;
            case GOT_TEMPERATURE:
                if (ConserveBW == 0) {
                    snprintf(statusMsg,64,"%s/%d/%d/Received io/%s",HeatingStr,actualTemp,targetTemp,rstReason);
                    sendNextMsg = true;
                }
                break;
        }
        lastState = mainState;
    }
    
    // manage states...
    //
    switch (mainState) {
        case IDLE_STATE:
            // nothing to do here
            break;
        case TOO_HOT:
            // return to idla 
            mainState = IDLE_STATE;
            // Turn off heating
            controlHeating(false);
            break;
        case TOO_COOL:
            // return to idla 
            mainState = IDLE_STATE;
            // Turn on heating
            controlHeating(true);
            break;
        default: //SET_TEMPERATURE or GOT_TEMPERATURE
            // Check it's either too hot (with the heating on), too cool (with the heating off) or if there's nothing to do
            if (actualTemp > targetTemp + targetDelta)
            {
               if(Heating==1) {mainState = TOO_HOT;}
               else {mainState = IDLE_STATE;}
            } else
            {
                if (actualTemp < targetTemp)
                {
                    // Too cool
                    if(Heating==0) {mainState = TOO_COOL;}
                    else {mainState = IDLE_STATE;}
                } else {mainState = IDLE_STATE;}
            }
            break;
    }

    if (sendNextMsg && Particle.connected())
    {
        sendNextMsg = false;
        Particle.publish("io_boiler",statusMsg,PRIVATE);
    }

}

// Set Bandwidth function will make Particle less busy on the wire
int setBandwidth(const char * BWValue) {

    if (atoi(BWValue) == 1) {
        ConserveBW = 1;
    } else {
        ConserveBW = 0;
    }

    return ConserveBW;
}

// Set target Temperature
int setTargetTemp(const char * tempValue) {
    targetTemp = atoi(tempValue);
    
    // perform calculations when target temperature changes
    // values under 6°C might leave the system open to freezing
    if (targetTemp > 60)
    {
        mainState = SET_TEMPERATURE;
        if (targetTemp < 160)
        {
            // In economy modes it is better to heat longer that to maintain a steady temperature
            targetDelta = 10;
        } else 
        {
            // A value of 3 is most comfortable, 4 or 5 would work too depending on thermometer placement
            targetDelta = 3;
        }
    }
    return targetTemp;
}

// Subscribed function which receives fetched temperature
void gotioTemp(const char *name, const char *data) {
    actualTemp = atoi(data);
    mainState = GOT_TEMPERATURE;
}

void controlHeating(bool TurnOn) {
    // initialise the servo
    servoDial.attach(D2);  // attaches the servo on the correct pin to the servo object

    if(TurnOn) {
       //Record new state
        pos = servoPositionOn;
        strcpy(HeatingStr,"ON");
        Heating = 1;
    } else {
        pos = servoPositionOff;
        strcpy(HeatingStr,"OFF");
        Heating =0;
    } 

     // allow 3 seconds to move the servo
    servoDial.write(pos);
    delay(30000);

    // no immediate need for the servo : detach
    servoDial.detach();
}

void sendStatusMsg() {
// wait up to 10 seconds for the cloud connection to be connected.
    if (waitFor(Particle.connected, 10000)) {
        
    }
}

// This function is used only when changing the servo for calibration
int setPosition(const char * posValue) {

    // initialise the servo
    servoDial.attach(D2);  // attaches the servo on the correct pin to the servo object

    pos = atoi(posValue);
    servoDial.write(pos);
     // allow 3 seconds to move the servo
    servoDial.write(pos);
    delay(30000);

    // no immediate need for the servo : detach
    servoDial.detach();

    return pos;
}


#8

First, I doubt your servo will actually require 30 seconds to move into the desired position.
Second, you could introduce an additional state in your FSM for the process and set that state inside your function handler and in your FSM that state could be handled like this

const uint32_t SWING_TIME = 1000; // that should easily do
uint32_t       stateTime  = 0;    // global variable to keep track of the time a state was active  

void controlHeating(bool TurnOn) {
  ...
  servoDial.write(pos);
  mainState = SERVO_SWING;
  stateTime = millis();   
}

void loop() {
  ...
    case SERVO_SWING:
      if (millis() - stateTime < SWING_TIME) return;
      servoDial.detach();
      stateTime = 0;
      mainState = IDLE_STATE;      
      break;
  ...
}

#9

I was aiming for 3 seconds :blush:

I’ve added the additional state in the FSM although I think a flag would have been more appropriate in this instance. The problem I can forsee is that a message could arrive during the swing and mainState would change without detaching the servo.


#10

I just wanted to give some feedback as early signs suggest that the problem has been solved, the Boron has not reset in 36 hours.

I’m leaning towards the problem being a faulty soldering job, I’m quite sceptical that my code was so bad that it would cause the hardware to reset at random intervals.

The upshot is that we’ve improved my code and I’ve learn’t some really useful pratices thanks to the ever helpful and reactive Particle community. I’m really thankfull for your help and picking up a Core was absolutely the right thing to do all those years ago.

One improvement is that Particle.publish() messages are now delivered in the right order. This was an infrequent problem with the Photon but porting my code over to the Boron had made things much worse.

The original Photon would also crash infrequently after many months of use. I’m confident that by replacing all string instances with char[] and by reducing the complexity of the particle functions we have created a more robust system and if it ever does crash I also now have the reboot reason in the status messages.

Lastly, I like how the Finite State Machine is easy to read. My implementation is not perfect, moving from TOO_HOT or TOO_COLD to IDLE should only happen if the servo is known to have worked but I havn’t built a sensor for that! I can also see how easy it would be to add a screen and some buttons to the system, or maybe an outside temperature sensor.

I will own up to leaving the servo attached all the time though. My high torque servo jumps clockwise by several degrees each time I issue the detach() command. It’s very predictable but also very odd and I was out of time on site so I’ve removed that feature for now. I did time the swing though, 3 seconds were needed, it’s moot now I’ve left it attached all the time but good to know.

Thanks again and good night to all :grinning:


#11

When you feel that the current setup is working you could explore a PID (Proportional Integral Differential) control algorithm that could help turning a temperature sensor signal into a control output - search for PID in the libraries there is one these.