SensorsIot / Oximeters-BLE-Hack-ESP32

33 stars 23 forks source link

Fix for the Berry Pulse Oximeter #2

Closed tobiasisenberg closed 4 years ago

tobiasisenberg commented 4 years ago

This is a fix, not an issue. The problems that cause the Berry Pulse Oximeter not to work with the original sketch is that (1) the test for (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(serviceUUID)) does not return true and (2) that the notifications do not start automatically. (1) can be solved by alternatively testing for the client's address (if ((advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(serviceUUID)) || (advertisedDevice.getAddress().equals(berryMed))) {). (2) can be solved by explicitly setting notifications to on with a few magic lines. Below is a sketch that works with my BerryMed BM1000C (potentially the UUIDs need to be adjusted for others). From the notified data, the sketch also interprets two versions of the measured pulse oximeter plethysmographic trace (PPG), one detailed one and one coarse one (divided by 7 and rounded). Of course, the SPO2 and BPM values are received as well. The sketch also reads another characteristics (only once) that does not notify but can be read, I am not yet sure how to interpret these numbers.

/*
 * A BLE client example that is rich in capabilities.
 * There is a lot new capabilities implemented.
 * author unknown
 * updated by chegewara
 */

#include "BLEDevice.h"
//#include "BLEScan.h"

// The remote service we wish to connect to.
static BLEUUID serviceUUID("49535343-fe7d-4ae5-8fa9-9fafd205e455");
// The characteristic of the remote service we are interested in.
static BLEUUID    charOximeterUUID("49535343-1e4d-4bd9-ba61-23c647249616");
static BLEUUID    charDeviceDataUUID("00005344-0000-1000-8000-00805f9b34fb");
// The address of the target device (needed for connection when the device does not properly advertise services)
static BLEAddress berryMed("00:a0:50:db:83:94");

static boolean doConnect = false;
static boolean connected = false;
static boolean doScan = false;
static BLERemoteCharacteristic* pRemoteCharacteristicOximeter;
static BLERemoteCharacteristic* pRemoteCharacteristicDeviceData;
static BLEAdvertisedDevice* myDevice;
static unsigned int connectionTimeMs = 0;

static void notifyCallback(
  BLERemoteCharacteristic* pBLERemoteCharacteristic,
  uint8_t* pData,
  size_t length,
  bool isNotify) {
//    Serial.print("Notify callback for characteristic ");
//    Serial.print(pBLERemoteCharacteristic->getUUID().toString().c_str());
//    Serial.print(" of data length ");
//    Serial.println(length);
//    Serial.print("data (HEX): ");
//    for (int i = 0; i < length; i++) {
//      Serial.print(pData[i],HEX);
//      Serial.print(" ");
//    }
//    Serial.println();

    // readable values 
    char output[45];
    for (int i = 0; i < length / 5; i++) {
      uint8_t value1 = pData[i*5 + 1]; // this seems to be the absoption value (pulse oximeter plethysmographic trace, PPG)
      uint8_t value2 = pData[i*5 + 2]; // this seems to be the PPG value, devided by 7 and rounded to an integer
      uint8_t bpm = pData[i*5 + 3];
      uint8_t spo2 = pData[i*5 + 4];
      sprintf(output, "PPG: %3u; PPG/7: %3u; BPM: %3u; SPO2: %2u", value1, value2, bpm, spo2);
      Serial.println(output);
    }

//    // output for use in CSV-based analysis (Excel etc.)
//    for (int i = 0; i < length / 5; i++) {
//      uint8_t value1 = pData[i*5 + 1];
//      uint8_t value2 = pData[i*5 + 2];
//      uint8_t bpm = pData[i*5 + 3];
//      uint8_t spo2 = pData[i*5 + 4];
//      Serial.print(value1,DEC);
//      Serial.print(",");
//      Serial.print(value2,DEC);
//      Serial.print(",");
//      Serial.print(bpm,DEC);
//      Serial.print(",");
//      Serial.print(spo2,DEC);
//      Serial.println("");
//    }
}

class MyClientCallback : public BLEClientCallbacks {
  void onConnect(BLEClient* pclient) {
  }

  void onDisconnect(BLEClient* pclient) {
    connected = false;
    Serial.println("onDisconnect");
  }
};

