การเขียนโปรแกรม ESP32 เพื่อใช้งาน BLE ด้วยไลบรารี NimBLE-Arduino (ตอนที่ 1)#


แนะนำ NimBLE-Arduino#

บทความนี้นำเสนอตัวอย่างการใช้งานไลบรารี NimBLE-Arduino สำหรับสื่อสารข้อมูลแบบไร้สายผ่าน Bluetooth Low Energy (BLE) โดยใช้บอร์ดไมโครคอนโทรลเลอร์ ESP32 / ESP32-C3

ในการเขียนโปรแกรมสำหรับชิป Espressif SoC เช่น ESP32 / ESP32-S3 เพื่อความสะดวก มักนิยมใช้ Arduino-ESP32 Core (C/C++) v3.3.x แทนการเขียนด้วย Espressif ESP-IDF SDK v5.5.x โดยตรง สำหรับงานด้าน BLE ทางบริษัท Espressif ได้พัฒนาไลบรารี ESP32-BLE มาให้ใช้งานอยู่แล้ว → ดูบทความที่เกี่ยวข้อง: "ตัวอย่างการเขียนโปรแกรมด้วย Arduino สำหรับใช้งาน ESP32 - Bluetooth LE"

อย่างไรก็ตาม อีกทางเลือกหนึ่งที่น่าสนใจคือการใช้ NimBLE-Arduino ซึ่งมีขนาดเล็กกว่า ประสิทธิภาพสูงกว่า และรองรับทั้งอุปกรณ์ตระกูล Espressif ESP32 และ Nordic Semiconductor nRF5x และหากใช้ Arduino IDE ก็จะต้องมีการติดตั้งไลบรารีใดังกล่าว ก่อนใช้งาน

รูป: การค้นหาและติดตั้งไลบรารี NimBLE-Arduino สำหรับ Arduino IDE

ตัวอย่างการใช้งาน BLE ด้วย NimBLE-Arduino

  • การสแกนอุปกรณ์ (BLE Device Scanning): ตรวจสอบว่าในบริเวณรอบ ๆ มีอุปกรณ์ BLE ใด เปิดใช้งานอยู่ และประกาศบริการ (Advertising) อะไรบ้าง
  • การทำงานในโหมด Peripheral (Server): ทำหน้าที่เป็นอุปกรณ์ให้บริการ เพื่อให้ BLE Central เข้ามาเชื่อมต่อและใช้งาน
  • การทำงานในโหมด Central (Client): เชื่อมต่อไปยังอุปกรณ์ BLE Peripheral เพื่อเข้าถึงบริการที่ประกาศไว้

 


ตัวอย่างโค้ด: BLE Scanner#

โค้ดตัวอย่างนี้แสดงการใช้งาน NimBLE-Arduino สำหรับสแกนอุปกรณ์ BLE รอบ ๆ ตัวบอร์ด
รองรับบอร์ด ESP32 / ESP32-S3 / ESP32-C3 และทดสอบบน Arduino ESP32 Core

/*
 * Arduino ESP32 Core v3.3.3
 * BLE scanner using NimBLE-Arduino v2.3.6
 * Target: ESP32 / ESP32-C3
 * Library: https://github.com/h2zero/NimBLE-Arduino
 */

#include <Arduino.h>
#include <NimBLEDevice.h>

#define LED_ON    (LOW)
#define LED_OFF   (!LED_ON)
#define LED_PIN   (22) // GPIO-22

static constexpr uint32_t SCAN_TIME_MS = 5 * 1000; // 5 seconds scan time (ms)

volatile boolean scanning = false;

// Scan callback class (uses NimBLEScanCallbacks API)
class ScanCallbacks : public NimBLEScanCallbacks {
    /** Called when a device is initially discovered (advertisement packet). */
    void onDiscovered( const NimBLEAdvertisedDevice* advertisedDevice ) override {
        digitalWrite( LED_PIN, LED_ON );
        delay(10);
        digitalWrite( LED_PIN, LED_OFF );
    }

