Реализация Web Bluetooth API на сайте
Web Bluetooth API позволяет браузеру напрямую общаться с Bluetooth Low Energy (BLE) устройствами без мобильного приложения. Пользователь открывает веб-страницу, нажимает «Подключить», выбирает устройство из списка — и сайт начинает читать данные с датчика, управлять лампой или передавать команды тренажёру.
Поддержка и ограничения
Поддерживается: Chrome 70+, Edge 79+, Chrome Android 56+. Не работает: Firefox (заморожено), Safari/iOS (заблокировано политикой Apple).
Только HTTPS. Только после user gesture — вызов 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 дней.