bool connectToServer() {
    Serial.print("Forming a connection to ");
    Serial.println(myDevice->getAddress().toString().c_str());

    BLEClient*  pClient  = BLEDevice::createClient();
    Serial.println(" - Created client");

    pClient->setClientCallbacks(new MyClientCallback());

    // Connect to the remove BLE Server.
    pClient->connect(myDevice);  // if you pass BLEAdvertisedDevice instead of address, it will be recognized type of peer device address (public or private)
    Serial.println(" - Connected to server");

    // Obtain a reference to the service we are after in the remote BLE server.
    BLERemoteService* pRemoteService = pClient->getService(serviceUUID);
    if (pRemoteService == nullptr) {
      Serial.print("Failed to find our service UUID: ");
      Serial.println(serviceUUID.toString().c_str());
      pClient->disconnect();
      return false;
    }
    Serial.println(" - Found our service");

    // Obtain a reference to the characteristic in the service of the remote BLE server.
    pRemoteCharacteristicDeviceData = pRemoteService->getCharacteristic(charDeviceDataUUID);
    if (pRemoteCharacteristicDeviceData == nullptr) {
      Serial.print("Failed to find our characteristic UUID: ");
      Serial.println(charDeviceDataUUID.toString().c_str());
      pClient->disconnect();
      return false;
    }
    Serial.print(" - Found our characteristic ");
    Serial.println(charDeviceDataUUID.toString().c_str());

    // Read the value of the characteristic.
    if(pRemoteCharacteristicDeviceData->canRead()) {
      Serial.println(" - Our characteristic can be read.");
      std::string value = pRemoteCharacteristicDeviceData->readValue();
      byte buf[64]= {0};
      memcpy(buf,value.c_str(),value.length());
      Serial.print("The characteristic value was: (0x) ");
      for (int i = 0; i < value.length(); i++) {
        Serial.print(buf[i],HEX);
        Serial.print(" ");
      }
      Serial.println();
      Serial.println("Past value                  : (0x) 94 83 DB 50 A0 00");
    }
    else {
      Serial.println(" - Our characteristic cannot be read.");
    }

    // Obtain a reference to the characteristic in the service of the remote BLE server.
    pRemoteCharacteristicOximeter = pRemoteService->getCharacteristic(charOximeterUUID);
    if (pRemoteCharacteristicOximeter == nullptr) {
      Serial.print("Failed to find our characteristic UUID: ");
      Serial.println(charOximeterUUID.toString().c_str());
      pClient->disconnect();
      return false;
    }
    Serial.print(" - Found our characteristic ");
    Serial.println(charOximeterUUID.toString().c_str());

    // Read the value of the characteristic.
    if(pRemoteCharacteristicOximeter->canRead()) {
      Serial.println(" - Our characteristic can be read.");
      std::string value = pRemoteCharacteristicDeviceData->readValue();
      byte buf[64]= {0};
      memcpy(buf,value.c_str(),value.length());
      Serial.print("The characteristic value was: ");
      for (int i = 0; i < value.length(); i++) {
        Serial.print(buf[i],HEX);
        Serial.print(" ");
      }
      Serial.println();
    }
    else {
      Serial.println(" - Our characteristic cannot be read.");
    }

    if(pRemoteCharacteristicOximeter->canNotify()) {
      Serial.println(" - Our characteristic can notify us, registering notification callback.");
      pRemoteCharacteristicOximeter->registerForNotify(notifyCallback, true);
    }
    else {
      Serial.println(" - Our characteristic cannot notify us.");
    }

    if (pRemoteCharacteristicOximeter->canIndicate() == true) {
      Serial.println(" - Our characteristic can indicate.");
    } else {
      Serial.println(" - Our characteristic cannot indicate.");
    }

    // needed to start the notifications:
    pRemoteCharacteristicOximeter->readValue();
    const uint8_t notificationOn[] = {0x1, 0x0};
    pRemoteCharacteristicOximeter->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)notificationOn, 2, true);

    connected = true;
    return true;
}
/**
 * Scan for BLE servers and find the first one that advertises the service we are looking for.
 */
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
 /**
   * Called for each advertising BLE server.
   */
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    Serial.print("\nBLE Advertised Device found: ");
    Serial.println(advertisedDevice.toString().c_str());

    Serial.print("Address: ");
    Serial.println(advertisedDevice.getAddress().toString().c_str());
    if (advertisedDevice.haveServiceUUID()) {
      Serial.println("Device has Service UUID");
      if (advertisedDevice.isAdvertisingService(serviceUUID)) {Serial.println("Device is advertising our Service UUID");}
      else {Serial.println("Device is not advertising our Service UUID");}
    }
    else {Serial.println("Device does not have Service UUID");}

    // We have found a device, let us now see if it contains the service we are looking for.
    if ((advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(serviceUUID)) || (advertisedDevice.getAddress().equals(berryMed))) {

      BLEDevice::getScan()->stop();
      myDevice = new BLEAdvertisedDevice(advertisedDevice);
      doConnect = true;
      doScan = true;

    } // Found our server
  } // onResult
}; // MyAdvertisedDeviceCallbacks

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

  connectionTimeMs = millis();
  Serial.println("Starting Arduino BLE Client application...");
  BLEDevice::init("");

  // Retrieve a Scanner and set the callback we want to use to be informed when we
  // have detected a new device.  Specify that we want active scanning and start the
  // scan to run for 5 seconds.
  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setInterval(1349);
  pBLEScan->setWindow(449);
  pBLEScan->setActiveScan(true);
  pBLEScan->start(5, false);
} // End of setup.

