Consulting Services
مقالاتإنترنت الأشياءلغة سي و متحكمات

كل ما يتعلق بـ ESP32 – الجزء الرابع 4

أساسيات Bluetooth Low Energy (BLE)

أجزاء سلسلة كل ما يتعلق بـESP32

متعلقات

 

إن بروتوكول Bluetooth Low Energy هو بروتوكول متعدد الطبقات multi-layer أو ما يدعى كمصطلح Bluetooth stack. إن خلفية جيدة بالمصطلحات المستخدمة في الـBLE هي متطلب أساسي قبل البدء بأي شيء عملي في طرفية البلوتوث في ESP32، وهذا تماماً السبب وراء كتابة مقال “مقدّمة مكثفة عن Bluetooth Low Energy“.

سيتم الشرح في هذا الجزء من سلسلة “كل ما يتعلّق بـESP32” أساسيات استخدام الـBLE. سوف نستخدم واحد من الأمثلة من الحزمة البرمجية الرسمية ESP32 SDK (ESP-IDF) كنقطة انطلاقة، وسوف نشير تكراراً لمحتوى تم شرحه في المقال “مقدّمة مكثفة عن Bluetooth Low Energy“، لذلك حاول الاطلاع عليه مسبقاً.

 

مثال مخدّم BLE GATT من الـESP-IDF

 

هذا المثال هو كتابة مخدّم server يحوي خدمات البلوتوث التي ستقوم الأجهزة client بالنفاذ إليها. سوف نبدأ بشرح الأجزاء الأهم من المثال الموجود في المسار التالي ESP-IDF-DIR\examples\bluetooth\gatt_server. سيعرّف هذا المثال اثنين من الـprofiles وسيقوم بنشر broadcast الإعلانات advertisements تحت اسم ESP_GATTS_DEMO. يحتوي كل profile على خدمة service واحدة وكل خدمة على characteristic واحد، وسوف نستخدم تطبيق الجوال NRF Connect للتفاعل مع الجهاز.

إن الـprofile حقيقة لا يعكس أي مدخل في جدول الـ attributes في الجهاز. بالواقع هو مجموعة معرّفة مسبقاً من الخدمات services. إن كلا الـprofile في هذا المثال  (Profile A & Profile B) يحويان على الخدمة نفسها و الـcharacteristic في الخدمة نفسه أيضاً، وإن التكرار الموجود له غرض تصميمي مذكور في التوثيق للمثال:

 

In this way, a single design, enabled by different Application Profiles, can behave differently when used by different smartphone apps, allowing the server to react differently according to the client app that is being used. In practice, each profile is seen by the client as an independent BLE service. It is up to the client to discriminate the services that it is interested in.

GATT Server Demo ESP32

 

التهيئة Initialization في الـ Main function

 

بالبدء من التابع main والجزء الخاص بالتهيئة

// Initialize NVS.
    ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    
    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    esp_bt_controller_init(&bt_cfg);
    
    esp_bt_controller_enable(ESP_BT_MODE_BLE);

    esp_bluedroid_init();

    esp_bluedroid_enable();

كما شاهدنا سابقاً في جزء سابق للاتصال باستخدام WiFi فإن التخزين غير المتلاشي يجب تهيئته حيث يتم تخزين بعض المتحولات غير قابلة للتلاشي في التقسيمة NVS partition.

ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }

ثم يبدأ الكود بتهيئة الـ Controller الجزء المهم من الـBluetooth stack باستخدام بارمترات افتراضية موجودة في  esp_bt.h. إن الـ Controller وكما تم شرحه في المقال السابق ينظّم ويدير كل ما يتعلق بالإشارات الراديوية، وبالإضافة إلى تهيئته يجب أيضاً تفعيل نمط الـBLE في الـ Controller.

esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
esp_bt_controller_init(&bt_cfg);
    
esp_bt_controller_enable(ESP_BT_MODE_BLE);

يمكن للـ Controller أن يعمل في أكثر من نمط على اعتبار أن شريحة ESP32 هي من النوع الذي شرحناه سابقاً “Bluetooth Smart Ready”. هذا يعني أنه يمكن أن يعمل في أحد الأنماط التالية BLE mode أو Classic Bluetooth mode  أو dual-mode.

