Управление bluetooth из js или как я реверсинжинирил умный чайник
- четверг, 6 марта 2025 г. в 00:00:10
Иногда требуется изготавливать оборудование подключаемое по беспроводной связи. Это часто упрощает конструкцию, уменьшает количество кабелей. Для беспроводного канала как правило применяю радиосвязь на приемопередатчиках типа nRF24L01 или Wi-Fi. Первый способ требует дополнительного устройства для передачи информации на компьютер. Второй проще для связи с ПК, но сложнее при написании программ.
Внимание привлек Bluetooth. Передатчик Bluetooth как правило встроен в ноутбуки или подключается к компьютеру через USB. Сложно найти телефон без поддержки этого стандарта. Помимо этого, разработка и программирование таких устройств просты, что делает Bluetooth привлекательным.
Большинство Bluetooth устройств поставляются вместе с приложением, но не сопровождаются описанием протокола. При подключении к такому устройству через собственную программу непонятно как взаимодействовать с ним. Надо найти способ реверсинжиниринга. На днях я стал счастливым обладателем умного чайника (указывать модель по понятном причинам не буду). И по классике к чайнику прилагалось приложение. Процесс установки и настройки приложения оказался полнейшей болью. Начиная с того что ссылку на скачивание получать с сайта и заканчивая обязательным указанием почты, которая для работы чайника никак не требуется.
Все это подтолкнуло меня написать собственное приложение для управления чайником. Опыта в этой области у меня не было, как и в работе с Bluetooth. Поэтому начал изучать как чайник подключается к телефону с самых простых методов.
Ранее у меня был опыт работы с nfc. Тогда для низкоуровневого редактирования меток использовал специальные приложения. Подобное приложение удалось найти для Bluetooth. LightBlue предоставляет исчерпывающую информацию о деталях связи между устройством и телефоном. после подключения к чайнику в приложении отобразились некоторые данные.
На скриншоте много информации, но интерес представляет Generic Attribute. Через них передаются данные между телефоном и чайником. UUID "6e400001-b5a3-f393-e0a9-e50e24dcca9e" – это UART Service. Два идентификатора ниже это TX Characteristic и RX Characteristic. Оказывается чайник передает информацию по UART.
UART через Bluetooth работает через две характеристики. Для отправки данных на чайник надо записать значение в TX Characteristic, для получения – подписаться на обновление RX Characteristic.
При отправлении данных на чайник соединение разрывается. Очевидно, что надо отправлять команды, которые ждет чайник.
При подписке чайник может прислать "55 1 6 0 0 0 0 0 62 1e 0 0 0 0 0 0 80 0 0 aa". Пока непонятно что тут записано, но контакт установлен.
Узнать какие команды отправлять чайнику можно двумя способами. Копаться в дизассемблированном коде приложения или подслушать что передается по Bluetooth. Второй вариант кажется чуть проще.
Телефон умеет записывать передачу данных по Bluetooth. Для активации этой опции необходимо попасть в "Параметры разработчика". По умолчанию этот пункт скрыт в меню, но его можно включить следуя инструкциям для конкретной версии android. Затем нужно включить "журнал HIC Bluetooth" и "Отчет об ошибке". Теперь на экране выключения появился пункт "отчет об ошибке" который можно скачать и поделиться.
Алгоритм извлечения команд из чайника выглядит следующим образом:
Включить журнал Bluetooth
Зайти в приложение, подключиться к чайнику.
Давать чайнику команды и записывать время отправки
Скачать журнал Bluetooth
Отчет об ошибке – это архив с кучей папок. Журнал Bluetooth находиться в папке "bugreport...\FS\data\log\bt". Файл с логами открывается в Wireshark.
Cопоставляя время, название устройства и тип пакетов определяется что именно передано с чайника и на чайник. В итоге вырисовывается такая картина:
Приложение подписывается на получения данных с чайника
Приложение отправляет чайнику 55 00 ff ec bd 51 70 5c 2e 5d 5d aa
Чайник отвечает 55 0 ff 2 aa
После этого чайник инициализирован, не разрывает соединение и при изменении своего стояния отправляет его приложению. Приложение может послать чайнику команды.
55 01 03 aa – Включить
55 01 04 aa – Выключить
55 01 06 aa – Обновить информацию
На что чайник отвечает сообщением типа "55 0 6 0 0 0 0 0 3a 1e 0 0 0 0 0 0 80 0 0" Каждый байт в этом сообщении несет определённую информацию. Например 8 байт "3a" – температура. Узнать за что отвечает каждый байт можно изменять настройки чайника и отслеживать изменения в ответе. Например 11 байт содержит "0" если чайник выключен, и "2" если – включен.
Я считаю опьимальным языком для создания приложений html+css+js. Это позволяет быстро разрабатывать приложения не требующие установки и работающие на большинстве устройств. Bluetooth в чистом js нет, но его можно получить через интерфейс Navigator. Поддержка Bluetooth осуществляется далеко не всеми браузерами, но для локального использования это не так важно.
Покопавшись в примерах сделал простенькое приложение для управления чайником.
Для начала запрашиваем у браузера Bluetooth устройства. В опциях прошу показать все устройства и обязательно сообщаю UUID сервиса UART
let device = await navigator.bluetooth.requestDevice({
acceptAllDevices: true,
optionalServices: ['6e400001-b5a3-f393-e0a9-e50e24dcca9e']//Сервис UART
});
Получаю сервис
let service = await server.getPrimaryService('6e400001-b5a3-f393-e0a9-e50e24dcca9e');
И настраиваю получение и отправку сообщений
let Notification = await TXcharacteristic.startNotifications();
Notification.addEventListener('characteristicvaluechanged', receive);
RXcharacteristic = await service.getCharacteristic('6e400002-b5a3-f393-e0a9-e50e24dcca9e');
Функция получения сообщений выглядит следующим образом.
function receive(event) {
let value = event.target.value;
value = value.buffer ? value : new DataView(value);
let data = [];
//Представление данных в виде массива
for (let i = 0; i < value.byteLength; i++) {
data[i] = value.getUint8(i);
}
let s = '';
for (let i of data){
s += i.toString(16) + ' '
}
log(`Получено: ${s}`);
}
Функция отправки:
async function send(data) {
try {
await RXcharacteristic.writeValueWithoutResponse(
new Uint8Array(data)
);
let s = '';
for (let i of data){
s += i.toString(16) + ' '
}
log(`Передано: ${s}`)
} catch (error) {
log(`Ошибка: ${error}`)
}
}//send
Добавив минимальный интерфейс было получено такое приложение.
Полный код приложения представлен ниже.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Blu</title>
<style>
body {
display: grid;
max-width: 500px;
margin: auto;
}
button {
min-height: 50px;
font-size: 20px;
}
textarea {
font-family: monospace;
font-size: 15px;
}
</style>
<script>
'use strict'
//Характеристика для передачи
let RXcharacteristic;
//Вывод сообщения на страницу
function log(l) {
loglist.value += l + '\n';
loglist.scrollTop = loglist.scrollHeight;
}
//Передать информацию
async function send(data) {
try {
await RXcharacteristic.writeValueWithoutResponse(
new Uint8Array(data)
);
let s = '';
for (let i of data){
s += i.toString(16) + ' '
}
log(`Передано: ${s}`)
} catch (error) {
log(`Ошибка: ${error}`)
}
}//send
//Получение информации
function receive(event) {
let value = event.target.value;
value = value.buffer ? value : new DataView(value);
let data = [];
//Представление данных в виде массива
for (let i = 0; i < value.byteLength; i++) {
data[i] = value.getUint8(i);
}
let s = '';
for (let i of data){
s += i.toString(16) + ' '
}
log(`Получено: ${s}`);
//если получено 20 байт -- извлечь температуру
if (data.length == 20)
temp.innerHTML = `Температура: ${data[8]}°C`
}//receive
//подключение
async function connect() {
try {
log('Запрос Bluetooth Device...');
let device = await navigator.bluetooth.requestDevice({
acceptAllDevices: true,
optionalServices: ['6e400001-b5a3-f393-e0a9-e50e24dcca9e']//Сервис UART
});
log('Подключение GATT Server...');
let server = await device.gatt.connect();
log('Получение Nordic UART Service...');
let service = await server.getPrimaryService('6e400001-b5a3-f393-e0a9-e50e24dcca9e');
log('Получение TX Characteristic...');
let TXcharacteristic = await service.getCharacteristic('6e400003-b5a3-f393-e0a9-e50e24dcca9e');
log('Подписка...');
let Notification = await TXcharacteristic.startNotifications();
log('Подключение обработчика...');
Notification.addEventListener('characteristicvaluechanged', receive);
log('Получение RX Characteristic...');
RXcharacteristic = await service.getCharacteristic('6e400002-b5a3-f393-e0a9-e50e24dcca9e');
log('Инициализация...');
send([0x55, 0x00, 0xff, 0xec, 0xbd, 0x51, 0x70, 0x5c, 0x2e, 0x5d, 0x5d, 0xaa]);
} catch (error) {
log(`Ошибка: ${error}`);
}
}//connect
</script>
</head>
<body>
<button onclick="connect()">Подключиться</button>
<button
onclick="send([0x55, 0x00, 0xff, 0xec, 0xbd, 0x51, 0x70, 0x5c, 0x2e, 0x5d, 0x5d, 0xaa])">Инициализация</button>
<button onclick="send([0x55, 0x01, 0x03, 0xaa])">Включить</button>
<button onclick="send([0x55, 0x01, 0x04, 0xaa])">Выключить</button>
<button onclick="send([0x55, 0x01, 0x06, 0xaa])">Обновить информацию</button>
<button onclick="loglist.value = ``">Очистить лог</button>
<textarea id="loglist" rows="15"></textarea>
<label id="temp"></label>
</body>
</html>
В результате получилось создать приложение, позволяющие управлять базовыми функциями чайника без официального приложения. Функционал приложения можно расширить, если проанализировать остальные команды. Но на данный момент этого не требуется.