// This is the Arduino main loop function.
void loop() {

  // If the flag "doConnect" is true then we have scanned for and found the desired
  // BLE Server with which we wish to connect.  Now we connect to it.  Once we are 
  // connected we set the connected flag to be true.
  if (doConnect == true) {
    if (connectToServer()) {
      Serial.println("We are now connected to the BLE Server.");
    } else {
      Serial.println("We have failed to connect to the server; there is nothin more we will do.");
    }
    doConnect = false;
  }

  // If we are connected to a peer BLE Server, update the characteristic each time we are reached
  // with the current time since boot.
  if (connected) {
    if (pRemoteCharacteristicOximeter->canWrite()) {
      // Set the characteristic's value to be the array of bytes that is actually a string.
      String newValue = "Time since boot: " + String(millis()/1000);
      Serial.println("Setting new characteristic value to \"" + newValue + "\"");
      pRemoteCharacteristicOximeter->writeValue(newValue.c_str(), newValue.length());
    }
  }
  else {
    if (doScan) {
      BLEDevice::getScan()->start(0);  // this is just an example to start scan after disconnect, most likely there is better way to do it in arduino
    }
    else { // enable connects if no device was found on first boot
      if (millis() > connectionTimeMs + 6000) {
        Serial.println("Enabling scanning.");
        doScan = true;
      }
    }
  }

  delay(1000); // Delay a second between loops.
} // End of loop
akeilox commented 4 years ago

I too am having issue with BerryMed. Mine is BM100 with manufacture date 20200401

I can see from nrf connect the uuids match

IMG_7404

However when I run your sketch I keep getting the following in serial; BLE Advertised Device found: Name: BerryMed Address: 00:a0:50:39:92:23 Device does not have Service UUID

Not sure why it would give false even though UUID matching....

tobiasisenberg commented 4 years ago

For me the same thing happens. Here's the output siplet from my BM1000C:

BLE Advertised Device found: Name: BerryMed
Address: 00:a0:50:db:83:94
Device does not have Service UUID
Forming a connection to 00:a0:50:db:83:94
 - Created client
 - Connected to server
 - Found our service
 - Found our characteristic 00005344-0000-1000-8000-00805f9b34fb
 - Our characteristic can be read.
The characteristic value was: (0x) 94 83 DB 50 A0 0 
Past value                  : (0x) 94 83 DB 50 A0 00
 - Found our characteristic 49535343-1e4d-4bd9-ba61-23c647249616
 - Our characteristic cannot be read.
 - Our characteristic can notify us, registering notification callback.
 - Our characteristic cannot indicate.
We are now connected to the BLE Server.
PPG:  49; PPG/7:   7; BPM:  88; SPO2: 95
PPG:  47; PPG/7:   7; BPM:  88; SPO2: 95
PPG:  45; PPG/7:   6; BPM:  88; SPO2: 95
PPG:  43; PPG/7:   6; BPM:  88; SPO2: 95
PPG:  40; PPG/7:   6; BPM:  88; SPO2: 95
PPG:  38; PPG/7:   5; BPM:  88; SPO2: 95
PPG:  35; PPG/7:   5; BPM:  88; SPO2: 95
...

