ตัวอย่างการเขียนโปรแกรมด้วย Arduino สำหรับใช้งาน ESP32 - Bluetooth LE#


Bluetooth LE#

ในปัจจุบันการใช้งาน Bluetooth Low Energy (BLE) เป็นที่นิยมอย่างแพร่หลาย BLE เป็นโพรโทคอลย่อยภายใต้มาตรฐาน Bluetooth ซึ่งดูแลโดยองค์กรที่เรียกว่า Bluetooth SIG เปิดตัวครั้งแรกใน Bluetooth 4.0 เมื่อปีค.ศ. 2009 ถัดจากเวอร์ชันก่อนหน้าคือ Bluetooth Classic 1.0 – v.0

BLE สามารถสื่อสารข้อมูลไร้สายระยะใกล้ ในย่านความถี่ 2.4GHz ด้วยอัตราการรับส่งข้อมูล (Data Transfer Rate) สูงสุด 1 Mbps (Bluetooth 4.0) และ 2 Mbps (Bluetooth 5.0) ออกแบบมาสำหรับการสื่อสารที่ใช้พลังงานต่ำ เหมาะกับอุปกรณ์ที่ใช้แบตเตอรี่ขนาดเล็ก เช่น เซนเซอร์ อุปกรณ์สุขภาพ และสมาร์ทวอทช์ เป็นต้น ดังนั้นการเรียนรู้และทดลองใช้งาน BLE จึงเป็นหัวข้อสำคัญอีกหัวข้อหนึ่งเกี่ยวกับระบบสมองกลฝังตัว (Embedded Systems) และ Internet of Things (IoT)

  • Bluetooth 5: เป็นเวอร์ชันที่พัฒนาต่อจาก Bluetooth 4.0 - 4.2 รองรับทั้ง Classic Bluetooth และ Bluetooth Low Energy (BLE)
    • Bluetooth 5.0 (2016)
    • Bluetooth 5.1 (2019)
    • Bluetooth 5.2 (2020)
    • Bluetooth 5.3 (2021)
    • Bluetooth 5.4 (2023)
  • Bluetooth Mesh Networking: ทำงานอยู่บนพื้นฐานของ BLE แต่เพิ่มความสามารถในการส่งข้อมูลต่อระหว่างอุปกรณ์ (Multi-hop Communication)

ชิปจากบริษัท Espressif หลายรุ่น เช่น ESP32, ESP32-S3, ESP32-C3, ESP32-C6 รองรับการสื่อสารข้อมูลไร้สายผ่านโปรโตคอลต่าง ๆ นอกเหนือจาก Wi-Fi ที่ทำงานบนย่านความถี่ 2.4GHz ได้แก่ เช่น BLE, Zigbee และ Thread

 


▷ หลักการทำงานของ BLE#

