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>
If this post helped you, please consider buying me a coffee or donating via PayPal to support research & publishing of new posts on TechOverflow