راهنمای جامع راه اندازی BLE یا بلوتوث کم انرژی ESP32 با آردوینو IDE

نویسنده

فهرست مطالب

مقدمه

امروزه ارتباط بلوتوث، یکی از متدوال ترین و پرکاربردترین روش های انتقال اطلاعات بین دستگاه ها می باشد و شما در طول روز، خواسته یا ناخواسته با آن سر و کار دارید. دستگاه های زیادی مانند گوشی های موبایل، ساعت ها و گجت های هوشمند، هدفون های بی سیم، ماوس و کیبوردهای بی سیم و … برای ارتباط با یکدیگر و انتقال اطلاعات از بلوتوث استفاده می کنند.

تراشه های ESP32 علاوه بر وای فای، دارای ارتباط بلوتوث نیز هستند و امکان ارتباط با دستگاه های دیگر با این ارتباط را نیز فراهم می کنند.

آنچه در این آموزش یاد می گیرید

  • BLE چیست؟
  • معماری BLE و مدهای کاری آن
  • راه اندازی BLE در ماژول ESP32 با استفاده از نرم افزار Arduino IDE
  • خواندن اطلاعات از روی یک ESP32 با استفاده از گوشی موبایل از طریق ارتباط BLE
  • اتصال دو ESP32 به یکدیگر از طریق ارتباط BLE

معرفی ارتباط بلوتوث

بلوتوث یک تکنولوژی بی سیم استاندارد و متعارف برای ارسال و دریافت اطلاعات در فاصله ی کم است و بیش از 20 سال است که مورد استفاده قرار می گیرد. از مزایای بلوتوث می توان به مصرف توان و هزینه ی کم اشاره کرد. ارتباط بلوتوث را می توان به دو دسته ی کلاسیک (نوع متداول آن) و کم توان (BLE) تقسیم کرد.

ESP32، هردو نوع ارتباط بلوتوث کلاسیک و کم توان را پشتیبانی می کند.

BLE چیست؟

BLE مخفف عبارت Bluetooth Low Energy به معنای بلوتوث کم مصرف است. در سال 2001، محققان شرکت نوکیا (Nokia) با ایجاد تغییراتی در بلوتوث های متداول و کم کردن مصرف توان آن، به دنبال نسخه ی جدیدی از این ارتباط بودند. نتایج این تحقیقات در سال 2004 به صورت تئوری منتشر شد.

سپس شرکت های مختلفی از جمله Logitech (در پروژه ی MIMOSA) برای بهبود این ارتباط تلاش کردند و درنهایت در سال 2006، نمونه ی عمومی این بلوتوث با نام Wibree منتشر شد که مصرف انرژی آن تقریبا 10 برابر کم تر از بلوتوث های متداول بود.

پس از مذاکرات و توافقاتی که با Bluetooth Special Interest Group انجام شد، در سال 2010 هسته ی بلوتوث نسخه ی 4 با همراه داشتن قابلیت بلوتوث کم انرژی منتشر شد. این بلوتوث با نام Bluetooth Smart نیز شناخته می شود.

اکثر دستگاه هایی که بلوتوث نسخه ی 4 و یا بالاتر را دارا هستند، ارتباط BLE را نیز پشتیبانی می کنند.

مقایسه ی بلوتوث کلاسیک و کم مصرف را می توانید در جدول زیر مشاهده کنید.

نگاه اجمالی به معماری BLE

پروتکل بلوتوث کم مصرف از بخش ها ولایه های مختلفی تشکیل شده است. هدف این بخش تنها معرفی برخی از بخش های مهم آن است. درصورت احتیاج به توضیحات کامل تر به فایل راهنمای بلوتوث ESP32 و سایت Bluetooth SIG مراجعه کنید.

معرفی پروتکل GAP

GAP مخفف Generic Access Profile است و از طریق آن دستگاه بلوتوث شما برای دیگر دستگاه های اطراف، قابل مشاهده (Visible) می شود. همچنین GAP نحوه ی اتصال و ارتباط دو دستگاه با بلوتوث را کنترل می کند.

GAP برای دستگاه ها چهار نقش مختلف تعیین می کند که هر دستگاه می تواند یکی از این نقش ها را دارا باشد:

