Site icon Flowduino

ESP-Now [Pin-Depth] with Automatic Discovery & Pairing (Part Three)

BLE Automatic Discovery - Master & Slave Devices

BLE Automatic Discovery - Master & Slave Devices

In Part Two…

… we looked at the identical wiring required for our Master and Slave demonstration ESP32 boards.

We defined our Discovery Strategy, and decided to set things up for the One-To-Many (Two-Way) Communications approach with ESP-Now.

We wrote the common code that will be used by both Master and Slave boards, and completed the Master code so that it can broadcast the WiFi Mac Address via Bluetooth Low Energy (BLE).

If you have not already completed Parts One and Two, please go back and do so before reading Part Three. It is important, as Part Three is written with the strict presumption that you are following this series in sequence.

With the Master device ready, it’s time to look at the Slave code.

Initialise, Scan, Identify, Connect, Obtain, Disconnect

As the heading says, we need to understand the process the Slave code will use to facilitate Discovery.

We already have our barebones code, enabling us to enter Discovery Mode (holding down our button for 3 seconds), automatic timeout for Discovery, and the ability to manually cancel Discovery (again, by holding down our button for 3 seconds).

Our LEDs are configured to indicate states.

Now we need to add to this the code to actually perform Discovery from the Slave/Client side.

Initialise

The first thing we need to do is add the required variables and method calls to Initialise Bluetooth Low Energy (BLE).

Let’s start with the Variables we need.

#define UUID_SERVICE          "d91fdc86-46f8-478f-8dec-ebdc0a1188b2"
#define UUID_CHARACTERISTIC   "56100987-749a-4014-bc22-0be2f5af59d0"

BLEScan* pBLEScan;
BLEClient* pBLEClient;
BLERemoteService* pRemoteService;
BLERemoteCharacteristic* pRemoteCharacteristic;
BLEAdvertisedDevice _advertisedDevice;
bool deviceFound = false;
bool clientConnected = false;
unsigned long discoveredAt;

As you can see, we define our Variables beneath our Constants expressing the UUIDs that uniquely identify our BLE Service and BLE Characteristic.

With all of our variables defined, let’s take a look at our setup() routine.

void setup() {
  // Initialise Serial first
  Serial.begin(115200); // Set Serial Monitor to 115200 baud

  BLEDevice::init("Flowduino Auto-Discovery Demo - Slave");

  pBLEScan = BLEDevice::getScan(); //create new scan
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setActiveScan(true); //active scan uses more power, but get results faster
  pBLEScan->setInterval(100);
  pBLEScan->setWindow(99);  // less or equal setInterval value

  // Set our Pin Modes
  pinMode(PIN_BUTTON, INPUT);     // Button Input
  pinMode(PIN_LED_RED, OUTPUT);   // Red LED Output
  pinMode(PIN_LED_BLUE, OUTPUT);  // Blue LED Output

  // Get the initial state of our Button
  buttonState = getButtonState();
}

This routine is pretty straight-forward. We once again Initialise the BLEDevice object, though the String value we provide in the parameter is fairly inconsequential as this device will never broadcast its identity over Bluetooth.

We create a BLEScan instance, which we shall use to scan for – and ultimately identify – our Master device.

We shall come to the MyAdvertisedDeviceCallbacks class definition in a moment, but this will ultimately serve to Identify each Discovered BLE Device in order to find the one we want (our Master device).

We then define property values that control the Scanning Process itself. Note that setActiveScan(true) will consume more power than setActiveScan(false), but it will get results faster. If Discovery Speed is more important to you than power consumption, stick with true.

Scan & Identify

Let’s now take a look at the code we need to deal with Scanning.

We’re going to start with the definition of MyAdvertisedDeviceCallbacks, which is invoked each time the BLE library scans and detects a Bluetooth Device.

class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
    void onResult(BLEAdvertisedDevice advertisedDevice) {
      if (clientConnected) { return; };
      
      if (advertisedDevice.haveServiceUUID() && advertisedDevice.getServiceUUID().toString() == UUID_SERVICE) {
        Serial.printf("Advertised Device: %s \n", advertisedDevice.toString().c_str());
        _advertisedDevice = advertisedDevice;
        deviceFound = true;
        return;
      }
    }
};

This block shall be placed beneath all of our Variable declarations.

Our onResult() method will compare the Service UUID of the Detected (Advertised) Device with the UUID_SERVICE constant defined in our Master and Slave code. If we have a match, we will store a Pointer Reference to the object representing this Device, then update a Flag named deviceFound to inform the rest of our code that we Discovered our Master Device.

