IoT

ESP32 Web Bluetooth API: Komunikasi BLE dari Browser

TOKEN

Pelajari cara membangun komunikasi BLE antara ESP32 dan browser menggunakan Web Bluetooth API tanpa aplikasi native

1. Apa itu Web Bluetooth API?

Web Bluetooth API adalah standar W3C yang memungkinkan halaman web JavaScript untuk berkomunikasi dengan perangkat Bluetooth Low Energy (BLE) secara langsung dari browser. Teknologi ini menghilangkan kebutuhan akan aplikasi native atau koneksi serial untuk berinteraksi dengan perangkat IoT berbasis BLE.

Dengan menggabungkan Web Bluetooth di sisi browser dan ESP32 sebagai BLE peripheral, kita bisa membangun aplikasi IoT yang sangat mudah diakses — cukup buka URL di browser, klik connect, dan mulai berkomunikasi dengan perangkat keras.

Mengapa Web Bluetooth Penting untuk IoT?

Aspek Aplikasi Native Web Bluetooth
DeploymentPerlu install di App Store/Play StoreCukup buka URL
Cross-platformPerlu develop per platformSatu kode untuk semua OS
UpdatePerlu update manualUpdate otomatis di server
AksesibilitasMembutuhkan installInstant access
Fitur BLELengkapGATT read/write/notify
âš ī¸ Keterbatasan

Web Bluetooth hanya didukung di Chrome, Edge, dan Opera di desktop. Safari (iOS/macOS) dan Firefox belum mendukung Web Bluetooth. Untuk iOS, pertimbangkan menggunakan Bridgefy SDK atau alternatif lain.

2. BLE Advertising di ESP32

BLE Advertising adalah mekanisme di mana perangkat BLE secara periodik mengirimkan paket data tanpa koneksi (connectionless). Perangkat lain yang mendengarkan (scanner) dapat membaca data advertising ini untuk mengetahui keberadaan dan informasi dasar perangkat BLE.

Pada ESP32, kita bisa mengirim Advertisement Data yang berisi nama perangkat, manufacturer-specific data, dan layanan yang tersedia.

C++ (Arduino)
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEAdvertising.h>

// Inisialisasi BLE dengan nama perangkat
void setupBLEAdvertising() {
  // Init BLE Device
  BLEDevice::init("ESP32-WebBT");

  // Buat server BLE
  BLEServer *pServer = BLEDevice::createServer();

  // Buat service
  BLEService *pService = pServer->createService(
    BLEUUID((uint16_t)0x181A)  // Environmental Sensing
  );

  // Buat characteristic untuk data suhu
  BLECharacteristic *pTempCharacteristic = pService->createCharacteristic(
    BLEUUID((uint16_t)0x2A6E),  // Temperature
    BLECharacteristic::PROPERTY_READ |
    BLECharacteristic::PROPERTY_NOTIFY
  );

  // Mulai service
  pService->start();

  // Konfigurasi advertising
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(BLEUUID((uint16_t)0x181A));
  pAdvertising->setScanResponse(true);

  // Atur interval dan power
  pAdvertising->setMinPreferred(0x06);
  pAdvertising->setMaxPreferred(0x12);
  pAdvertising->setTxPower(4);  // TX Power: +4 dBm

  // Mulai advertising
  BLEDevice::startAdvertising();
  Serial.println("BLE Advertising dimulai...");
}

void loop() {
  // Update nilai suhu
  float suhu = readTemperature();
  int16_t suhuRaw = (int16_t)(suhu * 100);

  pTempCharacteristic->setValue((uint8_t *)&suhuRaw, 2);
  pTempCharacteristic->notify();

  delay(2000);
}

Tipe Advertising Data

📡
Connectable Advertising
Perangkat dapat dikoneksi
  • ✅ Menerima koneksi dari central
  • ✅ GATT services tersedia
  • ✅ Read, Write, Notify
  • ❌ Interval lebih lambat
  • ✅ Cocok untuk Web Bluetooth