پخش کننده (Broadcaster): دستگاهی که فقط وظیفه ی ارسال بسته های (packets) اعلان (advertising) را دارد تا دستگاه های مشاهده کننده بتوانند آن را شناسایی کنند. این دستگاه تنها بسته های اعلان ها را منتشر می کند و نمی تواند به دیگر دستگاه ها متصل شود.
مشاهده کننده (Observer): این دستگاه وظیفه ی اسکن کردن محیط برای یافتن دستگاه های پخش کننده و گزارش کردن اطلاعات اسکن به برنامه (application) را دارد. این دستگاه تنها قادر به ارسال درخواست های اسکن می باشد و نمی تواند به دیگر دستگاه ها متصل شود.
دستگاه جانبی (Peripheral): دستگاه های جانبی معمولا دستگاه های کوچک و کم مصرفی هستند که می توانند با ارسال بسته های ارتباطی (connectable advertising packets)، به دیگر دستگاه ها متصل شوند. پس از اتصال، این دستگاه ها به عنوان اسلیو (Slave) شناخته می شوند. (مثل: سنسورها، تگ های مجاورتی BLE و …)
دستگاه مرکزی (Central): این دستگاه ها ارتباط با دیگر دستگاه ها را آغاز و مدیریت می کنند و  پس از ایجاد ارتباط، به عنوان مستر (Master) عمل می کنند. معمولا دستگاه مرکزی، دستگاهی با توانایی پردازش بالا و حافظه ی زیاد است (مثل: گوشی های تلفن همراه و تبلت ها و …)

از میان این نقش ها، دستگاه های جانبی و مرکزی عملکرد مهم تری دارند.

زمانی که دستگاه های جانبی بخواهند به دستگاه های مرکزی متصل شوند، ابتدا یک بازه های زمانی (advertising interval) را برای مشخص شدن زمان ارتباط تعریف می کنند و سپس در این بازه ها شروع به ارسال بسته های اعلان (advertising packets) می کنند (در هر بازه یک بار بسته ها را اسال می کنند). پس از این که این بسته ها توسط دستگاه مرکزی دریافت شد، پاسخی از دستگاه مرکزی به دستگاه جانبی ارسال می شود و ارتباط برقرار می شود.

زمانی که اتصال بین دستگاه مرکزی و جانبی برقرار شد، ارسال بسته های اعلان توسط دستگاه جانبی قطع می شود و باقی دستگاه های مرکزی، دیگر قادر به شناسایی دستگاه جانبی نیستند.

پس از ایجاد ارتباط از طریق GAP، با استفاده از GATT، ارسال و دریافت اطلاعات میسر می شود.

معرفی پروتکل GATT

ابتدا به وسیله ی GAP دو دستگاه یکدیگر را شناسایی کرده و به یکدیگر متصل می شوند و سپس با استفاده از GATT به انتقال اطلاعات می پردازند.

GATT مخفف Generic Attribute Profile است. وظیفه ی GATT تعریف کردن راهی برای ارسال و دریافت اطلاعات دو دستگاه متصل شده با بلوتوث های کم انرژی (BLE) است.

تبادل اطلاعات در GATT توسط دو مفهوم Services و Characteristics انجام می شود.

GATT برای ارسال و دریافت اطلاعات از پروتکل معروف Attribute Protocol یا همان ATT استفاده می کند که Services و Characteristics ها و دیتا های مرتبط را در جدولی (LUT) ذخیره می کند.

نحوه ی ارتباط دستگاه ها به وسیله ی GATT

مکانیزم ارتباطی GATT مشابه ارتباط سرور (Server) و کلاینت (Client) در مباحث شبکه است. در این ارتباط، می توان هر دستگاه جانبی را به عنوان یک سرور، و دستگاه مرکزی را به عنوان کلاینت در نظر گرفت.

هر دستگاه جانبی (سرور) دارای یک جدول ATT و مشخصات (Characteristic) و خدمات (Service) آن دستگاه است.

در زمان ایجاد ارتباط بین دستگاه جانبی و مرکزی، یک بازه ی زمانی (Connection Interval) برای مشخص شدن زمان ارتباط توسط دستگاه جانبی پیشنهاد می شود و دستگاه مرکزی سعی می کند تا در این بازه خط ارتباطی را بررسی کند که آیا اطلاعات جدیدی موجود است یا نه. البته ممکن است دستگاه مرکزی در آن بازه ی زمانی مشغول انجام عملیاتی مثل ارتباط با دستگاه جانبی دیگری باشد، به همین دلیل دستگاه مرکزی همیشه مطیع این بازه ی زمانی نیست.