Does the sketch continue for you (connect and print values)?

PS: I slightly updated the script above to also enable connections if the correct device was not found on bootup.

tobiasisenberg commented 4 years ago

PPS: What I see is different between your output and mine is the device's address. Yours is 00:a0:50:39:92:23, so you need to update the lines

// The address of the target device (needed for connection when the device does not properly advertise services)
static BLEAddress berryMed("00:a0:50:db:83:94");

in my example to

// The address of the target device (needed for connection when the device does not properly advertise services)
static BLEAddress berryMed("00:a0:50:39:92:23");
akeilox commented 4 years ago

@tobiasisenberg you are right, i thought i double checked the uids and mac.

Your final sketch works very well, thank you! Hope @SensorsIot can update the sketch for others benefit too. I ordered Jumper but received Berrymed so it appears to be more widely available (not an ISO device).

I was thinking of running the sketch on a WEMOS LOLIN 32 with the OLED (128x64) for remote display of the SPO2 and HR and also perhaps upload to Blynk for mobile phone monitoring. Have you experienced on the battery depletion time of Berrymed with rechargable AAA ? Would it survive a sleep study of 8 hours or so, cant seem to off the screen on it. I would be interested to hear your thoughts and experiences with these.

SensorsIot commented 4 years ago

This is a fix, not an issue. The problems that cause the Berry Pulse Oximeter not to work with the original sketch is that (1) the test for (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(serviceUUID)) does not return true and (2) that the notifications do not start automatically. (1) can be solved by alternatively testing for the client's address (if ((advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(serviceUUID)) || (advertisedDevice.getAddress().equals(berryMed))) {). (2) can be solved by explicitly setting notifications to on with a few magic lines. Below is a sketch that works with my BerryMed BM1000C (potentially the UUIDs need to be adjusted for others). From the notified data, the sketch also interprets two versions of the measured pulse oximeter plethysmographic trace (PPG), one detailed one and one coarse one (divided by 7 and rounded). Of course, the SPO2 and BPM values are received as well. The sketch also reads another characteristics (only once) that does not notify but can be read, I am not yet sure how to interpret these numbers.

/*
 * A BLE client example that is rich in capabilities.
 * There is a lot new capabilities implemented.
 * author unknown
 * updated by chegewara
 */

#include "BLEDevice.h"
//#include "BLEScan.h"

// The remote service we wish to connect to.
static BLEUUID serviceUUID("49535343-fe7d-4ae5-8fa9-9fafd205e455");
// The characteristic of the remote service we are interested in.
static BLEUUID    charOximeterUUID("49535343-1e4d-4bd9-ba61-23c647249616");
static BLEUUID    charDeviceDataUUID("00005344-0000-1000-8000-00805f9b34fb");
// The address of the target device (needed for connection when the device does not properly advertise services)
static BLEAddress berryMed("00:a0:50:db:83:94");

static boolean doConnect = false;
static boolean connected = false;
static boolean doScan = false;
static BLERemoteCharacteristic* pRemoteCharacteristicOximeter;
static BLERemoteCharacteristic* pRemoteCharacteristicDeviceData;
static BLEAdvertisedDevice* myDevice;
static unsigned int connectionTimeMs = 0;

static void notifyCallback(
  BLERemoteCharacteristic* pBLERemoteCharacteristic,
  uint8_t* pData,
  size_t length,
  bool isNotify) {
//    Serial.print("Notify callback for characteristic ");
//    Serial.print(pBLERemoteCharacteristic->getUUID().toString().c_str());
//    Serial.print(" of data length ");
//    Serial.println(length);
//    Serial.print("data (HEX): ");
//    for (int i = 0; i < length; i++) {
//      Serial.print(pData[i],HEX);
//      Serial.print(" ");
//    }
//    Serial.println();

    // readable values 
    char output[45];
    for (int i = 0; i < length / 5; i++) {
      uint8_t value1 = pData[i*5 + 1]; // this seems to be the absoption value (pulse oximeter plethysmographic trace, PPG)
      uint8_t value2 = pData[i*5 + 2]; // this seems to be the PPG value, devided by 7 and rounded to an integer
      uint8_t bpm = pData[i*5 + 3];
      uint8_t spo2 = pData[i*5 + 4];
      sprintf(output, "PPG: %3u; PPG/7: %3u; BPM: %3u; SPO2: %2u", value1, value2, bpm, spo2);
      Serial.println(output);
    }

//    // output for use in CSV-based analysis (Excel etc.)
//    for (int i = 0; i < length / 5; i++) {
//      uint8_t value1 = pData[i*5 + 1];
//      uint8_t value2 = pData[i*5 + 2];
//      uint8_t bpm = pData[i*5 + 3];
//      uint8_t spo2 = pData[i*5 + 4];
//      Serial.print(value1,DEC);
//      Serial.print(",");
//      Serial.print(value2,DEC);
//      Serial.print(",");
//      Serial.print(bpm,DEC);
//      Serial.print(",");
//      Serial.print(spo2,DEC);
//      Serial.println("");
//    }
}

