HTML & Javascript minimal Bluetooth Service explorer using navigator.bluetooth

This self-contained HTML & JavaScript example demonstrates how to use the Web Bluetooth API to connect to a Bluetooth device, discover its services and characteristics, and read/write values.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>BLE Service Characteristic Explorer</title>
  <style>
    body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; padding: 1rem; }
    button { margin: 0.25rem; }
    .characteristic { margin: 0.5rem 0; padding: 0.5rem; border: 1px solid #ddd; border-radius: 6px; }
    .props { font-size: 0.9rem; color: #444; }
    pre { background:#f7f7f7; padding:0.5rem; border-radius:4px; overflow:auto }
  </style>
</head>
<body>
  <h1>BLE Service Characteristic Explorer</h1>
  <p>Service UUID: <code id="serviceUuidDisplay">42355f23-e1c4-8481-4282-5add5f9e85aa</code></p>
  <p>
    <button id="connectBtn">Connect & List Characteristics</button>
    <button id="disconnectBtn" disabled>Disconnect</button>
  </p>

  <div id="status">Idle</div>

  <h2>Characteristics</h2>
  <div id="characteristics"></div>

  <script>
    // TODO Change this to your service UUID
    const SERVICE_UUID = '42355f23-e1c4-8481-4282-5add5f9e85aa';

    const connectBtn = document.getElementById('connectBtn');
    const disconnectBtn = document.getElementById('disconnectBtn');
    const statusEl = document.getElementById('status');
    const charContainer = document.getElementById('characteristics');
    document.getElementById('serviceUuidDisplay').textContent = SERVICE_UUID;

    let device = null;
    let server = null;
    let service = null;

    function setStatus(text) {
      statusEl.textContent = text;
    }

    function formatProps(props) {
      return Object.keys(props).filter(k => props[k]).join(', ') || 'none';
    }

    async function listCharacteristics() {
      try {
        setStatus('Discovering characteristics...');
        const characteristics = await service.getCharacteristics();
        charContainer.innerHTML = '';

        if (characteristics.length === 0) {
          charContainer.textContent = 'No characteristics found for this service.';
          return;
        }

        for (const ch of characteristics) {
          const el = document.createElement('div');
          el.className = 'characteristic';

          const title = document.createElement('div');
          title.innerHTML = `<strong>UUID:</strong> <code>${ch.uuid}</code>`;
          el.appendChild(title);

          const props = document.createElement('div');
          props.className = 'props';
          props.textContent = 'Properties: ' + formatProps(ch.properties);
          el.appendChild(props);

          const actions = document.createElement('div');

          // Read button
          if (ch.properties.read) {
            const readBtn = document.createElement('button');
            readBtn.textContent = 'Read';
            readBtn.addEventListener('click', async () => {
              try {
                setStatus(`Reading ${ch.uuid}...`);
                const value = await ch.readValue();
                const bytes = new Uint8Array(value.buffer);
                const hex = Array.from(bytes).map(b => b.toString(16).padStart(2,'0')).join(' ');
                showValue(el, hex, bytesToString(bytes));
                setStatus('Read ok');
              } catch (err) {
                setStatus('Read error: ' + err);
              }
            });
            actions.appendChild(readBtn);
          }

          // Notify toggle
          if (ch.properties.notify || ch.properties.indicate) {
            const notifyBtn = document.createElement('button');
            notifyBtn.textContent = 'Enable Notify';
            let notifying = false;
            async function handleNotification(e) {
              const v = e.target.value;
              const bytes = new Uint8Array(v.buffer);
              const hex = Array.from(bytes).map(b => b.toString(16).padStart(2,'0')).join(' ');
              showValue(el, hex, bytesToString(bytes), true);
            }
            notifyBtn.addEventListener('click', async () => {
              try {
                if (!notifying) {
                  await ch.startNotifications();
                  ch.addEventListener('characteristicvaluechanged', handleNotification);
                  notifyBtn.textContent = 'Disable Notify';
                  notifying = true;
                  setStatus('Notifications started for ' + ch.uuid);
                } else {
                  ch.removeEventListener('characteristicvaluechanged', handleNotification);
                  await ch.stopNotifications();
                  notifyBtn.textContent = 'Enable Notify';
                  notifying = false;
                  setStatus('Notifications stopped for ' + ch.uuid);
                }
              } catch (err) {
                setStatus('Notify error: ' + err);
              }
            });
            actions.appendChild(notifyBtn);
          }

          el.appendChild(actions);
          charContainer.appendChild(el);
        }

        setStatus('Characteristics listed.');
      } catch (err) {
        setStatus('Error listing characteristics: ' + err);
      }
    }

    function showValue(parentEl, hex, text, append=false) {
      let pre = parentEl.querySelector('pre');
      if (!pre) {
        pre = document.createElement('pre');
        parentEl.appendChild(pre);
      }
      const now = new Date().toISOString();
      const s = `${now}  hex: ${hex}\ntext: ${text}\n`;
      if (append)
        pre.textContent = pre.textContent + '\n' + s;
      else
        pre.textContent = s;
    }

    function bytesToString(bytes) {
      try { return new TextDecoder().decode(bytes); } catch { return '<binary>'; }
    }

    connectBtn.addEventListener('click', async () => {
      try {
        setStatus('Requesting device...');
        device = await navigator.bluetooth.requestDevice({
          acceptAllDevices: true,
          optionalServices: [SERVICE_UUID]
        });

        device.addEventListener('gattserverdisconnected', () => {
          setStatus('Device disconnected');
          disconnectBtn.disabled = true;
          connectBtn.disabled = false;
        });

        setStatus('Connecting to GATT server...');
        server = await device.gatt.connect();
        setStatus('Getting service...');
        service = await server.getPrimaryService(SERVICE_UUID);
        setStatus('Service found: ' + SERVICE_UUID);
        connectBtn.disabled = true;
        disconnectBtn.disabled = false;
        await listCharacteristics();
      } catch (err) {
        setStatus('Connection error: ' + err);
      }
    });

    disconnectBtn.addEventListener('click', async () => {
      if (!device) return;
      try {
        if (device.gatt.connected) device.gatt.disconnect();
        setStatus('Disconnected');
        disconnectBtn.disabled = true;
        connectBtn.disabled = false;
      } catch (err) {
        setStatus('Disconnect error: ' + err);
      }
    });
  </script>
</body>
</html>

BLE Explorer