typedef enum
{
    ESP_BT_MODE_IDLE       = 0x00,   /*!< Bluetooth is not running */
    ESP_BT_MODE_BLE        = 0x01,   /*!< Run BLE mode */ 
    ESP_BT_MODE_CLASSIC_BT = 0x02,   /*!< Run Classic BT mode */ 
    ESP_BT_MODE_BTDM       = 0x03,   /*!< Run dual mode */ } esp_bt_mode_t;
}

المهمّة الأخرى هي تفعيل الـBluetooth software stack وهو البرمجية الخلفيّة التي تقوم بعمليات داخليّة وتتحكم بالطبقات المختلفة للبروتوكول. إن ESP32 يستخدم نسخة معدلة من البرمجية Bluedroid.

esp_bluedroid_init();

esp_bluedroid_enable();

تتمثل الخطوة التالية بتسجيل الـprofiles التي تحدثنا عنها وتوابع callback الخاصة بمعالجة الأحداث المختلفة للـGATT و GAP.

esp_ble_gatts_register_callback(gatts_event_handler);
    
esp_ble_gap_register_callback(gap_event_handler);
    
esp_ble_gatts_app_register(PROFILE_A_APP_ID);
    
esp_ble_gatts_app_register(PROFILE_B_APP_ID);
    
esp_ble_gatt_set_local_mtu(500);

أكثر طبقتين ذات أهميّة لمعظم مطوري الـBLE هما: GAP وهي المسؤولة بشكل أساسي عن البث و الإعلان ، وطبقة GATT وهي المسؤولة عن تخزين وتبادل المعطيات الحقيقيّة من خلال مجموعة من الخدمات services والتي تحوي Characteristics والتي تحوي المعطيات المطلوبة.

قد تحصل الكثير من الأحداث على مستوى الـ GAP وGATT، وبالتالي تختلف الاستجابة تبعاً لنوع الحدث، ولهذا السبب يستخدم المطورون تابع تخديم لأحداث GAP و آخر GATT. يقوم BLE software stack لاحقاً باستدعاء هذه التوابع مع تمرير بعض الوسطاء parameters والمعلومات الخاصة بالحدث ليتم استخدامها في التابع call-back.

esp_ble_gatts_register_callback(gatts_event_handler);
    
esp_ble_gap_register_callback(gap_event_handler);

والأمر الآخر هو تسجيل كلا الـprofiles.

esp_ble_gatts_app_register(PROFILE_A_APP_ID);
    
esp_ble_gatts_app_register(PROFILE_B_APP_ID);

وأخيراً، نقوم بتحديد قيمة الـ Maximum Transmission Unit (MTU)  بـ 500 بايت. لكن ماذا يعني MTU ؟ إنه يقوم بتحديد حجم Attribute PDU (وليس الـATT في الـ ATT Table) . لمعرفة المزيد يمكن قراءة المقال الخاص بـMTU  من موقع Punchthrough. يقوم الـ Client و الـServer بتبادل حجم الـMTU عند إنشاء الاتصال. ملاحظة: إن ATT أطول من حجم MTU لا يمكن إرسالها.

 

البدء بـ Broadcasting/Advertisement

 

إن أول شيء يجب القيام لبدء بث الإعلانات هو تفعيل GAP advertisements بأن تحوي باكيتات من النوع القابل للاتصال وتحت اسم جهاز معين. لمعرفة المزيد من أنواع باكيتات الإعلانات يرجى قراءة الفقرة “الإعلان Advertising (المعلن Advertiser والماسح Scanner)” من المقال الأخير.

بالعودة إلى الكود، عندما نقوم بتسجيل الـprofile فإن حدث سيتم تفعيله وهو ESP_GATTS_REG_EVT، وبالمناسبة إن الأحداث المعرّفة في ESP-IDF يمكن أن تكون واحدة من التالي:

typedef enum 
{     
    ESP_GATTS_REG_EVT                 = 0,       /*!< When register application id, the event comes */
    ESP_GATTS_READ_EVT                = 1,       /*!< When gatt client request read operation, the event comes */
    ESP_GATTS_WRITE_EVT               = 2,       /*!< When gatt client request write operation, the event comes */
    ESP_GATTS_EXEC_WRITE_EVT          = 3,       /*!< When gatt client request execute write, the event comes */
    ESP_GATTS_MTU_EVT                 = 4,       /*!< When set mtu complete, the event comes */ 
    ESP_GATTS_CONF_EVT                = 5,       /*!< When receive confirm, the event comes */ 
    ESP_GATTS_UNREG_EVT               = 6,       /*!< When unregister application id, the event comes */ 
    ESP_GATTS_CREATE_EVT              = 7,       /*!< When create service complete, the event comes */ 
    ESP_GATTS_ADD_INCL_SRVC_EVT       = 8,       /*!< When add included service complete, the event comes */ 
    ESP_GATTS_ADD_CHAR_EVT            = 9,       /*!< When add characteristic complete, the event comes */
    ESP_GATTS_ADD_CHAR_DESCR_EVT      = 10,      /*!< When add descriptor complete, the event comes */ 
    ESP_GATTS_DELETE_EVT              = 11,      /*!< When delete service complete, the event comes */ 
    ESP_GATTS_START_EVT               = 12,      /*!< When start service complete, the event comes */ 
    ESP_GATTS_STOP_EVT                = 13,      /*!< When stop service complete, the event comes */
    ESP_GATTS_CONNECT_EVT             = 14,      /*!< When gatt client connect, the event comes */ 
    ESP_GATTS_DISCONNECT_EVT          = 15,      /*!< When gatt client disconnect, the event comes */ 
    ESP_GATTS_OPEN_EVT                = 16,      /*!< When connect to peer, the event comes */ 
    ESP_GATTS_CANCEL_OPEN_EVT         = 17,      /*!< When disconnect from peer, the event comes */
    ESP_GATTS_CLOSE_EVT               = 18,      /*!< When gatt server close, the event comes */ 
    ESP_GATTS_LISTEN_EVT              = 19,      /*!< When gatt listen to be connected the event comes */
    ESP_GATTS_CONGEST_EVT             = 20,      /*!< When congest happen, the event comes */
     /* following is extra event */ 
    ESP_GATTS_RESPONSE_EVT            = 21,      /*!< When gatt send response complete, the event comes */ 
    ESP_GATTS_CREAT_ATTR_TAB_EVT      = 22,      /*!< When gatt create table complete, the event comes */ 
    ESP_GATTS_SET_ATTR_VAL_EVT        = 23,      /*!< When gatt set attr value complete, the event comes */ 
    ESP_GATTS_SEND_SERVICE_CHANGE_EVT = 24,      /*!< When gatt send service change indication complete, the event comes */ 
} esp_gatts_cb_event_t;

لنأخذ نظرة عن كثب أكثر عن gatts_event_handler :

static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param)
{
    /* If event is register event, store the gatts_if for each profile */
    if (event == ESP_GATTS_REG_EVT) {
        if (param->reg.status == ESP_GATT_OK) {
            gl_profile_tab[param->reg.app_id].gatts_if = gatts_if;
        } else {
            ESP_LOGI(GATTS_TAG, "Reg app failed, app_id %04x, status %d\n",
                    param->reg.app_id,
                    param->reg.status);
            return;
        }
    }

    /* If the gatts_if equal to profile A, call profile A cb handler,
     * so here call each profile's callback */
    do {
        int idx;
        for (idx = 0; idx < PROFILE_NUM; idx++) {
            if (gatts_if == ESP_GATT_IF_NONE || /* ESP_GATT_IF_NONE, not specify a certain gatt_if, need to call every profile cb function */
                    gatts_if == gl_profile_tab[idx].gatts_if) {
                if (gl_profile_tab[idx].gatts_cb) {
                    gl_profile_tab[idx].gatts_cb(event, gatts_if, param);
                }
            }
        }
    } while (0);
}

ما يقوم هذا التابع وبشكل أساسي هو تفقد نوع الحدث، فإذا كان هو ESP_GATTS_REG_EVT فإن أول شيء يقوم به هو تسجيل interface number لكل profile. إن التوثيق الرسمي لهذا المشروع يقول أن:

“ GATT interface type in a different application on GATT client uses different gatt_if values.”

gl_profile_tab[param->reg.app_id].gatts_if = gatts_if;