    /**
     * Called when a scan result is available. If active scanning is enabled
     * this contains scan response data as well.
     */
    void onResult(const NimBLEAdvertisedDevice* advertisedDevice) override {
        // Optional: Blink LED when device is seen
        // Print MAC address
        Serial.printf( "Device MAC: %s\n", 
                       advertisedDevice->getAddress().toString().c_str() );
        // Print device name if present
        if (advertisedDevice->haveName()) {
            Serial.printf( "Device Name: '%s', ", 
                           advertisedDevice->getName().c_str() );
        }
        // Print if device is connectable
        if (advertisedDevice->isConnectable()) {
            Serial.printf( "Connectable, " );
        } 
        // Print TX power if advertised
        if (advertisedDevice->haveTXPower()) {
            Serial.printf( "TX Power: %d dBm, ", 
                           advertisedDevice->getTXPower() );
        } 
        Serial.printf( "RSSI: %d dBm\n", advertisedDevice->getRSSI() );
        Serial.printf( "--------------------------------------------------\n" );
    }

    /**
     * Called when the scan ends. The example restarts scanning here.
     * 'reason' is implementation-dependent; we print it for debugging.
     */
    void onScanEnd(const NimBLEScanResults& results, int reason) override {
        // results.getCount() should be 0 (no scan result stored)
        printf("BLE Scan ended.");
        scanning = false;
    }

} scanCallbacks;

void setup() {
    // Initialize Serial
    Serial.begin(115200);
    Serial.flush();

    // Setup LED pin
    pinMode( LED_PIN, OUTPUT );
    digitalWrite( LED_PIN, LED_OFF );

    Serial.println( "\n\nNimBLE continuous scanner..." );

    // Initialize NimBLE stack (empty name for scanner-only)
    NimBLEDevice::init("");

    // Acquire scan object
    NimBLEScan* pBLEScan = NimBLEDevice::getScan();

    // Set callback
    pBLEScan->setScanCallbacks( &scanCallbacks, false /*filter duplicates*/ );

    // Scan every 60 ms (interval = 60 / 0.625 = 96 units)
    pBLEScan->setInterval( 96 );

    // Scan duration within that interval: 30 ms 
    // (window = 30 / 0.625 = 48 units)
    pBLEScan->setWindow( 48 );

    // Active scan requests scan response packets 
    // (more info, but uses more airtime)
    pBLEScan->setActiveScan( true );

    // Do not store results inside NimBLEScan; rely on callbacks instead.
    // setMaxResults(0) means unlimited / don't stash results internally.
    pBLEScan->setMaxResults( 0 );

    // Start scanning (duration in ms, continue=false, restart=true)
    pBLEScan->start( SCAN_TIME_MS /*scan duration*/, 
                     false /*continue*/, true /*restart*/ );
    scanning = true;
    Serial.println( "Scanning started..." );
}

void loop() {
    // Monitor scanning state and optionally restart if it stopped.
    NimBLEScan* pScan = NimBLEDevice::getScan();
    if (!pScan->isScanning()) {
        Serial.println( "Scan stopped-restarting in 5 seconds..." );
        delay(5000);
        pScan->start( SCAN_TIME_MS, false, true );
        scanning = true;
    }
    delay(100);
}

แนวคิดหลักของโค้ดตัวอย่าง#

  • ใช้คลาส NimBLEDevice เพื่อเริ่มต้นระบบ BLE Stack
  • ใช้คลาส NimBLEScan สำหรับจัดการการสแกนอุปกรณ์ BLE
  • สร้างและใช้คลาสย่อย ScanCallbacks เพื่อรับเหตุการณ์ต่าง ๆ ขณะสแกน และมีฟังก์ชันที่เกี่ยวข้อง เช่น
    • พบอุปกรณ์ (onDiscovered)
    • ได้ผลลัพธ์การสแกน (onResult)
    • การสแกนสิ้นสุดลง (onScanEnd)
  • ใช้ LED ที่ขา GPIO-22 กระพริบสั้น ๆ เพื่อแสดงว่าพบอุปกรณ์ BLE ใหม่
  • ใช้ Callback-based Scanning (ไม่บันทึกผลลัพธ์ไว้ภายใน) เพื่อประหยัดหน่วยความจำ

