mgulsoy.github.io

Bir Kişisel Blog

Esp32 ile Bluetooth LE Server

19 Nov 2019

Esp32 platformu, yapabildiklerine ve fiyatına bakıldığında oldukça avantajlı bir platform. Bu yazıda da fazla detaya girmeden bir çevre birimi (Peripheral) ya da başka bir deyişle bir BLE SERVER sistemi yapımını anlatalım:

Geliştirme Ortamı

Yapacağımız işlemler için geliştirme ortamı olarak aşağıdaki bileşenleri kullanacağız:

  • Visual Studio Code (Kurulumunu anlatmayacağım)
  • VS Code üzerinde Platformio eklentisi (Kurulumunu anlatmayacağım)
  • Platformio esp-arduino platformu (Kurulumunu anlatmayacağım)
  • Uyumlu bir esp32 geliştirme kartı

Planlama

Şimdi projemizde neler yapacağız bunu planlayalım:

Amaçlar

  • Bluetooth LE kullanacağız.
  • Pil durumunu raporlayacağız. (Bluetooth SIG standardında tanımlandığı gibi)
  • Bir digital pin durumu okuyacağız ve bunu raporlayacağız.

BLE Mimarisi

Bluetooth Low Energy (BLE) ya da Bluetooth 4, Bluetooth SIG tarafından standartları belirlenen kablosuz erişim yapısıdır. BLE birebir bağlantı şekilli yapılardan biraz farklıdır. BLE, enerji verimliliği göz önünde bulundurularak tasarlandığından ötürü kullanım şekli de buna göre evrilmiştir.

BLE standardı 2 adet yapı tanımlar:

  • GAP (Generic Access Profile) : Cihazların birbirlerini bulması için bu yapı kullanılır.
  • GATT (Generic ATTribute) : Cihazların birbirlerine bağlanması için bu yapı kullanılır.

BLE standardı temel olarak şu rolleri tanımlar:

  • GATT Client: Genellikle MASTER veya CENTRAL olarak da bilinir. Bütün işi idare eden taraftır. Mesela bir bluetooth kalp sensörüne bağlanan cep telefonu bu roldedir.
  • GATT Server: Genellikle SLAVE veya PERIPHERAL olarak da bilinir. İdare edilen taraftır. Genelde pasiftir. Mesela bir bluetooth kalp sensörü bu roldedir.

GATT Server sunacak olduğu hizmetleri standartta tanımlandığı gibi listelemelidir. Bunu hizmet (GATT Service) tanımlayarak yapar. Her bir hizmetin tabiki bir adı yoktur. Hizmetlerin birer numarası vardır. Bu numara UUID olarak bilinir. Bluetooth SIG bir dizi standart hizmet (GATT Service) tanımlamıştır. Bu hizmetlerin birer standart numarası bulunur.

Her bir hizmetin (GATT Service) içinde hizmetin detaylarının tanımlandığı karakteristik özellikler (GATT Characteristic) bulunur. İletişimin gerçekleştiği noktalar karakteristik özelliklerdir. Bu sebeple kendi özelliklerini tanımlamazlar. Bunun için GATT Server her bir karakteristik özellik için bir tanımlayıcı (GATT Descriptor) oluşturur. Tamımlayıcılar karakteristik özelliklerin nasıl çalıştıklarını, ne tür bilgi içerdiklerini belirtir.

Bir karakteristik özellik (GATT Characteristic) tanımlayıcılar (GATT Descriptor) tarafından tanımlanan aşağıdaki yöntemler ile çalışırlar:

  • WRITE : Karakteristik özellik GATT Client tarafından yazılabilir özelliktedir. Bu sayede GATT Client tarafından GATT Server tarafına veri gönderilebilir.
  • READ : Karakteristik özellik GATT Client tarafından tek seferliğine okunabilir. Yani her READ işlemi bir defa veri okur.
  • NOTIFY : GATT Client bu karakteristik özelliği sürekli olarak dinler. GATT Server bu karakteristik özelliği gerektiği her zaman günceller ve karşı tarafı bilgilendirir. GATT Client bilgilendiği zaman hiç sesini çıkarmaz. Yani GATT Server tarafına mesajın ulaştığı bilgisini vermez. Bu sebeple hızlıdır(!)
  • INFORM : GATT Client bu karakteristik özelliği NOTIFY tanımında olduğu gibi dinler fakat bu mesajı aldığında GATT Server tarafına mesajı almış olduğu bilgisini verir. Bu sebeple NOTIFY tanımından yavaştır(!)