شکل زیر نمایی ساده از ارتباط بین سرور و کلاینت را نشان می دهد.

همان طور که از شکل فوق هم مشخص است، همواره شروع درخواست با دستگاه مرکزی (سرور) است و دستگاه جانبی (کلاینت) به آن درخواست پاسخ می دهد.

پروفایل، خدمات و مشخصات

GATT شامل شبکه ای از بخش های مختلف به نام های پروفایل ها (Profile)، خدمات (Service) و مشخصات (Characteristic) است. این بخش ها به صورت سلسله مراتبی با یکدیگر مرتبط هستند.

پروفایل ها

پروفایل مجموعه ای از خدمات هستند که توسط گروه Bluetooth SIG (گروه متولی ارتباط بلوتوث) و یا توسط طراحان دستگاه های جانبی، تعریف می شوند. برای مثال پروفایل سنسور های اندازه گیری فشار خون را می توانید از اینجا دانلود کنید.

خدمات (سرویس ها)

خدمات (Services)، مجموعه ای از اطلاعات ساده (مثل خواندن اطلاعات یک سنسور) هستند. هر خدمت، به زیر بخش هایی به نام “مشخصات” تقسیم می شود.

خدمات مختلفی از جمله خواندن میزان باتری، فشار خون، نرخ ضربان قلب و … توسط گروه  Bluetooth SIG تعریف شده است. هر خدمت دارای یک کد (ID) منحصر به فرد به نام UUID است. UUID می تواند 16 بیتی (برای خدمات تعریف شده به صورت رسمی) و یا 128 بیتی (برای خدماتی که توسط دیگر طراحان اضافه می شود) باشد. برای مثال UUID 16 بیتی مرتبط با فشار خون، 0X1810 است.

جدول کامل خدمات مختلفی که به صورت رسمی تعریف شده اند را می توانید از اینجا ببینید.

مشخصات

هر خدمت به یک یا چند مشخصه (Characteristic ) شکسته می شود. برای مثال خواندن اطلاعات شتاب خطی، دارای 3 مشخصه ی شتاب در جهت محور x، y و z است.

مشابه خدمات، مشخصات نیز دارای UUID 16 یا 128 بیتی منحصر به فرد هستند. جدول کامل مشخصات رسمی را می توانید در اینجا مشاهده کنید.

لوازمی که به آن احتیاج دارید

قطعات مورد نیاز

ESP32 × 2

تنها برای پیاده سازی مثال دوم نیاز به 2 ماژول ESP32 دارید. باقی مثال ها را می توانید با یک ماژول پیاده سازی کنید.

نرم افزار های مورد نیاز

Arduino IDE
nRF Connect for Android
nRF Connect for IOS

راه اندازی BLE ماژول ESP32 در نرم افزار آردوینو

برای استفاده از ارتباط بلوتوث کم انرژی (BLE) در ماژول های ESP32 به وسیله ی نرم افزار Arduino IDE، ابتدا باید کتابخانه های مورد نیاز را نصب کرده و بردهای ESP32 را به نرم افزار اضافه کرده باشید.

اگر اولین بار است که از ESP32 استفاده می کنید و کتابخانه ها را نصب نکرده اید، می توانید به آموزش راه اندازی ESP32 مراجعه کنید.

کتابخانه ی BLE library مثال های کاملی برای راه اندازی بلوتوث کم انرژی ESP32 دارد.

نکته

برای مشاهده ی مثال های کتابخانه ی ESP32 ابتدا باید از منوی Tools نوع برد خود را انتخاب کرده باشید.

راه اندازی BLE در مد سرور

از مسیر File>Examples>ESP32 BLE Arduino مثال BLE_server را باز کنید.

کد

/*
    Based on Neil Kolban example for IDF: https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLE%20Tests/SampleServer.cpp
    Ported to Arduino ESP32 by Evandro Copercini
    updates by chegewara
*/

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>

// See the following for generating UUIDs:
// https://www.uuidgenerator.net/

#define SERVICE_UUID        "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"