📡
Non-Connectable Advertising
Hanya broadcast data
  • ❌ Tidak bisa dikoneksi
  • ❌ Tidak ada GATT services
  • ✅ Interval lebih cepat
  • ✅ Daya lebih hemat
  • ✅ Cocok untuk Beacon

3. GATT Services & Characteristics

GATT (Generic Attribute Profile) adalah struktur data yang digunakan dalam BLE untuk mengorganisir informasi. GATT terdiri dari Services yang berisi Characteristics, dan masing-masing Characteristics memiliki Descriptors.

Hierarki GATT
đŸ“Ļ
Profile
Kumpulan Services
↓
🔧
Service A
UUID: 0x181A
🔧
Service B
UUID: 0x180F
↓
📊
Characteristic
Read / Write / Notify
📊
Characteristic
Read only
📝
Descriptor
CCCD / User Desc

Standard BLE UUIDs

UUID (16-bit) Nama Deskripsi
0x180ADevice InformationInformasi perangkat (model, serial)
0x180FBattery ServiceLevel baterai
0x181AEnvironmental SensingSuhu, kelembaban, tekanan
0x180DHeart RateDetak jantung
0x1809Health ThermometerSuhu dari termometer
0x1800Generic AccessInformasi dasar perangkat
💡 Tips

Untuk proyek custom, gunakan Custom UUID 128-bit yang dihasilkan dari UUID generator. Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. Service dan Characteristic dalam proyek yang sama harus menggunakan UUID yang konsisten.

4. Membuat BLE Server di ESP32

Sekarang kita akan membuat BLE Server lengkap di ESP32 yang akan berfungsi sebagai peripheral yang bisa diakses dari Web Bluetooth di browser.

C++ (Arduino)
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

// Custom UUIDs (generate di https://www.uuidgenerator.net/)
#define SERVICE_UUID        "12345678-1234-1234-1234-123456789abc"
#define CHAR_READ_UUID      "12345678-1234-1234-1234-123456789001"
#define CHAR_WRITE_UUID     "12345678-1234-1234-1234-123456789002"
#define CHAR_NOTIFY_UUID    "12345678-1234-1234-1234-123456789003"

BLECharacteristic *pReadChar;
BLECharacteristic *pWriteChar;
BLECharacteristic *pNotifyChar;
bool deviceConnected = false;
float suhu = 25.0;
int ledState = 0;

// Callback saat koneksi berubah
class MyServerCallbacks : public BLEServerCallbacks {
  void onConnect(BLEServer* pServer) {
    deviceConnected = true;
    Serial.println("Client terhubung!");
  }

  void onDisconnect(BLEServer* pServer) {
    deviceConnected = false;
    Serial.println("Client terputus, mulai advertising...");
    // Restart advertising setelah disconnect
    pServer->getAdvertising()->start();
  }
};

// Callback untuk write characteristic
class MyWriteCallbacks : public BLECharacteristicCallbacks {
  void onWrite(BLECharacteristic *pCharacteristic) {
    String value = pCharacteristic->getValue();
    if (value.length() > 0) {
      Serial.print("Data diterima: ");
      Serial.println(value.c_str());

      // Contoh: kontrol LED berdasarkan data yang ditulis
      if (value == "ON") {
        digitalWrite(LED_BUILTIN, HIGH);
        ledState = 1;
      } else if (value == "OFF") {
        digitalWrite(LED_BUILTIN, LOW);
        ledState = 0;
      }
    }
  }
};