BLE (Bluetooth Low Energy) มีแนวคิดสำคัญในการสื่อสารข้อมูลดังนี้:

  • การจำแนกประเภทของอุปกรณ์ หรือ บทบาททางกายภาพของอุปกรณ์:
    • Peripheral: อุปกรณ์ที่ให้บริการ
    • Central: อุปกรณ์ที่เชื่อมต่อเข้าไปยัง Peripheral เพื่อใช้บริการตามที่ประกาศไว้
  • บทบาทในการสื่อสารข้อมูลผ่าน GATT
    • Server: อุปกรณ์ที่มีข้อมูลและบริการให้ใช้งาน
    • Client: อุปกรณ์ที่ร้องขอบริการจาก Server
  • มีการกำหนด BLE Profiles และบริการ Services โดย Bluetooth SIG เช่น
    • Heart Rate Profile และ Heart Rate Service ใช้กับอุปกรณ์ เช่น อุปกรณ์วัดชีพจร สายรัดสุขภาพ (Fitness Band) เป็นต้น
  • การใช้งาน GATT (Generic Attribute Profile):
    • เป็นโปรโตคอลสำคัญที่ใช้ใน BLE เพื่อกำหนดรูปแบบการแลกเปลี่ยนข้อมูลระหว่างอุปกรณ์ โดยเฉพาะในระบบที่มีการทำงานแบบ Client และ Server
    • การแลกเปลี่ยนข้อมูลจะเกิดขึ้นตามรูปแบบที่กำหนดไว้โดย GATT โดยที่ใช้ส่งข้อมูลกับระหว่างอุปกรณ์ เป็นข้อมูล "ขนาดสั้น" เรียกว่า Attributes
    • ข้อมูลจะถูกจัดกลุ่มเป็น Service และ Characteristic
  • Service
    • เป็นชุดของข้อมูลหรือฟังก์ชันที่อุปกรณ์ BLE ให้บริการ เช่น Heart Rate Service, Environmental Sensing ตามมาตรฐาน (กำหนดโดย Bluetooth SIG) หรือ Custom Service (กำหนดหรือสร้างขึ้นเองโดยนักพัฒนา)
  • Characteristic
    • อยู่ภายใต้ Service แต่ละ Characteristic จะมีคุณสมบัติ (Properties) ที่ระบุการเข้าถึงข้อมูลแต่ละตัว และมีหน่วยข้อมูล (Value) ที่สามารถเขียนหรืออ่านได้
  • ประเภทการเข้าถึงข้อมูลใน Characteristic
    • Read: อ่านค่าได้
    • Write: เขียนค่าใหม่ได้
    • Notify: แจ้งเตือน Client เมื่อค่ามีการเปลี่ยนแปลง
    • Indicate: เหมือน Notify แต่ต้องการการยืนยันจาก Client
  • UUID (Universally Unique Identifier)
    • ใช้ระบุ Service และ Characteristic
    • โดยทั่วไป มีขนาด 128 บิต หรือ 16 ไบต์ หรือ จะระบุเป็น 16 บิต เช่น 0x2A6E (เป็นแบบย่อ หากใช้ตามมาตรฐานที่กำหนดไว้แล้ว และจะถูกแปลงเป็น 128 บิต โดยอัตโนมัติ)
    • ตัวอย่าง 128-bit UUID เช่น 00002a37-0000-1000-8000-00805f9b34fb
    • UUID มาตรฐานถูกกำหนดโดย Bluetooth SIG (Special Interest Group)
    • Service UUID: ระบุประเภทของบริการ
    • Characteristic UUID: ระบุคุณสมบัติย่อยภายใน Service
  • การโฆษณา (Advertising):
    • อุปกรณ์ Peripheral จะส่งข้อมูลโฆษณา (Broadcast) เพื่อให้ Central เห็นและเชื่อมต่อได้
  • ข้อจำกัดของ BLE
    • ไม่เหมาะกับการสตรีมข้อมูลปริมาณมาก เช่น วิดีโอ เสียง
    • ข้อมูลที่ส่งผ่าน Characteristic มีขนาดจำกัด
  • การจับคู่กับอุปกรณ์ (BLE Pairing) แต่ไม่จำเป็นต้องทำสำหรับอุปกรณ์ BLE มีหลายวิธี โดยมีระดับความปลอดภัยต่างกัน เช่น
    • ไม่ต้องใส่รหัส (Just Works)
    • ต้องใส่รหัส เช่น 6 หลัก (Passkey Entry)
    • ทั้งสองฝั่งแสดงรหัสเดียวกันที่ได้จากการสุ่ม และผู้ใช้ยืนยันว่าเหมือนกัน (Numeric Comparison)

ตัวอย่าง Service UUID สำหรับอุปกรณ์ประเภท BLE Environmental Sensing:

  • UUID (16-bit): 0x181A (Environmental Sensing)
  • ตัวอย่าง Characteristic UUIDs (16 บิต) ภายใต้ Service นี้
    • Temperature (0x2A6E): อุณหภูมิ หน่วยเป็นองศาเซลเซียส (°C)
    • Humidity (0x2A6F): ความชื้นสัมพัทธ์ หน่วยเป็นเปอร์เซ็นต์ (%)
    • Pressure (0x2A6D): ความดันบรรยากาศ หน่วยเป็น Pascal (Pa)
    • UV Index (0x2A76): ดัชนีรังสี UV
    • Wind Speed (0x2A70): ความเร็วลม หน่วยเป็น เมตร/วินาที

หากเป็นอุปกรณ์สมาร์ทโฟน เช่น Android ก็แนะนำให้ลองติดตั้งและใช้งาน nRF Connect ของบริษัท Nordic Semiconductor ผู้ใช้สามารถสแกนหาอุปกรณ์ BLE รอบ ๆ และลองเชื่อมต่อกับอุปกรณ์เหล่านั้นได้


▷ ตัวอย่างการตรวจหาอุปกรณ์ (BLE Scan)#

ถัดไปเป็นตัวอย่างโค้ดสำหรับ ESP32 สำหรับตรวจสอบว่า มีอุปกรณ์ BLE อยู่บริเวณรอบ ๆ หรือไม่ โดยใช้ไลบรารี BLE ที่รวมอยู่ใน Arduino-esp32 core แล้ว และมีการนำเข้าไฟล์ C Header สำหรับไลบรารี ดังนี้

  • <BLEDevice.h>
  • <BLEServer.h>
  • <BLEUtils.h>
  • <BLEScan.h>
  • <BLEAdvertisedDevice.h>
  • <BLESecurity.h>

File: esp32_ble_scan.ino

// BLE Scanner Example for ESP32 (Arduino-ESP32 core v3.0.0+)
// BLE: https://github.com/espressif/arduino-esp32/blob/master/libraries/BLE/

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>

int scanTime = 5; // BLE scan time (in seconds)
BLEScan* pBLEScan;

class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {

  void onResult(BLEAdvertisedDevice advertisedDevice) {
    // Show the BLE address
    Serial.printf("BLE Address: %s\n",
                  advertisedDevice.getAddress().toString().c_str());
    // Show device name if available
    if (advertisedDevice.haveName()) {
      Serial.printf("- Device Name: '%s'\n",
                    advertisedDevice.getName().c_str());
    }
    // Show the RSSI value (received signal strength indicator) if available
    if (advertisedDevice.haveRSSI()) {
      Serial.printf("- RSSI: %d\n", advertisedDevice.getRSSI() );
    }
    // Show the Tx power if available
    if (advertisedDevice.haveTXPower()) {
      Serial.printf("- Tx power: %d\n", advertisedDevice.getTXPower() );
    }
  }
};

void setup() {
  Serial.begin(115200);
  Serial.println("Starting ESP32 BLE Scanner...");

  BLEDevice::init(""); // Initialize the BLE device
  pBLEScan = BLEDevice::getScan(); // Get the BLE scanner
  // Set the AdvertisedDevice Callback
  pBLEScan->setAdvertisedDeviceCallbacks( 
               new MyAdvertisedDeviceCallbacks() );
  pBLEScan->setActiveScan(true); // Use active scan
  pBLEScan->setInterval(100); // Scan interval 100*0.625ms
  pBLEScan->setWindow(99); // Scan window 99*0.625ms (less than scan interval)
}

void loop() {
  Serial.printf( "Scanning for BLE devices (%d sec)...\n", scanTime);
  // Start BLE scan (blocking call, waits until the scan completes)
  BLEScanResults *results = pBLEScan->start(scanTime, false);
  Serial.println( "Scan completed.\n\n" );
  // Show the total number of BLE devices found after the BLE scan process
  int deviceCount = results->getCount();
  Serial.printf( "BLE devices found: %d\n\n", deviceCount );
  // Clear the BLE scan results
  pBLEScan->clearResults();
  delay( 10000 );
}

โค้ดตัวอย่างนี้ มีการสร้างคลาส (Class) ชื่อ MyAdvertisedDeviceCallbacks แบบกำหนดเองและสืบทอดมาจากคลาส BLEAdvertisedDeviceCallbacks และเมื่อนำไปใช้ จะต้องมีการทำคำสั่งต่อไปนี้ก่อน

pBLEScan->setAdvertisedDeviceCallbacks( new MyAdvertisedDeviceCallbacks() );

โดยที่ pBLEScan คือ ตัวแปรที่เก็บอ็อบเจกต์ของ BLEScan (ใช้ในการสแกน BLE)

ภายในคลาส มีการสร้างฟังก์ชันสมาชิก onResult(...) ทำหน้าที่เป็น Callback Function ซึ่งจะถูกเรียกใช้เมื่อมีการสแกนพบอุปกรณ์ BLE แต่ละตัว ดังนั้นจึงสามารถดูข้อมูลของอุปกรณ์ที่พบได้ (advertisedDevice) เช่น

  • แสดงหมายเลข BLE Address ของอุปกรณ์: advertisedDevice.getAddress()
  • แสดงชื่ออุปกรณ์: advertisedDevice.getName() (ถ้ามี)
  • แสดงค่า RSSI ของสัญญาณ: advertisedDevice.getRSSI()

หากต้องการสแกนหาอุปกรณ์ BLE ให้ทำซ้ำไปเรื่อย ๆ และมีการบันทึกและอัปเดตรายการอุปกรณ์ที่ตรวจพบ ตามจำนวนสูงสุดที่กำหนดไว้ (MAX_DEVICES) ก็มีแนวทางดังนี้

File: esp32_ble_scan_list.ino

#include <BLEDevice.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>
#include <vector>
#include <memory>
#include <algorithm>

#define MAX_DEVICES  (30)

typedef struct {
  BLEAddress addr;
  int minRSSI;
  int maxRSSI;
} ble_dev_info_t;

std::vector<std::shared_ptr<ble_dev_info_t>> deviceList;

int scanTime = 5; // Scan interval in seconds
BLEScan* pBLEScan;