والأمر الآخر هو استدعاء تابع التخديم call-back function لكل profile وهما: gatts_profile_a_event_handler و gatts_profile_b_event_handler .

gl_profile_tab[idx].gatts_cb(event, gatts_if, param);

حيث تم سابقاً إسناد توابع تخديم لكل profile أثناء التصريح عن المصفوفة gl_profile_tab

static struct gatts_profile_inst gl_profile_tab[PROFILE_NUM] = {
    [PROFILE_A_APP_ID] = {
        .gatts_cb = gatts_profile_a_event_handler,
        .gatts_if = ESP_GATT_IF_NONE,       /* Not get the gatt_if, so initial is ESP_GATT_IF_NONE */
    },
    [PROFILE_B_APP_ID] = {
        .gatts_cb = gatts_profile_b_event_handler,                   /* This demo does not implement, similar as profile A */
        .gatts_if = ESP_GATT_IF_NONE,       /* Not get the gatt_if, so initial is ESP_GATT_IF_NONE */
    },
};

والآن جزء من تابع تخديم gatts_profile_a_event_handler سوف يقوم بتحديد محتوى باكيت الإعلانات وتحديد أيضاً اسم الجهاز عبر التوابع esp_ble_gap_config_adv_data(&adv_data)  و esp_ble_gap_set_device_name(TEST_DEVICE_NAME) وبعد ذلك سيبدأ الحدث ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT داخل التابع esp_ble_gap_start_advertising ليقوم ببدء بث الإعلانات فعلياً باستخدام التابع esp_ble_gap_start_advertising وذلك عند الحدث ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT .

سيقوم الكود أيضاً بتعريف وإنشاء خدمة service داخل كل profile باستخدام esp_ble_gatts_create_service. إن هذا التابع يأخذ 3 وسطاء وأحدهم هو عدد المدخلات number of handlers/ATT التي نحتاجها في جدول الوسطاء ATT  على اعتبار أن كل خدمة يمكن أن تحوي عدد مختلف من الـcharacteristic. إن الخدمة بشكلها الأبسط يمكن أن تحوي characteristic واحدة وكل characteristic ستحوي على تعريف characteristic declaration وعلى مؤشّر على مكان تواجد القيمة الحقيقة Characteristic value attribute handle وعلى القيمة ذاتها. لذلك العدد الأساسي هو 4:

  • Service declaration
  • Characteristic declaration
  • Characteristic value attribute handle.
  • القيمة بحد ذاتها

لمزيد من المعلومات يرجى مراجعة الفقرة “كيف يتم تخزين المعطيات الفعلية في جهاز BLE؟” في مقالنا السابق وأيضاً إجابة من أحد مطوري شركة Espressif عن سؤال متعلق.

 

إنشاء اتصال وإرسال طلبات قراءة/كتابة

 

كما قلنا سابقاً، فإننا بحاجة إلى بث باكيتات إعلانية من النوع القابل للاتصال، لكي تسمح للـ Central أن يطلب الاتصال من الـ Peripheral ويمكننا من الكود رؤية نوع الباكيتات وهي قابلة للاتصال.

static esp_ble_adv_params_t adv_params = {
    .adv_int_min        = 0x20,
    .adv_int_max        = 0x40,
    .adv_type           = ADV_TYPE_IND,
    .own_addr_type      = BLE_ADDR_TYPE_PUBLIC,
    .channel_map        = ADV_CHNL_ALL,
    .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
};

لنستذكر أنواع الباكيتات بناءً على جدول من المقال السابق.

 

“Getting Started with Bluetooth Low Energy” Book

 

عندما يقوم الـCentral (مثلاً الموبايل) بطلب الاتصال وتحديداً بخدمة من profile A، فإن بارمترات الاتصال يتم تحديثها وسيصبح مثلاً الوقت قبل فصل الاتصال غير النشط إلى 4 ثواني. انظر على هذه الحالة داخل كود التابع gatts_profile_a_event_handler:

case ESP_GATTS_CONNECT_EVT: {
        esp_ble_conn_update_params_t conn_params = {0};
        memcpy(conn_params.bda, param->connect.remote_bda, sizeof(esp_bd_addr_t));
        /* For the IOS system, please reference the apple official documents about the ble connection parameters restrictions. */
        conn_params.latency = 0;
        conn_params.max_int = 0x20;    // max_int = 0x20*1.25ms = 40ms
        conn_params.min_int = 0x10;    // min_int = 0x10*1.25ms = 20ms
        conn_params.timeout = 400;    // timeout = 400*10ms = 4000ms
        ESP_LOGI(GATTS_TAG, "ESP_GATTS_CONNECT_EVT, conn_id %d, remote %02x:%02x:%02x:%02x:%02x:%02x:",
                 param->connect.conn_id,
                 param->connect.remote_bda[0], param->connect.remote_bda[1], param->connect.remote_bda[2],
                 param->connect.remote_bda[3], param->connect.remote_bda[4], param->connect.remote_bda[5]);
        gl_profile_tab[PROFILE_A_APP_ID].conn_id = param->connect.conn_id;
        //start sent the update connection parameters to the peer device.
        esp_ble_gap_update_conn_params(&conn_params);
        break;

يمكننا بعد الاتصال القراءة أو الكتابة أو الاشتراك بالتنبيهات طالما أنها كلها مسموحة في كل من A و B:

a_property = ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE | ESP_GATT_CHAR_PROP_BIT_NOTIFY;

لنلقِ نظرة عن طلبات القراءة ماذا ستعيد … سوف تعيد رد يحوي قيمة محددة من 4 بايتات موصوفة في الكود:

case ESP_GATTS_READ_EVT: {
        ESP_LOGI(GATTS_TAG, "GATT_READ_EVT, conn_id %d, trans_id %d, handle %d\n", param->read.conn_id, param->read.trans_id, param->read.handle);
        esp_gatt_rsp_t rsp;
        memset(&rsp, 0, sizeof(esp_gatt_rsp_t));
        rsp.attr_value.handle = param->read.handle;
        rsp.attr_value.len = 4;
        rsp.attr_value.value[0] = 0xde;
        rsp.attr_value.value[1] = 0xed;
        rsp.attr_value.value[2] = 0xbe;
        rsp.attr_value.value[3] = 0xef;
        esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id,
                                    ESP_GATT_OK, &rsp);
        break;

 

وأخيراً، تطبيق المثال بشكل عملي

 

لقد استخدمت تطبيق أندرويد يسمى NRF Connect لاستكشاف الخدمات على الجهاز والاتصال به وإرسال طلبات قراءة وكتابة.

 

NRF Connect App ESP GATTS Demo ADV

بث الإعلانات تحت اسم “ESP_GATTS_DEMO”

 

NRF Connect App ESP GATTS Demo Services

الخدمات المتاحة

 

NRF Connect App ESP GATTS Demo Read/writing request

تجريب إرسال طلبات قراءة وكتابة

 

الخاتمة

 

يجب للقارئ المطلع على المقال السابق “مقدّمة مكثفة عن Bluetooth Low Energy” وعلى هذا الجزء من السلسلة الذي شرح أهم أجزاء المثال “gatt_server” من ESP-IDF SDK أن تتشكل لديه الآن فكرة أساسية جداً عن مفهوم الـBLE وعن كيفيّة تصميم تطبيق بسيط بدءاً من مثال جاهز كالمطروح في هذا المقال.

على الرغم أن التوثيق الرسمي للمشروع على Github هو توثيق شامل إلى حدٍّ كبير إلا أنه كان من الأفضل إعادة صياغته على أسلوب أنسب للقادمين الجدد في هذا المجال.

في المقال القادم سنعمل على إنشاء خدمة للتحكم بليد موصول على الدارة بالاعتماد على كود المثال المستخدم هنا ولكن مع إعادة كتابة وترتيب الكثير من الأجزاء.

أجزاء سلسلة كل ما يتعلق بـESP32

متعلقات

Yahya Tawil

مؤسس عتاديات وأسعى لدعم المحتوى العربي عن النظم المضمنة في الويب العربي. خبرتي في مجال النظم المضمّنة تتركز في كتابة البرامج المضمنة وتصميم الدارات المطبوعة والمخططات وإنشاء المحتوى.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

هذا الموقع يستخدم Akismet للحدّ من التعليقات المزعجة والغير مرغوبة. تعرّف على كيفية معالجة بيانات تعليقك.

زر الذهاب إلى الأعلى