(!) Hızlıdır çünkü mesajı alan taraf geriye mesajın ulaştığına bağlı olarak bilgi dönmez (ACKnowledge).

Numaralar (Assigned Number, UUID)

BLE sisteminde her yapının bir numarası olduğunu belirtmiştik. Bu numaralar standardın içinde tanımlanan yapılar için haliyle önceden tanımlanmıştır. Tasarımlarda bu önceden tanımlanan numaralar kullanıldığında uygulamalar otomatik olarak bu hizmet ve alt yapılarını görüp işlem yapar. Örneğin pil durumu hizmeti (Battery Service) standartta belirtildiği gibi tanımlandığında bağlanılan bütün cihaz, bilgisayar, mobil telefon gibi yapılar pil durumunu ek bir kodlama yapmadan gösterir.

Numaralar UUID denen bir yapı ile ifade edilir. UUID çok büyük bir sayıdır. 16 byte (128 bit) genişliğe sahiptir. Şöyle gösterilir: a62206b9-8cd9-4f02-ae53-1755928a54e1

Bluetooth SIG tarafından yayınlanan önceden atanmış numaraların standart olması dolayısı ile numarayı oluşturan rakamların büyük bir kısmı aynıdır. Bu sebeple standart numaralar kısa formda gösterilir. Örneğin pil seviyesi karakteristik özelliğinin önceden tanımlanmış numarası 0x2A19 olup bu şekilde ifade edilir. Fakat bunu UUID formatında göstermek gerekirse, yani uzun haliyle ifade edilirse: 00002A19-0000-1000-8000-00805F9B34FB olur. Örnekteki numaranın 2A19 olan kısa formu hariç diğer standart numaralar için geri kalan kısmı aynıdır. Yani numarası 0x180F olan pil durumu hizmetinin UUID gösterimi 0000180F-0000-1000-8000-00805F9B34FB olacaktır.

Fakat biz tasarımlarımızda standart olarak tanımlanan numarala dışında numara üretip bunları kullanabilir. Tabi bu hizmetlerin veya karakteristik özelliklerin çalışabilmesi için kullanılacak olduğu platform, bilgisayar, mobil cihazlarda kendimiz kodlama yapmamız gereklidir.

Bizim Sistemimizin Yapısı

Bizim tasarlayacağımız sistemde aşağıdaki Bluetooth yapıları olacak (standart isimleri ve tanımları ile):

Hizmetler (GATT Service)

Hizmet Uniform Type ID Numarası
Battery Service org.bluetooth.service.battery_service 0x180F
Sensor Service Standart değil! Kendimiz tanımladık a62206b9-8cd9-4f02-ae53-1755928a54e1

Karakteristik Özellikler (GATT Characteristic)

Hizmet Karakteristik Özellik Numarası Çalışma Yöntemi
Battery Service Battery Level 0x2A19 READ
Sensor Service Sensor Value a62206b9-8cd9-4f02-ae53-1755928a54e2 READ,NOTIFY

Tanımlayıcılar (GATT Descriptor)

Karakteristik Özellik Adı Numarası Tanım
Battery Level Characteristic User Description 0x2901 Seviye türü tanımı
Sensor Value Client Characteristic Configuration 0x2902 NOTIFY yöntemini belirten tanım

NOT: Bazı tanımlayıcılar esp32 platformu tarafından otomatik olarak oluşturulur!

Dikkat Edilmesi Gereken Noktalar