void updateDeviceList(BLEAdvertisedDevice& foundDev) {
  BLEAddress addr = foundDev.getAddress();
  int rssi = foundDev.getRSSI();

  // Check if device already exists
  for (auto& dev: deviceList) {
    if (dev->addr.equals(addr)) {
      if (rssi < dev->minRSSI) { dev->minRSSI = rssi; }
      if (rssi > dev->maxRSSI) { dev->maxRSSI = rssi; }
      return;
    }
  }
  // Not found: insert if space
  if (deviceList.size() < MAX_DEVICES) {
    auto newDev = std::make_shared<ble_dev_info_t>(ble_dev_info_t{addr,rssi,rssi});
    deviceList.push_back(newDev);
  } else {
    // Replace weakest if new one is stronger
    auto weakest = std::min_element(deviceList.begin(), deviceList.end(),
      [](const std::shared_ptr<ble_dev_info_t>& a, 
         const std::shared_ptr<ble_dev_info_t>& b) 
      {
        int a_rssi_avg = (a->minRSSI + a->maxRSSI) / 2;
        int b_rssi_avg = (b->minRSSI + b->maxRSSI) / 2;
        return a_rssi_avg > b_rssi_avg;
      });

    int weakest_rssi = ((*weakest)->minRSSI + (*weakest)->maxRSSI) / 2;
    if (rssi > weakest_rssi) {
      *weakest = std::make_shared<ble_dev_info_t>(ble_dev_info_t{addr,rssi,rssi});
    }
  }
  // Optional: sort by strongest avg. RSSI
  std::sort(deviceList.begin(), deviceList.end(),
    [](const std::shared_ptr<ble_dev_info_t>& a, 
       const std::shared_ptr<ble_dev_info_t>& b) 
    {
      int a_rssi_avg = (a->minRSSI + a->maxRSSI) / 2;
      int b_rssi_avg = (b->minRSSI + b->maxRSSI) / 2;
      return a_rssi_avg > b_rssi_avg; // Stronger first
    });
}

class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice advertisedDevice) override {
    updateDeviceList(advertisedDevice);
  }
};

void printDeviceList() {
  Serial.println("----------------------------------------");
  Serial.println("   BLE Address          | RSSI [min,max]");
  Serial.println("----------------------------------------");
  int idx = 1;
  for (const auto& dev : deviceList) {
    String ble_addr_str = dev->addr.toString();
    ble_addr_str.toUpperCase();
    Serial.printf("%2d) '%s' | %d, %d\n",
                  idx++, ble_addr_str.c_str(),
                  dev->minRSSI, dev->maxRSSI);
  }  
  Serial.println("----------------------------------------");
}

void setup() {
  Serial.begin(115200);
  while (!Serial) delay(1);
  Serial.println("Starting BLE scan...");

  BLEDevice::init("");
  pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setActiveScan(true);
  pBLEScan->setInterval(100);
  pBLEScan->setWindow(99);
}

void loop() {
  Serial.printf( "Scanning for BLE devices (%d sec)...\n", scanTime);
  // Start BLE scan (blocking call, waits until the scan completes)
  pBLEScan->start(scanTime, false);
  Serial.println("Scan done....\n");
  printDeviceList();
  delay(5000);
}

รูป: ตัวอย่างข้อความจากการทำงานแสดงของ ESP32 ที่ให้เห็นรายการอุปกรณ์ BLE ที่ตรวจพบ

 


▷ ตัวอย่างโค้ด Arduino ESP32: BLE Sensor#

ถัดไปเป็นโค้ดสาธิตการใช้อุปกรณ์ ESP32 ให้ทำหน้าที่เป็น BLE Peripheral / Server โดยจำลองสถานการณ์ให้อุปกรณ์ดังกล่าว สามารถอ่านค่าจากเซนเซอร์สิ่งแวดล้อม เช่น วัดค่าอุณหภูมิ (Air Temperature) ความชื้นสัมพัทธ์ในอากาศ (Relative Humidity) และความดันบรรยากาศ (Barometric Pressure) เป็นต้น และให้อุปกรณ์ BLE Central / Client เข้ามาเชื่อมต่อเพื่ออ่านข้อมูลดังกล่าวได้ ด้วยวิธี Notify เมื่อมีการอัปเดตค่าจากอุปกรณ์เซนเซอร์

File: esp32_env_sensor.ino

// ESP32 BLE Environmental Sensor (Server / Peripheral)

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

// UUIDs for Environmental Sensing
#define SERVICE_UUID    0x181A // Environmental sensing
#define CHAR_UUID_TEMP  0x2A6E // Temperature Characteristic
#define CHAR_UUID_HUMID 0x2A6F // Hunmidity Characteristic
#define CHAR_UUID_PRESS 0x2A6D // Barometric Pressure Characteristic

BLEServer* pServer = NULL;
BLECharacteristic *pTemperatureCharacteristic;
BLECharacteristic *pHumidityCharacteristic;
BLECharacteristic *pPressureCharacteristic;

bool advertising = false;
bool connected = false;

// Set initial sensor values (for simulation purpose)
float temperature = 30.0;   // deg.C
float humidity    = 60.0;   // %RH
float pressure    = 1010.0; // hPa

class MyServerCallbacks : public BLEServerCallbacks {
  void onConnect(BLEServer* pServer) {
    connected = true;    
    Serial.println("Client connected");
    advertising = false;
    BLEDevice::getAdvertising()->stop();
  }
  void onDisconnect(BLEServer* pServer) {
    connected = false;
    Serial.println("Client disconnected");
  }
};