class MyClientCallback : public BLEClientCallbacks {
  void onConnect(BLEClient* pclient) {
  }

  void onDisconnect(BLEClient* pclient) {
    connected = false;
    Serial.println("onDisconnect");
  }
};

bool connectToServer() {
    Serial.print("Forming a connection to ");
    Serial.println(myDevice->getAddress().toString().c_str());

    BLEClient*  pClient  = BLEDevice::createClient();
    Serial.println(" - Created client");

    pClient->setClientCallbacks(new MyClientCallback());

    // Connect to the remove BLE Server.
    pClient->connect(myDevice);  // if you pass BLEAdvertisedDevice instead of address, it will be recognized type of peer device address (public or private)
    Serial.println(" - Connected to server");

    // Obtain a reference to the service we are after in the remote BLE server.
    BLERemoteService* pRemoteService = pClient->getService(serviceUUID);
    if (pRemoteService == nullptr) {
      Serial.print("Failed to find our service UUID: ");
      Serial.println(serviceUUID.toString().c_str());
      pClient->disconnect();
      return false;
    }
    Serial.println(" - Found our service");

    // Obtain a reference to the characteristic in the service of the remote BLE server.
    pRemoteCharacteristicDeviceData = pRemoteService->getCharacteristic(charDeviceDataUUID);
    if (pRemoteCharacteristicDeviceData == nullptr) {
      Serial.print("Failed to find our characteristic UUID: ");
      Serial.println(charDeviceDataUUID.toString().c_str());
      pClient->disconnect();
      return false;
    }
    Serial.print(" - Found our characteristic ");
    Serial.println(charDeviceDataUUID.toString().c_str());

    // Read the value of the characteristic.
    if(pRemoteCharacteristicDeviceData->canRead()) {
      Serial.println(" - Our characteristic can be read.");
      std::string value = pRemoteCharacteristicDeviceData->readValue();
      byte buf[64]= {0};
      memcpy(buf,value.c_str(),value.length());
      Serial.print("The characteristic value was: (0x) ");
      for (int i = 0; i < value.length(); i++) {
        Serial.print(buf[i],HEX);
        Serial.print(" ");
      }
      Serial.println();
      Serial.println("Past value                  : (0x) 94 83 DB 50 A0 00");
    }
    else {
      Serial.println(" - Our characteristic cannot be read.");
    }

    // Obtain a reference to the characteristic in the service of the remote BLE server.
    pRemoteCharacteristicOximeter = pRemoteService->getCharacteristic(charOximeterUUID);
    if (pRemoteCharacteristicOximeter == nullptr) {
      Serial.print("Failed to find our characteristic UUID: ");
      Serial.println(charOximeterUUID.toString().c_str());
      pClient->disconnect();
      return false;
    }
    Serial.print(" - Found our characteristic ");
    Serial.println(charOximeterUUID.toString().c_str());

    // Read the value of the characteristic.
    if(pRemoteCharacteristicOximeter->canRead()) {
      Serial.println(" - Our characteristic can be read.");
      std::string value = pRemoteCharacteristicDeviceData->readValue();
      byte buf[64]= {0};
      memcpy(buf,value.c_str(),value.length());
      Serial.print("The characteristic value was: ");
      for (int i = 0; i < value.length(); i++) {
        Serial.print(buf[i],HEX);
        Serial.print(" ");
      }
      Serial.println();
    }
    else {
      Serial.println(" - Our characteristic cannot be read.");
    }

    if(pRemoteCharacteristicOximeter->canNotify()) {
      Serial.println(" - Our characteristic can notify us, registering notification callback.");
      pRemoteCharacteristicOximeter->registerForNotify(notifyCallback, true);
    }
    else {
      Serial.println(" - Our characteristic cannot notify us.");
    }

    if (pRemoteCharacteristicOximeter->canIndicate() == true) {
      Serial.println(" - Our characteristic can indicate.");
    } else {
      Serial.println(" - Our characteristic cannot indicate.");
    }

    // needed to start the notifications:
    pRemoteCharacteristicOximeter->readValue();
    const uint8_t notificationOn[] = {0x1, 0x0};
    pRemoteCharacteristicOximeter->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)notificationOn, 2, true);

    connected = true;
    return true;
}
/**
 * Scan for BLE servers and find the first one that advertises the service we are looking for.
 */
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
 /**
   * Called for each advertising BLE server.
   */
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    Serial.print("\nBLE Advertised Device found: ");
    Serial.println(advertisedDevice.toString().c_str());

    Serial.print("Address: ");
    Serial.println(advertisedDevice.getAddress().toString().c_str());
    if (advertisedDevice.haveServiceUUID()) {
      Serial.println("Device has Service UUID");
      if (advertisedDevice.isAdvertisingService(serviceUUID)) {Serial.println("Device is advertising our Service UUID");}
      else {Serial.println("Device is not advertising our Service UUID");}
    }
    else {Serial.println("Device does not have Service UUID");}

    // We have found a device, let us now see if it contains the service we are looking for.
    if ((advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(serviceUUID)) || (advertisedDevice.getAddress().equals(berryMed))) {

      BLEDevice::getScan()->stop();
      myDevice = new BLEAdvertisedDevice(advertisedDevice);
      doConnect = true;
      doScan = true;

    } // Found our server
  } // onResult
}; // MyAdvertisedDeviceCallbacks

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

  connectionTimeMs = millis();
  Serial.println("Starting Arduino BLE Client application...");
  BLEDevice::init("");

  // Retrieve a Scanner and set the callback we want to use to be informed when we
  // have detected a new device.  Specify that we want active scanning and start the
  // scan to run for 5 seconds.
  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setInterval(1349);
  pBLEScan->setWindow(449);
  pBLEScan->setActiveScan(true);
  pBLEScan->start(5, false);
} // End of setup.