BLE sistemi sürekli bağlantı yapısında tasarlanmamıştır. Örneğin Client cihazlar belirli aralıklar ile Server cihazlardan veri aktarırlar. Bu sayede enerji efektif olarak çalışır. Fakat bu durum düşük bant genişliği gibi bazı dezavantajları ortaya çıkarır. Bazı gözlemleri listelemek gerekirse:

  • Örneğin aksi belirtilmedikçe windows işletim sistemleri için veri transfer aralığı 60ms civarındadır. Bazı Android sistemler 40 ~ 80 ms aralıklarında veri transfer ederler.
  • Bluetooth 4.1 için bir veri paketi yükü maksimum 23 byte’tır. 4.2 için 25 byte civarındadır. Maksimum paket yükünü aşan transferler için paket bölünmesi gözlenir. Bu da transfer zamanını arttırır.
  • Bu durumda 1 sn içinde maksimum (1000 / 60ms) x 23 =~383 byte veri aktarılabilir. Bu kabaca 3 KBit transfer hızı demektir ki bu çok düşüktür. Fakat gerçek zamanlı olmayacak sistemler için bu hız yeterlidir.
  • Bluetoot LE gerçek zamanlı sistemler ile kullanılması önerilmeyen bir sistemdir!
  • Fakat Bluetooth LE sistemi HID (Human Interface Device) olarak tasarlanmış cihazlara destek verir. Bu cihazlar gerçek zamanlı olarak düşünülebilir. Örneğin bir Bluetooth Mouse böyle bir cihazdır. Bu durumda bağlanan Client sisteme yüksek hız ihtiyacı bildirilebilir.
  • Windows işletim sistemleri bir Server cihazına bağlantı kurduğunda şunlar gerçekleşir:
    • Eğer cihaz için tanımlanan bir sürücü var ise bu sürücü bağlantı gereksinimlerine göre hızı ve gerekli diğer parametreleri ayarlar.
    • Eğer cihaz Gatt Peripheral Preferred Connection Parameter characteristic özelliğini tanımlıyorsa bu tanıma bir sonraki bağlantıda uyulur. Fakat hemen uyulması isteniyorsa Server tarafından L2Cap Connection Parameter Update Request gönderilir.
      • esp32 platformunda bu işlemin yapılabilmesi için bu fonksiyon kullanılır. Fakat biz bir üst kütüphane kullanacağız ve bu kütüphane bu işlemi arka planda gerçekleştirecek.
  • Bağlantı kalitesini etkileyen parametreler şunlardır:
    • minimum preferred connection interval : Tercih edilen en kısa bağlantı aralığı süresi. 1,25ms ‘lik adımlar ile hesaplanır. Yani değer 10 ise süre 10 x 1,25ms = 20ms olur.
    • maximum preferred connection interval : Tercih edilen en uzun bağlantı aralığı süresi. 1,25ms ‘lik adımlar ile hesaplanır. Yani değer 20 ise süre 20 x 1,25ms = 40ms olur.
    • preferred slave latency : Tercih edilen gecikme, yani kaybolduğunda tolere edilebilecek paket sayısı.
    • preferred supervision timeout : Tercih edilen zaman aşımı süresi. Bu süre dolduğunda cihaz bağlantısı kesilebilir. 6,25ms ‘lik adımlar ile hesaplanır. Yani değer 100 ise süre 100 x 6,25ms = 625ms olur.

Kodlama

Öncelikle kullanacağımız kütüphaneleri platformio eklentisine belirtiyoruz. Bunun için projemizdeki platfomio.ini dosyasına gerekli tanımı yapıyoruz:

lib_deps = ESP32 BLE Arduino

Kullanacağımız kütüphaneleri ekliyoruz:

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

Program dosyamıza gerekli sabit tanımlarını yapıyoruz:

#define BatteryServiceUUID             BLEUUID((uint16_t)0x180F)
#define BatteryLevelCharacteristicUUID BLEUUID((uint16_t)0x2a19)
#define BatteryLevelDescriptorUUID     BLEUUID((uint16_t)0x2901)
#define SensorServiceUUID              "a62206b9-8cd9-4f02-ae53-1755928a54e1" //<--Sondaki sayı 1
#define SensorCharacteristicUUID       "a62206b9-8cd9-4f02-ae53-1755928a54e2" //<--Sondaki sayı 2

Hizmet ve diğer yapılar için değişkenler tanımlıyoruz. Bunları global olarak tanımlıyoruz:

BLEServer *pServer;
BLEService *pBatteryService, *pSensorService;
BLECharacteristic *pBatteryLevelCharx, *pSensorValueCharx;
BLEDescriptor BatteryLevelDescriptor(BatteryLevelDescriptorUUID);
BLE2902 ble2902 = BLE2902(); //NOTIFY yöntemi tanımlayıcısı

Server’a bağlantı gerçekleştiğinde haberdar olmak için bir Callback sınıfı tanımlıyoruz. Bu sınıf ile bağlantı için istediğimiz hız parametrelerini de Client cihazına bildireceğiz. Bunu yapabilmek için bağlantının kurulmuş olması gerekli:

class BaglantiCallback : public BLEServerCallbacks
{
  void onConnect(BLEServer* pServer, esp_ble_gatts_cb_param_t *param) {
    
    Serial.print("Baglandi. ID: ");
    Serial.print(param->connect.conn_id, DEC);
    Serial.print(" client addr: ");
    Serial.printf(ESP_BD_ADDR_STR
      , (uint8_t)param->connect.remote_bda[0]
      , (uint8_t)param->connect.remote_bda[1]
      , (uint8_t)param->connect.remote_bda[2]
      , (uint8_t)param->connect.remote_bda[3]
      , (uint8_t)param->connect.remote_bda[4]
      , (uint8_t)param->connect.remote_bda[5] );
    Serial.println();
        
    pServer->updateConnParams(  //Bağlantı parametrelerini ayarlıyoruz. BKZ: "Dikkat Edilmesi Gereken Noktalar"
      param->connect.remote_bda,
      10, 20, 0, 500
    );
    
  }

  void onDisconnect(BLEServer *pServer)
  {
    deviceConnected = false;
    Serial.println("Disconnected.");
  }
};

Setup fonksiyonu içinde gerekli ayarları yapıyoruz. Hizmetleri ve alt bileşenlerini tanımlıyoruz:

void setup() {
 ...
 ...
 BLEDevice::init("BizimBLE"); //cihazın görünen adı
 
 pServer = BLEDevice::createServer();

 pServer->setCallbacks(new BaglantiCallback());
 
 //Hizmet oluştur
 pBatteryService = pServer->createService(BatteryServiceUUID);
 pDengeService = pServer->createService(SensorServiceUUID);

  //karakteristik özellikleri oluştur
  pBatteryLevelCharx = pBatteryService->createCharacteristic(
      BatteryLevelCharacteristicUUID,
      BLECharacteristic::PROPERTY_READ);

  pSensorValueCharx = pSensorService->createCharacteristic(
      SensorCharacteristicUUID,
      BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);

  //Tanımlayıcıyı ayarla
  BatteryLevelDescriptor.setValue("Percentage 0 - 100");

  //ve ekle
  pBatteryLevelCharx->addDescriptor(&BatteryLevelDescriptor); // 2901
  pSensorValueCharx->addDescriptor(&ble2902);                 //Notifications descriptor -- new BLE2902()

  //Hizmetleri başlat
  pBatteryService->start();
  pSensorService->start();

  //Sunucunun duyuru yapmasını sağla
  pServer->getAdvertising()->start();
  ...
  ...
}

Loop fonksiyonu içinde sistemin bilgi sağlayacağı kodları yazıyoruz:

void loop(){
  ...
  ...
  int pil_seviyesi_adc = analogRead(pil_olcum_pini);
  uint8_t pil_seviyesi = map (pil_seviyesi_adc, 0, 4096, 0, 100);
  pBatteryLevelCharx->setValue(&pil_seviyesi, 1); //sadece READ işlemi yapılacak
  
  if( digitalRead(sensor_pin) == HIGH ) {
    pSensorValueCharx->setValue("Acik");
  } else {
    pSensorValueCharx->setValue("Kapali");
  }
  
  if (ble2902.getNotifications() == true) {
    pSensorValueCharx->notify(); //Bildir!
  }
  
  delay(1000);
}

Sonuç

Bir esp32 geliştirme kartı üzerinde basit bir sensör yapısı kurup bunu çalışır hale getirdik. Bu işlem esnasında bağlantı hızı ile ilgili düzenlemeler yaptık. Burada sistemin veri aktarım hızını da bir miktar arttırmış olduk.