void setup() {
  Serial.begin(115200);
  Serial.println("Starting ESP32 BLE Environmental Sensor");

  // Init the BLE device and specify the device name
  BLEDevice::init("ESP32 BLE Demo");

  // Set TX power to +9dBm (max.)
  BLEDevice::setPower(ESP_PWR_LVL_P9); // +9 dBm

  // Create a BLE server 
  pServer = BLEDevice::createServer();   
  // Set the user-defined BLE server callback object
  pServer->setCallbacks(new MyServerCallbacks());  
  // Create service  
  BLEService *pService = 
      pServer->createService( BLEUUID((uint16_t)SERVICE_UUID) );
  // Create GATT services
  BLECharacteristic *pCharacteristic;

  // 1) Create temperature characteristic
  pCharacteristic = pService->createCharacteristic(
                                BLEUUID((uint16_t)CHAR_UUID_TEMP),
                                BLECharacteristic::PROPERTY_READ | 
                                BLECharacteristic::PROPERTY_NOTIFY);
  pCharacteristic->addDescriptor(new BLE2902());
  pTemperatureCharacteristic = pCharacteristic;

  // 2) Create humidity characteristic
  pCharacteristic = pService->createCharacteristic(
                              BLEUUID((uint16_t)CHAR_UUID_HUMID),
                              BLECharacteristic::PROPERTY_READ | 
                              BLECharacteristic::PROPERTY_NOTIFY);
  pCharacteristic->addDescriptor(new BLE2902());  
  pHumidityCharacteristic = pCharacteristic;

  // 3) Create pressure characteristic
  pCharacteristic = pService->createCharacteristic(
                              BLEUUID((uint16_t)CHAR_UUID_PRESS),
                              BLECharacteristic::PROPERTY_READ | 
                              BLECharacteristic::PROPERTY_NOTIFY);
  pCharacteristic->addDescriptor(new BLE2902());
  pPressureCharacteristic = pCharacteristic;

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

  // Start service advertising
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(BLEUUID((uint16_t)SERVICE_UUID));
  pAdvertising->setScanResponse(true);
  pAdvertising->setMinPreferred(0x06);
  pAdvertising->setMinPreferred(0x12);
  pAdvertising->start();
  advertising = true;
  Serial.println("BLE device is advertising...");
}

static int32_t randomInt(int32_t min, int32_t max ) {
  int32_t value = esp_random() % (max-min+1) + min;
  return value;
}

void loop() {
  // Simulate envronmental sensor updates
  temperature += (randomInt(-2,2)   * 0.1);
  humidity    += (randomInt(-10,10) * 0.1);
  pressure    += (randomInt(-5,5)   * 0.1);

  // Clamp values
  temperature = constrain(temperature, 25.0, 40.0); // deg.C
  humidity    = constrain(humidity, 45.0, 90.0);    // %
  pressure    = constrain(pressure, 990.0, 1010.0); // hPa

  int16_t temp   = (int16_t)(temperature*100);  // 0.01 deg.C 
  int16_t humid  = (int16_t)(humidity*100);     // 0.01 % 
  uint32_t press = (uint32_t)(pressure*100*10); // 0.1 Pa 

  if (connected) {
    // Update the values
    pTemperatureCharacteristic->setValue( (uint8_t *)&temp, sizeof(temp) );
    pHumidityCharacteristic->setValue( (uint8_t *)&humid, sizeof(humid) );
    pPressureCharacteristic->setValue( (uint8_t *)&press, sizeof(press) );
    // Notify the value changes
    pTemperatureCharacteristic->notify();
    pHumidityCharacteristic->notify();
    pPressureCharacteristic->notify();
  } 
  else {
    if (!advertising) {
      advertising = true;
      // Restart advertising so the server is discoverable again
      BLEDevice::getAdvertising()->start();
      Serial.println("Advertising restarted");
    }
  }

  Serial.printf("Temperature: %.2f °C, ", temperature);
  Serial.printf("Humidity: %.2f %%RH, ", humidity);
  Serial.printf("Pressure: %.1f hPa\n", pressure);
  delay(1000); // Send update every 1 sec
}

รูป: ตัวอย่างข้อความเอาต์พุตจากการทำงานของ ESP32

ผู้ใช้สามารถติดตั้งแอพพลิเคชัน nRF Connect ในสมาร์ทโฟน และลองเชื่อมต่อกับอุปกรณ์ ESP32 BLE

รูป: การใช้ nRF Connect App บนสมาร์ทโฟน Android เชื่อมต่อกับอุปกรณ์ ESP32 BLE

 


▷ การเขียนโค้ด Arduino: ESP32 BLE Client#

ถัดไปเป็นตัวอย่างการเขียนโค้ด Arduino Sketch โดยใช้บอร์ด ESP32 อีกบอร์ดหนึ่ง ให้ทำหน้าที่เป็น BLE Client เพื่อสแกนหาอุปกรณ์ ESP32 BLE Peripheral ตามหมายเลขแอดเดรสที่กำหนดไว้ แล้วเชื่อมต่อเพื่อรับการแจ้งเตือน เมื่อมีการอัปเดตค่าเซนเซอร์