Now that our Callback Handler is in place, we can take a look at the Slave variation of loopDiscovering() method.

// The Loop routine when our Device is in Discovering Mode
inline void loopDiscovering() {
  flashBlueLED();

  // Scan for BLE Devices
  deviceFound = false;
  BLEScanResults foundDevices = pBLEScan->start(3, false);
  pBLEScan->clearResults();   // delete results fromBLEScan buffer to release memory

  if (deviceFound) {
    if (connectToDevice()) { return; }
  }

  ButtonState currentState = getButtonState();

  // Down to Up
  if (buttonState == ButtonDown && currentState == ButtonUp) {
    buttonState = currentState; // Update the global variable accordingly
    return; // Need not proceed further
  }

  // Up to Down
  if (buttonState == ButtonUp && currentState == ButtonDown) {
    // The Button was just pressed down...
    buttonHoldStart = millis();
    buttonState = currentState;
    Serial.println("Button Hold Started");
    return; // Need not proceed further
  }

  // Held Down OR Timed Out
  if (
       (buttonState == ButtonDown && currentState == ButtonDown && millis() > buttonHoldStart + BUTTON_HOLD_TIME) ||
       (millis() > discoveryStart + DISCOVERY_TIMEOUT)
     ){
    // We now initiate Discovering!
    Serial.println("Cancelling Discovering");
    deviceMode = Waiting;
    setRedLED(true);
    digitalWrite(PIN_LED_BLUE, LOW); // Ensure Blue LED is OFF
    buttonHoldStart = millis();
  }
}

We want our Scan to loop, just in case there are any delays between the Master and Slave entering Discovery Mode.

To ensure this happens, we add the code that handles Scanning inside the loop that executes when in Discovery Mode.

First, we update the Flashing state of our Blue LED (which, as you may remember, indicates that our device is in Discovery Mode).

Then, we ensure that we initialise our Flag, deviceFound, to False, because we have to be pessimistic in this case.

Next, we start a Scan for no longer than 3 seconds, then we clear the Results in order to free Memory. We actually don’t care about how many results we get… we’re only interested in our Master Device.

We then interrogate our Flag, deviceFound, to see if the Scan found our Master Device.

If our Master Device was found, we invoke the method we shall look at next, connectToDevice(), and – if this method returns True, we prematurely return from the Loop.

if we haven’t found our Master Device, the Loop will repeat again until either our Master Device is found, or Discovery Mode times out.

Connect, Obtain, and Disconnect

Now it’s time to look at what happens when we’ve identified our Master Device.

Let’s take a look at the code, and then break down what it is doing:

inline bool connectToDevice() {
  pBLEClient = BLEDevice::createClient();
  Serial.println("Connecting To \"Flowduino Auto-Discovery Demo - Master\"");
  clientConnected = pBLEClient->connect(&_advertisedDevice);
  
  if (!clientConnected) {
    Serial.println("Connection Failed!");
    return false;
  }
    
  Serial.println("Connected... let's get the ESPNow Address!");
  pRemoteService = pBLEClient->getService(UUID_SERVICE);
  if (pRemoteService == nullptr) {
    Serial.println("Unable to get our Service from the Controller!");
    return false;
  }

  Serial.println("Service acquired!");

  pRemoteCharacteristic = pRemoteService->getCharacteristic(UUID_CHARACTERISTIC);
  
  if (pRemoteCharacteristic == nullptr) {
    Serial.println("Couldn't get the pRemoteCharacteristic");
    return false;
  }

  Serial.println("YAY! We got the pRemoteCharacteristic!");

  uint8_t mac[6];
  char macStr[18] = { 0 };
  const char* rawData = pRemoteCharacteristic->readValue().c_str();//pRemoteCharacteristic->readRawData();
  
  sprintf(macStr, "%02X:%02X:%02X:%02X:%02X:%02X", rawData[0], rawData[1], rawData[2], rawData[3], rawData[4], rawData[5]);
  
  Serial.print("Value is: ");
  Serial.println(String(macStr));

  deviceMode = Discovered;
  discoveredAt = millis();

  return true;
}

First, we create a BLEClient instance. This is what will be connecting to our Master Device via BLE to get the Mac Address.

Next, we attempt the Bluetooth Connection. This, of course, could fail, so we immediately follow this with an early return condition… just in case.