void setup() {
  Serial.begin(115200);
  pinMode(LED_BUILTIN, OUTPUT);

  // 1. Init BLE Device
  BLEDevice::init("ESP32-WebBT");

  // 2. Buat Server
  BLEServer *pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());

  // 3. Buat Service
  BLEService *pService = pServer->createService(SERVICE_UUID);

  // 4. Buat Characteristics

  // Read-only characteristic (suhu)
  pReadChar = pService->createCharacteristic(
    CHAR_READ_UUID,
    BLECharacteristic::PROPERTY_READ
  );
  pReadChar->setValue("25.0");

  // Write-only characteristic (kontrol LED)
  pWriteChar = pService->createCharacteristic(
    CHAR_WRITE_UUID,
    BLECharacteristic::PROPERTY_WRITE
  );
  pWriteChar->setCallbacks(new MyWriteCallbacks());

  // Notify characteristic (data periodik)
  pNotifyChar = pService->createCharacteristic(
    CHAR_NOTIFY_UUID,
    BLECharacteristic::PROPERTY_NOTIFY
  );
  pNotifyChar->addDescriptor(new BLE2902());

  // 5. Mulai Service
  pService->start();

  // 6. Konfigurasi & Mulai Advertising
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pAdvertising->setScanResponse(true);
  pAdvertising->setMinPreferred(0x06);
  pAdvertising->setMaxPreferred(0x12);
  BLEDevice::startAdvertising();

  Serial.println("BLE Server siap! Menunggu koneksi...");
}

void loop() {
  // Kirim data suhu via notify setiap 2 detik
  if (deviceConnected) {
    suhu += random(-5, 6) / 10.0;
    String suhuStr = String(suhu, 1);
    pNotifyChar->setValue(suhuStr.c_str());
    pNotifyChar->notify();
    Serial.println("Suhu dikirim: " + suhuStr + " °C");
  }
  delay(2000);
}
â„šī¸ Penjelasan Kode

Kode di atas membuat 3 characteristic: read untuk membaca suhu, write untuk mengontrol LED, dan notify untuk mengirim data suhu secara periodik ke client yang terhubung.

5. Web Bluetooth Client di Browser

Sekarang mari kita buat sisi browser yang akan terhubung ke ESP32 BLE Server menggunakan Web Bluetooth API.

JavaScript (HTML)
<!DOCTYPE html>
<html lang="id">
<head>
  <meta charset="UTF-8">
  <title>ESP32 Web Bluetooth Controller</title>
  <style>
    body { font-family: 'Segoe UI', sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; }
    .card { border: 1px solid #ddd; border-radius: 12px; padding: 20px; margin: 10px 0; }
    .status { padding: 10px; border-radius: 8px; margin: 10px 0; }
    .connected { background: #d4edda; color: #155724; }
    .disconnected { background: #f8d7da; color: #721c24; }
    button { padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer;
             background: #007bff; color: white; font-size: 16px; margin: 5px; }
    button:hover { background: #0056b3; }
    button:disabled { background: #ccc; cursor: not-allowed; }
    #tempDisplay { font-size: 48px; font-weight: bold; text-align: center; color: #333; }
    .control-panel { display: flex; gap: 10px; flex-wrap: wrap; }
  </style>
</head>
<body>
  <h1>đŸŒĄī¸ ESP32 Web Bluetooth</h1>

  <div class="card">
    <div id="status" class="status disconnected">🔴 Terputus</div>
    <button id="connectBtn" onclick="connectBLE()">🔗 Connect ke ESP32</button>
    <button id="disconnectBtn" onclick="disconnectBLE()" disabled>❌ Disconnect</button>
  </div>

  <div class="card">
    <h3>Suhu Real-time</h3>
    <div id="tempDisplay">-- °C</div>
    <button onclick="readTemperature()">📖 Baca Suhu</button>
  </div>

  <div class="card">
    <h3>Kontrol LED</h3>
    <div class="control-panel">
      <button onclick="sendCommand('ON')">💡 LED ON</button>
      <button onclick="sendCommand('OFF')">âŦ› LED OFF</button>
    </div>
  </div>

  <div class="card">
    <h3>Log Aktivitas</h3>
    <div id="log" style="height:150px;overflow-y:auto;background:#f5f5f5;padding:10px;
         border-radius:8px;font-family:monospace;font-size:13px;"></div>
  </div>

  <script>
    // Custom UUIDs — harus sama dengan yang di ESP32!
    const SERVICE_UUID        = '12345678-1234-1234-1234-123456789abc';
    const CHAR_READ_UUID      = '12345678-1234-1234-1234-123456789001';
    const CHAR_WRITE_UUID     = '12345678-1234-1234-1234-123456789002';
    const CHAR_NOTIFY_UUID    = '12345678-1234-1234-1234-123456789003';

    let device, server, readChar, writeChar, notifyChar;

    function log(msg) {
      const el = document.getElementById('log');
      el.innerHTML += `[${new Date().toLocaleTimeString()}] ${msg}<br>`;
      el.scrollTop = el.scrollHeight;
    }

    function setStatus(text, connected) {
      const el = document.getElementById('status');
      el.textContent = connected ? `đŸŸĸ ${text}` : `🔴 ${text}`;
      el.className = `status ${connected ? 'connected' : 'disconnected'}`;
    }

    async function connectBLE() {
      try {
        log('Mencari perangkat BLE...');

        // Request perangkat dari user
        device = await navigator.bluetooth.requestDevice({
          filters: [{ services: [SERVICE_UUID] }],
          optionalServices: [SERVICE_UUID]
        });

        log(`Perangkat ditemukan: ${device.name}`);
        device.addEventListener('gattserverdisconnected', onDisconnected);

        // Connect ke GATT Server
        log('Menghubungkan ke GATT Server...');
        server = await device.gatt.connect();
        setStatus(`${device.name} terhubung!`, true);

        // Dapatkan service
        const service = await server.getPrimaryService(SERVICE_UUID);
        log('Service ditemukan!');

        // Dapatkan characteristics
        readChar = await service.getCharacteristic(CHAR_READ_UUID);
        writeChar = await service.getCharacteristic(CHAR_WRITE_UUID);
        notifyChar = await service.getCharacteristic(CHAR_NOTIFY_UUID);
        log('Semua characteristics ditemukan!');

        // Aktifkan notify
        await notifyChar.startNotifications();
        notifyChar.addEventListener('characteristicvaluechanged', onNotify);
        log('Notify diaktifkan — menunggu data...');

        // Enable tombol
        document.getElementById('connectBtn').disabled = true;
        document.getElementById('disconnectBtn').disabled = false;

      } catch (error) {
        log(`Error: ${error.message}`);
        setStatus('Koneksi gagal', false);
      }
    }

    function onDisconnected() {
      setStatus('Terputus', false);
      log('Perangkat terputus');
      document.getElementById('connectBtn').disabled = false;
      document.getElementById('disconnectBtn').disabled = true;
    }

    function disconnectBLE() {
      if (device && device.gatt.connected) {
        device.gatt.disconnect();
      }
    }

    async function readTemperature() {
      if (!readChar) return;
      try {
        const value = await readChar.readValue();
        const decoder = new TextDecoder('utf-8');
        const suhu = decoder.decode(value);
        document.getElementById('tempDisplay').textContent = `${suhu} °C`;
        log(`Suhu dibaca: ${suhu} °C`);
      } catch (error) {
        log(`Error read: ${error.message}`);
      }
    }

    function onNotify(event) {
      const decoder = new TextDecoder('utf-8');
      const suhu = decoder.decode(event.target.value);
      document.getElementById('tempDisplay').textContent = `${suhu} °C`;
      log(`Suhu notify: ${suhu} °C`);
    }

    async function sendCommand(cmd) {
      if (!writeChar) return;
      try {
        const encoder = new TextEncoder();
        await writeChar.writeValue(encoder.encode(cmd));
        log(`Perintah dikirim: ${cmd}`);
      } catch (error) {
        log(`Error write: ${error.message}`);
      }
    }
  </script>
</body>
</html>

6. Characteristic Read & Write

Operasi Read dan Write adalah operasi BLE dasar yang paling sering digunakan. Read memungkinkan client membaca nilai dari characteristic, sedangkan Write memungkinkan client mengirim data ke peripheral.

Read dengan Multiple Formats

JavaScript
// Membaca berbagai tipe data dari characteristic
async function readCharacteristicData(char) {
  const value = await char.readValue();

  // Baca sebagai unsigned 8-bit integer
  const uint8 = value.getUint8(0);
  console.log('Uint8:', uint8);

  // Baca sebagai signed 16-bit integer (little-endian)
  const int16 = value.getInt16(0, true);  // true = little-endian
  console.log('Int16:', int16);

  // Baca sebagai float 32-bit
  const float32 = value.getFloat32(0, true);
  console.log('Float32:', float32);

  // Baca sebagai string UTF-8
  const decoder = new TextDecoder('utf-8');
  const text = decoder.decode(value);
  console.log('Text:', text);

  // Baca sebagai array byte
  const bytes = new Uint8Array(value.buffer);
  console.log('Bytes:', Array.from(bytes));
}

// Write berbagai tipe data
async function writeCharacteristicData(char, data) {
  const encoder = new TextEncoder();

  // Write string
  await char.writeValue(encoder.encode('Hello ESP32'));

  // Write angka 8-bit
  const buffer8 = new Uint8Array([42]);
  await char.writeValue(buffer8);

  // Write angka 16-bit (little-endian)
  const buffer16 = new Uint16Array([1000]);
  await char.writeValue(buffer16);

  // Write campuran byte
  const bufferMixed = new Uint8Array([0x01, 0xFF, 0x2A, 0x00]);
  await char.writeValue(bufferMixed);

  // Write With Response (reliable)
  await char.writeValueWithResponse(encoder.encode('confirmed!'));

  // Write Without Response (unreliable, lebih cepat)
  await char.writeValueWithoutResponse(encoder.encode('fire-and-forget'));
}
âš ī¸ Perbedaan Write With/Without Response

Write With Response menunggu konfirmasi dari peripheral bahwa data diterima dengan benar. Write Without Response mengirim data tanpa menunggu konfirmasi — lebih cepat tapi bisa kehilangan data. Untuk data penting, gunakan With Response.

7. Notify & Indicate

Selain Read/Write, BLE juga mendukung mekanisme push data dari peripheral ke client menggunakan Notify dan Indicate.

Fitur Notify Indicate
KonfirmasiTidak ada konfirmasiAda konfirmasi (ACK)
ReliabilityBest-effortReliable
ThroughputLebih tinggiLebih rendah
DescriptorCCCD bit 0 = 1CCCD bit 1 = 1
Kasus PakaiSensor data periodikData kritis / alarm
C++ (Arduino - ESP32)
#include <BLE2902.h>

// Setup Notify Characteristic
BLECharacteristic *pNotifyChar = pService->createCharacteristic(
  CHAR_NOTIFY_UUID,
  BLECharacteristic::PROPERTY_NOTIFY
);

// Tambahkan Client Characteristic Configuration Descriptor (CCCD)
BLE2902 *pDesc = new BLE2902();
pDesc->setNotifications(true);
pDesc->setIndications(true);
pNotifyChar->addDescriptor(pDesc);

// Setup Indicate Characteristic
BLECharacteristic *pIndicateChar = pService->createCharacteristic(
  CHAR_INDICATE_UUID,
  BLECharacteristic::PROPERTY_INDICATE
);
pIndicateChar->addDescriptor(new BLE2902());

// Callback saat client mengaktifkan/menonaktifkan notify/indicate
class MyNotifyCallbacks : public BLECharacteristicCallbacks {
  void onNotify(BLECharacteristic *pCharacteristic) {
    Serial.println("Client mengaktifkan NOTIFY");
  }

  void onIndicate(BLECharacteristic *pCharacteristic) {
    Serial.println("Client mengaktifkan INDICATE");
  }
};

void loop() {
  if (deviceConnected) {
    // Kirim data via Notify
    float suhu = readDHT22();
    pNotifyChar->setValue((uint8_t *)&suhu, sizeof(float));
    pNotifyChar->notify();  // Kirim tanpa konfirmasi

    delay(1000);

    // Kirim data via Indicate
    float humidity = readDHT22_Humidity();
    pIndicateChar->setValue((uint8_t *)&humidity, sizeof(float));
    pIndicateChar->indicate();  // Kirim dengan konfirmasi
  }
}

8. Proyek Praktis: Monitor Suhu Web Bluetooth

Mari kita buat proyek lengkap yang menggabungkan semua konsep: ESP32 sebagai BLE server yang membaca sensor DHT22, dan browser sebagai client yang menampilkan data secara real-time.

Skema Koneksi Hardware

DHT22 Pin ESP32 Pin Keterangan
VCC3.3VVoltase power
DATAGPIO 4Data dengan pull-up 10kΊ
NC-Not connected
GNDGNDGround
C++ (Arduino - ESP32)
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <DHT.h>

// Pin & sensor config
#define DHTPIN 4
#define DHTTYPE DHT22

DHT dht(DHTPIN, DHTTYPE);

// BLE UUIDs
#define SERVICE_UUID       "a0b1c2d3-e4f5-6789-abcd-ef0123456789"
#define CHAR_TEMP_UUID     "a0b1c2d3-e4f5-6789-abcd-ef0123456790"
#define CHAR_HUMID_UUID    "a0b1c2d3-e4f5-6789-abcd-ef0123456791"
#define CHAR_CONFIG_UUID   "a0b1c2d3-e4f5-6789-abcd-ef0123456792"

BLECharacteristic *pTempChar;
BLECharacteristic *pHumidChar;
BLECharacteristic *pConfigChar;
bool deviceConnected = false;
unsigned long lastSend = 0;
uint16_t sendInterval = 2000;  // Default 2 detik

class ServerCallbacks : public BLEServerCallbacks {
  void onConnect(BLEServer *pServer) {
    deviceConnected = true;
    Serial.println("✓ Client terhubung");
  }
  void onDisconnect(BLEServer *pServer) {
    deviceConnected = false;
    Serial.println("✗ Client terputus");
    pServer->getAdvertising()->start();
  }
};

class ConfigCallbacks : public BLECharacteristicCallbacks {
  void onWrite(BLECharacteristic *pChar) {
    uint8_t *data = pChar->getData();
    uint16_t len = pChar->getLength();
    if (len == 2) {
      sendInterval = data[0] | (data[1] << 8);
      sendInterval = constrain(sendInterval, 500, 30000);
      Serial.printf("Interval diubah: %d ms\n", sendInterval);
    }
  }
};

void setup() {
  Serial.begin(115200);
  dht.begin();

  // Init BLE
  BLEDevice::init("TempMonitor-BT");
  BLEServer *pServer = BLEDevice::createServer();
  pServer->setCallbacks(new ServerCallbacks());

  BLEService *pService = pServer->createService(SERVICE_UUID, 10);

  // Temperature characteristic (Notify)
  pTempChar = pService->createCharacteristic(
    CHAR_TEMP_UUID,
    BLECharacteristic::PROPERTY_READ |
    BLECharacteristic::PROPERTY_NOTIFY
  );
  pTempChar->addDescriptor(new BLE2902());

  // Humidity characteristic (Notify)
  pHumidChar = pService->createCharacteristic(
    CHAR_HUMID_UUID,
    BLECharacteristic::PROPERTY_READ |
    BLECharacteristic::PROPERTY_NOTIFY
  );
  pHumidChar->addDescriptor(new BLE2902());

  // Config characteristic (Write — set interval)
  pConfigChar = pService->createCharacteristic(
    CHAR_CONFIG_UUID,
    BLECharacteristic::PROPERTY_WRITE
  );
  pConfigChar->setCallbacks(new ConfigCallbacks());

  pService->start();

  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pAdvertising->setScanResponse(true);
  BLEDevice::startAdvertising();

  Serial.println("đŸŒĄī¸ Temperature Monitor BLE siap!");
}

void loop() {
  if (deviceConnected && (millis() - lastSend > sendInterval)) {
    float temp = dht.readTemperature();
    float humid = dht.readHumidity();

    if (!isnan(temp) && !isnan(humid)) {
      // Kirim temperature
      pTempChar->setValue((uint8_t *)&temp, sizeof(float));
      pTempChar->notify();

      // Kirim humidity
      pHumidChar->setValue((uint8_t *)&humid, sizeof(float));
      pHumidChar->notify();

      Serial.printf("T: %.1f°C  H: %.1f%%\n", temp, humid);
    }

    lastSend = millis();
  }
}

Kode Browser untuk Monitor Suhu

JavaScript (HTML)
<!DOCTYPE html>
<html lang="id">
<head>
  <meta charset="UTF-8">
  <title>Monitor Suhu Web Bluetooth</title>
  <style>
    * { box-sizing: border-box; }
    body { font-family: 'Segoe UI', sans-serif; background: #1a1a2e; color: #eee; padding: 20px; }
    .dashboard { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; max-width: 700px; margin: auto; }
    .sensor-card { background: #16213e; border-radius: 16px; padding: 30px; text-align: center; }
    .sensor-value { font-size: 56px; font-weight: 700; margin: 10px 0; }
    .sensor-label { font-size: 14px; opacity: 0.7; text-transform: uppercase; letter-spacing: 2px; }
    .temp-color { color: #ff6b6b; }
    .humid-color { color: #4ecdc4; }
    .chart-area { grid-column: 1/-1; background: #16213e; border-radius: 16px; padding: 20px; }
    canvas { width: 100%; height: 200px; }
    .controls { grid-column: 1/-1; display: flex; gap: 10px; justify-content: center; }
    button { padding: 12px 24px; border: none; border-radius: 10px; font-size: 16px; cursor: pointer; }
    .btn-connect { background: #00b894; color: white; }
    .btn-config { background: #6c5ce7; color: white; }
  </style>
</head>
<body>
  <h1 style="text-align:center">đŸŒĄī¸ Monitor Suhu BLE</h1>
  <div class="dashboard">
    <div class="sensor-card">
      <div class="sensor-label">Suhu</div>
      <div class="sensor-value temp-color" id="temp">--°C</div>
    </div>
    <div class="sensor-card">
      <div class="sensor-label">Kelembaban</div>
      <div class="sensor-value humid-color" id="humid">--%</div>
    </div>
    <div class="chart-area">
      <canvas id="chart"></canvas>
    </div>
    <div class="controls">
      <button class="btn-connect" onclick="connect()" id="connBtn">🔗 Connect</button>
      <button class="btn-config" onclick="setInterval_(1000)">1 detik</button>
      <button class="btn-config" onclick="setInterval_(3000)">3 detik</button>
      <button class="btn-config" onclick="setInterval_(5000)">5 detik</button>
    </div>
  </div>

  <script>
    const SERVICE  = 'a0b1c2d3-e4f5-6789-abcd-ef0123456789';
    const TEMP_CH  = 'a0b1c2d3-e4f5-6789-abcd-ef0123456790';
    const HUMID_CH = 'a0b1c2d3-e4f5-6789-abcd-ef0123456791';
    const CFG_CH   = 'a0b1c2d3-e4f5-6789-abcd-ef0123456792';
    let tempHistory = [];

    async function connect() {
      const dev = await navigator.bluetooth.requestDevice({
        filters: [{ services: [SERVICE] }]
      });
      const server = await dev.gatt.connect();
      const service = await server.getPrimaryService(SERVICE);

      // Subscribe temperature
      const tempChar = await service.getCharacteristic(TEMP_CH);
      await tempChar.startNotifications();
      tempChar.addEventListener('characteristicvaluechanged', e => {
        const val = e.target.value.getFloat32(0, true);
        document.getElementById('temp').textContent = val.toFixed(1) + '°C';
        tempHistory.push(val);
        if (tempHistory.length > 50) tempHistory.shift();
        drawChart();
      });

      // Subscribe humidity
      const humidChar = await service.getCharacteristic(HUMID_CH);
      await humidChar.startNotifications();
      humidChar.addEventListener('characteristicvaluechanged', e => {
        const val = e.target.value.getFloat32(0, true);
        document.getElementById('humid').textContent = val.toFixed(1) + '%';
      });

      document.getElementById('connBtn').textContent = '✅ Connected';
      window._cfgChar = await service.getCharacteristic(CFG_CH);
    }

    async function setInterval_(ms) {
      if (!window._cfgChar) return;
      const buf = new Uint16Array([ms]);
      await window._cfgChar.writeValue(buf);
    }

    function drawChart() {
      const canvas = document.getElementById('chart');
      const ctx = canvas.getContext('2d');
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = 400;
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      if (tempHistory.length < 2) return;

      const min = Math.min(...tempHistory) - 1;
      const max = Math.max(...tempHistory) + 1;
      const w = canvas.width / (tempHistory.length - 1);

      ctx.strokeStyle = '#ff6b6b';
      ctx.lineWidth = 3;
      ctx.beginPath();
      tempHistory.forEach((v, i) => {
        const x = i * w;
        const y = canvas.height - ((v - min) / (max - min)) * canvas.height;
        i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
      });
      ctx.stroke();
    }
  </script>
</body>
</html>

9. Kompatibilitas Browser & Tips

Browser Platform Web Bluetooth Catatan
ChromeDesktop (Win/Mac/Linux)✅ DidukungChrome 56+
ChromeAndroid✅ DidukungChrome 56+
ChromeiOS❌ Tidak didukungBatasan iOS
EdgeDesktop✅ DidukungEdge 79+ (Chromium)
OperaDesktop✅ DidukungBerdasarkan Chromium
SafariiOS/macOS❌ Tidak didukungBelum ada rencana
FirefoxSemua❌ Tidak didukungBelum ada rencana

Tips & Best Practices

💡 Alternatif untuk iOS

Karena Web Bluetooth tidak didukung di iOS, untuk proyek yang harus kompatibel dengan iOS, pertimbangkan menggunakan ESP32 dengan WiFi dan WebSocket, atau gunakan nRF Connect / LightBlue sebagai app BLE reader.

✅
Kapan Pakai Web Bluetooth
Gunakan untuk kasus ini
  • ✅ Dashboard monitoring
  • ✅ Setup & configuration tool
  • ✅ Quick data visualization
  • ✅ Prototype & development
  • ✅ Desktop-first application
❌
Kapan TIDAK Pakai
Hindari untuk kasus ini
  • ❌ iOS-first app
  • ❌ High throughput streaming
  • ❌ Background operation
  • ❌ Multi-device simultaneous
  • ❌ Production-critical systems

10. Quiz Pemahaman

Uji pemahaman kamu tentang ESP32 Web Bluetooth API:

Pertanyaan 1: Protokol W3C yang memungkinkan browser berkomunikasi dengan perangkat BLE disebut?

a) Web Serial API
b) Web Bluetooth API
c) Web USB API
d) Web NFC API

Pertanyaan 2: GATT menggunakan model komunikasi apa antara BLE peripheral dan central?

a) Peer-to-peer
b) Broadcast only
c) Client-Server
d) Master-Slave

Pertanyaan 3: Perbedaan utama Notify dan Indicate pada BLE adalah?

a) Notify lebih cepat dari Indicate
b) Indicate menggunakan konfirmasi (ACK), Notify tidak
c) Notify hanya untuk data numerik
d) Indicate tidak memerlukan CCCD descriptor

Pertanyaan 4: Web Bluetooth API memerlukan halaman dijalankan di protokol apa?

a) HTTP saja
b) HTTPS atau localhost
c) File protocol (file://)
d) Tidak ada batasan protokol

Pertanyaan 5: Browser mana yang TIDAK mendukung Web Bluetooth API?

a) Chrome Desktop
b) Edge (Chromium)
c) Safari iOS
d) Opera Desktop
← Sebelumnya ESP32 Bluetooth BLE Selanjutnya → Rust on ESP32
🔍 Zoom
100%
🎨 Tema