void setup() {
  Serial.begin(115200);
  Serial.println("Starting BLE work!");

  BLEDevice::init("Long name works now");
  BLEServer *pServer = BLEDevice::createServer();
  BLEService *pService = pServer->createService(SERVICE_UUID);
  BLECharacteristic *pCharacteristic = pService->createCharacteristic(
                                         CHARACTERISTIC_UUID,
                                         BLECharacteristic::PROPERTY_READ |
                                         BLECharacteristic::PROPERTY_WRITE
                                       );

  pCharacteristic->setValue("Hello World says Neil");
  pService->start();
  // BLEAdvertising *pAdvertising = pServer->getAdvertising();  // this still is working for backward compatibility
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pAdvertising->setScanResponse(true);
  pAdvertising->setMinPreferred(0x06);  // functions that help with iPhone connections issue
  pAdvertising->setMinPreferred(0x12);
  BLEDevice::startAdvertising();
  Serial.println("Characteristic defined! Now you can read it in your phone!");
}

void loop() {
  // put your main code here, to run repeatedly:
  delay(2000);
}

توضیح بخش های مختلف کد

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>

دستورات فوق، کتابخانه های موردنیاز برای ساخت سرور BLE را به کد اضافه می کند.

#define SERVICE_UUID        "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"

مطابق توضیحات فوق، یک UUID برای Service و یک UUID برای Characteristic تعریف می شود. این دو UUID به صورت رندوم تولید شده اند. می توانید UUID دلخواه خود را با استفاده از این سایت بسازید.
در بخش setup، تنظیمات مورد نیاز برای راه اندازی سرور انجام می شود.

Serial.begin(115200);

برای مشاهده ی نتایج در پنجره ی سریال، ارتباط سریال با بادریت 115200 بیت بر ثانیه شروع به کار می کند.

BLEDevice::init("Long name works now");
با دستور init می توانید نام سرور خود را وارد کنید. به جای عبارت “Long name works now” نام مدنظر خود را وارد کنید.
BLEServer *pServer = BLEDevice::createServer();

با این دستور، سروری با نامی که تعریف کرده اید، ساخته می شود.

BLEService *pService = pServer->createService(SERVICE_UUID);
BLECharacteristic *pCharacteristic = pService->createCharacteristic(
                                       CHARACTERISTIC_UUID,
                                       BLECharacteristic::PROPERTY_READ |
                                       BLECharacteristic::PROPERTY_WRITE
                                       );

دو دستور فوق Service و Characteristic که از قبل تعریف کرده اید را روی سرور ایجاد می کند.
PROPERTY_READ، PROPERTY_WRITE و … اجازه های دسترسی به این Characteristic را مشخص می کنند. مثلا برای این Characteristic، کلاینتی که به این سرور متصل می شود، هم می تواند مقدار Characteristic را بخواند و هم می تواند مقدار آن را تغییر دهد.
بعد از ساخت Characteristic شما می توانید با متد setValue() به آن مقدار دهید.

pCharacteristic->setValue("Hello World says Neil");

در این مثال، مقدار Characteristic یک متن ساده (Hello World says Neil) است. شما بسته به نیاز خود می توانید آن را به متن دیگر و یا مقدار خروجی یک یا چند سنسور تغییر دهید.

pService->start();
پس از انجام تنظیمات سرور، با دستور start() می توانید سرور خود را راه اندازی کنید. پس از شروع به کار سرور، نیاز است تا سرور شما به طور پیوسته شروع به ارسال بسته های اعلان (Advertising) کند تا سرور شما برای دیگر دستگاه ها قابل مشاهده و شناسایی باشد.
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();

کد فوق وظیفه ی ارسال بسته های اعلان را دارد.
این مثال، یک نمونه ی ساده برای ساخت یک سرور BLE است و در بخش loop() کد، کاری انجام نمی شود. شما می توانید بر اساس نیاز خود، این بخش را تکمیل کنید. (برای مثال به BLE_notify مراجعه کنید.)

یافتن سرور ساخته شده با گوشی موبایل

من ابتدا نام سرور را به “My_ESP32_Server” و Characteristic را به ” Hello World from Electropeak ” تغییر دادم. شما می توانید مشخصات دلخواه خود را تعریف کنید.