ฟังก์ชันสำคัญของ NimBLE ที่ใช้ในโค้ดตัวอย่าง#

  • NimBLEDevice::init("") ใช้เริ่มต้น BLE Stack และใส่ชื่ออุปกรณ์ได้ (กรณีทำหน้าที่เป็น Peripheral)
  • NimBLEDevice::getScan() คืนค่าพอยน์เตอร์ (Pointer) ไปยังอ็อบเจ็กต์ NimBLEScan เพื่อควบคุมการสแกน BLE แล้วอ้างอิงโดยตัวแปร pBLEScan
  • pBLEScan->setScanCallbacks(&scanCallbacks, false) เชื่อมต่อคลาส Callback (ScanCallbacks) เพื่อรับเหตุการณ์ระหว่างสแกน ไม่กรองอุปกรณ์ซ้ำ
  • pBLEScan->setInterval(96) ตั้งค่าช่วงเวลา Scan Interval (หน่วย 0.625 ms) ค่า 96 = 60 ms (รอบสแกนต่อเนื่องทุก 60 มิลลิวินาที)
  • pBLEScan->setWindow(48) ตั้งค่าช่วงเวลาที่สแกนในแต่ละรอบ (หน่วย 0.625 ms) ค่า 48 = 30 ms (สแกนจริง 30 ms จากทุก ๆ 60 ms ของรอบ)
  • pBLEScan->setActiveScan(true) เปิดโหมด Active Scan เพื่อร้องขอข้อมูลเพิ่มเติมจากอุปกรณ์ที่พบ (ได้ข้อมูลมากขึ้น เช่น ชื่ออุปกรณ์ แต่ใช้พลังงานมากขึ้น)
  • pBLEScan->setMaxResults(0) ไม่จำกัดจำนวนผลลัพธ์ ไม่เก็บผลลัพธ์ไว้ในหน่วยความจำภายใน
  • pBLEScan->start(SCAN_TIME_MS, false, true) เริ่มการสแกน BLE โดยมีพารามิเตอร์:
    • SCAN_TIME_MS ระยะเวลาสแกน (5 วินาที)
    • falseไม่ให้ต่อเนื่องอัตโนมัติ
    • true ให้รีสตาร์ทสแกนใหม่เมื่อสิ้นสุด

การทำงานฟังก์ชัน Callback ที่เกี่ยวข้อง#

  • onDiscovered(const NimBLEAdvertisedDevice* advertisedDevice) ถูกเรียกเมื่อพบอุปกรณ์ BLE ครั้งแรก
  • onResult(const NimBLEAdvertisedDevice* advertisedDevice) ถูกเรียกเมื่อได้ผลลัพธ์การสแกน (รวม Scan Response ถ้ามี) เช่น แสดงข้อมูลอุปกรณ์ เช่น MAC Address, RSSI, TX Power และความสามารถในการเชื่อมต่อได้
  • onScanEnd(const NimBLEScanResults& results, int reason) ถูกเรียกเมื่อการสแกนสิ้นสุดลง ใช้ตรวจสอบสถานะและเริ่มการสแกนใหม่

ดูเอกสาร Online Doc / NimBLE-Arduino API เพื่อศึกษารายละเอียดการใช้คำสั่ง

รูป: ตัวอย่างข้อความเอาต์พุตที่ได้จากบอร์ด ESP32 เมื่อทดลองด้วยโค้ดตัวอย่าง

 


ตัวอย่างโค้ด: BLE Server + Button & LED Service#

โค้ดนี้เป็นตัวอย่างการสร้าง BLE Server บนบอร์ด ESP32 โดยใช้ไลบรารี NimBLE-Arduino ทำหน้าที่รับคำสั่งจาก BLE Client เพื่อควบคุมแอลอีดี (LED) และส่งสถานะของปุ่ม (Button) กลับไปยัง BLE Client แบบ Notify

เมื่อรันโค้ดตัวอย่างนี้ จะทำให้บอร์ด ESP32 จะเริ่มโฆษณาการบริการ (BLE Services) ภายใต้ชื่อ "ESP32-BLE-IO" และทำให้ซอฟต์แวร์ที่ทำหน้าที่เป็น BLE Client เช่น ซอฟต์แวร์ nRF Connect หรือ LightBlue จะเห็นอุปกรณ์นี้ ซึ่งมีสองบริการ ดังนี้

  • LED Service: ถ้าส่งข้อความ "ON" หรือ "OFF" ก็สามารถควบคุมสถานะของ LED ได้
  • Button Service: สามารถอ่านค่าของปุ่มกด หรือรับการแจ้งเตือน (Notify) เมื่อปุ่มถูกกดหรือปล่อย
