BLE - Getting started help

The definition of the peer device object can be found here which is part of the header file in the open source repo I already provided and here are the docs dealing with that object and its methods (although I don't really consult the docs but rather the code for complete and native info).
Unfortunately the method you are looking for is not documented (yet) but can be found in the class definition and since the method names are intentionally kept speaking it shouldn't be surprising that the method is called getServiceByUuid().
Additionally the class also features a method discoverAllServices() to retrieve a vector which holds a "list" of the services provided by the device to iterate over and search for what you want "manually".

But, since you ultimately are not really after the service but rather the characteristics of that service you could also use "shorthand" getCharactersiticByUUID() which is documented and also used in the UART Central example that way.

Yup, but only characteristics feature that method - services don't.
Hence the "shorthand" mentioned above.

Sure, it is complicated, but that's owed to the complexity of BLE and its demand to be versatile and flexible.
However, BLE.scan() will give you "list" of BleScanResults which in turn contain one or two BleAdvertisingData entries which can hold different kinds of info (speaking of BLE versatility/complexity) but do provide methods to get the data you are looking for (when available).

Having said all that, while it may at the end boil down to only a few lines of code but due to the versatility/complexity it's not that simple to provide you with a code that does what you want. As I said, with your sensor at hand I would have wipped that up in no time but since I don't have such a device I also have spent lots of time trying to explain how the general idea of BLE works and how to find the way through the jungle and I don't even get anything off it once your project were to be finished.

It may be a simple guide for one purpose but a creating a simple guide for litterally thousands of thinkable combinations of circumstances is not a simple task anymore.

But I mentioned this in my (admittedly brief) breakdown of the process

To which you (at first) asked but then deleted the question


Hence I figured (obviously wrongly) that you may have read the comment in my code and realised the answer was there already - at least in parts.

Following that sentence I quoted myself saying that I suspect Xiaomi to only advertise one "service" (which seems to be their brand UUID) indicating that there are more (hence "Incomplete List ...") to be requested when you are looking for one of their devices (which you obviously do). To do that you need to connect to that device and investigate - as I said in that quote too.

Your screenshot I took that from tells you so: "Incomplete List of 16-bit Service UUIDs: 0xFE95"
That incomplete list contains exactly one entry 0xFE95.

Nope, that's not one line but an entire process governed by the BLE standard which I roughly broke down earlier

That process would be just the same even on an ESP32.

As I said: That's owed to the complexity/versatility of BLE and the lack of access to that very sensor you want a solution for.
In maths a list of all results for any given possible combination of numbers and operators is neither helpful nor possible but explaining the underlying concept to be used on any of these is.

The risk with that is to mistake copy/pasting with minor tweaking for actual understanding which is the ultimate goal of learning.

Comparing SPI with BLE is like comparing a steam powered loom with a CNC machine.
Have you seen an Arduino or ESP32 3-5 line solution for this problem that doesn't require some understanding the fundamental BLE concepts?


Just to illustrate that this is not a 3-5 line endeavour I cobbled together this sample implementation

#define CENTRAL             // comment out to create a mockup sensor with ModeChange and LiveSensorData characteristics
#define CORRECT_BEHAVIOUR   // comment out if not working correctly

#include "Particle.h"

SYSTEM_MODE(MANUAL)
SYSTEM_THREAD(ENABLED)

SerialLogHandler log(LOG_LEVEL_ALL);

const uint16_t vendor = 0xFE95;
const BleUuid serviceUuidRoot("0000FE95-0000-1000-8000-00805F9B34FB");

const BleUuid serviceUuidGeneric("00001800-0000-1000-8000-00805F9B34FB");
const BleUuid charUuid_r0x0003("00002800-0000-1000-8000-00805F9B34FB");

const BleUuid serviceUuidData("00001204-0000-1000-8000-00805F9B34FB");
const BleUuid charUuid_w0x0033("00001A00-0000-1000-8000-00805F9B34FB");
const BleUuid charUuid_r0x0035("00001A01-0000-1000-8000-00805F9B34FB");
const BleUuid charUuid_r0x0038("00001A02-0000-1000-8000-00805F9B34FB");
const BleUuid charUuid_r0x003c("00001A11-0000-1000-8000-00805F9B34FB");
const BleUuid charUuid_w0x003e("00001A10-0000-1000-8000-00805F9B34FB");
const BleUuid charUuid_r0x0041("00001A12-0000-1000-8000-00805F9B34FB");

#if defined(CENTRAL)
BleAddress        peerAddress;
BlePeerDevice     peerDevice;
BleCharacteristic peerModeChangeCharacteristic;
BleCharacteristic peerRealTimeSensorCharacteristic;

bool rescan = true;
bool readyToConnect = false;

void setup()
{
  Serial.begin();
  System.on(button_click, clickHandler);

  pinMode(D7, OUTPUT);
}

void loop() {
  static uint32_t ms = 0;

  if (readyToConnect) {
    if (attachPeer(peerAddress, peerDevice))
      readyToConnect = false;                                               // already connected 
    else {
      Log.warn("retry to connect in 5 seconds");
      delay(5000);
    }
  }

  if (millis() - ms < 1000)
    return;
  ms = millis();

  if (rescan)
  {
    rescan = false;
    int count;
    Log.info("rescan");
    if ((count = BLE.scan(scanResultCallback, NULL)))
      Log.info("%d devices found", count);
  }

  Serial.print('.');
  digitalWrite(D7, !digitalRead(D7));
}

void scanResultCallback(const BleScanResult *scanResult, void *context) {
  if (BLE.connected())
    BLE.disconnectAll();

  int countServ = 5;                                                          // how many do we expect
  BleUuid services[countServ];                                                // reserve that many service UUIDs
  countServ = scanResult->advertisingData.serviceUUID(services, countServ);   // request up to countUuid services from advertisingData
  Log.info("Found %d UUIDs in advertisingData", countServ);
  for (int s = 0; s < countServ; s++)
  { // iterate over found services
    Log.info("%d. UUID: %s", s + 1, (const char *)services[s].toString());
    if (services[s] == serviceUuidRoot || services[s].shorted() == vendor)
    {                                                                         // check against expected values
      Log.trace("%s ?= %s || %04x ?= %04x", (const char*)services[s].toString(), (const char*)serviceUuidRoot.toString(), services[s].shorted(), vendor);
      peerAddress = scanResult->address;
      readyToConnect = true;
      BLE.stopScanning();
      break;
    }
  }
}

bool attachPeer(BleAddress addr, BlePeerDevice& peer) {
  if (peer.connected())
    peer.disconnect();

  peer = BLE.connect(addr); // connect to the peer device
  if (!peer.connected()) {
    Log.warn("Couldn't connect to device");
    return false;
  }

  Log.info("found device and connected to it");

#if defined(CORRECT_BEHAVIOUR)
  // due to some potential quirk with the test device implementation this does not work for me 
  if (peer.getCharacteristicByUUID(peerModeChangeCharacteristic, charUuid_r0x0035))
  {
    Log.info("Found LiveSensorDataCharacteristic - hooking up NOTIFY callback");
    peerRealTimeSensorCharacteristic.onDataReceived(onDataReceived, NULL);
  }
  else
    Log.warn("LiveSensorDataCharacteristic (%s) not found", (const char *)charUuid_r0x0035.toString());

  if (peer.getCharacteristicByUUID(peerModeChangeCharacteristic, charUuid_w0x0033)) {
    uint8_t val[2];
    peerModeChangeCharacteristic.getValue((uint8_t *)&val, sizeof(val));  // request the current value (into a buffer of fitting size)
    Log.info("Found ModeChangeCharacteristic, current value %04x", *(uint16_t *)&val);
    val[0] = 0xa0;                                                        // prepare endiannes-agnostic buffer for new value
    val[1] = 0x1f;
    peerModeChangeCharacteristic.setValue(val, sizeof(val));              // set new value
    peerModeChangeCharacteristic.getValue((uint8_t *)&val, sizeof(val));  // read back set value
    Log.info("new value %04x", *(uint16_t *)&val);
  }
  else
    Log.warn("ModeChangeCharacteristic (%s) not found", (const char *)charUuid_w0x0033.toString());
#else
  const int countChar = 10;
  BleCharacteristic chars[countChar];
  int foundChar = peer.discoverAllCharacteristics(chars, countChar);        // discover its exposed characteristics

  Log.info("Found %d of %d characteristics", foundChar, countChar);

  for (int c = 0; c < foundChar; c++)
  {
    Log.info("%d. Characteristic: %s", c + 1, (const char *)chars[c].UUID().toString());

    Log.info("%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x", chars[c].UUID().rawBytes()[0], chars[c].UUID().rawBytes()[1], chars[c].UUID().rawBytes()[2], chars[c].UUID().rawBytes()[3], chars[c].UUID().rawBytes()[4], chars[c].UUID().rawBytes()[5], chars[c].UUID().rawBytes()[6], chars[c].UUID().rawBytes()[7], chars[c].UUID().rawBytes()[8], chars[c].UUID().rawBytes()[9], chars[c].UUID().rawBytes()[10], chars[c].UUID().rawBytes()[11], chars[c].UUID().rawBytes()[12], chars[c].UUID().rawBytes()[13], chars[c].UUID().rawBytes()[14], chars[c].UUID().rawBytes()[15]);
    Log.trace("LifeSensorDataCharacteristic: %s ?= %s || %04x ?= %04x", (const char *)chars[c].UUID().toString(), (const char *)charUuid_r0x0035.toString(), chars[c].UUID().shorted(), charUuid_r0x0035.shorted());

    if ((const BleUuid)chars[c].UUID() == charUuid_w0x0033 
    || chars[c].UUID().shorted() == charUuid_w0x0033.shorted() 
    || chars[c].UUID().shorted() == 0x1A00)                                 // hardcoded for some quirky behaviour of .shorted()
    {                                                                       // check against expected value
      uint8_t val[2];
      peerModeChangeCharacteristic = chars[c];                              // store characteristics reference globally
      peerModeChangeCharacteristic.getValue((uint8_t *)&val, sizeof(val));  // request the current value (into a buffer of fitting size)
      Log.info("Found ModeChangeCharacteristic, current value %04x", *(uint16_t *)&val);
      val[0] = 0xa0;                                                        // prepare endiannes-agnostic buffer for new value
      val[1] = 0x1f;
      peerModeChangeCharacteristic.setValue(val, sizeof(val));              // set new value
      peerModeChangeCharacteristic.getValue((uint8_t *)&val, sizeof(val));  // read back set value
      Log.info("new value %04x", *(uint16_t *)&val);
    }
    else if ((const BleUuid)chars[c].UUID() == charUuid_r0x0035
    || chars[c].UUID().shorted() == charUuid_r0x0035.shorted()
    || chars[c].UUID().shorted() == 0x1A01)                                 // hardcoded for some quirky behaviour of .shorted()
    { // check aganist other expected value
      Log.info("Found LiveSensorDataCharacteristic - hooking up NOTIFY callback");
      peerRealTimeSensorCharacteristic = chars[c];                          // when found store reference globally and hook-up callback
      peerRealTimeSensorCharacteristic.onDataReceived(onDataReceived, NULL);
    }
  }
#endif

  return true;
}

void onDataReceived(const uint8_t *data, size_t len, const BlePeerDevice &peer, void *context) {
  for (size_t ii = 0; ii < len; ii++) {
    Serial.write(data[ii]);
  }
}

void clickHandler(system_event_t event, int param)
{
  rescan = true;
}

#else

// test device
BleCharacteristic charModeChange("mode", BleCharacteristicProperty::READ | BleCharacteristicProperty::WRITE_WO_RSP, charUuid_w0x0033, serviceUuidData, onDataReceived, NULL);
BleCharacteristic charLiveSensorData("temp", BleCharacteristicProperty::NOTIFY, charUuid_r0x0035, serviceUuidData);
const uint32_t UPDATE_INTERVAL_MS = 2000;
uint32_t lastUpdate = 0;
uint16_t mode = 0xCAFE;

void setup()
{
  BLE.addCharacteristic(charModeChange);
  BLE.addCharacteristic(charLiveSensorData);

  BleAdvertisingData advData;
  BleAdvertisingData srData;
  advData.appendLocalName("TD");
  advData.appendServiceUUID(BleUuid(serviceUuidData.rawBytes(), serviceUuidData.shorted()));

  advData.appendServiceUUID(vendor);
  srData.appendServiceUUID(serviceUuidData);
  BLE.setScanResponseData(&srData);
  BLE.advertise(&advData);

  charModeChange.setValue((uint8_t *)&mode, sizeof(mode));
}

void loop()
{
  if (millis() - lastUpdate >= UPDATE_INTERVAL_MS)
  {
    lastUpdate = millis();
    if (BLE.connected())
    {
      uint8_t buf[6];
      snprintf((char*)buf, sizeof(buf), "%5lu", millis());
      charLiveSensorData.setValue(buf, sizeof(buf));
      Log.info((char*)buf);
    }
  }
}

void onDataReceived(const uint8_t *data, size_t len, const BlePeerDevice &peer, void *context) {
  Log.info("Received %d bytes from %s (context: %08x) ", len, (const char *)peer.address().toString(), context);
  for (size_t i=0; i < len; i++)
    Log.printf("%02x", data[i]);
  Log.print("\r\n");

  mode = *(uint16_t *)data;
}
#endif

This central device works as far as I can tell.
For testing I have also attached a peripheral implementation which exposes the ChangeMode and LiveSensorData characteristics for the central to attach to.
While putting this together I came across some aparent quirks in the Particle BLE framework for which I had to come up with some workarounds.
The current version expects the sensor to behave better than the test peripheral. If it doesn't work with the default implementation comment out the #define CORRECT_BEHAVIOUR line to enable the workaround.

I think I found a bug in the implementation of the BLE framework and filed an issue about it here
https://github.com/particle-iot/device-os/issues/1993