پس از آپلود کد روی ESP32، بلوتوث گوشی خود را روشن کرده و نرم افزار nRF Connect (می توانید از دیگر نرم افزار های مشابه استفاده کنید) را باز کنید. با اسکن BLE های موجود، باید بتوانید سرور خود را پیدا کنید.

کلید “CONNECT” را بزنید تا به سرور خود متصل شوید. پس از اتصال به سرور می توانید اطلاعات آن را مشاهده کنید. برای مشاهده ی مقدار Characteristic، فلش به سمت پایین را در نرم افزار فشار دهید. (فلش دارای کادر سبز در شکل زیر)

راه اندازی BLE در مد کلاینت

از مسیر مثالها، مثال BLE_client را باز کنید.

کد

/**
 * A BLE client example that is rich in capabilities.
 * There is a lot new capabilities implemented.
 * author unknown
 * updated by chegewara
 */

#include "BLEDevice.h"
//#include "BLEScan.h"

// The remote service we wish to connect to.
static BLEUUID serviceUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b");
// The characteristic of the remote service we are interested in.
static BLEUUID    charUUID("beb5483e-36e1-4688-b7f5-ea07361b26a8");

static boolean doConnect = false;
static boolean connected = false;
static boolean doScan = false;
static BLERemoteCharacteristic* pRemoteCharacteristic;
static BLEAdvertisedDevice* myDevice;

static void notifyCallback(
  BLERemoteCharacteristic* pBLERemoteCharacteristic,
  uint8_t* pData,
  size_t length,
  bool isNotify) {
    Serial.print("Notify callback for characteristic ");
    Serial.print(pBLERemoteCharacteristic->getUUID().toString().c_str());
    Serial.print(" of data length ");
    Serial.println(length);
    Serial.print("data: ");
    Serial.println((char*)pData);
}

class MyClientCallback : public BLEClientCallbacks {
  void onConnect(BLEClient* pclient) {
  }

  void onDisconnect(BLEClient* pclient) {
    connected = false;
    Serial.println("onDisconnect");
  }
};

bool connectToServer() {
    Serial.print("Forming a connection to ");
    Serial.println(myDevice->getAddress().toString().c_str());
    
    BLEClient*  pClient  = BLEDevice::createClient();
    Serial.println(" - Created client");

    pClient->setClientCallbacks(new MyClientCallback());

    // Connect to the remove BLE Server.
    pClient->connect(myDevice);  // if you pass BLEAdvertisedDevice instead of address, it will be recognized type of peer device address (public or private)
    Serial.println(" - Connected to server");

    // Obtain a reference to the service we are after in the remote BLE server.
    BLERemoteService* pRemoteService = pClient->getService(serviceUUID);
    if (pRemoteService == nullptr) {
      Serial.print("Failed to find our service UUID: ");
      Serial.println(serviceUUID.toString().c_str());
      pClient->disconnect();
      return false;
    }
    Serial.println(" - Found our service");


    // Obtain a reference to the characteristic in the service of the remote BLE server.
    pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
    if (pRemoteCharacteristic == nullptr) {
      Serial.print("Failed to find our characteristic UUID: ");
      Serial.println(charUUID.toString().c_str());
      pClient->disconnect();
      return false;
    }
    Serial.println(" - Found our characteristic");

    // Read the value of the characteristic.
    if(pRemoteCharacteristic->canRead()) {
      std::string value = pRemoteCharacteristic->readValue();
      Serial.print("The characteristic value was: ");
      Serial.println(value.c_str());
    }

    if(pRemoteCharacteristic->canNotify())
      pRemoteCharacteristic->registerForNotify(notifyCallback);

    connected = true;
}
/**
 * Scan for BLE servers and find the first one that advertises the service we are looking for.
 */
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
 /**
   * Called for each advertising BLE server.
   */
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    Serial.print("BLE Advertised Device found: ");
    Serial.println(advertisedDevice.toString().c_str());

    // We have found a device, let us now see if it contains the service we are looking for.
    if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(serviceUUID)) {

      BLEDevice::getScan()->stop();
      myDevice = new BLEAdvertisedDevice(advertisedDevice);
      doConnect = true;
      doScan = true;

    } // Found our server
  } // onResult
}; // MyAdvertisedDeviceCallbacks