If the Bluetooth Connection is successful, we need to obtain our Bluetooth Service from the Master Device. This is addressed using UUID_SERVICE, which matches on both the Master and Slave devices.

Again, it is possible that the Remote Device might not provide our Service, so we need an early return condition… just in case.

If we are successful in obtaining the Service we need, we then attempt to obtain the Characteristic (identified by UUID_CHARACTERISTIC), which bears the Mac Address we need as a Property.

Once again, it is possible that the Remote Service might not provide our Characteristic, so we need another early return condition… just in case.

If we successfully obtain our Characteristic, we can get the raw bytes representing our Mac Address, which – for the sake of outputting the value in the Console – we convert into a String.

All Together, Now!

Here is the complete Slave code:

#include <Arduino.h>

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>

#define PIN_BUTTON    34 // GPIO34
#define PIN_LED_RED   23 // GPIO23
#define PIN_LED_BLUE  22 // GPIO22

#define UUID_SERVICE          "d91fdc86-46f8-478f-8dec-ebdc0a1188b2"
#define UUID_CHARACTERISTIC   "56100987-749a-4014-bc22-0be2f5af59d0"

BLEScan* pBLEScan;
BLEClient* pBLEClient;
BLERemoteService* pRemoteService;
BLERemoteCharacteristic* pRemoteCharacteristic;
BLEAdvertisedDevice _advertisedDevice;
bool deviceFound = false;
bool clientConnected = false;
unsigned long discoveredAt;

// We use an Enum to define the Mode of our Device
enum DeviceMode {
  Waiting, // Not Discovering, not timed out
  Discovering, // We're in Discovering mode
  Discovered,  // Discovering Succeeded
  Failed,  // Discovering Failed (Timed Out)
};

DeviceMode deviceMode = Waiting; // We are initially Waiting

enum ButtonState {
  ButtonDown, // The button is being pressed/held
  ButtonUp    // The button has been released
};

ButtonState buttonState;

class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
    void onResult(BLEAdvertisedDevice advertisedDevice) {
      if (clientConnected) { return; };
      
      if (advertisedDevice.haveServiceUUID() && advertisedDevice.getServiceUUID().toString() == UUID_SERVICE) {
        Serial.printf("Advertised Device: %s \n", advertisedDevice.toString().c_str());
        _advertisedDevice = advertisedDevice;
        deviceFound = true;
        return;
      }
    }
};

inline ButtonState getButtonState() {
  return digitalRead(PIN_BUTTON) == HIGH ? ButtonDown : ButtonUp;
}

unsigned long nextFlash; // This will hold the millis() value of our next Flash
#define INTERVAL_FLASH  500 // This is our Flash Interval (500ms, 0.5 seconds)

inline void flashBlueLED() {
  if (millis() < nextFlash) { return; } // It isn't time yet, so nothing to do.

  digitalWrite(PIN_LED_BLUE, !digitalRead(PIN_LED_BLUE)); // Toggle the LED State

  nextFlash = millis() + INTERVAL_FLASH; // Sets the time it should toggle again.
}

inline void setRedLED(bool ledOn) {
  digitalWrite(PIN_LED_RED, ledOn); // True = HIGH, False = LOW
}

void setup() {
  // Initialise Serial first
  Serial.begin(115200); // Set Serial Monitor to 115200 baud

  BLEDevice::init("Flowduino Auto-Discovery Demo - Slave");

  pBLEScan = BLEDevice::getScan(); //create new scan
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setActiveScan(true); //active scan uses more power, but get results faster
  pBLEScan->setInterval(100);
  pBLEScan->setWindow(99);  // less or equal setInterval value

  // Set our Pin Modes
  pinMode(PIN_BUTTON, INPUT);     // Button Input
  pinMode(PIN_LED_RED, OUTPUT);   // Red LED Output
  pinMode(PIN_LED_BLUE, OUTPUT);  // Blue LED Output

  // Get the initial state of our Button
  buttonState = getButtonState();
}

unsigned long buttonHoldStart; // The millis() value of the initial Button push down
#define BUTTON_HOLD_TIME  3000 // The number of millis for which we must hold the button
unsigned long discoveryStart; // The millis() value at which Discovering started
#define DISCOVERY_TIMEOUT   30000 // 30 seconds in milliseconds for Timeout

