در این مقاله راه اندازی بلوتوث کم مصرف در ماژول ESP32 را با هم بررسی خواهیم کرد. همچنین روش اتصال گوشی هوشمند به آن، ارتباط دو ماژول ESP32 از طریق بلوتوث کم مصرف در نقش سرور و کلاینت، آموزش اسکن دستگاه های بلوتوث و راه اندازی بلوتوث کم مصرف در مد notify را بصورت مرحله به مرحله انجام خواهیم داد. همراه ما باشید.
معرفی بلوتوث کم مصرف
بلوتوث کم مصرف، یا به اختصار BLE، نوعی از بلوتوث است که در مصرف برق صرفه جویی می کند. هدف اصلی BLE انتقال مقادیر کمی اطلاعات (پهنای باند کم) از راه دور است. برخلاف بلوتوث کلاسیک که همیشه روشن است، BLE بطور مداوم در حالت آماده به کار (خواب) است، مگر در مواردی که اتصال برقرار شود. این امر منجر به مصرف برق بسیار کمی می شود. BLE تقریباً 100 برابر برق کمتری نسبت به بلوتوث کلاسیک مصرف می کند (بسته به موارد مورد استفاده). BLE نه تنها از ارتباط نقطه به نقطه، بلکه از حالت پخش دیتا و شبکه مش نیز پشتیبانی می کند.
با توجه به ویژگی های BLE، این بلوتوث برای برنامه هایی مناسب است که نیاز به تبادل مقادیر کمی اطلاعات بصورت دوره ای دارند. به عنوان مثال، BLE در صنایع مراقبت های بهداشتی، تناسب اندام، ردیابی، چراغ های روشنایی، امنیت و اتوماسیون خانگی بسیار کاربرد دارد. همچنین با توجه به اهمیت باتری و انرژی مصرفی در پروژه های IoT این حالت از بلوتوث می تواند در این کاربردها بسیار مفید باشد.
معماری BLE
پروتکل بلوتوث کم مصرف همانند تصویر زیر از بخش ها و لایه های مختلفی تشکیل شده است. در اینجا تنها به معرفی برخی از بخش های مهم می پردازیم.
معرفی پروتکل GAP
GAP مخفف Generic Access Profile است و از طریق آن دستگاه بلوتوث برای دیگر دستگاه های اطراف، قابل مشاهده (Visible) می شود. همچنین GAP نحوه ی اتصال و ارتباط دو دستگاه با بلوتوث را کنترل می کند.
GAP برای دستگاه ها چندین نقش و مد کاری مختلف تعیین می کند که هر دستگاه می تواند یکی از این مد ها را اجرا کند.
مدهای کاری بلوتوث کم مصرف
- سرور یا دستگاه مرکزی (Central) : ارتباط با دیگر دستگاه ها را آغاز و مدیریت می کند و پس از ایجاد ارتباط، به عنوان مستر (Master) عمل می کند. بنابراین توسط دستگاه های دیگر پیدا می شود و حاوی داده هایی است که مشتری می تواند بخواند. این دستگاه ها معمولا دستگاه هایی با توانایی پردازش بالا و حافظه ی زیاد می باشند (مثل: گوشی های تلفن همراه و تبلت ها و …).
- مشتری (Client or Slave) : دستگاه های نزدیک را اسکن می کند و هنگامی که سرور مورد نظر خود را پیدا می کند، یک اتصال برقرار می کند و به دیتا های ورودی گوش می دهد. این دستگاه ها کوچک و کم مصرف هستند و پس از اتصال، به عنوان مشتری یا اسلیو (Slave) شناخته می شوند. اگر مستر و مشتری به تبادل اطلاعات دو طرفه بپردازند آنگاه این ارتباط نقطه به نقطه نامیده می شود. مثل: سنسورها، تگ های مجاورتی BLE و …
- حالت پخش (Advertising) : دستگاهی که تنها وظیفه آن ارسال بسته های (packets) اعلان (advertising) است تا دستگاه های مشاهده کننده بتوانند آن را شناسایی کنند. این دستگاه نمی تواند به دیگر دستگاه ها متصل شود.
- مشاهده کننده (Observer): این دستگاه وظیفه ی اسکن کردن محیط برای یافتن دستگاه های پخش کننده و گزارش کردن اطلاعات اسکن به برنامه (application) را دارد. این دستگاه تنها قادر به ارسال درخواست های اسکن می باشد و نمی تواند به دیگر دستگاه ها متصل شود.
- شبکه مش (mesh networking) : تمام دستگاه ها متصل هستن و این اتصالات بسیار زیاد است. اگرچه پیاده سازی شبکه های پخش و مش امکان پذیر است، اما اخیراً توسعه یافته اند. بنابراین در حال حاضر نمونه های زیادی برای ESP32 اجرا نشده است.
معرفی پروتکل GATT
GATT مخفف Generic Attributes است و ساختار داده های سلسله مراتبی را تعریف می کند که در معرض دستگاه های متصل BLE قرار می گیرد. ابتدا به وسیله ی GAP دو دستگاه یکدیگر را شناسایی کرده و به یکدیگر متصل می شوند. سپس با استفاده از GATT به انتقال اطلاعات می پردازند. وظیفه ی GATT نحوه ارسال و دریافت اطلاعات استاندارد توسط دو دستگاه متصل شده با بلوتوث های کم مصرف (BLE) است. تبادل اطلاعات در GATT توسط دو مفهوم Services و Characteristics انجام می شود. GATT برای ارسال و دریافت اطلاعات از پروتکل معروف Attribute Protocol یا همان ATT استفاده می کند که Services و Characteristics ها و دیتا های مرتبط را در جدولی (LUT) ذخیره می کند. جهت دریافت اطلاعات بیشتر می توانید به سایت www.bluetooth.com مراجعه کنید.
راه اندازی بلوتوث کم مصرف و اتصال گوشی هوشمند به آن
برای شروع موارد زیر را از قبل آماده کنید:
- یک ماژول ESP32
- کابل سریال USB
- نرم افزار Arduino IDE
- اپلیکیشن BLE Scanner جهت اتصال گوشی به بلوتوث
برای راه اندازی این پروژه از مد سرور استفاده می شود و اتصال گوشی هوشمند به آن بررسی می شود. کتابخانه های مورد استفاده در این پروژه <BLEDevice.h> و <BLEUtils.h> و <BLEServer.h> می باشند. کد ساخت مد سرور این بلوتوث به صورت زیر است:
#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" class MyCallbacks: public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *pCharacteristic) { std::string value = pCharacteristic->getValue(); if (value.length() > 0) { Serial.println("*********"); Serial.print("New value: "); for (int i = 0; i < value.length(); i++) { Serial.print(value[i]); } Serial.println(); Serial.println("*********"); } } }; void setup() { Serial.begin(115200); Serial.println("1- Download and install an BLE scanner app in your phone"); Serial.println("2- Scan for BLE devices in the app"); Serial.println("3- Connect to ESP32-BLE_Server"); Serial.println("4- Go to CUSTOM CHARACTERISTIC in CUSTOM SERVICE and write something"); Serial.println("5- See the magic =)"); BLEDevice::init("ESP32-BLE-Server"); BLEServer *pServer = BLEDevice::createServer(); BLEService *pService = pServer->createService(SERVICE_UUID); BLECharacteristic *pCharacteristic = pService->createCharacteristic( CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE ); pCharacteristic->setCallbacks(new MyCallbacks()); pCharacteristic->setValue("Hello World"); pService->start(); BLEAdvertising *pAdvertising = pServer->getAdvertising(); pAdvertising->start(); } void loop() { delay(2000); }
خروجی این کد به شکل زیر خواهد بود:
پس از آپلود کد ابتدا بلوتوث گوشی هوشمند خود را فعال کرده و وارد اپلیکیشن شوید. در سربرگ Near By نام دستگاه های بلوتوث فعال نشان داده شده است. به دستگاه ESP32-BLE-Server متصل شوید. در قسمت CUSTOM SERVICE و با کلیک بر گزینه R پیام ارسالی از ماژول “Hello World” را مشاهده می کنید. با کلیک بر روی W کادری باز می شود که می توان در آن متنی را تایپ کرده و برای ماژول ارسال کنید و در سریال مانیتور Arduino IDE مشاهده کنید. تمام موارد گفته شده در تصاویر قابل مشاهده است.
راه اندازی بلوتوث کم مصرف ماژول ESP32 در مد سرور و کلاینت
در این پروژه قصد داریم بین دو ماژول ESP32 از طریق بلوتوث کم مصرف ارتباط برقرار کنیم و به تبادل اطلاعات بپردازیم. هر دستگاه BLE را می توان به عنوان سرور یا کلاینت پیکربندی کرد. ماژول ESP32 نیز از این قاعده مستثنی نیست.در واقع ESP32 می تواند به عنوان یک server حضور خود را اعلام کند تا کلاینت ها اطلاعات آن را بخوانند. همچنین می تواند به عنوان Client سرورها را اسکن کند و برای دریافت اطلاعات از سرور ارتباط برقرار کند. کتابخانه های مورد نیاز در این پروژه برای مد سرور <BLEDevice.h> و <BLEUtils.h> و <BLEServer.h> خواهند بود. کد زیر را در دستگاه سرور آپلود کنید:
#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" /* BLEServer *pServer = BLEDevice::createServer(); BLEService *pService = pServer->createService(SERVICE_UUID); BLECharacteristic *pCharacteristic = pService->createCharacteristic( CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE ); */ BLEServer *pServer; BLEService *pService; BLECharacteristic *pCharacteristic; void setup() { Serial.begin(115200); Serial.println("Starting BLE Server!"); BLEDevice::init("ESP32-BLE-Server"); pServer = BLEDevice::createServer(); pService = pServer->createService(SERVICE_UUID); pCharacteristic = pService->createCharacteristic( CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE ); /* 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!"); pService->start(); //BLEAdvertising *pAdvertising = pServer->getAdvertising(); 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(); //pAdvertising->start(); Serial.println("Characteristic defined! Now you can read it in the Client!"); } void loop() { std::string value = pCharacteristic->getValue(); Serial.print("The new characteristic value is: "); Serial.println(value.c_str()); delay(2000); }
برای راه اندازی مد کلاینت به کتابخانه “BLEDevice.h” نیاز داریم. کد زیر را در دستگاه کلاینت آپلود کنید:
#include "BLEDevice.h" /* Specify the Service UUID of Server */ static BLEUUID serviceUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b"); /* Specify the Characteristic UUID of Server */ 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"); } }; /* Start connection to the BLE Server */ 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 remote 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 */ /* Initial value is 'Hello, World!' */ 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; return 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; } } }; void setup() { Serial.begin(115200); Serial.println("Starting Arduino BLE Client application..."); BLEDevice::init("ESP32-BLE-Client"); /* 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); } 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()/2000); 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()); /* You can see this value updated in the Server's Characteristic */ } else if(doScan) { BLEDevice::getScan()->start(0); // this is just example to start scan after disconnect, most likely there is better way to do it in arduino } delay(2000); /* Delay a second between loops */ }
خروجی دو کد بالا به صورت زیر است:
دستگاه ESP32 BLE Server سرور BLE را راه اندازی می کند و اعلان خدمات خود را آغاز می کند. اگر کد Client ESP32 را به درستی اپلود کنید، ما از UUID سرویس ESP32 سرور و Characteristic UUID استفاده می کنیم. این بدان معناست که Client ESP32 دستگاه های BLE را اسکن می کند و اگر BLE با UUID های خاص پیدا شود، اتصال ایجاد می کند. هنگامی که اتصال برقرار شد، ESP32 BLE Client ابتدا مقدار مشخصه را از سرور می خواند (ما آن را در سرور “!Hello, World“تنظیم کرده ایم) و در ترمینال چاپ می کند. پس از آن، کلاینت سعی می کند هر چند ثانیه مقدار مشخصه سرور را با مقدار جدیدی تنظیم کند. در سرور، هنگامی که BLE Server را راه اندازی می کنید، هر دو ثانیه مقدار مشخصه خود را می خواند.
اسکن دستگاه های بلوتوث
ایجاد اسکنر ESP32 BLE ساده است. یک ماژول ESP32 بردارید (در حالی که یک ESP32 دیگر سرور BLE را اجرا می کند) در این جا سرور مثال قبل مد نظر می باشد. کد زیر را در IDE Arduino آپلود کنید. این کد، ESP32 را به عنوان یک دستگاه BLE راه اندازی می کند و دستگاه های نزدیک را اسکن می کند. همان طور که مشاهده می شود اولین دستگاه در لیست اسکن دستگاه سرور مثال قبل است.
include <BLEDevice.h> #include <BLEUtils.h> #include <BLEScan.h> #include <BLEAdvertisedDevice.h> int scanTime = 5; //In seconds BLEScan* pBLEScan; class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks { void onResult(BLEAdvertisedDevice advertisedDevice) { Serial.printf("Advertised Device: %s \n", advertisedDevice.toString().c_str()); } }; void setup() { Serial.begin(115200); Serial.println("Scanning..."); BLEDevice::init(""); 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 } void loop() { // put your main code here, to run repeatedly: BLEScanResults foundDevices = pBLEScan->start(scanTime, false); Serial.print("Devices found: "); Serial.println(foundDevices.getCount()); Serial.println("Scan done!"); pBLEScan->clearResults(); // delete results fromBLEScan buffer to release memory delay(2000); }
راه اندازی بلوتوث کم مصرف در مد notify
در این حالت ESP32 مانند یک سرور عمل می کند. با این تفاوت که در حالت عادی کلاینت از سرور درخواست اطلاعات می کند ولی در حالت notify برعکس این اتفاق رخ می دهد. از این حالت زمانی استفاده می شود که سرور بخواهد در صورت تغییر یک متغیر، آن را به کلاینت اعلام کند. جهت انجام این مثال کد زیر را روی ماژول ESP32 آپلود کنید. در صورتی که این مثال را با گوشی و اپلیکیشن nRF Connect تست کنید، هربار که مقدار متغیر تغییر می کند، مقدار جدید خود به خود روی گوشی شما نمایش داده می شود، در صورتی که در حالت سرور این قابلیت وجود نداشت و برای هر بار خواندن مقدار Characteristic، کلاینت باید درخواستی به سرور می فرستاد.
include <BLEDevice.h> #include <BLEServer.h> #include <BLEUtils.h> #include <BLE2902.h> 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; } }
5 دیدگاه