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

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

    Quick Recap…

    In Part One, we looked at:

    • A brief introduction to ESP-Now as a means of communicating between ESP32 devices
    • The communication scenarios ESP-Now provides
    • Static Pairing as a concept, as well as the pros and cons
    • Dynamic Pairing as a concept, as well as the pros and cons
    • Automatic Discovery as a concept
    • Real-world examples of Automatic Discovery
    • Multiple means of automating discovery, as well as their pros and cons

    Now, let’s take a look at how we can facilitate automatic discovery, using only the hardware package contained within the ESP32 itself.

    In This Article…

    … we will walk through, step-by-step, setting up a Master ESP32 for Automatic Discovery.

    Follow Carefully!

    This article is written to be of value not only to the pre-experienced, but also to the inexperienced. Consequently, it is structured as a “step-by-step” guide; each paragraph advancing on all that came before it.

    I would encourage all readers to be patient, and read through it in sequence, rather than skipping ahead.

    I apologise in advance to the more experienced reader, as I know you might feel that it is slightly condescending being told how to do things you already know how to do.

    To follow this build, you will need…

    For the purpose of this article, it is necessary to be able to initiate Discovery between our ESP32 devices, and for some form of visual indication to enable us to see whether or not the Discovery was successful.

    With that in mind, each ESP32 device will need:

    • One Push Button
    • One Blue LED
    • One Red LED

    Note that you can modify the code examples herein as you see fit. If you have a preferred means of indicating success or failure (such as a display module) you are certainly able to use it instead of the LEDs.

    Also, it is possible to initiate the Discovery process automatically, either immediately on startup, or after a pre-defined interval. Likewise, you can use some state conditions (such as sensor readings, or even time) to initiate the Discovery process. If you do this, you can eliminate the need to attach a Push Button on your devices.

    Bluetooth and its Magic Powers

    Bluetooth is an extremely complex topic… primarily because it is so diversely applied, for an immense range of purposes. We’re not going to attempt to fully define Bluetooth in this article, but I will provide a brief explanation for those unfamiliar.

    A brief aside, the origin of the name Bluetooth is a short but interesting read.

    Bluetooth is a short-range communication standard intended to replace cables between devices that need to communicate.

    Your wireless headphones are almost certainly using Bluetooth to communicate with your smartphone. Your smartwatch uses it to talk to your phone. If your car has integrated hands-free… that’s also Bluetooth.

    Wireless mice, keyboards, headsets, speakers, lights, GPS devices, barcode scanners, RFID scanners, printers, robotics devices, pretty-much any kind of sensor… they can all operate over Bluetooth.

    If you want to study Bluetooth more in-depth, you should go to the official Bluetooth website.

    Can ESP32 Devices Communicate via Bluetooth?

    While it is entirely possible for your ESP32 devices to communicate solely via Bluetooth, it should be noted that the wireless range for Bluetooth devices is generally very low; and Bluetooth is not used where low-latency communication is required.

    It is, of course, down to you to decide whether or not it is acceptable for your project(s) to use only Bluetooth between ESP32 devices to handle all of their communication needs.

    For the purposes of this series of articles, we’re going to establish a higher-speed ESP-Now connection between ESP32s.

    So, why are you talking about Bluetooth, then?

    Well, we’re going to use Bluetooth – more-specifically, Bluetooth Low Energy (BLE) – to provide the all-important automatic discovery required to establish that ESP-Now connection.

    Put simply: Bluetooth Low Energy (BLE) is going to make it possible for our ESP32 devices to exchange MAC addresses, which can then be used to connect them together using ESP-Now.

    Magic, right?

    The Discovery Strategy

    To use BLE to facilitate automatic discovery between ESP32 devices, it is important that we first define and understand our Discovery Strategy.

    The Discovery Strategy outlines the process a user of your devices must perform in order to initiate Discovery between multiple ESP32 devices.

    Constraints

    It is essential that you define rational limitations for the end-user. This will simplify the process, even if that process may take longer to complete as a result. Remember: it is easier to identify problems when there are as few variables as possible.
    For this reason, I have decided to enforce the constraint that only two devices may be Discovered at one time.

    This does not mean that we can only ever have two ESP32 devices communicating with each other. It simply means that this Discovery process will need to be repeated for each additional ESP32 wishing to communicate with the others.

    This constraint will fundamentally define how we develop our code, so it is important to remember this.

    Communication Strategy

    Remember in Part One where we took a look at the different Communication Strategies made possible by ESP-Now?

    Well, the Discovery Strategy and the way we develop our code are significantly affected by this decision. For this reason, we need to clearly specify our intended Communication Strategy.

    One-to-Many, Many-to-One (Two-Way) Communication Strategy

    ESP-Now One-To-Many, Many-To-One Pairing Diagram

    The above diagram illustrates the Communication Strategy we will be developing in this series of articles.

    Highlighted in red, in the centre of the above diagram, is our “Master” device. When this device switches into Discovery Mode, it will activate a Bluetooth “beacon” to announce its WiFi Radio’s MAC Address.

    When any one of the 4 surrounding ESP32 devices (highlighted in blue) is in Discovery Mode, it will scan for all Bluetooth Low Energy (BLE) devices nearby. Ultimately, it will obtain the MAC address from the “Master” device that is broadcasting at that time.

    With our Constraints and intended Communication Strategy in mind, let’s formalise our Discovery Strategy now.

    Formalising the Discovery Strategy

    I can summarise this strategy in bullet points:

    • The User may only initiate Discovery for two devices at a time (the constraint I just explained above)
    • The User will initiate Discovery between devices by holding a button for 3 seconds on the two respective devices to be Discovered. This will put the devices into “Discovery Mode.”
    • On the Master device, when this button has been held for 3 seconds, the Bluetooth Low Energy (BLE) Radio will begin broadcasting its presence to all nearby devices.
      • This broadcast will continue for 30 seconds
      • After 30 seconds, our code will disable the Bluetooth broadcast (this is called a “Timeout”) if no Devices have established a Bluetooth connection with the Master.
    • On the Slave device, when this button has been held for 3 seconds, the Bluetooth Low Energy (BLE) Radio will perform repeated Scans, looking specifically for our Master device.
      • These scans will repeat for a total of 30 seconds.
      • If our Master device has not been discovered within 30 seconds, our code will cease scanning and switch off the Bluetooth radio.
    • On both devices, a blue LED shall flash at an interval of twice per second for the entire time that our devices are in “Discovery Mode.
    • Where the Slave successfully discovers and connects to the Master, the blue LED on both devices shall cease to flash, indicating that both devices are no longer in “Discovery Mode.”
    • Where each device times out (30 seconds elapses without a successful Bluetooth Low Energy [BLE] connection between the devices) a red LED shall illuminate, indicating that the Discovery was unsuccessful.

    It is important to understand the above Discovery Strategy and its implications for our code.

    Essentially, Discovery Mode will only be enabled until either a single Slave device successfully Disconnects from BLE, or until the 30 second timeout is reached. This means that the Master device can only perform Discovery with one Slave device each time it enters Discovery Mode.
    We cannot hold the buttons on all 4 Slave devices and have them Discover all at once.

    Improvements in the Future

    It should be noted that we will, in a later improvement to this demonstration, eliminate the concept of Master and Slave roles being explicitly assigned to an ESP32 device, and enable all ESP32 devices to switch between these roles automatically and dynamically.
    This improvement is essential for the sake of supporting the Combination Communication Scenario described in Part One.

    Wiring

    Since we’re going to be using a push button to initiate “Discovery Mode,” and two LEDs to indicate Success or Failure of Discovery, we will need to select three pins on our ESP32 units to use.

    These three pins will be identical on all of our ESP32 units.

    ESP32 - ESP-Now Demo Wiring
    ESP32 – ESP-Now Demo Wiring

    In the above wiring illustration, starting left-to-right:

    • Black bridging wire (GND bus to GND bus on the breadboard)
    • Red bridging wire (VCC bus to VCC bus on the breadboard)
    • R1 = 1kOhm resistor from VCC bus to one leg of…
    • S1 = Momentary-Push Button
    • Purple wire from diagonally-opposing leg of S1, connects to GPIO34 of the ESP32
    • Orange wire between R2 and GPIO23 of the ESP32
    • R2 = 220Ohm resistor, connected to Positive leg of D1
    • D1 = Red LED, with Negative leg connected to GND bus
    • Blue wire between R3 and GPIO22 of the ESP32
    • R3 = 220Ohm resistor, connected to Positive leg of D2
    • D2 = Blue LED, with Negative leg connected to GND bus
    • Red bridging wire from V-out on the ESP32 to the VCC bus
    • Black bridging wire from GND on the ESP32 to GND bus

    So, the GPIO connections on the ESP32 will be as follows:

    GPIO PinDirectionComponent
    GPIO34InputS1 (Push Button)
    GPIO23OutputR2 (Red LED)
    GPIO22OutputR1 (Blue LED)

    Please note that my GPIO selections are fairly arbitrary. You do not need to use the same GPIO pins as we are using in this example. Just remember to update the pin definitions in the example code (later in this article) to match your own.

    Let’s start to write the code!

    We’re about to begin looking at the code to achieve Automated Discovery using Bluetooth Low Energy (BLE) for the ESP32.

    Examples are NOT the way you should write your code!

    Please note that, for the sake of keeping this article concise, these code examples will follow the common “Arduino IDE” approach of putting all of our code into a single source file (a Sketch in the context of the Arduino IDE).

    I want to preface this by stating that I do not condone developing code in this manner for real-world projects. Real-world code should always be written with the SOLID principles of development at its core.

    Our Example Projects on GitHub

    At the end of this series of articles, you will be provided with a link to a GitHub repository containing complete example projects for Bluetooth Low Energy (BLE) Automatic Discovery, and ESP-Now communication.

    In this repository, the code will be written properly, and compartmentalised into discrete Classes, with appropriate Types defined as necessary.

    If you’re wondering why there is no link to this repository here on this article, it is because this series intends to progress you through a careful understanding of the code, progressively, so that you know what each instruction is doing. You are urged to not skip over the articles and download the sample project.

    The “Master” Sketch

    We shall begin by developing a Sketch for the “Master” device.

    This Sketch will enable any ESP32 flashed with it to initiate Discovery mode, broadcast its WiFi radio’s MAC address to any “Slave” devices that happen to scan for it, and will indicate the success or failure (timeout) status of Discovery.

    Let’s begin by taking care of the basics.

    Basic Definitions

    We need a blank Sketch, and to define a few pin constants for our button and LEDs.

    #define PIN_BUTTON    34 // GPIO34
    #define PIN_LED_RED   23 // GPIO23
    #define PIN_LED_BLUE  22 // GPIO22
    
    void setup() {
      // put your setup code here, to run once:
    
    }
    
    void loop() {
      // put your main code here, to run repeatedly:
    
    }

    Method Stubs

    When writing “sandbox” code to test things out, I like to define the method stubs first.

    This makes it easier for me to keep the overall process flow in mind as I fill in the implementation.

    #define PIN_BUTTON    34 // GPIO34
    #define PIN_LED_RED   23 // GPIO23
    #define PIN_LED_BLUE  22 // GPIO22
    
    inline void startDiscovery() {
      // This method will switch on BLE and set it up to broadcast the WiFi Mac Address
    }
    
    inline void stopDiscovery() {
      // This method will switch off BLE.
    }
    
    inline void flashBlueLED() {
      // This method will flash the Blue LED at an interval
    }
    
    inline void setRedLED(bool ledOn) {
      // This method will switch the Red LED on or Off
    }
    
    void setup() {
      // put your setup code here, to run once:
    
    }
    
    void loop() {
      // put your main code here, to run repeatedly:
    
    }

    Now that we have our method stubs in place, we can begin some of the more simple implementation details.

    Mode States

    First, we need to be able to define and interrogate the Mode our ESP32 is in at any given moment.

    The best way to do this is with an enumerable (enum):

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

    The above code enables a single variable deviceMode to tell the rest of our Sketch what mode it is in.

    Flashing LEDs

    Now, let’s implement our code to turn the Red LED on and off, and to make the Blue LED flash at a fixed interval:

    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
    }

    You may notice that I do not use Sleep() or Delay() in my code. This is because those methods are blocking, and won’t allow other code to execute on the same CPU until they return (thus, unblocking the execution).

    Instead, I use a variable to hold a Reference Time Value called nextFlash at which to toggle the state of the Blue LED.

    The Push Button

    Okay, so now that we can make the Blue LED flash, and we can switch the Red LED on or off as necessary, let’s take care of handling the Button input.

    Remember: we want to hold the button down for 3 seconds to initiate Discovery Mode.

    To make our code easier to read, I will define an Enumerable (Enum) to contain the Button State.

    enum ButtonState {
      ButtonDown, // The button is being pressed/held
      ButtonUp    // The button has been released
    };
    
    ButtonState buttonState;
    
    inline ButtonState getButtonState() {
      return digitalRead(PIN_BUTTON) == HIGH ? ButtonDown : ButtonUp;
    }
    

    The getButtonState() method above acts as a Macro encapsulating a basic Ternary expression to map the int return value of digitalRead() to a ButtonState value.

    We need to determine the initial Button State, so let’s quickly implement our setup() method.

    void setup() {
      // Initialise Serial first
      Serial.begin(115200); // Set Serial Monitor to 115200 baud
    
      // 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();
    }

    Now, we need to put the code in place to determine when our Button has been held down for 3 seconds.

    First, I will (as I always do) stub out the methods and variables that I know I will need:

    #define BUTTON_HOLD_TIME    3000 // The number of millis for which we must hold the button
    unsigned long discoveryStart; // The millis() value at which Discovery started
    #define DISCOVERY_TIMEOUT   30000 // 30 seconds in milliseconds for Timeout
    
    // The Loop routine when our Device is in Waiting Mode
    inline void loopWaiting() {
      
    }
    
    // The Loop routine when our Device is in Discovery Mode
    inline void loopDiscovering() {
      
    }
    
    // The Loop routine when our Device is in Discovery Mode
    inline void loopDiscovered() {
      
    }
    
    // The Loop routine when our Device is in Failed Mode
    inline void loopFailed() {
      
    }

    With the stubs in place, I can now implement my loop() method.

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

    In this case, what I want the loop() method to do is entirely determined by the deviceMode, so our loop() method will simply delegate the request to the appropriate mode-specific method. For this, we use a simple Switch Statement.

    Implement loopWaiting()

    Now that our main loop() method will delegate its calls to the appropriate Mode-Specific loop method, we need to begin implementing them.

    We shall start with loopWaiting(), as this is our initial state.

    // 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 Discovery!
        Serial.println("Initiating Discovery");
        deviceMode = Discovering;
        setRedLED(false);
        discoveryStart = millis();
        buttonHoldStart = discoveryStart;
        startDiscovery();
      }
    }

    The above code can be made more streamlined, but I have intentionally left it slightly conflated for ease of understanding in this article.

    In essence, if the currentState is Up or changes to Up, there is nothing for this loop to do, and so it shall simply pass the currentState to the global buttonState, and return.

    If the buttonState is Up (i.e. the Button was not being pressed/held) then we need to record the reference time (millis()) so that we can begin counting 3 seconds. I have added a Serial.println() call so you can see the state change in your Serial Monitor.

    If neither of the first two conditional states is true, then – by deduction – the Button is already being held down, and we need to check for 3 seconds elapsing.

    If 3 or more seconds have passed, we want to initiate Discovery. I’ve added another Serial.println() call at this point so you can see this Mode change in your Serial Monitor.

    We set discoveryStart to the current millis() reference time so that we can enforce our 30 second Timeout.

    Note that we will implement startDiscovery() shortly. For now, it is unimportant, because we have provided a stub for this method already.

    Implement loopDiscovering()

    So, we now have a Sketch that can respond to holding down a button for 3 seconds, and will invoke our method to broadcast the Mac Address via Bluetooth Low Energy (BLE).

    However, this Discovery mode must be able to be cancelled by the user using the same Button, and must Time Out after 30 seconds.

    Let’s implement this now:

    // The Loop routine when our Device is in Discovery Mode
    inline void loopDiscovering() {
      flashBlueLED();
    
      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 Discovery!
        Serial.println("Cancelling Discovery");
        deviceMode = Waiting;
        setRedLED(true);
        digitalWrite(PIN_LED_BLUE, LOW); // Ensure Blue LED is OFF
        buttonHoldStart = millis();
        stopDiscovery();
      }
    }

    So, we can now initiating Discovery Mode by holding our Button down for 3 seconds. We can prematurely cancel Discovery Mode by holding the same Button down for 3 seconds, and Discovery Mode will cancel on its own after 30 seconds.

    Almost Ready to startDiscovery()

    Now we get to the heart of things! We’re about to start using BLE.

    First, we need to add the Includes for the BLE libraries.

    #include <BLEDevice.h>
    #include <BLEUtils.h>
    #include <BLEServer.h>
    #include <esp_wifi.h>
    #include <WiFi.h>
    
    inline void startDiscovery() {
      // This method will switch on BLE and set it up to broadcast the WiFi Mac Address
    }
    
    inline void stopDiscovery() {
      // This method will switch off BLE.
    }

    With the Includes in place, we need to make a small modification to setup() to initialise BLE.

    void setup() {
      // Initialise Serial first
      Serial.begin(115200); // Set Serial Monitor to 115200 baud
    
      BLEDevice::init("Flowduino Auto-Discovery Demo");
    ...
    }

    This additional line will initialise BLE. We put the instruction in setup() because we only want to do this one time.

    Note that the Parameter Value given will reflect the Name that shows up when scanning for BLE devices in range.

    Lastly, we need to define two UUIDs. I’ll explain the reason why shortly.

    #define UUID_SERVICE          "d91fdc86-46f8-478f-8dec-ebdc0a1188b2"
    #define UUID_CHARACTERISTIC   "56100987-749a-4014-bc22-0be2f5af59d0"
    
    inline void startDiscovery() {
      // This method will switch on BLE and set it up to broadcast the WiFi Mac Address
    }

    These two UUIDs will need to be shared in the code between the Master and Slave devices. You should make sure both of them are unique to your compatible ESP32 projects.

    Duplicate this Sketch!!!

    What we have written here forms the foundation of both the Master and Slave devices. At this point, it is far quicker to duplicate the code we presently have, so that we can focus the remainder of our efforts on implementing startDiscovery() and stopDiscovery().

    Once you have duplicated the Sketch, return the original (for the Master device) and we shall begin implementing Master startDiscovery().

    Program Too Big!

    Arduino IDE - Program Too Big error
    Arduino IDE – Program Too Big error

    You may see the above error when trying to build your Sketch.

    This is, unfortunately, normal! At the time of writing, the BLE libraries from Espressif are, for want of a better description, profoundly massive.

    Hopefully, Espressif will resolve this issue in future updates to the libraries.

    In the meantime, we need to change our selected partitioning scheme to compensate.

    In the Menu, go to Tools > Partition Scheme, and select Huge APP (3MB No OTA/1MB SPIFFS).

    Note that this Partition Scheme will disable the ability to deploy Over-The-Air Updates to your ESP32. For the sake of this article, that is not important.
    You may need to define a Custom Partition Scheme for your own project(s), however.

    Implement startDiscovery()

    This is where our code becomes distinct between Master and Slave.

    BLEServer *bleServer;
    BLEService *bleService;
    BLECharacteristic *bleCharacteristic;
    BLEAdvertising *bleAdvertising;
    bool bleClientConnected = false;
    unsigned long discoveredAt;
    
    class BLECallbacks: public BLEServerCallbacks {
       void onConnect(BLEServer* pServer) {
          Serial.println("BLE Client Connected!");
          bleClientConnected = true;
          digitalWrite(PIN_LED_BLUE, HIGH); // Keep the Blue LED On!
        };
    
        void onDisconnect(BLEServer* pServer) {
          Serial.println("BLE Client Disconnected!");
          bleClientConnected = false;
          deviceMode = Discovered;
          discoveredAt = 0;
        } 
    };
    
    inline void startDiscovery() {
      if (bleServer == nullptr) {
        Serial.println("First Time Discovering");
        // Get the MAC Address
        WiFi.mode(WIFI_MODE_STA);
        uint8_t mac[6];
        esp_wifi_get_mac(WIFI_IF_STA, mac);
    
        // Prepare our BLE Server
        bleServer = BLEDevice::createServer();
        bleServer->setCallbacks(new BLECallbacks());
    
        // Prepare our Service
        bleService = bleServer->createService(UUID_SERVICE);
    
        // A Characteristic is what we shall use to provide Clients/Slaves with our MAC Address.
        bleCharacteristic = bleService->createCharacteristic(UUID_CHARACTERISTIC, BLECharacteristic::PROPERTY_READ);
    
        // Provide our Characteristic with the MAC Address "Payload"
        bleCharacteristic->setValue(&mac[0], 6);
        // Make the Property visible to Clients/Slaves.
        bleCharacteristic->setBroadcastProperty(true);
    
        // Start the BLE Service
        bleService->start();
      
        // Advertise it!
        bleAdvertising = BLEDevice::getAdvertising();
        bleAdvertising->addServiceUUID(UUID_SERVICE);
        bleAdvertising->setScanResponse(true);
        bleAdvertising->setMinPreferred(0x06);
        bleAdvertising->setMinPreferred(0x12);
        BLEDevice::startAdvertising();
        return;
      }
      // Start the BLE Service
      bleService->start();
    
      // Advertise it!
      bleAdvertising = BLEDevice::getAdvertising();
      BLEDevice::startAdvertising();
    }

    There’s a lot happening here, so let’s break it down.

    BLEServer *bleServer;
    BLEService *bleService;
    BLECharacteristic *bleCharacteristic;
    BLEAdvertising *bleAdvertising;
    bool bleClientConnected = false;
    unsigned long discoveredAt;

    Declaration of our essential Variables. We place these outside of the scope of startDiscovery() because, as you will see shortly, we need to be able to access them from stopDiscovery() and an update we shall make to loopDiscovering().

    class BLECallbacks: public BLEServerCallbacks {
       void onConnect(BLEServer* pServer) {
          Serial.println("BLE Client Connected!");
          bleClientConnected = true;
          digitalWrite(PIN_LED_BLUE, HIGH); // Keep the Blue LED On!
        };
    
        void onDisconnect(BLEServer* pServer) {
          Serial.println("BLE Client Disconnected!");
          bleClientConnected = false;
          deviceMode = Discovered;
          discoveredAt = 0;
        } 
    };

    The BLE library uses Callbacks to notify our implementation when a device Connects and Disconnects. Indeed, there are many other Callback Methods we can override and implement in the BLEServerCallbacks class, but we need only these two for this demonstration.

    For our purposes, we shall update the state of bleClientConnected on both Connect and Disconnect, and we shall call stopDiscovery() when a device Disconnects.

    inline void startDiscovery() {
      if (bleServer == nullptr) {
        // Get the MAC Address
        WiFi.mode(WIFI_MODE_STA);
        uint8_t mac[6];
        esp_wifi_get_mac(WIFI_IF_STA, mac);
    ...

    If we have already invoked Discovery Mode, we don’t need to reinitialise most things.

    If it’s the first time, we need to begin by getting the WiFi MAC Address. To do this, we first set WiFi.mode() to WIFI_MODE_STA, which means “Station” mode. If we don’t do this, we do not get the correct MAC Address for ESP-Now.

    The MAC Address is then placed into an Array called mac.

    ...
        // Prepare our BLE Server
        bleServer = BLEDevice::createServer();
        bleServer->setCallbacks(new BLECallbacks());
    ...

    This initialises the BLE Server, and attaches an instance of our Callback Class, ensuring that every time a Client Connects or Disconnects, our Implementation is invoked.

    ...
        // Prepare our Service
        bleService = bleServer->createService(UUID_SERVICE);
    ...

    This creates our BLE Service, bearing the UUID we defined earlier.

    ...
        // A Characteristic is what we shall use to provide Clients/Slaves with our MAC Address.
        bleCharacteristic = bleService->createCharacteristic(UUID_CHARACTERISTIC, BLECharacteristic::PROPERTY_READ);
    
        // Provide our Characteristic with the MAC Address "Payload"
        bleCharacteristic->setValue(&mac[0], 6);
        // Make the Property visible to Clients/Slaves.
        bleCharacteristic->setBroadcastProperty(true);
    
        // Start the BLE Service
        bleService->start();
      
        // Advertise it!
        bleAdvertising = BLEDevice::getAdvertising();
        bleAdvertising->addServiceUUID(UUID_SERVICE);
        bleAdvertising->setScanResponse(true);
        bleAdvertising->setMinPreferred(0x06);
        bleAdvertising->setMinPreferred(0x12);
        BLEDevice::startAdvertising();
        return;
      }
    ...

    This creates the Characteristic bearing the UUID we defined earlier, containing our MAC Address as a READ-ONLY Property/Value.

    ...
      // Start the BLE Service
      bleService->start();
    
      // Advertise it!
      bleAdvertising = BLEDevice::getAdvertising();
      BLEDevice::startAdvertising();
    }

    The above occur whether it is the first time we enter Discovery Mode, or any subsequent time we enter Discovery Mode.

    In effect, this starts our BLE Service, and begins Advertising/Broadcasting its availability to all scanning Clients within range.

    Implementing stopDiscovery()

    inline void stopDiscovery() {
      BLEDevice::stopAdvertising();
      bleService->stop();
    }

    This method is pretty explanatory. All it does is stop BLE from Advertising our Device to nearby Devices, and stops the BLE Service.

    Modify loopDiscovering()

    // The Loop routine when our Device is in Discovery Mode
    inline void loopDiscovering() {
      if (bleClientConnected) { return; }
      flashBlueLED();

    We add the new first line of loopDiscovering() to prevent Discovery Mode from Timing Out prematurely.

    Implementing loopDiscovered()

    // The Loop routine when our Device is in Discovery Mode
    inline void loopDiscovered() {
      // Because we cannot call stopDiscovery from a Callback method...
      if (discoveredAt == 0) {
        stopDiscovery();
        discoveredAt = millis();
        return;
      }
      
      if (millis() > discoveredAt + BUTTON_HOLD_TIME) {
        digitalWrite(PIN_LED_BLUE, LOW);
        deviceMode = Waiting;
        Serial.println("Going back to Waiting mode");
      }
    }

    The above code will wait for 3 seconds after a successful Discovery, keeping the Blue LED on for that duration. After 3 seconds, the Blue LED will be switched off, and the Device will return to Waiting Mode.

    It is extremely important to understand that we cannot call stopDiscovery() from within the onDisconnect() callback method. Doing so will make it impossible to re-enter Discovery mode.

    To conclude Part Two…

    This segment has covered a considerable amount of ground, and we wish to avoid making the quantity of information overwhelming. With that in mind, we shall conclude Part Two here.

    We now have an ESP32 that, by holding a button for 3 seconds, can be discovered by other devices using Bluetooth Low Energy (BLE), and provides its WiFi MAC Address.

    We have prepared the wiring, the common code, and addressed a common problem that occurs when using BLE on the ESP32.

    There is a considerable amount of information in this article, so if you have just “skimmed” it, you may wish to go through it again.

    In Part Three

    … we shall set up another ESP32 as a Slave, to automatically discover the ESP32 we have programmed in this article.

    Don’t worry… we won’t have to rewrite everything we had to write here in Part Two!

    We get to share a significant amount of the effort between devices, so Part Three will be a more concise segment in this series.

    Thank you for reading!

    I want to finish by thanking you for taking the time to read this article. I hope you have found the information herein useful.

    Should you have any feedback or questions, you are more than welcome to leave a comment on this article, and responses will be given (where appropriate) at the earliest opportunity.

    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

    7 COMMENTS

    1. Great article! Using the NimBLE stack could help you reduce the size of your program. It is a slimed down version of the standard Bluetooth stack which only support BLE.

      • I haven’t yet looked at NimBLE… but anything that reduces overall space and memory consumption on an MCU is a huge win in my book, so I’ll give it a look and publish the results. I’ll be finishing up Part Three this coming week. Ideally on Tuesday. It was delayed due to major house renovations I’ve just completed.

    2. Hi there,

      This is a really helpfull series and definately what I need to impliment my current project. Just wondering if there is a part 3? It isn’t linked in this article.

      Many Thanks

    3. I successfully followed your instructions yesterday. All worked fine. Today I wanted to debug your simple sketch again. Without changing anything I get the error { Failed to launch GDB: .pioinit: 11: Error in sourced command file: Remote communication error. Target disconnected.: Success. (from interpreter-exec console “source.pioinit”) }

    Leave a Reply to 1kc 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.