#include <Arduino.h>
#include <NimBLEDevice.h>

// LED and Button pin definitions
#define LED_ON     (LOW)     // LED is active-low: writing LOW turns it on
#define LED_OFF    (!LED_ON)
#define LED_PIN    (22)      // Onboard LED pin
#define BUTTON_PIN (14)      // Push button pin (active-low)

// BLE Server and Characteristics pointers
NimBLEServer* pServer = nullptr;
NimBLECharacteristic* pLedCharacteristic = nullptr;
NimBLECharacteristic* pButtonCharacteristic = nullptr;

// Global state variables
bool deviceConnected = false; // Tracks if a BLE client is connected
bool lastButtonState = HIGH;  // Tracks the button state for change detection

/* UUID definitions for BLE services and characteristics */
#define SERVICE_LED_UUID "19b10010-e8f2-537e-4f6c-d104768a1214"
#define CHARACTERISTIC_LED_UUID "19b10011-e8f2-537e-4f6c-d104768a1214"
#define SERVICE_BUTTON_UUID "19b10020-e8f2-537e-4f6c-d104768a1214"
#define CHARACTERISTIC_BUTTON_UUID "19b10021-e8f2-537e-4f6c-d104768a1214"

// BLE Server callback class
class ServerCallbacks : public NimBLEServerCallbacks {
  void onConnect( NimBLEServer* pServer, 
                  NimBLEConnInfo& connInfo ) override {
    deviceConnected = true;
    Serial.printf( "Client connected: %s\n", 
                    connInfo.getAddress().toString().c_str() );

    // Optional: adjust connection parameters 
    // (min interval, max interval, latency, timeout)
    // Min. connection interval = 24 × 1.25 ms = 30 ms.
    // Max. connection interval = 48 × 1.25 ms = 60 ms.
    // Connection timeout = 180 × 10 ms = 1800 ms (1.8 s).
    pServer->updateConnParams( connInfo.getConnHandle(), 24, 48, 0, 180 );
  }

  void onDisconnect( NimBLEServer* pServer, 
                     NimBLEConnInfo& connInfo, int reason ) override {
    deviceConnected = false;
    Serial.printf( "Client disconnected (%d), restarting advertising.\n", reason );

    // Restart advertising so new clients can connect
    NimBLEDevice::startAdvertising();
  }
};

// LED characteristic write callback class
class LedCharacteristicCallbacks : public NimBLECharacteristicCallbacks {
  void onWrite( NimBLECharacteristic* pCharacteristic, 
                NimBLEConnInfo& connInfo ) override {
    std::string value = pCharacteristic->getValue(); // Read the written value
    if (!value.empty()) {
      String cmd = String( value.c_str() ); // Convert to Arduino String
      cmd.toUpperCase();  // Convert command to uppercase 

      if (cmd.equals("ON")) {
        digitalWrite( LED_PIN, LED_ON );  // Turn LED on
        Serial.println( "LED ON" );
      } 
      else if (cmd.equals("OFF")) {
        digitalWrite( LED_PIN, LED_OFF ); // Turn LED off
        Serial.println( "LED OFF" );
      } 
      else {
        Serial.printf( "Unknown LED command: %c\n", cmd ); 
      }
    }
  }
};

