Photon Particle.subscribe() misses messages when System.sleep(Di,RISING,time)

I am creating a temperature sensor (for refrigerators and freezers) that logs to a local MQTT broker (mosquitto running on a Raspberry Pi). This device will be running on battery and in order to increase time between recharge I want to put the device into a reasonable sleep mode for 30+ minutes at a time (sleep is currently 15 sec whilst testing).

The problem with this is that the code runs quickly (>5 sec) and I want to be able to deliver OTA updates.
I have tried subscribing to both MQTT and Particle.subscribe events so that I can delay the next sleep cycle and flash an update (I will look at automating this later with MQTT and some Python).
Most messages are missed. I assume that these are missed because the Photon was asleep when the message was published. I was hoping that the messages would be pushed to the Photon once it woke up and reconnected to Particle Cloud.
I am currently publishing from another Photon with the following

if (digitalRead(butnPin)) {
        // debounce button
        while(digitalRead(butnPin)){}
        Serial.println("buton pressed");
        Particle.publish("OTA","Available",60, PRIVATE);
        // publish this public event
    }

I have subscribed to these events with

Particle.subscribe("OTA", particleSubscribeHandler, MY_DEVICES);

The particleSubscribeHandler() just flashes the onboard LED at the moment to signal that the event was heard.

Is there anything I need do to make sure that messages sent whilst asleep are received when the photon wakes up.

Or is there any other way to be able to flash OTA updates on a device that is only awake briefly between sleeps (other than setting a specific time each day to stay awake longer).

Any help is much appreciated.
Cheers
Sean

Despite the TTL setting for Particle events there isn't a time-to-live - these are merely fire-and-forget.
But even if there was, your 60sec TTL would be too short for a 30min sleep period.

Not out the box.
Vie MQTT you should be able to achieve what you want. You can send a retained massage to the broker and the waking device can subscribe to that message an should get delivered as soon the client shows interest.

2 Likes

Thanks ScruffR. I figured you would be quick off the ball with a responce:+1:
I was aware that the TTL 60 would be too short for a 30min sleep. Am currently testing sleep for 15sec and will increase from there.

Thank you for the update regarding Particle events. I will stop testing along that path.

I will dig deeper into MQTT retained messages. And post here how I go.

The other option that I thought might work is a http request. I could set a local (or public) web page that would with provide a simple notification that an update was available. I will look into it this week. If anyone has any pointers it would be appreciated.

I was planning on migrating the system over to an Argon-Xenon mesh network after I had it working on Photon (only because I am more comfortable with Photon and have not worked with the mesh devices).
That plan was brought forward in order to solve this issue.
I now have an Argon that is always on and connected to power. This gateway received Mesh.publish messages from the Nodes and forwards specific ones on to the MQTT broker.
The Nodes (Xenon’s) are all running on battery and sleep most of the time.

I solve the problem buy the following

The Xenon’s - Nodes

  • all have a constant int containing their current firmware version.
  • Each time they wake up they send out a Mesh.publish(“awake”).
  • They subscribe to Mesh.subscribe(“firmware”) messages that contain the current firmware version available.
  • If this version is higher than the current running version then they publish a message stating ready for upgrade and run an infinite loop while(true). This makes it possible to flash new firmware.

The Argon - Gateway

  • Has a Particle function that allows the int meshFirmware to be changed. When new mesh firmware is available then I update this variable. This also defaults to 1 if the Argon is reset.
  • It subscribes to Mesh.subscribe(“awake”) and pushes out a Mesh.publish(“firmware”, meshFrimware) immediately to catch the Xenon before it goes to sleep.

Argon code - not yet parsing mqtt messages to broker, that is the next step.

int meshFirmware = 1;

void setup() {
    Serial.begin(9600);
    pinMode(D7, OUTPUT);
    System.on(button_status, button_handler);
    Mesh.subscribe("toggle-led", toggleLed);
    Mesh.subscribe("tempLog", mqttParser);
    Mesh.subscribe("tempBatt", mqttParser);
    Mesh.subscribe("awake", meshDeviceAwake);
    
    if(!Particle.function("updateMeshFirmware", updateMeshFirmware)){
        Particle.publish("Function ERROR", "sensorsJSON", 60, PRIVATE);
    }
}

void loop() {

}
void button_handler(system_event_t event, int duration, void* ) {
    if (!duration) {
        // Just pressed.
        Mesh.publish("toggle-led");
        Serial.println("Button push published!");
        digitalWrite(D7, HIGH);
    } else {
        // Button released.
        digitalWrite(D7, LOW);
    }
}

void toggleLed(const char *event, const char *data) {
    digitalWrite(D7, HIGH);
    delay(500);
    digitalWrite(D7, LOW);
}

void mqttParser(const char *event, const char *data) {
    Serial.printlnf("Topic: %s Payload: %s",event,data);
}


void meshDeviceAwake(const char *event, const char *data) {
    Serial.println("Mesh devise is awake. Sending current firmware version");
    Mesh.publish("firmware",String(meshFirmware));
}
// update firmware version number to Mesh devices
int updateMeshFirmware(String versionStr)
{
    versionStr.trim();
    Serial.print("New Mesh firmware version: '");
    Serial.print(versionStr);
    Serial.print("'");
    
    
    // make sure input is a number
    if(isValidNumber(versionStr)){
        // send to Mesh devices
        meshFirmware = versionStr.toInt();
        Serial.println(" is a valid number");
        return 1;
    }
    else{
        Serial.println(" is NOT a valid number");
        return -1;
    }
}

boolean isValidNumber(String numStr){
    // test that each char is a digit
    for(int i=0;i<numStr.length();i++){
        if(numStr.charAt(i)<'0' || numStr.charAt(i)>'9'){
            return false;
        }
    }
    return true;
}

Xenon code

// This #include statement was automatically added by the Particle IDE.
#include <DS18B20.h>

#include <math.h>

const int firmwareVersion = 1;
const int pinOneWire = D2;
const int MAXRETRY = 10;
const int sleepTime = 10; //1800; // 30 min

DS18B20  ds18b20(pinOneWire);

uint8_t sensorAddress[8];
float sensorTemp;
float battVolt;
time_t lastReadTime;

String sensorsJSON;
String batteryJSON;
void setup() {
    Serial.begin(115200);
    Mesh.subscribe("firmware", checkFirmware);
    setAddress();
    
}

void loop() {
    System.sleep(D1,RISING,sleepTime);
    
    // wait until Mesh connects and is ready
    while(!Mesh.ready()){}
    // send message that i am awake
    Mesh.publish("awake", String(firmwareVersion));
    
    sensorTemp = getTemp(sensorAddress);
    if(!isnan(sensorTemp)){
        lastReadTime = Time.now();
    }else{
        lastReadTime = 0;
    }
    
    updateSensorsJSON();
    Serial.println(sensorsJSON);
    Mesh.publish("tempLog", sensorsJSON);
    // delay(2000);
    
    // get battery details
    battVolt = analogRead(BATT) * 0.0011224;
    updateBatteryJSON();
    Serial.println(batteryJSON);
    Mesh.publish("tempBatt", batteryJSON);
    delay(1000);
    
}

void setAddress(){
    boolean srchResult = false;
    while(!srchResult){
        ds18b20.resetsearch();                 // initialise for sensor search
        srchResult = ds18b20.search(sensorAddress);
    }
}

String addressString(){
    char szInfo[16];
    snprintf(szInfo, sizeof(szInfo), "%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x", sensorAddress[0],
                                                                            sensorAddress[1],
                                                                            sensorAddress[2],
                                                                            sensorAddress[3],
                                                                            sensorAddress[4],
                                                                            sensorAddress[5],
                                                                            sensorAddress[6],
                                                                            sensorAddress[7]);
    return szInfo;
}

double getTemp(uint8_t addr[8]) {
    // Serial.print("getTemp() ");
    double _temp;
    int   i = 0;
    
    do {
        _temp = ds18b20.getTemperature(addr);
    } while (!ds18b20.crcCheck() && MAXRETRY > i++);
    
    if (i < MAXRETRY) {
        //_temp = ds18b20.convertToFahrenheit(_temp);
        // Serial.println(_temp);
    }
    else {
        _temp = NAN;
        // Serial.println("Invalid reading");
    }
    
    return _temp;
}

/*************************************
 * create JSON valid string representation
 * for temp, address, and time
********************************/
void updateSensorsJSON(){
    // initialise temporary string
    String json = "";
        // if sensor is valid
    if(lastReadTime!=0){
        //start array element
        
        json = String(json + "{");
        // address
        json = String(json + "\"address\":\"" + addressString() +"\",");
        //temp
        json = String(json + "\"temp\":\"" + sensorTemp +"\",");
        //lastReadTime
        json = String(json + "\"lastReadTime\":\"" + lastReadTime +"\"");
        
        //close array element
        json = String(json + "}");
    }
        
    //update global variable
    sensorsJSON = json;
}

/*************************************
 * create JSON valid string representation
 * for battery voltage, address, and time
********************************/
void updateBatteryJSON(){
    // initialise temporary string
    String json = "";
        // if sensor is valid
    if(lastReadTime!=0){
        //start array element
        
        json = String(json + "{");
        // address
        json = String(json + "\"address\":\"" + addressString() +"\",");
        //temp
        json = String(json + "\"battery\":\"" + battVolt +"\",");
        //lastReadTime
        json = String(json + "\"lastReadTime\":\"" + lastReadTime +"\"");
        
        //close array element
        json = String(json + "}");
    }
        
    //update global variable
    batteryJSON = json;
}

// handler for Mesh.subscribe('firmware')
// check value of payload against firmwareVersion
// if newer version is available then do not sleep
void checkFirmware(const char *event, const char *data) {
    if(atoi(data)>firmwareVersion){
        // new firmware is available
        // send mesage to notify that device is ready to update
        Mesh.publish("tempLog", String::format("Device %s ie ready to update",System.deviceID()));
        // loop and do nothing until updated. ie do not go to sleep
        while(true){}
    }
}

Any comments would be welcome. Also would it be better to create a new thread about OTA updates and Sleep, as that was the main problem that I wanted to address.

@seanM, some quick comments on the Xenon code:

  • The size of szInfo[] in addressString() needs to be large enough to include the NULL string terminator. Currently, you are printing 16 characters so the char array needs to have a size of 17.
  • Avoid using Arduino String in updateSensorsJSON() and updateBatteryJSON(). Instead, much like addressString(), use a char array and snprintf() to create your JSON string. You can use strncpy() to copy the char array to the global xxxxJSON char arrays (which you will need to change from String).

As you may know, String uses dynamic memory allocation which can eventually lead to memory fragmentation and software crash/failure.

1 Like

Thanks @peekay123.
As you can see, some of the code has evolved from similar projects from Arduino devices.
I have become somewhat lazy by using String instead of char*. As I intend for these devices to be running along time in between reboots I appreciate the note about String and memory issues.

Cheers
Sean

1 Like