Friday, March 29, 2024
More
    HomeESPESP-NowESP-Now with Automatic Discovery & Pairing (Part Three)

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

    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.

    Simon Stuart
    Simon Stuarthttp://flowduino.com
    Simon has been creating digital things for the last 30 years. Whether it be video games, complex systems for the aerospace and automotive industries, workflow management systems for data-intensive and manufacturing industries, or specialised electronics devices... Simon has tinkered with it, personally and professionally.
    RELATED ARTICLES

    8 COMMENTS

      • I have Part 4 in draft, but have had a number of work and life issues getting in the way of completing it.
        I am expecting to finish it in the next three weeks (ideally sooner rather than later).

    1. I’m looking forward to part 4, but I just wonder if it would be possible to achieve the same using broadcast during discovery with ESPnow (mac FF FF FF FF FF FF) in stead of using BLE?

      • Is it possible to do broadcast during discovery with ESPNow? I hadn’t considered it, because I was of the belief that you can only broadcast ANYTHING on ESPNow if you’re already paired, or know the MAC Address of the target recipient(s).

        If you have any info to suggest otherwise, please do pass it along to me and I’ll take a look. If we can avoid using BLE for pairing, this will reduce complexity, app size, and improve the overall process flow massively.

    Leave a Reply to Simon Stuart Cancel reply

    Please enter your comment!
    Please enter your name here

    Discuss/Discord

    Want to discuss this article in more detail? Why not join our community, including our authors, on Discord? You will find a community of like-minded Makers, where you can ask questions, make suggestions, and show off your own projects.