void setup() {
  Serial.begin( 115200 );  // Initialize Serial
  Serial.setDebugOutput( false );

  Serial.flush();
  Serial.println( "\n\n\nESP32-BLE server demo" );
  Serial.println( "Arduino-ESP32 v3.3.0 + NimBLE v2.3.6)..." );

  // Configure GPIO pins
  pinMode( LED_PIN, OUTPUT );
  pinMode( BUTTON_PIN, INPUT_PULLUP ); // Button with internal pull-up

  // Initialize BLE device
  NimBLEDevice::init( "ESP32 BLE-IO (LED-Button) Service" );

  // Create BLE server and set callbacks
  pServer = NimBLEDevice::createServer();
  pServer->setCallbacks( new ServerCallbacks() );

  // LED Service
  NimBLEService* ledService = pServer->createService( SERVICE_LED_UUID );

  // Create LED characteristic (write-only)
  pLedCharacteristic = ledService->createCharacteristic(
    CHARACTERISTIC_LED_UUID,
    NIMBLE_PROPERTY::WRITE
  );
  pLedCharacteristic->setCallbacks( new LedCharacteristicCallbacks() );
  ledService->start();

  // Button Service
  NimBLEService* buttonService = pServer->createService( SERVICE_BUTTON_UUID );

  // Create Button characteristic (read + notify)
  pButtonCharacteristic = buttonService->createCharacteristic(
    CHARACTERISTIC_BUTTON_UUID,
    NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY
  );
  pButtonCharacteristic->setValue( "RELEASED" ); // Initial state: released
  buttonService->start();

  // Start BLE advertising
  NimBLEAdvertising* advertising = NimBLEDevice::getAdvertising();
  advertising->setName( "ESP32-BLE-IO" );
  advertising->addServiceUUID( SERVICE_LED_UUID );
  advertising->addServiceUUID( SERVICE_BUTTON_UUID );
  advertising->enableScanResponse( true );
  advertising->start();

  Serial.println( "BLE advertising started..." );

  // Flash LED 3 times on startup
  for (int i = 0; i < 3; i++) {
    digitalWrite( LED_PIN, LED_ON );
    delay( 100 );
    digitalWrite( LED_PIN, LED_OFF );
    delay( 100 );
  }
}

void loop() {
  // Read button state (active-low)
  bool buttonState = digitalRead( BUTTON_PIN );
  // Detect state change
  if (buttonState != lastButtonState) {
    lastButtonState = buttonState;

    // Update characteristic value: "PRESSED" or "RELEASED"
    String val = buttonState ? "RELEASED" : "PRESSED";
    pButtonCharacteristic->setValue( val.c_str() );

    // Notify connected client
    if (deviceConnected) {
      pButtonCharacteristic->notify();
    }

    // Print state to Serial
    Serial.printf( "Button: %s\n", val.c_str() );
  }
  delay(100);
}

ภาพรวมการทำงานของโค้ดตัวอย่าง#

  1. เริ่มต้นระบบการทำงานของ BLE
    • ใช้คำสั่ง NimBLEDevice::init(...) เพื่อเริ่มต้น BLE Stack และตั้งชื่ออุปกรณ์เป็น "ESP32 BLE-IO"
  2. สร้าง BLE Server
    • ใช้ NimBLEDevice::createServer() เพื่อสร้าง BLE Server ที่จะให้บริการ
    • กำหนดฟังก์ชัน Callback ผ่านคลาส ServerCallbacks สำหรับจัดการเหตุการณ์ เช่น
      • onConnect() เมื่อ Client เชื่อมต่อเข้ามา
      • onDisconnect() เมื่อ Client ยกเลิกหรือตัดการเชื่อมต่อ (จะเริ่มโฆษณาใหม่โดยอัตโนมัติ)
  3. สร้างบริการแยกตามการทำงาน
    • LED Service (UUID: 19b10010-e8f2-537e-4f6c-d104768a1214)
      • มีการกำหนด Service Characteristic สำหรับเขียนคำสั่ง (WRITE) ใช้ควบคุม LED (ON / OFF)
    • Button Service (UUID: 19b10020-e8f2-537e-4f6c-d104768a1214)
      • มีการกำหนด Service Characteristic สำหรับอ่านสถานะ (READ) และแจ้งเตือน (NOTIFY) โดยจะส่งข้อความ "PRESSED" หรือ "RELEASED" ให้กับ Client
  4. เริ่มการโฆษณา (BLE Advertising)
    • ตั้งชื่ออุปกรณ์เป็น "ESP32-BLE-IO"
    • เพิ่ม Service UUID ทั้งสอง ในแพ็กเกจโฆษณา
    • เริ่มโฆษณาเพื่อรอการเชื่อมต่อจาก BLE Client
  5. ในฟังก์ชัน loop()
    • อ่านสถานะปุ่มจากขา GPIO-14 (active-low)
    • เมื่อสถานะเปลี่ยน จะอัปเดตค่าใน Characteristic ของปุ่มกด
    • ถ้ามี BLE Client เชื่อมต่อ จะส่งค่าไปให้ผ่านฟังก์ชัน notify() และแสดงสถานะปุ่ม (PRESSED / RELEASED) ผ่าน Serial Monitor