// ESP32 BLE Client (Central)
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
#include <BLEClient.h>
#include <BLE2902.h>
#include <BLEAdvertisedDevice.h>

// Service UUID and Characteristic UUIDs provided by the BLE server
#define SERVICE_UUID    BLEUUID((uint16_t)0x181A)
#define CHAR_UUID_TEMP  BLEUUID((uint16_t)0x2A6E)
#define CHAR_UUID_HUMID BLEUUID((uint16_t)0x2A6F)
#define CHAR_UUID_PRESS BLEUUID((uint16_t)0x2A6D)

static BLEAddress *sensorAddress;
static boolean doConnect = false;
static boolean connected = false;

BLEClient* pClient;
BLERemoteCharacteristic* pTempChar;
BLERemoteCharacteristic* pHumidChar;
BLERemoteCharacteristic* pPressChar;

void notifyCallback(
  BLERemoteCharacteristic* pCharacteristic,
  uint8_t* pData, size_t length, bool isNotify)
{
  if (pCharacteristic->getUUID().equals(CHAR_UUID_TEMP)) {
    int16_t tempRaw = *(int16_t*)pData;
    Serial.printf("Temperature: %.2f °C\n", tempRaw / 100.0);
  } 
  else if (pCharacteristic->getUUID().equals(CHAR_UUID_HUMID)) {
    int16_t humidRaw = *(int16_t*)pData;
    Serial.printf("Humidity: %.2f %%\n", humidRaw / 100.0);
  } 
  else if (pCharacteristic->getUUID().equals(CHAR_UUID_PRESS)) {
    uint32_t pressRaw = *(uint32_t*)pData;
    Serial.printf("Pressure: %.1f hPa\n", pressRaw / 1000.0);
  }
}

class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {

  void onResult(BLEAdvertisedDevice advertisedDevice) {
    sensorAddress = new BLEAddress(advertisedDevice.getAddress());
    Serial.printf("BLE MAC: %s\n", sensorAddress->toString().c_str()); 

    if (advertisedDevice.haveServiceUUID() && 
       advertisedDevice.isAdvertisingService(SERVICE_UUID)) 
    {
      Serial.println("Found a BLE environmental sensor!");
      sensorAddress = new BLEAddress(advertisedDevice.getAddress());
      advertisedDevice.getScan()->stop(); // Stop BLE scanning
      doConnect = true;
    }
  }
};

bool connectToServer() {
  pClient = BLEDevice::createClient();
  Serial.println("Connecting to server...");

  if (!pClient->connect(*sensorAddress)) {
    Serial.println("Failed to connect.");
    return false;
  }

  BLERemoteService* pService = pClient->getService(SERVICE_UUID);
  if (pService == nullptr) {
    Serial.println("Service not found.");
    return false;
  }
  pTempChar  = pService->getCharacteristic(CHAR_UUID_TEMP);
  pHumidChar = pService->getCharacteristic(CHAR_UUID_HUMID);
  pPressChar = pService->getCharacteristic(CHAR_UUID_PRESS);

  if (pTempChar && pTempChar->canNotify()) {
    pTempChar->registerForNotify(notifyCallback);
  }
  if (pHumidChar && pHumidChar->canNotify()) {
    pHumidChar->registerForNotify(notifyCallback);
  }
  if (pPressChar && pPressChar->canNotify()) {
    pPressChar->registerForNotify(notifyCallback);
  }
  return true;
}

void setup() {
  Serial.begin(115200);
  Serial.println("Starting ESP32 BLE Client");

  BLEDevice::init("");
  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(
              new MyAdvertisedDeviceCallbacks() );
  pBLEScan->setInterval(1349);
  pBLEScan->setWindow(449);   
  pBLEScan->setActiveScan(true); // Send a scan request to devices found
  pBLEScan->start(60, false); // Start scanning for 60 sec without blocking
}

void loop() { 
  if (doConnect && !connected) {
    if (connectToServer()) {
      connected = true;
      Serial.println("Connected to sensor!");
    } else {
      Serial.println("Failed to connect, retrying...");
      Serial.println("Restart BLE scan");
      // Restart BLE scan, non-blocking
      BLEDevice::getScan()->stop();
      BLEDevice::getScan()->start(0, false);
      connected = false;
    }
    doConnect = false;
  } 
  delay(1000);
}

รูป: ตัวอย่างข้อความเอาต์พุตที่ได้จากการทำงานของ ESP32 BLE Client ซึ่งแสดงให้เห็นว่า สามารถเชื่อมต่อกับอุปกรณ์ ESP32 BLE Peripheral (Server) ได้

 


▷ การเขียนโค้ด Python โดยใช้ไลบรารี bleak#

ถัดไปเป็นตัวอย่างการเขียนโค้ดด้วยภาษา Python โดยได้เลือกใช้ไลบรารี bleak เพื่อตรวจสอบหาอุปกรณ์ BLE