// This is the Arduino main loop function.
void loop() {

  // If the flag "doConnect" is true then we have scanned for and found the desired
  // BLE Server with which we wish to connect.  Now we connect to it.  Once we are 
  // connected we set the connected flag to be true.
  if (doConnect == true) {
    if (connectToServer()) {
      Serial.println("We are now connected to the BLE Server.");
    } else {
      Serial.println("We have failed to connect to the server; there is nothin more we will do.");
    }
    doConnect = false;
  }

  // If we are connected to a peer BLE Server, update the characteristic each time we are reached
  // with the current time since boot.
  if (connected) {
    if (pRemoteCharacteristicOximeter->canWrite()) {
      // Set the characteristic's value to be the array of bytes that is actually a string.
      String newValue = "Time since boot: " + String(millis()/1000);
      Serial.println("Setting new characteristic value to \"" + newValue + "\"");
      pRemoteCharacteristicOximeter->writeValue(newValue.c_str(), newValue.length());
    }
  }
  else {
    if (doScan) {
      BLEDevice::getScan()->start(0);  // this is just an example to start scan after disconnect, most likely there is better way to do it in arduino
    }
    else { // enable connects if no device was found on first boot
      if (millis() > connectionTimeMs + 6000) {
        Serial.println("Enabling scanning.");
        doScan = true;
      }
    }
  }

  delay(1000); // Delay a second between loops.
} // End of loop

Can you send me a pull request with your changes?

tobiasisenberg commented 4 years ago