ฟังก์ชันสำคัญที่ใช้ในโค้ด#

  • NimBLEDevice::init(name) เริ่มต้น BLE Stack และตั้งชื่ออุปกรณ์
  • NimBLEDevice::createServer() สร้าง BLE Server สำหรับให้บริการ
  • createService(UUID) สร้าง BLE Service ด้วย UUID ที่กำหนด
  • createCharacteristic(UUID, properties) สร้าง BLE Characteristic พร้อมกำหนดคุณสมบัติ (เช่น READ, WRITE, NOTIFY)
  • setCallbacks(...) กำหนดคลาส Callback สำหรับจัดการเหตุการณ์ (เขียนค่า, เชื่อมต่อ, ตัดการเชื่อมต่อ)
  • setValue(value) / notify() ใช้กำหนดค่าและแจ้งเตือน (Notify) ไปยัง BLE Client
  • NimBLEDevice::startAdvertising() เริ่มโฆษณาเพื่อให้ BLE Client เข้ามาเชื่อมต่อ

การทำงานของฟังก์ชัน Callback

  1. ServerCallbacks
    • onConnect() ถูกเรียกเมื่อ BLE Client เชื่อมต่อเข้ามา
    • onDisconnect() ถูกเรียกเมื่อ BLE Client ออกจากการเชื่อมต่อ และรีสตาร์ทการโฆษณาใหม่
  2. LedCharacteristicCallbacks
    • onWrite() ถูกเรียกเมื่อ BLE Client เขียนค่ามายัง LED Characteristic ("ON" หรือ"OFF")

 


การทดลองด้วยแอป nRF Connect for Mobile#

ขั้นตอนถัดไปคือการใช้แอป nRF Connect for Mobile (จากบริษัท Nordic Semiconductor)
เพื่อยืนยันการทำงานของบริการ (Service) และลักษณะข้อมูล (Characteristic) ที่อุปกรณ์ได้เปิดให้ใช้งานจริง

จุดประสงค์ของการทดสอบด้วย nRF Connect

  • ตรวจสอบว่า ESP32 ประกาศชื่อและบริการ BLE ได้ถูกต้อง
  • ทดสอบการอ่านหรือเขียนค่าจาก Service Characteristic โดยตรง
  • ตรวจสอบว่า Service Characteristic สำหรับปุ่มส่ง การแจ้งเตือน (Notify) ได้ตามที่คาดไว้
  • ตรวจสอบโครงสร้าง UUID ของ Service และ Characteristic

ขั้นตอนการใช้งานเบื้องต้น

  1. เปิดแอป nRF Connect for Mobile บนสมาร์ทโฟน (Android / iOS)
  2. เปิด Bluetooth บนอุปกรณ์ แล้วกด SCAN เพื่อค้นหาอุปกรณ์ BLE รอบตัว
  3. มองหาอุปกรณ์ชื่อ ESP32-BLE-IO แล้วกด CONNECT เพื่อเชื่อมต่อ
  4. แอปจะแสดงรายชื่อ Services และ Characteristics ที่ ESP32 เปิดให้ใช้งาน เช่น
  5. 19B10010-E8F2-537E-4F6C-D104768A1214 (LED Service)
  6. 19B10020-E8F2-537E-4F6C-D104768A1214 (Button Service)
  7. ทดสอบการส่งหรือรับข้อมูล
  8. เลือก LED Characteristic แล้วกด WRITE แล้วพิมพ์ส่งค่า "ON" หรือ "OFF" เพื่อควบคุม LED
  9. เปิด Notifications บน Button Characteristic แล้วลองกดปุ่มจริงบนบอร์ด
    จะเห็นข้อความ "PRESSED" / "RELEASED" แสดงบนหน้าจอ

รูป: ตัวอย่างการเชื่อมต่อด้วย nRF Connect for Mobile บนสมาร์ทโฟน

 


การเชื่อมต่อเพื่อใช้บริการ BLE บนหน้าเว็บด้วย Chrome#