// The Loop routine when our Device is in Waiting Mode
inline void loopWaiting() {
  ButtonState currentState = getButtonState();

  // Down to Up
  if (buttonState == ButtonDown && currentState == ButtonUp) {
    buttonState = currentState; // Update the global variable accordingly
    return; // Need not proceed further
  }

  // Up to Down
  if (buttonState == ButtonUp && currentState == ButtonDown) {
    // The Button was just pressed down...
    buttonHoldStart = millis();
    buttonState = currentState;
    Serial.println("Button Hold Started");
    return; // Need not proceed further
  }

  // Held Down
  if (buttonState == ButtonDown && currentState == ButtonDown && millis() > buttonHoldStart + BUTTON_HOLD_TIME) {
    // We now initiate Discovering!
    Serial.println("Initiating Discovering");
    deviceMode = Discovering;
    setRedLED(false);
    discoveryStart = millis();
    buttonHoldStart = discoveryStart;
  }
}

inline bool connectToDevice() {
  pBLEClient = BLEDevice::createClient();
  Serial.println("Connecting To \"Flowduino Auto-Discovery Demo - Master\"");
  clientConnected = pBLEClient->connect(&_advertisedDevice);
  
  if (!clientConnected) {
    Serial.println("Connection Failed!");
    return false;
  }
    
  Serial.println("Connected... let's get the ESPNow Address!");
  pRemoteService = pBLEClient->getService(UUID_SERVICE);
  if (pRemoteService == nullptr) {
    Serial.println("Unable to get our Service from the Controller!");
    return false;
  }

  Serial.println("Service acquired!");

  pRemoteCharacteristic = pRemoteService->getCharacteristic(UUID_CHARACTERISTIC);
  
  if (pRemoteCharacteristic == nullptr) {
    Serial.println("Couldn't get the pRemoteCharacteristic");
    return false;
  }

  Serial.println("YAY! We got the pRemoteCharacteristic!");

  uint8_t mac[6];
  char macStr[18] = { 0 };
  const char* rawData = pRemoteCharacteristic->readValue().c_str();
  
  sprintf(macStr, "%02X:%02X:%02X:%02X:%02X:%02X", rawData[0], rawData[1], rawData[2], rawData[3], rawData[4], rawData[5]);
  
  Serial.print("Value is: ");
  Serial.println(String(macStr));

  deviceMode = Discovered;
  discoveredAt = millis();

  return true;
}

// The Loop routine when our Device is in Discovering Mode
inline void loopDiscovering() {
  flashBlueLED();

  // Scan for BLE Devices
  deviceFound = false;
  BLEScanResults foundDevices = pBLEScan->start(3, false);
  pBLEScan->clearResults();   // delete results fromBLEScan buffer to release memory

  if (deviceFound) {
    if (connectToDevice()) { return; }
  }

  ButtonState currentState = getButtonState();

  // Down to Up
  if (buttonState == ButtonDown && currentState == ButtonUp) {
    buttonState = currentState; // Update the global variable accordingly
    return; // Need not proceed further
  }

  // Up to Down
  if (buttonState == ButtonUp && currentState == ButtonDown) {
    // The Button was just pressed down...
    buttonHoldStart = millis();
    buttonState = currentState;
    Serial.println("Button Hold Started");
    return; // Need not proceed further
  }

  // Held Down OR Timed Out
  if (
       (buttonState == ButtonDown && currentState == ButtonDown && millis() > buttonHoldStart + BUTTON_HOLD_TIME) ||
       (millis() > discoveryStart + DISCOVERY_TIMEOUT)
     ){
    // We now initiate Discovering!
    Serial.println("Cancelling Discovering");
    deviceMode = Waiting;
    setRedLED(true);
    digitalWrite(PIN_LED_BLUE, LOW); // Ensure Blue LED is OFF
    buttonHoldStart = millis();
  }
}

// The Loop routine when our Device is in Discovered Mode
inline void loopDiscovered() {
  if (discoveredAt == 0) {
    discoveredAt = millis();
    return;
  }
  
  if (millis() > discoveredAt + BUTTON_HOLD_TIME) {
    digitalWrite(PIN_LED_BLUE, LOW);
    deviceMode = Waiting;
    Serial.println("Going back to Waiting mode");
  }
}

void loop() {
  switch (deviceMode) {
    case (Waiting):
      loopWaiting();
      break;
    case (Discovering):
      loopDiscovering();
      break;
    case (Discovered):
      loopDiscovered();
      break;
  }
}

Success!

We now have the WiFi Mac Address of the Master Device, Automatically Discovered by our Slave Device, using Bluetooth Low Energy (BLE).

In Part Four, we shall take this Mac Address, and use it to Pair our Master and Slave devices using ESP-Now.

Exit mobile version