ให้สร้าง Python Virtual Environment แล้วติดตั้งไลบรารี bleak ด้วยคำสั่งดังนี้

$ pip install bleak 

โค้ดต่อไปนี้ ใช้สำหรับการค้นหาอุปกรณ์ BLE ที่อยู่รอบ ๆ เมื่อตรวจพบอุปกรณ์ ก็จะเลือกหนึ่งอุปกรณ์ ที่มีค่า RSSI สูงสุด ซึ่งจะเป็นอุปกรณ์ที่อยู่ใกล้ ๆ กับคอมพิวเตอร์ของผู้ใช้ แล้วแสดงข้อมูลเกี่ยวกับอุปกรณ์ เช่น Service UUID เป็นต้น

File: bleak_ble_scan.py

import asyncio
from bleak import BleakScanner, BleakClient # use bleak v0.22.x

async def main():
    # Discover devices with advertisement data
    results = await BleakScanner.discover(return_adv=True)
    if not results:
        print("No BLE devices found.")
        return

    # Show all found devices
    for i, (addr, (dev, adv_data)) in enumerate(results.items()):
        print(f"[{i+1}] MAC: {addr} RSSI: {adv_data.rssi}")

    # Pick device with highest RSSI
    dev_addr, (_,adv_data) = max(results.items(), key=lambda item: item[1][1].rssi)
    print(f"\nConnecting to device: {dev_addr} (RSSI: {adv_data.rssi})\n")

    # Connect and list services
    async with BleakClient(dev_addr) as client:
        if client.is_connected:
            print(f"Connected to MAC: {dev_addr}, ",
                  f"name: '{adv_data.local_name}', ", 
                  f"tx_power: {adv_data.tx_power}") 
            services = client.services
            for service in services: # show service UUIDs
                print(f"Service UUID: {service.uuid}")

asyncio.run(main())

เมื่อสามารถสแกนหาอุปกรณ์ BLE ได้แล้ว โค้ดตัวอย่างถัดไป สาธิตการค้นหาอุปกรณ์ และเมื่อพบว่า มีอุปกรณ์ที่มี Service UUID (0x181A) ตรงตามที่กำหนดไว้ ก็ให้เชื่อมต่อและอ่านข้อมูลจากอุปกรณ์ดังกล่าว ในตัวอย่างนี้ได้ใช้บอร์ด ESP32 ทำหน้าที่เป็นอุปกรณ์ BLE Peripheral ที่ให้ข้อมูลเป็นค่าตัวเลขจากเซนเซอร์สิ่งแวดล้อม (Environmental Sensor)

File: bleak_ble_client.py

from bleak import BleakClient
import asyncio

# Specify the target BLE MAC address of the ESP32 board
TARGET_BLE_ADDR = "7C:DF:A1:FD:D9:15" 

# Service UUID and Characteristic UUIDs to be used
SERVICE_UUID = "0000181a-0000-1000-8000-00805f9b34fb"
TEMP_UUID    = "00002a6e-0000-1000-8000-00805f9b34fb"
HUMIDITY_UUID = "00002a6f-0000-1000-8000-00805f9b34fb"
PRESSURE_UUID = "00002a6d-0000-1000-8000-00805f9b34fb"

def bytes2int(data):
    if data is None or len(data) < 2:
        return 0  # หรือ raise Exception
    return int.from_bytes(data, byteorder='little', signed=True)

async def readSensorValues(client: BleakClient):
    try:
        data_raw = await asyncio.wait_for(
                        client.read_gatt_char(TEMP_UUID), timeout=1.0)
        temp = bytes2int(data_raw) / 100.0

        data_raw = await asyncio.wait_for(
                        client.read_gatt_char(HUMIDITY_UUID), timeout=1.0)
        humidity = bytes2int(data_raw) / 100.0

        data_raw = await asyncio.wait_for(
                        client.read_gatt_char(PRESSURE_UUID), timeout=1.0)
        pressure =  bytes2int(data_raw) / 10.0

        print(f"Temperature: {temp:.2f} °C")
        print(f"Humidity:    {humidity:.2f} %")
        print(f"Pressure:    {pressure / 100:.2f} hPa\n")

    except asyncio.TimeoutError:
        print('Read timeout.')

async def main():
    service_found = False
    async with BleakClient(TARGET_BLE_ADDR, timeout=4.0) as client:
        # Wait until the client is connected
        if client.is_connected:
            print("Connected to BLE device.")
            services = client.services
            print("Services discovered:")
            for service in client.services:
                print(f" • UUID: {service.uuid} ({service.description})")
                if service.uuid == SERVICE_UUID:
                    print(f"   - Environmental Sensing service found.")
                    service_found = True
                    break

            if not service_found:
                print('No service UUID found')
                return

            service = client.services.get_service(SERVICE_UUID)
            if not service:
                print("Environmental Sensing service not found.")
                return
            print(60*'-')

            print(f"Service: {service.uuid} ({service.description})")
            for char in service.characteristics:
                print(f" • Characteristic UUID: {char.uuid}")
                print(f"   - Properties: {char.properties}")
            print(60*'-')

            # Read values multiple times
            for i in range(10):
                await readSensorValues(client)
                await asyncio.sleep(1.0) 
        else:
            print("Failed to connect to BLE device.")