No experiences yet. Only that, in contrast to what I heard somewhere (maybe in one of Andreas' videos, but I am not sure), the BerryMed does work with rechargeable batteries. It shows the battery level to be low (only one or two bars maximum), but it works just fine. But I have not used it for hours at a time yet.

Also not sure yet on longer-term recording in general. I am still contemplating the data capture and recording via MQTT. The BPM and SPO2 values should not be a problem, one could send an average every second or so. But the PPG values (i.e., a representation of a heartbeat curve) come very fast. There are approx. 25 new value packs every second, with 4 sets of values in each pack. So we would have 3 bytes for PPG, SPO2, and BPM x 4 sets + 4 bytes for a ms timestamp x 25 (packets per second) = 400 bytes per second. Then for an 8h recording we would send approx. 11MiB overall. I was thinking of sending them once every 1--10 seconds, but I am not yet sure on how robust this MQTT transfer would be, how long sending a message takes (and if this impacts the continued BLE recording on the ESP32), etc. If one would send an MQTT message every 10s, for example, the message would have a 4000 bytes payload, and hopefully this can be sent of with a large enough security margin before the next message needs to be sent. And then a server has to save all these messages to a file, again without loosing any packets. But if this works then one could do the value interpretation (singling out the packets, time stamp computation for the values in a pack etc.) in an offline batch process, and then prepare a nice analysis with graphs for the night's recording.

tobiasisenberg commented 4 years ago

@SensorsIot Created the PR, I hope I did not break the display code. Note that you may still have to adjust the UUIDs and the address, these are mine right now.

SensorsIot commented 4 years ago

@SensorsIot Created the PR, I hope I did not break the display code. Note that you may still have to adjust the UUIDs and the address, these are mine right now.

Thank you, Tobias. Merged.

akeilox commented 4 years ago

Thank you for the input @tobiasisenberg . The battery indeed shows two bars but it keeps going for few hours it seems. I too noticed PPG coming too fast when experimenting with Blynk view.

If you made any progress with the ideal timing kindly drop a note here for the benefit of others.

tobiasisenberg commented 4 years ago

In the meantime I managed to post the values to MQTT using PubSubClient and Mosquitto. It seems that 15s worth of data (6000 bytes plus overhead) works, 20s worth of data (8000 bytes plus overhead) does not. Maybe the limit is 8KiB, so 8192 bytes, but I am not sure. The sending of a 6000 byte message only takes about 3ms, so well within the safety margin. I still use two buffers and switch between them when I store the data on the ESP32, and that seems to work fine. I just don't know yet if values are lost, I will only see that once I analyze the recorded data. Also, initially I planned to just subscribe to the data topic via my OpenHAB system to then write it to a file, but I forgot that each value change is logged by OpenHAB and that would flood my logs. So now I need to see that I can start an external process for the data recording and the transfer to a file, named based on when the sensor was started. I also have the feeling that the whole thing is not stable enough yet, need to do some more debugging. So maybe one could even send larger packets, not sure. But even 4 messages a minute should be fine and not overload the server.

About your battery tests, how long did the batteries last, could you tell?

SensorsIot commented 4 years ago

Maybe you average the values before you send them to MQTT. The underlying values (SPO2 and Hart rate) should not change so often.

tobiasisenberg commented 4 years ago

Yes, you are absolutely right that the SPO2 and BPM values do not change that dramatically, sending even only selected (non-averaged) values once every 15 or even 60 seconds would probably be fine. What I am shooting for here, however, is recording also the PPG values (the pulse oximeter plethysmographic trace; see for example here) because they can also indicate issues such as irregular heart rhythm (see the same link). My vision is to record the whole trace for a night, and then use a Python script to prepare a set of graphs as PDFs to look at, in a way similar to the elaborate tool you showed in your video (or make the data available as a CSV file). And now that it turned out that the actual data transfer via MQTT is not a fundamental roadblock I am mostly looking for making the ESP32/MQTT part stable and find an easy way to record and process the data on the server. I will likely use a simple Python script to do the data recording from MQTT, and run this as a background process while data continues to come. And then use a separate Python script to create the graphs. One issue may be that I will likely use a control of the data recording process via my OpenHAB installation (that also gets the SPO2 and BPM data) which is very specific to my own setup, but that can be solved later once the process works reliable. Anyway, I will need a few more days to get this going, I'll report back here.

SensorsIot commented 4 years ago

Ok. Then it is probably better to bundle a few values as you suggested to get the MQTT data rate down. I assume you have a huge overhead if you send one byte per message...

akeilox commented 4 years ago

@tobiasisenberg I got around 3.5 hours with rechargeable NiCd batteries. With lithium AAA it should complete a full sleep study like you envisioned with no problem I believe.

The irregular heart beat detection in fact very interesting, please do keep us posted if you implement your vision.

tobiasisenberg commented 4 years ago

@akeilox Thanks for the information, but are you sure about the battery types? Typical rechargeable 1.2V batteries these days are NiMH (not NiCd anymore), on average they also have a higher capacity than NiCd. And lithium-based rechargeable batteries should not work at all, LiFePO4 would have 3.2 V each and lithium-ion batteries would even have 3.6V or 3.7V each, likely too much for the device when 2 are used. Or did you mean non-rechargeable lithium batteries?

akeilox commented 4 years ago

sorry i meant NiMh, not NiCd. There are also lithium chemistry AAA batteries like Energizer Ultimate Lithium AAA

On the note of rechargeable lithium, one can 'theoretically' use 2x 10440 lithium rechargeable batteries in parallel connection as Berrymed connects 2x AAA on series i believe.

tobiasisenberg commented 4 years ago

Ok, this makes sense. The idea of the parallel 10440 lithiums may be interesting ...

tobiasisenberg commented 4 years ago

Quick update: ESP32 sketch is more stable now and posts correct data. I also now have a Python script that records the data from MQTT and writes it to a CSV file, automatically starting a new file if the oximeter is newly connected. If I log for a minute or two I see that the value traces connect nicely between successive data packets. Now I need run the script as a service, and that should take care of the data recording completely independent of OpenHAB. Then comes a test for a night, to (a) see how long the batteries last, (b) see if things are stable, and (c) get a longer data trace. I need this realistic trace to be able to look into the data plotting, since I am afraid that I will have a lot of data. :-)

