Pull to refresh

Управление bluetooth из js или как я реверсинжинирил умный чайник

Level of difficultyMedium
Reading time6 min
Views5.3K

Иногда требуется изготавливать оборудование подключаемое по беспроводной связи. Это часто упрощает конструкцию, уменьшает количество кабелей. Для беспроводного канала как правило применяю радиосвязь на приемопередатчиках типа nRF24L01 или Wi-Fi. Первый способ требует дополнительного устройства для передачи информации на компьютер. Второй проще для связи с ПК, но сложнее при написании программ.

Внимание привлек Bluetooth. Передатчик Bluetooth как правило встроен в ноутбуки или подключается к компьютеру через USB. Сложно найти телефон без поддержки этого стандарта. Помимо этого, разработка и программирование таких устройств просты, что делает Bluetooth привлекательным.

Большинство Bluetooth устройств поставляются вместе с приложением, но не сопровождаются описанием протокола. При подключении к такому устройству через собственную программу непонятно как взаимодействовать с ним. Надо найти способ реверсинжиниринга. На днях я стал счастливым обладателем умного чайника (указывать модель по понятным причинам не буду). И по классике к чайнику прилагалось приложение. Процесс установки и настройки приложения оказался полнейшей болью. Начиная с того что ссылку на скачивание получать с сайта и заканчивая обязательным указанием почты, которая для работы чайника никак не требуется.

Все это подтолкнуло меня написать собственное приложение для управления чайником. Опыта в этой области у меня не было, как и в работе с Bluetooth. Поэтому начал изучать как чайник подключается к телефону с самых простых методов.

Определение основных параметров соединения

Ранее у меня был опыт работы с nfc. Тогда для низкоуровневого редактирования меток использовал специальные приложения. Подобное приложение удалось найти для Bluetooth. LightBlue предоставляет исчерпывающую информацию о деталях связи между устройством и телефоном. после подключения к чайнику в приложении отобразились некоторые данные.

Информация о соединении в LightBlue
Информация о соединении в 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

Алгоритм извлечения команд из чайника выглядит следующим образом:

  1. Включить журнал Bluetooth

  2. Зайти в приложение, подключиться к чайнику.

  3. Давать чайнику команды и записывать время отправки

  4. Скачать журнал Bluetooth

Отчет об ошибке – это архив с кучей папок. Журнал Bluetooth находится в папке "bugreport...\FS\data\log\bt". Файл с логами открывается в Wireshark.

Wireshark
Wireshark

Cопоставляя время, название устройства и тип пакетов определяется что именно передано с чайника и на чайник. В итоге вырисовывается такая картина:

  1. Приложение подписывается на получения данных с чайника

  2. Приложение отправляет чайнику 55 00 ff ec bd 51 70 5c 2e 5d 5d aa

  3. Чайник отвечает 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>

Выводы

В результате получилось создать приложение, позволяющие управлять базовыми функциями чайника без официального приложения. Функционал приложения можно расширить, если проанализировать остальные команды. Но на данный момент этого не требуется.

Tags:
Hubs:
Total votes 39: ↑39 and ↓0+51
Comments16

Articles