SleepHelper library (preliminary)

@rickkas7 ,

I have updated the repo with new code that will use permanent (FRAM) storage. I was not sure how to generalize this so someone could select other storage implementations such as Flash or EEPROM so it is baked into setup of the main source file. Open to suggestions here if there is a better way.

Updated Repository: GitHub - chipmc/SleepHelper-Demo

I am also starting to add Particle functions that will be important to deploying devices such as the ability to enable and disable sleep (important when installing and testing devices).

Now that I have put in all the basic elements (watchdog, storage objects, measurements, behavior and particle functions) I want to see if I could make this code function well enough to do some real world testing. The publishQueuePosixRK work is one thing we have discussed but I have a few more questions about how to add some essential functionality.

  1. At times, things will go wrong (sensor fails to initialize, device fails to connect, battery charge level drops too far) and we will need to capture and report these events in order to monitor the health of the fleet. Any suggestions on how to do this?
  2. Something is off about the organization of the HTML documentation as there is no way to get to this page (that I have found) through the navigation menu at the top of the page.
    SleepExample1: Callback functions you can register
  3. I am struggling a bit putting the new capabilities into action. Is there any reference material or tutorials you might point me to for C11 lamdas? I particular, I am not clear without more examples, what to put inside the parenthesis of functions like .withSleepReadyFunction(). This is a new notation and I clearly need some basic education on use and syntax.
  4. In previous application notes for the sleep / measure / report use case, there was a need for a “firmware update” state to make sure the device stayed awake long enough for a firmware update to complete downloading before sleep. Since the deviceOS@2.x branch does not have the “resume downloading” feature, is this still needed?
    Wake publish sleep example | Firmware | Particle

Sorry about all the questions, I just want to see if I can start testing this new library quickly and provide real world feedback.

Thanks,

Chip

1 Like

I’ll come back to the other questions later, but C++11 lambda functions are described here.

They’re always optional. Anywhere where you see one, you can just substitute a regular C function.

In this example:

    SleepHelper::instance().withSleepReadyFunction([](AppCallbackState &, system_tick_t) {
        // Return false if it's OK to sleep or true to stay awake
        return !Time.isValid();
    });

If you look at the prototype of withSleepReadyFunction, it looks like this:

    SleepHelper &withSleepReadyFunction(std::function<bool(AppCallbackState &, system_tick_t)> fn) {
        sleepReadyFunctions.add(fn); 
        return *this;
    }

What this translates to is a callback with the prototype:

bool sleepReadyCallback(AppCallbackState &, system_tick_t)
  • The part in the () for the lamba are the parameters to the lambda function.
  • The part in the [] for the lamba is which local variables to capture. The capture variables are available to the lambda when it’s executed later. One common thing is [this] which makes a C++ class instance available to the lambda.
  • The return value of the lambda isn’t specified anywhere; it’s deduced from the return value of the lambda.
3 Likes

I can see the power in this approach. The link you sent was excellent as well as the examples you provide with the library, I will need to practice using the C++11 Lambda's do seem to make for cleaner code than the call backs or state machine approaches I have been using.

I understand from your documentation that I can also use this approach to extend the capabilities of the library. By that, I understand that I can write a block of code, inside the {} of the function that is connected to one of the existing .with...... functions provided by the library such as .withSleepConfigurationFunction. So, as long as there is an appropriate .with.... function to extend, I can include all manner of extra functionality. If I have this wrong, please correct me. Otherwise, some of my questions above could be answered by figuring out which function to use to hold and trigger the code I would like to add.

Thanks,

Chip

Yes, that is correct.

1 Like

The bug with PublishQueuePosixRK is actually in that library, so the fix is there, but I released a new version of SleepHelper with an updated more-examples/50-publish-queue to test this

0.0.3 (2022-06-21)

  • Updated to PublishQueuePosix 0.0.4 to fix two bugs when using SleepHelper and PublishQueuePosixRK at the same time:
    • Events in the file queue would not be sent in some cases
    • After sending events, the device would not go back to sleep in some cases

The link to the HTML API documentation is at the top of the README, under Full Browsable HTML documentation.

Repository details:

3 Likes

@rickkas7 ,

Thank you for the updates. With SleepHelper and publishQueuePosixRK updated, the publishing is working well. In looking at the activity now, it is clear that the ability to combine payloads into a single data operation has significant advantages. In the screen shot below, 22 web hooks with significant redundancy were reduced to a single publish - nice!

A couple quick questions -

  1. It seems the SOC value in the combined JSON payload is always showing 100% which does not align to the actual battery charge level - why might this be the case?
  2. Can we have access to the value of the “ttc” variable for our own purposes? If so, how would we do that?
  3. If I were to create a webhook called - “sleepHelper” - could I send this data to the webservice of my choice?

Thanks,

Chip

2 Likes

WOW, that's quite an improvement!

1 Like

All,

Wanted to share an update and ask a couple questions on error handling and how to manage persistent storage in this new model.

I focused today on updating the persistent storage. I needed to implement an approach that would recognize changes to the storage objects and update them in FRAM but, I did not want this to happen too often so I created a function that generates a hash for each object (current and system) and checks to see if the hash has changed each second. If I am reinventing the wheel here or if there is a better way to do it, I am all ears. The full repo link is shared above but the net is this:

I define the system objects in the storage_objects.h and .cpp files. The objects are global - here is System:

struct systemStatus_structure {                     // Where we store the configuration / status of the device
  uint8_t structuresVersion;                        // Version of the data structures (system and current)
  int currentConnectionLimit;                       // Here we will store the connection limit in seconds
  bool verboseMode;                                 // Turns on extra messaging
  bool solarPowerMode;                              // Powered by a solar panel or utility power
  bool enableSleep;                                 // Low Power Mode will disconnect from the Cellular network to save power
  uint8_t wakeTime;                                 // Hour to start operations (0-23)
  uint8_t sleepTime;                                // Hour to go to sleep for the night (0-23)
};
extern struct systemStatus_structure sysStatus;

there is a function that is run in setup() that will check to see if the object structure is changed, initialize with defaults if it has and then load from FRAM if it has not:

bool storageObjectStart() {
    // Next we will load FRAM and check or reset variables to their correct values
  fram.begin();                                     // Initialize the FRAM module
  byte tempVersion;
  fram.get(FRAM::versionAddr, tempVersion);         // Load the FRAM memory map version into a variable for comparison
  if (tempVersion != FRAMversionNumber) {           // Check to see if the memory map in the sketch matches the data on the chip
    fram.erase();                                   // Reset the FRAM to correct the issue
    fram.put(FRAM::versionAddr, FRAMversionNumber); // Put the right value in
    fram.get(FRAM::versionAddr, tempVersion);       // See if this worked
    if (tempVersion != FRAMversionNumber) {
      // Need to add an error handler here as the device will not work without FRAM will need to reset
    }
    loadSystemDefaults();                           // Since we are re-initializing the storage objects, we need to set the right default values
  }
  else {
    fram.get(FRAM::systemStatusAddr,sysStatus);     // Loads the System Status array from FRAM
    fram.get(FRAM::currentStatusAddr,current);      // Loead the current values array from FRAM
  }

  return true;
}

Notice that, in the example above, the FRAM could fail to initialize - I need some way to catch these issues, report them, and then take steps to resolve them.

Finally, a function in the main loop will implement the once a second hashing and storing if needed. Here is what it looks like for the system object:

