Иногда требуется изготавливать оборудование подключаемое по беспроводной связи. Это часто упрощает конструкцию, уменьшает количество кабелей. Для беспроводного канала как правило применяю радиосвязь на приемопередатчиках типа 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>
Выводы
В результате получилось создать приложение, позволяющие управлять базовыми функциями чайника без официального приложения. Функционал приложения можно расширить, если проанализировать остальные команды. Но на данный момент этого не требуется.