ถัดไปเป็นตัวอย่างโค้ด HTML + JavaScript สาธิตการทำงานของ Web Bluetooth Application ในเบื้องต้น ใช้สำหรับทดสอบและสาธิตการสื่อสาร BLE ระหว่างเว็บเบราว์เซอร์ Google Chrome กับบอร์ด ESP32 ซึ่งทำหน้าที่เป็น BLE Server ที่มี LED Service และ Button Service ตามตัวอย่างโค้ดที่แล้ว

วัตถุประสงค์ของการทดสอบ#

  • แสดงการเชื่อมต่อ ESP32 BLE Server ผ่าน Web Bluetooth API
  • ส่งคำสั่งควบคุม LED (ON / OFF) ผ่าน LED Characteristic แบบ WRITE
  • อ่านและรับการแจ้งเตือน (Notify) จากปุ่มกดบนบอร์ด ESP32
  • สาธิตการอัปเดตสถานะเรียลไทม์บนหน้าเว็บ โดยไม่ต้องใช้แอปมือถือ

รูป: เปิดใช้งาน Web Bluetooth Experimental Feature ใน Google Chrome Browser (ไปที่ chrome://flags)

ลำดับการทำงานของโค้ดตัวอย่างโดยสรุป#

  1. เมื่อกดปุ่ม "Connect"
    • เว็บเพจจะค้นหาอุปกรณ์ BLE ที่ชื่อขึ้นต้นด้วย "ESP32-BLE-IO"
    • เมื่อเลือกอุปกรณ์แล้ว จะเชื่อมต่อไปยัง GATT Server ที่ให้บริการโดย ESP32
  2. หลังจากเชื่อมต่อสำเร็จ
    • ดึงข้อมูล LED Service และ Button Service
    • ติดต่อกับ Service Characteristic แต่ละบริการ
    • เริ่มรับการแจ้งเตือนจากปุ่มกดบนบอร์ด ESP32
  3. เมื่อปุ่มบนบอร์ด ESP32 ถูกกดหรือปล่อย
    • ESP32 จะส่งข้อความ "PRESSED" หรือ "RELEASED"
    • เว็บเพจจะอัปเดตค่าบนหน้าจอ (Button state: ...) แบบเรียลไทม์
  4. ปุ่ม "LED Toggle"
    • ใช้สลับสถานะของ LED ระหว่าง "ON" และ "OFF"
    • เมื่อกดแล้ว จะส่งข้อความไปยัง ESP32 ผ่าน LED Characteristic
    • ปุ่มจะอัปเดตข้อความแสดงสถานะล่าสุด (LED ON / LED OFF)
  5. ปุ่ม "Disconnect"
    • เมื่อเชื่อมต่ออยู่และกดอีกครั้ง จะตัดการเชื่อมต่อ BLE
  6. รีเซ็ตสถานะ UI และปิดการใช้งานปุ่มควบคุม

File: index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>ESP32 BLE Toggle Test</title>
</head>
<body>
  <h2>ESP32 BLE Web Toggle</h2>
  <button id="connectBtn">Connect</button>
  <button id="ledBtn" disabled>LED Toggle</button>
  <p>Button state: <span id="btnState">?</span></p>

  <script>
    // UUIDs for ESP32 BLE services and characteristics
    const SERVICE_LED = "19b10010-e8f2-537e-4f6c-d104768a1214"; // LED service
    const CHAR_LED = "19b10011-e8f2-537e-4f6c-d104768a1214";    // LED characteristic (write)
    const SERVICE_BTN = "19b10020-e8f2-537e-4f6c-d104768a1214"; // Button service
    const CHAR_BTN = "19b10021-e8f2-537e-4f6c-d104768a1214";    // Button characteristic (read/notify)

    // Variables to store device, GATT server, and characteristics
    let device, server, ledChar, btnChar;
    let ledState = false; // Track LED state: false = OFF, true = ON

    // DOM elements for buttons
    const connectBtn = document.getElementById("connectBtn");
    const ledBtn = document.getElementById("ledBtn");

    // Connect / Disconnect button click handler
    connectBtn.onclick = async () => {
      // If already connected, disconnect and reset UI
      if (device && device.gatt.connected) {
        await device.gatt.disconnect();      // Disconnect from BLE device
        connectBtn.textContent = "Connect";  // Update button text
        ledBtn.disabled = true;              // Disable LED toggle button
        document.getElementById("btnState").textContent = "?"; // Reset button state display
        console.log("Disconnected"); 
        return;
      }

      try {
        // Request device with filters: name prefix + advertised service
        device = await navigator.bluetooth.requestDevice({
          filters: [{ namePrefix: 'ESP32-BLE-IO', services: [SERVICE_LED] }],
          optionalServices: [SERVICE_LED, SERVICE_BTN] // Required to access both services
        }); 

        server = await device.gatt.connect(); // Connect to GATT server

        // Get LED characteristic from LED service
        const ledService = await server.getPrimaryService(SERVICE_LED);
        ledChar = await ledService.getCharacteristic(CHAR_LED);

        // Get Button characteristic from Button service
        const btnService = await server.getPrimaryService(SERVICE_BTN);
        btnChar = await btnService.getCharacteristic(CHAR_BTN);

        // Subscribe to button notifications
        await btnChar.startNotifications(); // Enable notifications
        btnChar.addEventListener('characteristicvaluechanged', e => {
          const val = new TextDecoder().decode(e.target.value); // Decode value
          document.getElementById("btnState").textContent = val; // Update UI
        });

        // Read initial button state immediately after connecting
        const initVal = await btnChar.readValue();
        document.getElementById("btnState").textContent = new TextDecoder().decode(initVal);

        // Update UI to show connected state
        connectBtn.textContent = "Disconnect";
        ledBtn.disabled = false;  // Enable LED toggle
        console.log("Connected to ESP32 BLE!");
      } catch (err) {
        console.error(err);  // Log any errors
      }
    };

    // LED toggle button click handler
    ledBtn.onclick = async () => {
      if (!ledChar) return;                   // Exit if characteristic not available
      const cmd = ledState ? "OFF" : "ON";    // Toggle command
      await ledChar.writeValue(new TextEncoder().encode(cmd)); // Write command to characteristic
      ledState = !ledState;     // Update local LED state
      ledBtn.textContent = ledState ? "LED ON" : "LED OFF"; // Update button text
    };
  </script>
</body>
</html>

รูป: ตัวอย่างหน้าเว็บแสดงการเชื่อมต่อกับอุปกรณ์ ESP32 ด้วย BLE

รูปตัวอย่างเป็นการเปิดใช้งาน Local Web Server ผู้ใช้สามารถติดตั้ง Live Server Extension ใน VS Code IDEโดยคลิกขวาที่ index.html แล้วเลือก "Open with Live Server" เบราว์เซอร์จะเปิดขึ้นและรันไฟล์บน localhost

 


กล่าวสรุป#

บทความนี้แนะนำการใช้งาน BLE (Bluetooth Low Energy) บนบอร์ด ESP32 / ESP32-S3 ผ่านไลบรารี NimBLE-Arduino โดยตัวอย่างโค้ดครอบคลุมทั้งการสแกนอุปกรณ์ BLE และการสร้าง BLE Server ที่ให้บริการควบคุม LED และอ่านสถานะปุ่มกด (Button) ผ่าน BLE Characteristic มีการใช้ฟังก์ชัน Callback ในการจัดการเหตุการณ์ต่าง ๆ เช่น การเชื่อมต่อ/ตัดการเชื่อมต่อ และการเขียนค่าไปยัง Characteristic ทั้งนี้ยังมีตัวอย่างการทดลองเชื่อมต่อผ่านแอป nRF Connect for Mobile

นอกจากนี้ บทความยังสาธิตการเชื่อมต่อ BLE กับเว็บเบราว์เซอร์ Google Chrome ผ่าน Web Bluetooth API โดยสร้างหน้าเว็บ HTML + JavaScript ที่สามารถเชื่อมต่อกับ ESP32 BLE Server เพื่อ อ่านสถานะปุ่มแบบเรียลไทม์ (Notify) และ ควบคุม LED ซึ่งช่วยให้ผู้ใช้งานสามารถทดลองสื่อสารกับอุปกรณ์ BLE ได้โดยไม่ต้องใช้แอปมือถือ ทำให้การพัฒนาและทดสอบอุปกรณ์ BLE มีความยืดหยุ่นและสะดวกขึ้นทั้งบนมือถือและเว็บเบราว์เซอร์

บทความที่เกี่ยวข้อง

 


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

Created: 2026-01-20 | Last Updated: 2026-01-20