void setup() {
  Serial.begin(115200);
  Serial.println("Starting Arduino BLE Client application...");
  BLEDevice::init("");

  // Retrieve a Scanner and set the callback we want to use to be informed when we
  // have detected a new device.  Specify that we want active scanning and start the
  // scan to run for 5 seconds.
  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setInterval(1349);
  pBLEScan->setWindow(449);
  pBLEScan->setActiveScan(true);
  pBLEScan->start(5, false);
} // End of setup.


// This is the Arduino main loop function.
void loop() {

  // If the flag "doConnect" is true then we have scanned for and found the desired
  // BLE Server with which we wish to connect.  Now we connect to it.  Once we are 
  // connected we set the connected flag to be true.
  if (doConnect == true) {
    if (connectToServer()) {
      Serial.println("We are now connected to the BLE Server.");
    } else {
      Serial.println("We have failed to connect to the server; there is nothin more we will do.");
    }
    doConnect = false;
  }

  // If we are connected to a peer BLE Server, update the characteristic each time we are reached
  // with the current time since boot.
  if (connected) {
    String newValue = "Time since boot: " + String(millis()/1000);
    Serial.println("Setting new characteristic value to \"" + newValue + "\"");
    
    // Set the characteristic's value to be the array of bytes that is actually a string.
    pRemoteCharacteristic->writeValue(newValue.c_str(), newValue.length());
  }else if(doScan){
    BLEDevice::getScan()->start(0);  // this is just eample to start scan after disconnect, most likely there is better way to do it in arduino
  }
  
  delay(1000); // Delay a second between loops.
} // End of loop

توضیح بخش های مختلف کد

#include "BLEDevice.h"

از کتابخانه ی BLEDevice برای ساخت کلاینت استفاده می شود.

static BLEUUID serviceUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b");
static BLEUUID    charUUID("beb5483e-36e1-4688-b7f5-ea07361b26a8");

مشابه حالت سرور، با دو خط فوق UUID های Service و Characteristic تعریف می شود. دقت کنید که این UUID ها باید با سرور یکی باشد.

static void notifyCallback(
  BLERemoteCharacteristic* pBLERemoteCharacteristic,
  uint8_t* pData,
  size_t length,
  bool isNotify) {
    Serial.print("Notify callback for characteristic ");
    Serial.print(pBLERemoteCharacteristic->getUUID().toString().c_str());
    Serial.print(" of data length ");
    Serial.println(length);
    Serial.print("data: ");
    Serial.println((char*)pData);
تابع notifyCallback برای نوتیفیکیشن دادن مقدار Characteristic خوانده شده استفاده می شود.
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
 /**
   * Called for each advertising BLE server.
   */
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    Serial.print("BLE Advertised Device found: ");
    Serial.println(advertisedDevice.toString().c_str());

    // We have found a device, let us now see if it contains the service we are looking for.
    if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(serviceUUID)) {

      BLEDevice::getScan()->stop();
      myDevice = new BLEAdvertisedDevice(advertisedDevice);
      doConnect = true;
      doScan = true;

    } // Found our server
  } // onResult
}; // MyAdvertisedDeviceCallbacks

کلاس فوق، محیط را برای یافتن یک سرور که Service های مورد نظر را داشته باشد جستجو می کند. این کلاس برروی تمامی سرور های اطراف چک می شود تا سروری که Service مورد نظر ما را دارد، پیدا شود و بلافاصله بعد از یافتن سرور مورد نظر، اسکن محیط را قطع می کند.
در بخش setup تنظیمات اولیه برای راه اندازی کلاینت انجام می شود.

void loop() {
  if (doConnect == true) {
    if (connectToServer()) {
      Serial.println("We are now connected to the BLE Server.");
    } else {
      Serial.println("We have failed to connect to the server; there is nothin more we will do.");
    }
    doConnect = false;
  }
  if (connected) {
    String newValue = "Time since boot: " + String(millis()/1000);
    Serial.println("Setting new characteristic value to \"" + newValue + "\"");
    
    // Set the characteristic's value to be the array of bytes that is actually a string.
    pRemoteCharacteristic->writeValue(newValue.c_str(), newValue.length());
  }else if(doScan){
    BLEDevice::getScan()->start(0);  // this is just eample to start scan after disconnect, most likely there is better way to do it in arduino
  }
  
  delay(1000); // Delay a second between loops.
}