...
  if (millis() - lastCheckMillis > 1000) {          // Check once a second
    lastCheckMillis = millis();                     // Limit all this math to once a second
    size_t sysStatusHash =  std::hash<byte>{}(sysStatus.structuresVersion) + \
                      std::hash<int>{}(sysStatus.currentConnectionLimit)+ \
                      std::hash<bool>{}(sysStatus.verboseMode) + \
                      std::hash<bool>{}(sysStatus.solarPowerMode) + \
                      std::hash<bool>{}(sysStatus.enableSleep) + \
                      std::hash<byte>{}(sysStatus.wakeTime) + \
                      std::hash<byte>{}(sysStatus.sleepTime);
    if (sysStatusHash != lastSysStatusHash) {       // If hashes don't match write to FRAM
      Log.info("system Object stored and hash updated");
      fram.put(FRAM::systemStatusAddr,sysStatus);
      lastSysStatusHash = sysStatusHash;
      returnValue = true;                           // In case I want to test whether values changed
    } 
...

This seems to work well in testing but I have a few questions for anyone who has done something like this:

  1. Is there an easier way to see if an object has changed? Computing the hash seems like overkill. I thought about simply adding all the values (they are all numeric) but as some are boolean, I could imagine an undetected change.
  2. Is there a way to see if this is too much overhead? I figured once a second was reasonable and tolerable from a data loss perspective. Perhaps I could trap the mills in the log output?
  3. In my old code, I had an ERROR_STATE where I could resolve issues like the FRAM not initializing in setup(). Without a state machine (that I have access to at least), how do I handle errors?
  4. Could there be another function in the Sleep Helper library that handles this - perhaps .withObjectChangedFunction ?

Thanks,

Chip

1 Like

@rickkas7 ,

The best way forward will be for me to use the sleepHelper webhook the library creates and modify it to match what my back-end service - Ubidots - expects. This can be done a number of ways:

  • I could modify (perhaps?) the structure of the sleepHelper JSON payload to match the API Ubidots expects. Is this possible?
  • I could leave the sleepHelper JSON payload as it is (for compatibility with future Particle back-end) and create a new Particle compatible webhook using the JSONParserGenerator library. This option has high overhead - does it make sense?
  • I could send the sleepHelper to Ubidots, and modify it using a plugin, function as a service (UbiFunction). Will explore this with Ubidots.
  • Ubidots could update their Particle library to accommodate sleepHelper as an input with a compatible webhook as an output. Will explore this with Ubidots.
  • Some new capability may be baked into your plans for the Particle backend - don’t know what to expect here but trust you are cooking up something good. :wink:
  • Finally, I could modify a standard Particle webhook to do this translation with the understanding that it is static and will only work if the number of data points in the sleepHelper payload match the number of datapoints hard-coded into the webhook. As this is the easiest thing to do, I will start here.

Here is the API endpoint we are looking at using:

Any help would be greatly appreciated - I know I am asking a lot of questions. :wink:

Chip

In the Tracker configuration we to use a hash to see if it changed. I use the same algorithm (murmur3) in SleepHelper because it’s fast and the code is very small and it’s a relatively good 32-bit hashing function. It’s exposed as the static function SleepHelper::CloudSettingsFile::murmur3_32. The seed is a random number that you should generate once, typically stored as a const value in your code.

static uint32_t murmur3_32(const uint8_t* buf, size_t len, uint32_t seed);

I think it should be possible to allow code to override the format of the payload, but I’ll have to think about it some more. Certainly I can expose any values that SleepHelper knows internally so you can use them in a user-generated event.

2 Likes

The booleans could be considered 1 and 0.
But I think adding them can lead to undetected changes. Imagine you have only 2 values. One changes in +1, the other in -1. The addition gives you the same result, but the variables changed. Not good :neutral_face:

1 Like

Thank you all for your help and support - especially you @rickkas7

Here is the progress I have made with this next release (v0.09):

  • I got rid of the individual web hooks, I now use the sleepHelper publish without modification and then use a static webhook structure (more on this below) to send to Ubidots.
  • I have implemented persistent storage and validated it works. I was not able to use the murmur3_32 hashing with the two storage objects but I have validated the functionality of the hash function I have put together.
  • The device now reliably operates in both “enable sleep” and “disabled sleep” modes using the Particle function and the user button to move between them (more on this also).
  • I have updated all the #includes, header information and implemented better logging for troubleshooting.

So, here are a couple areas I would appreciate any help advice:

  1. This library allows you to specify how often data points will be collected and how often the device will connect to the Particle cloud and send its data off in the sleepHelper webhook. With some help from Ubidots (thank you David!) I was able to build a custom JSON webhook that would take the unmodified sleepHelper JSON and reformat it for Ubidots using their bulk upload API.
// Here is the JSON from sleepHelper:
{
  "soc": 96,
  "ttc": 12959,
  "rr": 20,
  "eh": [
    {
      "t": 1656265440,
      "bs": 4,
      "c": 22.9304
    },
    {
      "t": 1656265560,
      "bs": 4,
      "c": 23.3333
    },
    {
      "t": 1656265680,
      "bs": 4,
      "c": 22.9304
    },
    {
      "t": 1656265800,
      "bs": 4,
      "c": 23.4139
    }
  ]
}

// And here is the JSON in the Particle Webhook that sends the data to Ubidots
[
  {
    "device": "{{{PARTICLE_DEVICE_ID}}}",
    "values": [
      {
        "battery": "{{{soc}}}",
        "temp": "{{{eh.0.c}}}",
        "connecttime": "{{{ttc}}}",
        "timestamp": "{{{eh.0.t}}}000"
      },
      {
        "battery": "{{{soc}}}",
        "temp": "{{{eh.1.c}}}",
        "connecttime": "{{{ttc}}}",
        "timestamp": "{{{eh.1.t}}}000"
      },
      {
        "battery": "{{{soc}}}",
        "temp": "{{{eh.2.c}}}",
        "connecttime": "{{{ttc}}}",
        "timestamp": "{{{eh.2.t}}}000"
      },
      {
        "battery": "{{{soc}}}",
        "temp": "{{{eh.3.c}}}",
        "connecttime": "{{{ttc}}}",
        "timestamp": "{{{eh.3.t}}}000"
      }
    ]
  }
]

This works well but it has an obvious problem - the webhook is specific to the number of data points collected between uploads to Particle. In looking at @rickkas7 's Intermediate Webhook tutorial, it seems that it may be possible to build the webhook so it can accommodate as many data points as needed.

However, I have not been able to get this to work. Also, what if we want to include other payloads beyond data points - such as alerts or status messages? Any advice would be appreciated!

  1. I am struggling a bit with managing the sleep wake cycles of this device. Here is what I am trying to do:
  • A new device, put into service will connect to Particle and stay on-line (for configuration).
  • Once configured using the Particle console, a Particle function call will enable sleep.
  • The device will stay in this mode - normal operations - but will delay going back to sleep for 90 seconds when it connects in case I would need to remotely disable sleep for configuration / testing.
  • If physical access is possible, pressing the user button will wake the device and force it to connect.

I have the first two of these attributes working now but would appreciate any advice on how to handle the last two. Here is what I have now in the sleep_helper_config.cpp file:

        .withSleepConfigurationFunction([](SystemSleepConfiguration &sleepConfig, SleepHelper::SleepConfigurationParameters &params) {
            // Add a GPIO wake on button press
            sleepConfig.gpio(BUTTON_PIN, CHANGE);   // My debounce time constant prevents detecting FALLING
            delay(2000);                            // This is a debugging line - to connect to USB serial for logging
            Log.info("Woke on button press");
            if (!digitalRead(BUTTON_PIN)) {         // The BUTTON is active low - this is a button press
                sysStatus.enableSleep = false;      // Pressing the button diables sleep - at least that is the intent
                Log.info("Button press - sleep enable is %s", (sysStatus.enableSleep) ? "true" : "false");
            }
            return true;
        })
        .withSleepReadyFunction([](SleepHelper::AppCallbackState &, system_tick_t) {
            if (sysStatus.enableSleep) return false;// Boolean set by Particle.function - If sleep is enabled return false
            else return true;                       // If we need to delay sleep, return true
        })

If I press the user button repeatedly, I can get the device to disable sleep but not reliably. As for the 90 second delay. I could create a delay function but, how would I trigger it only when the device makes a Particle connection?

Thank you all for your help,

Chip

1 Like

Update,

Was able (with help from @Ubidots ) to implement a better Webhook that would not need to be changed as the number of samples per reporting period changes.

This is based on @rickkas7's intermediate webhook tutorial. After experimenting with the different approaches, it turns out that with this form, you can have as few or as many samples per reporting period and use the standard Ubidots bulk API (link above) endpoint. The structure looks like this:

{
	"array":[
		{{#a}}
		{
			"banana":{{b}},
			"capybara":{{c}}
		},
		{{/a}}
		{}
	]
}

As Rick points out in his tutorial, the extra {} after the datapoints makes for a compliant JSON payload. Ubidots has no issues ingesting this webhook:

I will provide a specific example of this in the thread on the garden watering project.

With this piece, You can use the sleepHelper library today as we wait to see what the back-end process will be.

Thanks, Chip

3 Likes

@rickkas7 ,

Couple quick questions / observations:

  1. As expected, the device will periodically send a device update. For some reason however, the values in the Particle console are not being updated. Not sure if this is related to sleepHelper or not.

Note the battery level is listed as 9% - as it has for the past two weeks. But the battery data in the diagnostics update shows the batter is almost full:

Screen Shot 2022-07-19 at 2.17.15 PM

Interestingly, other items in the diagnostics update such as the cellular signal strength are being updated, it is only the battery. Also, I have the same issue on a 2nd device running the sleepHelper code.

Thanks,

Chip

My apologies if this is obvious/dumb question but have you refreshed the browser window or tried opening the same page within a different browser window or tab?

What I've realized is the "Last Vitals" widget within the Particle Console does not get updated with each new device vitals being reported from the device. That widget within the Particle Console seems to only refresh itself on first page load (must query the Particle database on the last vitals that was reported by the device) OR when clicking the little refresh button (asks the device to send new device vitals and then refreshes that widget). Given this, the refresh button just asks the device to publish new device vitals so if it's actually offline/sleeping, the device vitals won't update with this button either.
image

When I'm running ongoing tests like you are, I have a tendency to keep the console open all the time as well in order to capture every publish/handshake with the cloud and likewise, many times would have to reload the page to see the latest vitals. The disadvantage is it clears out all events listed in the event stream log area when you refresh the page. Maybe a minor enhancement could be done to the Particle Console web app to correct this?

1 Like

@jgskarda ,

I am a bit embarrassed to admit it but, you are exactly correct about what is happening here and why I have not refreshed the window during testing. I resist refreshing the browser as I loose the history in the console. That said, I would also like the “last vitals” to live up to its name and I cannot easily catch these sleepy devices while on-line.

@Colleen , any chance the “last vitals” could update in the same way the “events” section does? At a minimum, could there be a tool tip for folks like me that are oblivious to the obvious?

Thanks,

Chip

Keeping the same webpage open for 2 weeks is impressive! :slight_smile: but yeah. I'm afraid to admit it but I had the same question/issue when monitoring the console until this behavior became apparent.

Would be nice to have that widget/component of the console would update automatically with any new content of /spark/device/diagnostics/update event name. Alternatively, if I click the "refresh" button, IF the device does not return a device vitals event... can it just go look up the most recent entry in the Particle DB that was captured?

2 Likes