akeilox commented 4 years ago

great to hear that @tobiasisenberg I'm rather new to OpenHab but would like to give it a try to see possibilities for inspiration. Is there a github page that one can access to that sketch? I am also interested to hear about your experience on detecting irregular beats

tobiasisenberg commented 4 years ago

@akeilox I'll put things on GitHub when I have reached a presentable stage. At the moment things are still too incomplete. The good news is that my test last night showed that, with two regular 800mAh NiMh batteries, the BerryMed BM1000C lasted the whole night. And the batteries were not even freshly charged. The bad news is that I only managed to get data from a stretch of about 2h, then my process stopped (I am not sure if the problem was the ESP32 sketch or the Python-based MQTT recorder). Today I made some improvements to the ESP32 sketch and the data recording including making it yet more robust and adding more debugging, so we'll see how this fares.

The other piece of good news is that I made progress with the data visualization, see the example from the 2h recording here. Initially I had wanted to arrange several of the traces on a page, but with one per page and continued viewing in a PDF reader this actually works quite well, so I will probably leave it as it is for now. The file first contains the BPM and SPO2 summary graphs with a 200 sample (i.e., about 2 seconds) averaging covering 1 hour each, then the BPM and SPO2 summary graphs without averaging and each graph covering 10 minutes, followed by the detail graphs including PPG with each graph covering 1 minute. The downside is that the plotting library I use, Plotly, is really cool but also really slow for large plots when writing to PDF. Right now the processing of the 2h data recording takes 10--20 minutes, unfortunately.

tobiasisenberg commented 4 years ago

@akeilox and @SensorsIot I have now managed to successfully record a whole night worth of data, and produced a visualization. I think things are now stable enough. I thus have committed the first version of my project to GitHub, have a look: https://github.com/tobiasisenberg/OxiVis I would love to have feedback, in particular on the installation procedure and the requirements as described in the readme file. If you find anything that is missing, incorrect, or unclear please let me know. Also if you have additional ideas, also let me know.

akeilox commented 4 years ago

@tobiasisenberg that looks amazing! i will definitely give it a try once i get back.

tobiasisenberg commented 4 years ago

@akeilox Any news, did you manage to try it?

akeilox commented 4 years ago

Hi @tobiasisenberg I was trying to get the openhab mqtt running in a windows 10 machine with IIS and lot of other things going on(!) finally managed to get openhab running on its own port, and now trying to configure the MQTT. I'll update back at https://github.com/tobiasisenberg/OxiVis as I progress with it. Paths like filenameLocationBasis = "/var/log/openhab2/oximeter-" will be changed to filenameLocationBasis = "c:/openhab2/oximeter-" or is it the "c:\openhab2\oximeter-" not sure at this stage