Sensirion / arduino-ble-gadget

Create your own Do-It-Yourself BLE enabled sensor gadget on the ESP32 platform.
BSD 3-Clause "New" or "Revised" License
63 stars 20 forks source link

Data download and device sleep mode #29

Closed NetForces closed 7 months ago

NetForces commented 1 year ago

Hi,

For a sensor that is working on battery and goes to sleep every 30s, waking up only to get a SCD-30 reading, how would you suggest that offline data download be handled ?

NetForces commented 1 year ago

Here is a slightly modified Example7_SCD30_BLE_Gadget_with_RHT code:

// Download the SeeedStudio SCD30 Arduino driver here:
//  => https://github.com/Seeed-Studio/Seeed_SCD30/releases/latest

#include "Sensirion_Gadget_BLE.h"
#include <SCD30.h>

static int64_t lastMeasurementTimeMs = 0;
static int measurementIntervalMs = 1900;

NimBLELibraryWrapper lib;
DataProvider provider(lib, DataType::T_RH_CO2);

void setup() {
  Serial.begin(115200);
  delay(100);

  // Initialize the GadgetBle Library
  provider.begin();
  Serial.print("Sensirion GadgetBle Lib initialized with deviceId = ");
  Serial.println(provider.getDeviceIdString());

  // Initialize the SCD30 driver
  Wire.begin(25,26);
  scd30.initialize();
  scd30.setAutoSelfCalibration(1);
  scd30.setTemperatureOffset(3);
}

void loop() {
  float result[3] = {0};

  if (scd30.isAvailable()) {
    scd30.getCarbonDioxideConcentration(result);

    provider.writeValueToCurrentSample(result[0], SignalType::CO2_PARTS_PER_MILLION);
    provider.writeValueToCurrentSample(result[1], SignalType::TEMPERATURE_DEGREES_CELSIUS);
    provider.writeValueToCurrentSample(result[2], SignalType::RELATIVE_HUMIDITY_PERCENTAGE);

    provider.commitSample();
    lastMeasurementTimeMs = millis();

    // Provide the sensor values for Tools -> Serial Monitor or Serial Plotter
    Serial.print("CO2[ppm]:");
    Serial.print(result[0]);
    Serial.print("\t");
    Serial.print("Temperature[℃]:");
    Serial.print(result[1]);
    Serial.print("\t");
    Serial.print("Humidity[%]:");
    Serial.println(result[2]);
  }

  provider.handleDownload();
  delay(3);

  // Light sleep for 30s
  esp_sleep_enable_timer_wakeup((30)*1000000);
  esp_sleep_enable_gpio_wakeup();
  esp_light_sleep_start();
}

With this code, I do get the updates every 30s on the MyAmbiance app. But if I turn off the app for let's say 15m and they start it again and try to fetch the saved data on the app it never connects.

If I remove the sleep 30and put back the if (millis() - lastMeasurementTimeMs >= measurementIntervalMs) throttling it works as expected.

Any help or hints welcome.

stlljonas commented 1 year ago

Hi,

so to get this to work you need a bit of background info:

  1. Once a download is started, a small bit if data is sent in every loop when provider.handleDownload() is called. With the short delay of 3 milliseconds, the loop runs very rapidly and the download completes within seconds. However if you put the device to sleep for 30 seconds every loop, in the best case the download will take forever, but most likely it will break.
  2. Bluetooth isn't necessarily the fastest, so if the gadget is only awake for a few milliseconds before going to sleep for a long time, there is not enough time for a connection to be established or for the download to even start. This is likely what is causing issues for you.
  3. As noted above, the loop runs very rapidly to allow for a quick download. The if (millis() - lastMeasurementTimeMs >= measurementIntervalMs) part is mainly there to not do a reading every few milliseconds, filling up the memory too quickly, but at a more useful rate, namely every measurementIntervalMs. You shouldn't remove this line.

Now to actually do this without breaking the download, you need to put the device to sleep only if there is no download running and keep the device awake for at least 5 seconds for a connection to be established in a non blocking way (i.e. the loop can run s.t. provider.handleDownload() is called often). Concretely I would suggest you amend your code like this:

...
static int awakeTimeMs = 5000;
static int64_t lastWakeUpTimeMs = 0;

void setup() {
...
}

void loop() {
  float result[3] = {0};

  if (millis() - lastMeasurementTimeMs >= measurementIntervalMs) {
    ...
  }

  delay(3);
  provider.handleDownload();

  if ((provider.isDownloading() == false) and (millis() - lastWakeUpTimeMs >= awakeTimeMs)) {
    // Light sleep for 30s
    esp_sleep_enable_timer_wakeup((30)*1000000);
    esp_sleep_enable_gpio_wakeup();
    esp_light_sleep_start();
    lastWakeUpTimeMs = millis();
  }

Note how your gadget will only go to sleep once there is no download going on and it has been awake for at least awakeTimeMs after the last sleep. This is just a workaround and hasn't been tested in depth, so it might be unstable.

IMPORTANT: the provider.isDownloading() method is on the master branch as of today, but not yet part of the official release (which you would get by installing via the Arduino IDE Library manager). You will need to manually download the library as a .zip file from GitHub and add it via Sketch -> Include Library -> Add .ZIP Library..., after having uninstalled your current installation of the library.

NetForces commented 1 year ago

Awesome. Will give it a try and report back on the status.

NetForces commented 1 year ago

Status update.

I still have some tests to do, but it seems to be working. I changed a bit the algorithm. I do multiple small 5s sleeps instead of a big 30s one, so the sensor is more responsive with the app.

NetForces commented 1 year ago

Actually not quite. It never seems to start the download. The app shows that it connects, download indicator shows 0% and stay stuck there.

I added some debug log in the library and it never seems to go in the onDownloadRequest method... I'll do more tests today, reduce the code to a minimum and provide you what I tried along with logs.

NetForces commented 1 year ago

Ok so I added some debug logs in the DataProvider class and hardcoded the _historyIntervalMilliSeconds to 30s to match my test frequency. I used the following code for the sensor:

// Download the SeeedStudio SCD30 Arduino driver here:
//  => https://github.com/Seeed-Studio/Seeed_SCD30/releases/latest

#include "Sensirion_Gadget_BLE.h"
#include <SCD30.h>

static int64_t lastMeasurementTimeMs = 0;
static int measurementIntervalMs = 30000;

NimBLELibraryWrapper lib;
DataProvider provider(lib, DataType::T_RH_CO2);

static int awakeTimeMs = 5000;
static int64_t lastWakeUpTimeMs = 0;

void setup() {
  Serial.begin(115200);
  delay(100);

  // Initialize the GadgetBle Library
  provider.begin();
  Serial.print("Sensirion GadgetBle Lib initialized with deviceId = ");
  Serial.println(provider.getDeviceIdString());

  // Initialize the SCD30 driver
  Wire.begin(22, 21);
  scd30.initialize();
  scd30.setAutoSelfCalibration(0);
  scd30.setTemperatureOffset(3);
}

void loop() {
  float result[3] = {0};

  if (millis() - lastMeasurementTimeMs >= measurementIntervalMs) {

    if (scd30.isAvailable()) {
      scd30.getCarbonDioxideConcentration(result);

      provider.writeValueToCurrentSample(result[0], SignalType::CO2_PARTS_PER_MILLION);
      provider.writeValueToCurrentSample(result[1], SignalType::TEMPERATURE_DEGREES_CELSIUS);
      provider.writeValueToCurrentSample(result[2], SignalType::RELATIVE_HUMIDITY_PERCENTAGE);
      Serial.printf("%d : ---> sending a sample\n",millis()); Serial.flush();

      provider.commitSample();
      lastMeasurementTimeMs = millis();
    }
  }

  provider.handleDownload();
  delay(3);

  if ((provider.isDownloading() == false) and (millis() - lastWakeUpTimeMs >= awakeTimeMs)) {
    Serial.printf("%d : ---> going to sleep 30*1s\n",millis()); Serial.flush();
    for(int i=0;i<30;i++) {
      Serial.print("."); Serial.flush();
      esp_sleep_enable_timer_wakeup((1)*1000000);
      esp_sleep_enable_gpio_wakeup();
      esp_light_sleep_start();
      lastWakeUpTimeMs = millis();
      provider.handleDownload();
      delay(3);
    }
    Serial.println(""); Serial.flush();
  }
}

And this is the log I see:

136 : ---> In DataProvider::begin
Sensirion GadgetBle Lib initialized with deviceId = ec:da
5001 : ---> going to sleep 30*1s
..............................
35116 : ---> In DataProvider::writeValueToCurrentSample)
35121 : ---> In DataProvider::writeValueToCurrentSample)
35126 : ---> In DataProvider::writeValueToCurrentSample)
35131 : ---> sending a sample
35134 : ---> In DataProvider::commitSample going to putSample)
35139 : ---> In DataProvider::commitSample _downloadState == INACTIVE)
40111 : ---> going to sleep 30*1s
..............................
70225 : ---> In DataProvider::writeValueToCurrentSample)
70230 : ---> In DataProvider::writeValueToCurrentSample)
70235 : ---> In DataProvider::writeValueToCurrentSample)
70240 : ---> sending a sample
70242 : ---> In DataProvider::commitSample going to putSample)
70248 : ---> In DataProvider::commitSample _downloadState == INACTIVE)
75220 : ---> going to sleep 30*1s
..............................
105335 : ---> In DataProvider::writeValueToCurrentSample)
105340 : ---> In DataProvider::writeValueToCurrentSample)
105345 : ---> In DataProvider::writeValueToCurrentSample)
105350 : ---> sending a sample
105353 : ---> In DataProvider::commitSample going to putSample)
105358 : ---> In DataProvider::commitSample _downloadState == INACTIVE)
110330 : ---> going to sleep 30*1s
..............................
140446 : ---> In DataProvider::writeValueToCurrentSample)
140451 : ---> In DataProvider::writeValueToCurrentSample)
140456 : ---> In DataProvider::writeValueToCurrentSample)
140461 : ---> sending a sample
140464 : ---> In DataProvider::commitSample going to putSample)
140469 : ---> In DataProvider::commitSample _downloadState == INACTIVE)
145438 : ---> going to sleep 30*1s
..............................
175551 : ---> In DataProvider::onConnectionEvent
175560 : ---> In DataProvider::writeValueToCurrentSample)
175565 : ---> In DataProvider::writeValueToCurrentSample)
175571 : ---> In DataProvider::writeValueToCurrentSample)
175576 : ---> sending a sample
175578 : ---> In DataProvider::commitSample going to putSample)
175584 : ---> In DataProvider::commitSample _downloadState == INACTIVE)
180547 : ---> going to sleep 30*1s
..............................
210662 : ---> In DataProvider::writeValueToCurrentSample)
210667 : ---> In DataProvider::writeValueToCurrentSample)
210672 : ---> In DataProvider::writeValueToCurrentSample)
210677 : ---> sending a sample
210680 : ---> In DataProvider::commitSample going to putSample)
210686 : ---> In DataProvider::commitSample _downloadState == INACTIVE)
215556 : ---> In DataProvider::onConnectionEvent
215655 : ---> going to sleep 30*1s
......................

On the app when I go to download data I see the trace where it goes in onConnectionEvent but I never see it going into onDownloadRequest

I have also tried to remove the entire if ((provider.isDownloading() == false) and (millis() - lastWakeUpTimeMs >= awakeTimeMs)) section, and it's the same. I get the onConnectionEvent, the app shows "Download state: 0%" and stays there. No data is ever downloaded.

@stlljonas any ideas ?

NetForces commented 1 year ago

FYI I reverted to v0.14.0 and adjusted my code and data download seem to be working correctly. So it's probably a regression when the refactor was performed on the library.

qfisch commented 7 months ago

I will close this issue as it has been dormant. Please reopen it if you need assistance.

On this topic, unfortunately MyAmbience expects the device to be responsive when trying to download. It might be worth looking into sleeping only when no BT connection is ongoing. This way MyAmbience will either be able to connect to the gadget (and download) or it will not start the download. Anyway it is great to hear that you found a workaround

Q