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.
hi man! where is your part 4 ?
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).
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.
I think this example on YouTube shows, that it is possible to use broadcast.
https://www.youtube.com/watch?v=w4R9VoY96h8
Also looking forward to Part 4 soon!
Hello Simon,
Did Part 4 of this excellent series ever get published?
Regards
How’s part 4 and 5 coming along?
Hello – please publish the final parts! This is great