# Run the main function
try:
    asyncio.run(main())
except KeyboardInterrupt:
    print('Terminated by user...')
except Exception as e:
    print(f"Failed to connect or communicate: {e}")
finally:
    print('Done...')

รูป: ตัวอย่างข้อความเอาต์พุตจากการทำงานของโค้ด Python (ทดลองใช้กับ Python 3 ในระบบปฏิบัติการ Windows 11) ซึ่งสามารถเชื่อมต่อและรับค่าจาก BLE ESP32 Device ได้สำเร็จ

 


▷ การกรองหาอุปกรณ์ BLE ด้วยคำสั่ง bluetoothctl#

การกรองหาอุปกรณ์ (Device Filtering) อาจใช้วิธีตรวจสอบชื่ออุปกรณ์ หรือตรวจสอบ BLE Address และสามารถใช้คำสั่ง เช่น bluetoothctl สำหรับ Ubuntu / x86_64 หรือ Raspbian-OS / Raspberry Pi เพื่อทดลองสแกนหาอุปกรณ์ BLE ทดลองจับคู่อุปกรณ์ (Pairing) การเชื่อมต่อ (Connecting) และ การเชื่อถืออุปกรณ์ (Trusting) เพื่อให้ระบบจำไว้และเชื่อมต่ออัตโนมัติในอนาคต เป็นต้น

ดังนั้นหากมีอุปกรณ์ ESP32 ที่ถูกโปรแกรมให้ทำหน้าที่เป็นอุปกรณ์ BLE Peripheral ก็สามารถใช้คำสั่ง bluetoothctl เพื่อทดลองเชื่อมต่อกับอุปกรณ์ดังกล่าวได้

ตัวอย่างคำสั่งของ bluetoothctl ที่มีการใช้งาน เช่น

คำสั่ง คำอธิบาย
power on เปิดการทำงานของอะแดปเตอร์ Bluetooth เช่น hci0
agent on เปิด agent สำหรับการจับคู่หรือใส่รหัส PIN / passkey
list แสดงรายการของอะแดปเตอร์ Bluetooth ที่มีอยู่ในเครื่อง
scan on เริ่มต้นการค้นหาอุปกรณ์ Bluetooth รอบ ๆ
scan off หยุดการค้นหาอุปกรณ์ Bluetooth
info [MAC] แสดงรายละเอียดของอุปกรณ์ที่ระบุด้วย MAC address
connect [MAC] เชื่อมต่อกับอุปกรณ์ Bluetooth ตามที่อยู่ MAC
devices แสดงอุปกรณ์ทั้งหมดที่เคยถูกค้นพบหรือจับคู่
pair [MAC] จับคู่กับอุปกรณ์ Bluetooth
trust [MAC] ตั้งให้อุปกรณ์ตามหมายเลขแอดเดรส เป็นที่เชื่อถือ เพื่อให้เชื่อมต่ออัตโนมัติได้ในอนาคต
remove [MAC] ลบอุปกรณ์ตามหมายเลขแอดเดรสที่เคยจับคู่ไว้ ออกไป
quit ออกจากโปรแกรม bluetoothctl

 

ตัวอย่างการใช้คำสั่ง เป็นไปตามข้อความดังต่อไปนี้ (ใช้บอร์ด Raspberry Pi 4 สำหรับการสาธิต)

$ bluetoothctl

[bluetooth]# power on

[bluetooth]# agent on

[bluetooth]# list

[bluetooth]# scan on

[bluetooth]# scan off

[bluetooth]# info 7C:DF:A1:FD:D9:15

[bluetooth]# connect 7C:DF:A1:FD:D9:15

[bluetooth]# devices 

[bluetooth]# info 7C:DF:A1:FD:D9:15

[bluetooth]# quit

รูป: ตัวอย่างการทำคำสั่ง bluetoothctl โดยใช้บอร์ด Raspberry Pi 4

 


กล่าวสรุป#

บทความนี้นำเสนอตัวอย่างการเขียนโค้ด Arduino Sketch สำหรับบอร์ดไมโครคอนโทรลเลอร์ ESP32 เพื่อใช้งานเป็นอุปกรณ์ BLE Peripheral จำลองการทำงานของเซนเซอร์วัดค่าสิ่งแวดล้อม เช่น อุณหภูมิ ความชื้นสัมพัทธ์ และความดันบรรยากาศ อีกทั้งสามารถเชื่อมต่อกับคอมพิวเตอร์ของผู้ใช้ผ่านโค้ด Python โดยใช้ไลบรารี bleak

 


This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Created: 2025-04-25 | Last Updated: 2025-04-26