در بخش loop اگر پرچم (flag) doConnect دارای مقدار true باشد، کلاینت به دنبال سرور موردنظر می گردید تا به آن متصل شود.
بعد از اتصال به سرور موردنظر، هر ثانیه یک بار مقدار Characteristic تغییر داده می شود. در این مثال، مقدار جدید Characteristic در هر لحظه برابر است با زمان سپری شده از زمان شروع به کار ESP32. شما می توانید به دلخواه این مقدار را تغییر دهید.

اتصال یک سرور و یک کلاینت به هم

روی یک ESP32 کد سرور و روی دیگری کد کلاینت را آپلود کنید. سپس پنجره ی سریال ESP32 که رو آن کد کلاینت است را باز کنید.

کلاینت، دو دستگاه سرور BLE را پیدا کرده است اما با توجه به این که مقدار UUID های تعریف شده در سرور My_ESP32_Server با مقادیر تعریف شده در کلاینت برابر است، کلاینت به این سرور متصل می شود.

راه اندازی BLE در مد notify

در این حالت ESP32  مانند یک سرور عمل می کند. با این تفاوت که در حالت عادی کلاینت از سرور درخواست اطلاعات می کند ولی در حالت notify تقریبا برعکس این اتفاق رخ می دهد. این حالت زمانی استفاده می شود که سرور بخواهد در صورت تغییر یک متغیر (مثلا تغییر دما)، آن را به به کلاینت اعلام کند.

از مسیر مثالها، مثال BLE_notify را باز کنید.

کد

/*
    Video: https://www.youtube.com/watch?v=oCMOYS71NIU
    Based on Neil Kolban example for IDF: https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLE%20Tests/SampleNotify.cpp
    Ported to Arduino ESP32 by Evandro Copercini
    updated by chegewara

   Create a BLE server that, once we receive a connection, will send periodic notifications.
   The service advertises itself as: 4fafc201-1fb5-459e-8fcc-c5c9c331914b
   And has a characteristic of: beb5483e-36e1-4688-b7f5-ea07361b26a8

   The design of creating the BLE server is:
   1. Create a BLE Server
   2. Create a BLE Service
   3. Create a BLE Characteristic on the Service
   4. Create a BLE Descriptor on the characteristic
   5. Start the service.
   6. Start advertising.

   A connect hander associated with the server starts a background task that performs notification
   every couple of seconds.
*/
#include 
#include 
#include 
#include 

BLEServer* pServer = NULL;
BLECharacteristic* pCharacteristic = NULL;
bool deviceConnected = false;
bool oldDeviceConnected = false;
uint32_t value = 0;

// See the following for generating UUIDs:
// https://www.uuidgenerator.net/

#define SERVICE_UUID        "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"


class MyServerCallbacks: public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
      deviceConnected = true;
    };

    void onDisconnect(BLEServer* pServer) {
      deviceConnected = false;
    }
};



void setup() {
  Serial.begin(115200);

  // Create the BLE Device
  BLEDevice::init("ESP32");

  // Create the BLE Server
  pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());

  // Create the BLE Service
  BLEService *pService = pServer->createService(SERVICE_UUID);

  // Create a BLE Characteristic
  pCharacteristic = pService->createCharacteristic(
                      CHARACTERISTIC_UUID,
                      BLECharacteristic::PROPERTY_READ   |
                      BLECharacteristic::PROPERTY_WRITE  |
                      BLECharacteristic::PROPERTY_NOTIFY |
                      BLECharacteristic::PROPERTY_INDICATE
                    );

  // https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.descriptor.gatt.client_characteristic_configuration.xml
  // Create a BLE Descriptor
  pCharacteristic->addDescriptor(new BLE2902());

  // Start the service
  pService->start();

  // Start advertising
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pAdvertising->setScanResponse(false);
  pAdvertising->setMinPreferred(0x0);  // set value to 0x00 to not advertise this parameter
  BLEDevice::startAdvertising();
  Serial.println("Waiting a client connection to notify...");
}

