Інтеграція Web Bluetooth API на сайті
Web Bluetooth API дозволяє браузеру спілкуватися безпосередньо з пристроями Bluetooth Low Energy (BLE) без мобільного додатку. Користувач відкриває веб-сторінку, натискає «Підключити», вибирає пристрій зі списку — і сайт починає читати дані датчика, керувати лампою або передавати команди тренажеру.
Підтримка та обмеження
Підтримується: Chrome 70+, Edge 79+, Chrome Android 56+. Недоступне: Firefox (заморожено), Safari/iOS (заблоковано політикою Apple).
Тільки HTTPS. Тільки після користувацького жесту — виклик requestDevice() має бути всередині обробника клику. Web Workers не підтримуються.
Архітектура BLE
BLE-пристрій містить Services → Characteristics. Наприклад, сервіс «Пульсометр» (UUID 0x180D) містить характеристику «Вимірювання пульсу» (UUID 0x2A37). UUID стандартних сервісів визначає Bluetooth SIG, кастомні — 128-бітні UUID виробника.
Підключення до пристрою
interface BluetoothDevice {
name: string
gatt: BluetoothRemoteGATTServer
}
async function connectToDevice(serviceUUID: string): Promise<BluetoothRemoteGATTServer> {
const device = await navigator.bluetooth.requestDevice({
filters: [
{ services: [serviceUUID] },
// Або пошук за іменем:
// { namePrefix: 'Mi Band' },
],
optionalServices: [
'battery_service', // Стандартний UUID
'0x180A', // Device Information
],
})
console.log(`Підключаємся до: ${device.name}`)
device.addEventListener('gattserverdisconnected', () => {
console.log('Пристрій відключився')
// Спроба переподключитися тут
})
const server = await device.gatt!.connect()
return server
}
Читання даних: Пульсометр
class HeartRateMonitor {
private server: BluetoothRemoteGATTServer | null = null
private characteristic: BluetoothRemoteGATTCharacteristic | null = null
async connect() {
this.server = await connectToDevice('heart_rate')
const service = await this.server.getPrimaryService('heart_rate')
this.characteristic = await service.getCharacteristic('heart_rate_measurement')
// Підписуємося на сповіщення (дані поступають автоматично)
await this.characteristic.startNotifications()
this.characteristic.addEventListener(
'characteristicvaluechanged',
this.handleHeartRateMeasurement.bind(this)
)
}
private handleHeartRateMeasurement(event: Event) {
const value = (event.target as BluetoothRemoteGATTCharacteristic).value!
const flags = value.getUint8(0)
let heartRate: number
if (flags & 0x01) {
// 16-бітний формат
heartRate = value.getUint16(1, true)
} else {
// 8-бітний формат
heartRate = value.getUint8(1)
}
// RR-інтервали (відстань між ударами в мс) — опціонально
const rrIntervals: number[] = []
if (flags & 0x10) {
for (let i = 2; i + 1 < value.byteLength; i += 2) {
rrIntervals.push(value.getUint16(i, true) / 1024 * 1000)
}
}
console.log(`ЧСС: ${heartRate} уд/хв, RR: [${rrIntervals.join(', ')}] мс`)
}
async disconnect() {
await this.characteristic?.stopNotifications()
this.server?.disconnect()
}
}
Запис даних: керування розумною лампою
class SmartLightController {
private server: BluetoothRemoteGATTServer | null = null
private controlChar: BluetoothRemoteGATTCharacteristic | null = null
// UUID кастомного сервіса (приклад: Govee H6003)
private readonly SERVICE_UUID = '00010203-0405-0607-0809-0a0b0c0d1910'
private readonly CONTROL_UUID = '00010203-0405-0607-0809-0a0b0c0d2b11'
async connect() {
this.server = await connectToDevice(this.SERVICE_UUID)
const service = await this.server.getPrimaryService(this.SERVICE_UUID)
this.controlChar = await service.getCharacteristic(this.CONTROL_UUID)
}
async setColor(r: number, g: number, b: number) {
// Протокол конкретного пристрою — читаємо з документації або реверс-інжиниринг
const command = new Uint8Array([
0x33, 0x05, 0x02, // Заголовок команди кольору
r, g, b, // RGB
0x00, 0x00, 0x00, // Паддинг
r ^ g ^ b, // Контрольна сума XOR
])
await this.controlChar!.writeValueWithResponse(command)
}
async setBrightness(level: number) {
// level: 0–100
const command = new Uint8Array([
0x33, 0x04,
Math.round(level * 2.55),
0x00,
])
await this.controlChar!.writeValueWithoutResponse(command)
// writeValueWithoutResponse — швидше, без підтвердження
}
async readBatteryLevel(): Promise<number> {
const service = await this.server!.getPrimaryService('battery_service')
const char = await service.getCharacteristic('battery_level')
const value = await char.readValue()
return value.getUint8(0) // 0–100%
}
}
React-хук
function useBluetooth() {
const [device, setDevice] = useState<BluetoothRemoteGATTServer | null>(null)
const [isConnected, setIsConnected] = useState(false)
const [isSupported] = useState(() => 'bluetooth' in navigator)
const [error, setError] = useState<string | null>(null)
const connect = useCallback(async (serviceUUID: string) => {
try {
setError(null)
const server = await connectToDevice(serviceUUID)
setDevice(server)
setIsConnected(true)
} catch (err) {
if ((err as Error).name === 'NotFoundError') {
setError('Пристрій не вибраний')
} else {
setError((err as Error).message)
}
}
}, [])
const disconnect = useCallback(() => {
device?.disconnect()
setDevice(null)
setIsConnected(false)
}, [device])
return { device, isConnected, isSupported, error, connect, disconnect }
}
Переподключення після розриву
device.addEventListener('gattserverdisconnected', async () => {
let retries = 0
while (retries < 3) {
await new Promise((r) => setTimeout(r, 1000 * (retries + 1)))
try {
await device.gatt!.connect()
await resubscribeCharacteristics()
console.log('Переподключено')
return
} catch {
retries++
}
}
console.error('Не удалось переподключиться')
})
Що ми робимо
Вивчаємо документацію на конкретний BLE-пристрій або реверс-інжиниримо протокол (Wireshark + BLE-сніффер, якщо документації немає). Реалізуємо підключення, читання характеристик через сповіщення або поллінг, запис команд. Додаємо переподключення, обробку помилок, UI статусу підключення.
Строк: інтеграція з задокументованим BLE-пристроєм — 3–5 днів. З пристроєм без документації (реверс протоколу) — 7–12 днів.