void loop() {
    // notify changed value
    if (deviceConnected) {
        pCharacteristic->setValue((uint8_t*)&value, 4);
        pCharacteristic->notify();
        value++;
        delay(1000); // bluetooth stack will go into congestion, if too many packets are sent, in 6 hours test i was able to go as low as 3ms
    }
    // disconnecting
    if (!deviceConnected && oldDeviceConnected) {
        delay(500); // give the bluetooth stack the chance to get things ready
        pServer->startAdvertising(); // restart advertising
        Serial.println("start advertising");
        oldDeviceConnected = deviceConnected;
    }
    // connecting
    if (deviceConnected && !oldDeviceConnected) {
        // do stuff here on connecting
        oldDeviceConnected = deviceConnected;
    }
}

توضیح بخش های مختلف کد

پس از اضافه کردن کتابخانه های مورد نیاز باید UUID هایی برای Service و  Characteristic تعریف کنید.
class MyServerCallbacks: public BLEServerCallbacks {
void onConnect(BLEServer* pServer) {
deviceConnected = true;
};

void onDisconnect(BLEServer* pServer) {
deviceConnected = false;
}
};
کلاس MyServerCallbacks در صورت اتصال کلاینت به سرور مقدار متغیر deviceConnected را true و در غیر این صورت مقدار آن را false می کند. در بخش setup تنظیمات اولیه انجام می شود. همان طور که قبلا هم اشاره شد، ESP32 در حالت سرور شروع به کار می کند.
BLEDevice::init("ESP32");
نام سرور خود را می توانید در کد فوق تغییر دهید.
pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
با استفاده از دو خط فوق، سروری با نامی که شما تعریف کرده اید ساخته می شود.
BLEService *pService = pServer->createService(SERVICE_UUID);
سپس سرویسی با UUID تعریف شده روی سرور ساخته می شود.
pCharacteristic = pService->createCharacteristic( CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_INDICATE );
پس از آن، یک Characteristic با UUID تعریف شده روی سرور ساخته می شود. PROPERTY_READ، PROPERTY_WRITE و … اجازه های دسترسی به این Characteristic را مشخص می کنند.
pService->start();
با دستور start() ، سرور تعریف شده، شروع به کار می کند.
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(false);
pAdvertising->setMinPreferred(0x0); // set value to 0x00 to not advertise this parameter
BLEDevice::startAdvertising();
با دستورات فوق، سرور شروع به ارسال بسته های اعلان (Advertising) می کند. در بخش loop می توانید کد اصلی خود را بنویسید. برای مثال، در این کد:
if (deviceConnected) {
pCharacteristic->setValue((uint8_t*)&value, 4);
pCharacteristic->notify();
value++;
delay(3); // bluetooth stack will go into congestion, if too many packets are sent, in 6 hours test i was able to go as low as 3ms
}
اگر کلاینتی به سروری که ساخته ایم متصل باشد، سرور هر 3 میلی ثانیه یک بار مقدار value را یک واحد افزایش می دهد و این تغییر را به کلاینت اطلاع (notify) می دهد.
هشدار

بهتر است زمان تغییر متغیر بیشتر از 3 میلی ثانیه باشد. در غیر این صورت ممکن است برخی از تغییرات از دست برود و به کلاینت ارسال نشود.

if (!deviceConnected && oldDeviceConnected) {
delay(500); // give the bluetooth stack the chance to get things ready
pServer->startAdvertising(); // restart advertising
Serial.println("start advertising");
oldDeviceConnected = deviceConnected;
} 

اگر اتصال بین کلاینت و سرور قطع شود، سرور مجددا شروع به ارسال بسته های اعلان می کند تا برای کلاینت ها قابل مشاهده باشد.

 

تست کد فوق با گوشی موبایل

ابتدا نام سرور به “My-ESP32_Notify” و زمان تغییر متغیر value به جای هر 3 میلی ثانیه یک بار، به هر ثانیه یک بار تغییر داده شده است.

برای مشاهده ی تغییرات متغیر value، کلیدی که در شکل زیر مشخص شده است را بزنید.

نکته

همانطور که در فیلم فوق نیز مشاهده می کنید، هربار که مقدار متغیر تغییر می کند، مقدار جدید خود به خود روی گوشی شما نمایش داده می شود، در صورتی که در حالت سرور (کد اول این آموزش) این قابلیت وجود نداشت و  برای هر بار خواندن مقدار Characteristic، کلاینت باید درخواستی به سرور می فرستاد.

از این مطلب خوشتان آمده؟

جدیدترین آموزش‌ها را از بهترین‌ها دریافت کنید

بیشتر بخوانید

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *