Очиститель воздуха. Думаю, что многие задумывались о необходимости его приобретения. Если вы живете загородом, а вокруг вас зелёный лес, то покупка такого девайса будет сомнительна. Однако, если вас окружают многоэтажные дома, дороги, забитые машинами, промышленные предприятия, то наличие очистителя вполне может улучшить качество вашего домашнего воздуха. Многие современные очистители воздуха достаточно просто можно интегрировать в системы управления вашим «умным» домом, что позволит настроить различные сценарии при взаимодействии с другими датчиками.

В моём случае случилось так, что был приобретён очиститель воздуха Dyson Air purifier TP07, который должен стоять на страже чистоты воздуха в одной из комнат. Он исправно несёт свою службу, вот, только просто подключить его к Home Assistant (HA) не представляется возможным. В купленной модели отсутствует модуль Wi-fi. Что же делать? Будем ревёрсить. В данной статье будет разобран формат диагностических данных очистителя Dyson, которые можно считать и направить в HA.


Отвёртки в руки и вперёд

Главная плата очистителя воздуха
Главная плата очистителя воздуха

Вооружившись инструментами и мультиметром, подразобрав очиститель, был получен доступ к главной плате. За управление девайсом оказался ответственен микроконтроллер STM32F429, построенный на ядре ARM Cortex-M4. В его характеристики и возможности вдаваться в рамках данной статьи нет необходимости. На плате был обнаружен разъём для подсоединения отсутствующего в данной модели модуля Wi-fi. Самое интересное, что также имеется диагностический 4-х пиновый разъём, линии данных которого уходят в микроконтроллер.

Вывод проводов через диагностическое отверстие
Вывод проводов через диагностическое отверстие

Инженеры Dyson благоразумно заложили в конструкцию корпуса очистителя специальное технологическое отверстие для подсоединения к диагностическому разъёму. Не придётся сверлить корпус.

Вооружившись спецификацией на STM32F429, определяем, что данные передаются по протоколу UART, и периодическое падение напряжения на линии TX подтверждает, что микроконтроллер с нами общается. Подпаиваемся к выводам разъёма и бежим за логическим анализатором.

Не всё так просто

Логический анализатор. Группы пакетов данных.
Логический анализатор. Группы пакетов данных.

Есть немало видео в инете, где подключают анализатор или преобразователь UART-USB к какому-нибудь устройству, роутеру, например, и в приложении наблюдают за поступлением текстовых данных в формате ASCII или Unicode, выделяя нужную информацию. Подключившись, я ожидал увидеть нечто подобное. К моему сожалению, вместо текста поступало неплохое количество визуально нечитаемых байт (около килобайта в секунду), для анализа которых необходимо время. Оно и потребовалось, потому что поиск на Github и на других ресурсах какого-либо парсера для протокола от Dyson не дал результатов. Собственно, этот факт, а также спортивный интерес и побудил меня разобраться написать данную статью.

Формат данных пакета от Dyson

Используя логический анализатор, сохраняем побольше UART данных в различных режимах работы очистителя в формат .csv и приступаем к анализу.

Пример .csv файла
Time [s],Value,Parity Error,Framing Error
-0.000017291666667,18,,
0.000069333333333,25,,
0.000156000000000,0,,
0.000242666666667,134,,
0.000329250000000,1,,
0.000415958333333,192,,
0.000502666666667,1,,
0.000589166666667,21,,
0.000675875000000,0,,
0.000762750000000,4,,
0.000849625000000,49,,
0.000936458333333,219,,
0.001023000000000,222,,
0.001109625000000,0,,
0.001196458333333,1,,
0.001283291666667,0,,
0.001370166666667,100,,
0.001456916666667,4,,
0.001543500000000,0,,
0.001630125000000,0,,
0.001717083333333,0,,
0.001803875000000,0,,
0.001890833333333,0,,
0.001977458333333,0,,
0.002064000000000,0,,
0.002150791666667,0,,
0.002237666666667,144,,
0.002324500000000,115,,
0.002411375000000,80,,
0.002498000000000,63,,
0.002584666666667,18,,
0.132283458333333,18,,
0.132370166666667,21,,
0.132456791666667,0,,
0.132543416666667,25,,
0.132630041666667,1,,
0.132716583333333,192,,
0.132803250000000,1,,
0.132889833333333,21,,
0.132976416666667,0,,
0.133063291666667,4,,
0.133150041666667,49,,
0.133236958333333,2,,
0.133323666666667,0,,
0.133410208333333,2,,
0.133496875000000,0,,
0.133583500000000,98,,
0.133670041666667,4,,
0.133756708333333,0,,
0.133843333333333,0,,
0.133929875000000,0,,
0.134016750000000,0,,
0.134103625000000,80,,
0.134190458333333,60,,
0.134277250000000,76,,
0.134363750000000,195,,
0.134450375000000,18,,
0.134537333333333,18,,
0.134624166666667,57,,
0.134710958333333,0,,
0.134797625000000,172,,
0.134884125000000,1,,
0.134970791666667,192,,
0.135057708333333,1,,
0.135144500000000,21,,
0.135231291666667,0,,
0.135317958333333,4,,
0.135404458333333,49,,
0.135491208333333,2,,
0.135578166666667,0,,
0.135664958333333,3,,
0.135751625000000,0,,
0.135838291666667,107,,
0.135924750000000,5,,
0.136011708333333,0,,
0.136098625000000,0,,
0.136185375000000,0,,
0.136272041666667,0,,
0.136358666666667,102,,
0.136445208333333,3,,
0.136532166666667,24,,
0.136619083333333,64,,
0.136705750000000,0,,
0.136792416666667,0,,
0.136879041666667,0,,
0.136965708333333,224,,
0.137052666666667,102,,
0.137139583333333,3,,
0.137226166666667,24,,
0.137312791666667,64,,
0.137399416666667,0,,
0.137486208333333,0,,
0.137573166666667,0,,
0.137659958333333,0,,
0.137746500000000,0,,
0.137833208333333,174,,
0.137919875000000,210,,
0.138006750000000,64,,
0.138093750000000,0,,
0.138180375000000,0,,
0.138267041666667,0,,
0.138353708333333,0,,
0.138440458333333,128,,
0.138527375000000,119,,
0.138614291666667,223,,
0.138700791666667,64,,
0.138787375000000,7,,
0.138874083333333,0,,
0.138960916666667,0,,
0.139047833333333,0,,
0.139134666666667,10,,
0.139221250000000,173,,
0.139307875000000,8,,
0.139394541666667,67,,
0.139481041666667,248,,
0.139567708333333,88,,
0.139654375000000,149,,
0.139740916666667,254,,
0.139827500000000,18,,
0.139914375000000,18,,
0.140001166666667,29,,
0.140088083333333,0,,
0.140174750000000,243,,
0.140261208333333,1,,
0.140347958333333,192,,
0.140434750000000,1,,
0.140521625000000,21,,
0.140608458333333,0,,
0.140695000000000,4,,
0.140781666666667,49,,
0.140868416666667,2,,
0.140955250000000,0,,
0.141042166666667,5,,
0.141128875000000,0,,
0.141215375000000,102,,
0.141301916666667,3,,
0.141388833333333,0,,
0.141475583333333,0,,
0.141562375000000,0,,
0.141649000000000,0,,
0.141735583333333,0,,
0.141822166666667,0,,
0.141909083333333,0,,
0.141995916666667,0,,
0.142082541666667,4,,
0.142169166666667,0,,
0.142255750000000,0,,
0.142342416666667,0,,
0.142429375000000,119,,
0.142516166666667,203,,
0.142602708333333,104,,
0.142689375000000,185,,
0.142775916666667,18,,

Сходу можно определить, что данные поступают пакетами переменной длины. Начало и конец пакета задаются старт/стоп байтом 0х12. При таком подходе необходимо исключить передачу числа 0x12 внутри пакета для недопущения преждевременного завершения пакета. Для этого применяется byte stuffing. Все возможные числа 0x12 заменяются на последовательную пару чисел 0xDB и 0xDE. При получении пакета декодер, встретив такую пару чисел, возвращает все на свои места.

А внутри пакета всё весело. Ни байта текстовой информации, только числовые данные в различных форматах – (uint8_t, uint16_t, uint32_t, double). Тут сразу необходимо учесть остроконечность микроконтроллера STM32 и архитектуры ARM в целом, при отправке многобайтной переменной через UART.

Сходу можно определить, что данные поступают пакетами переменной длины. Начало и конец пакета задаются старт/стоп байтом 0х12. При таком подходе необходимо исключить передачу числа 0x12 внутри пакета для недопущения преждевременного завершения пакета. Для этого применяется byte stuffing. Все возможные числа 0x12 заменяются на последовательную пару чисел 0xDB и 0xDE. При получении пакета декодер, встретив такую пару чисел, возвращает все на свои места.

А внутри пакета всё весело. Ни байта текстовой информации, только числовые данные в различных форматах – (uint8_t, uint16_t, uint32_t, double). Тут сразу необходимо учесть остроконечность микроконтроллера STM32 и архитектуры ARM в целом, при отправке многобайтной переменной через UART.

Пример: имеем 4-х байтовое uint32_t = 0xDDCCBBAA,  первым в UART отправляется младший байт 0xAA. В пакете это выглядит AA BB CC DD.

Во остальном имеются все признаки добротного пакета: указана длина данных в пакете (защищено CRC8), имеется контрольная сумма CRC32 для проверки корректности пакета.

Так что же нам хочет сообщить STM32? А рассказывает он нам с достаточной периодичностью (одинаковые по патернам пакеты передаются раз в секунду) о состоянии неких своих регистров (назовём их так). Номер регистра - это 4 однобайтных или 2 двухбайтных числа (на ваш выбор), которые следуют сразу за числом 0х31. После номера регистра следует указание формата и количества данных.

Например:
0х62 0х04 – в пакете далее следуют 4 однобайтных целых числа
0х64 0х05 – в пакете далее следуют 5 двухбайтных целых числа
0х66 0х02– в пакете далее следуют 2 четырёхбайтных целых числа
0х6B 0х0D – в пакете далее следуют 13 восьмибайтных чисел с плавающей точкой

Сравнивая данные при изменении внешних условий в комнате, а также в различных режимах работы самого очистителя воздуха, удалось выявить регистры, отвечающие за:

  • текущую температуру воздуха

  • влажность

  • уровень загрязнения воздуха частицами в 2.5 и 10 микрон

  • скорость потока воздуха (или скорость вращения вентилятора)

  • текущий режим работы вентилятора (от 1 до 10)

  • время с последнего включения в розетку в секундах

Эксперимент

Всю полученную информацию необходимо будет переправить в HA по какому-нибудь беспроводному протоколу. Под рукой была отладочная плата с ESP32, ей то мы и воспользуемся. Создаем прошивку для получения данных с UART, пишем модуль парсера UART->DYSON->Data пакетов. Et voilà, получаем работающее устройство, для считывания и дальнейшей передачи параметров очистителя. Прошивка написана на Си без использования библиотек Ардуино, хотелось поработать с FreeRTOS на ESP32.

Интеграция с Home Assistant

Теперь займёмся передачей информации в HA, который поднят в docker`е на маленькой коробочке LenovoThinkСentre  под управлением Debian. В первом приближении была идея воспользоваться ESPHome – инструмент, позволяющий быстро связать HA и устройство на базе ESP32/ESP8266.  Сразу скажу: так не вышло.

Удобство и простота использования ESPHome накладывают значительные ограничения на функциональность устройства. Поскольку мы не имеем прямого доступа к коду прошивки, генерируемой ESPHome, реализовать что-то, кроме подключения датчиков с передачей данных на сервер, используя только файлы конфигурации достаточно непросто. А запрограммировать опрос UART c последующим анализом данных вообще нереально. Ищем другие варианты.

Один из таких вариантов является создание внутри HA собственных датчиков, используя RESTful sensors. При этом HA будет опрашивать наше устройство, используя GET и POST запросы, в ответ на которые модуль ESP32 должен подготовить и выслать ответы, содержащие текущие значения параметров очистителя воздуха. Поскольку для подключения ESP32 к точке доступа Wi-Fi была использована библиотека WifiManager, которая поднимает на устройстве HTTP сервер, реализовать механизмы пересылки ответов на запросы HA будет очень просто. Дело на одну функцию, которая вызывается при поступлении запроса на устройство:

static esp_err_t sensors_get_handler(httpd_req_t *req)
{
	static const char *TAG = "REQUEST HANDLER";
    esp_log_level_set(TAG, ESP_LOG_INFO);
    if(strcmp(req->uri, "/api/states/sensors") == 0)
    {

		ESP_LOGI(TAG, "New GET request. Serving page /sensors");
        cJSON* sensor = cJSON_CreateObject();
        cJSON_AddStringToObject(sensor, "dev_name", "Dyson ESP Purifier");
        cJSON_AddNumberToObject(sensor, "state", GetWorkStateFromReg(&dyson_reg));
        cJSON_AddNumberToObject(sensor, "temperature", GetTempFromReg(&dyson_reg));
        cJSON_AddNumberToObject(sensor, "humidity", GetHumFromReg(&dyson_reg));
        cJSON_AddNumberToObject(sensor, "vent_level", GetVentLevelFromReg(&dyson_reg));
        cJSON_AddNumberToObject(sensor, "pm25_level", GetPart25FromReg(&dyson_reg));
        cJSON_AddNumberToObject(sensor, "pm10_level", GetPart10FromReg(&dyson_reg));
     
        // Serialize JSON payload and send to Home Assistant
        char* output = cJSON_Print(sensor);
        ESP_LOGD(TAG, "JSON = %s",output);
		httpd_resp_set_status(req, "200 OK");
		httpd_resp_set_type(req, "application/json");
		httpd_resp_send(req, output, strlen(output));
	}
	else
    {
		/* send a 404 otherwise */
		httpd_resp_send_404(req);
	}

	return ESP_OK;
}

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

Прошивка для ESP32 готова, компилируем и грузим. После запуска устройство создаёт свою точку доступа. Подключаемся к ней, нас перебросит на веб-портал с перечнем Wi-fi сетей вокруг нас. После выбора нужной точки доступа, устройство подключится к ней, получит по DHCP ip-адрес. Запоминаем его, он нам понадобится.

На стороне сервера в HA редактируем основной конфигурационный файл configuration.yaml. Добавим в его конец следующий текст, с указанием ip-адреса нашего ESP32.

sensor:
  - platform: rest
    name: DysonDevice
    json_attributes:
    - state
    - temperature
    - humidity
    - vent_level
    - pm25_level
    - pm10_level
    resource: http://<ваш esp32 IP адрес>/api/states/sensors
    value_template: "{{ value_json}}"
  - platform: template
    sensors:
      state:
        friendly_name: "On/off State"
        unique_id: "Dyson_state"
        value_template: "{{'On' if state_attr('sensor.dysondevice', 'state')|int>0 else 'Off'}}"
      temperature:
        friendly_name: "Temperature"
        unique_id: "Dyson_temp"
        value_template: "{{ state_attr('sensor.dysondevice', 'temperature') }}"
        device_class: temperature
        unit_of_measurement: " C"
      humidity:
        unique_id: "Dyson_hum"
        friendly_name: "Humidity"
        value_template: "{{ state_attr('sensor.dysondevice', 'humidity') }}"
        device_class: humidity
        unit_of_measurement: "%"
      fan_level:
        unique_id: "Dyson fan level"
        friendly_name: "Fan level"
        value_template: "{{ state_attr('sensor.dysondevice', 'vent_level') }}"
        device_class: wind_speed
        unit_of_measurement: " lv"
      pm25_level:
        unique_id: "Dyson pm25 level"
        friendly_name: "pm2.5 level"
        value_template: "{{ state_attr('sensor.dysondevice', 'pm25_level') }}"
        device_class: pm25
        unit_of_measurement: "µg/m³"
      pm10_level:
        unique_id: "Dyson pm10 level"
        friendly_name: "pm10 level"
        value_template: "{{ state_attr('sensor.dysondevice', 'pm10_level') }}"
        device_class: pm10
        unit_of_measurement: "µg/m³"

После перезагрузки конфигурации в HA, будут созданы новые сенсоры, а мы сможем вывести на dashboard новый информационный блок.

Группа сенсоров в Home Assistant dashboard
Группа сенсоров в Home Assistant dashboard

Что же дальше?

В общем-то, пока всё. Многое еще предстоит сделать.

  1. Необходимо ещё поиграться с данными в регистрах для выявления большего числа параметров увлажнителя, таких как: наличие в воздухе органических соединения, уровень CO2. Сымитировать контролированное изменение этих параметров несколько сложнее, поэтому оставим на финальный этап.

  2. Пока не удалось выявить используемый в CRC32 полином и другие настройки. Без этого, скорее всего, невозможно будет послать команду в Dyson по UART. Если не получится, то буду имитировать посылку команд через ИК-порт.

  3. Ну и, наконец, спроектировать небольшое устройство в собственном корпусе, чтобы смотрелось прилично на очистителе.

Надеюсь, сообществу статья будет интересна, а кому-то и полезна. Продолжение должно быть. Прошивка для ESP32 выложена на github, там же можно взять отдельно и библиотеку парсера Dyson пакетов. Она написана без привязки к какой-либо конкретной платформе, поэтому её легко можно будет использовать в проектах с другими микроконтроллерами. Останется только считать данные с UART.

Комментарии (5)


  1. foxyrus
    00.00.0000 00:00
    +4

    Как то резко перешли с сырых данных, к структуре пакетов, можно подробнее как среди всего этого нашли границы чисел. Сам пытался разобраться в протоколе умной кнопки (диммера), но далее получения сырых данных PulseView не продвинулся.


    1. Alex_Trusk Автор
      00.00.0000 00:00
      +4

      В первую очередь необходимо выделить пакеты среди всего массива данных. Принимающая сторона должна быть способна отделить один пакет от другого. Есть несколько способов: добавить старт-стоп байт в пакет (как в этом случае) или сделать паузы между пакетами, тогда можно будет ловить пакеты на основании Idle Line прерывания.

      Сразу удалось выявить старт/стоп байт 0x12 (18 в десятичной системе), пакеты мы нашли и отделили. Дальше записываем их рядом и сравниваем. Видим что они разной длины,которая указана во втором (и как оказалось в третьем) байте.

      Для проверки корретности получения информации обычно используют некую контрольную сумму, которую добавляют в конец пакета (в основном). 4-х байтный CRC32 удалось выделить достаточно быстро. А дальше сложнее, если не используется какой-либо определенный стандарт формата пакета (modbus, например), приходится много сидеть и сравнивать отличия в разных пакетах, искать патерны и зависимости.

      Данные по UART передаются в основном 8-ми битном формате, с определенной скоростью. Чтобы передать значение двух-восьми байтной переменной, нужно делить на байты. Очень полезно понимать, как записываются многобайтовые переменные в памяти (little-endian и big-endian), какой формат представления для вещественных чисел (float, double).

      Можно экселем обойтись, но это долго при большом количестве данных. Я написал библиотечку парсера для удобства. Я сегодня выложу на гитхаб (сылка в конце статьи) excel файл с даными после работы парсера. Посморите визуально на разные пакеты, может станет понятнее.


  1. MJVidak
    00.00.0000 00:00
    +1

    Вчера только сдался с dyson и HA. У меня PH01 с wifi модулем, но от этого не легче. Приложение mydyson в России не работает. vpn, fakeGPS не помогают, зависает на последнем этапе подключения намертво. Мой ph01 не создаёт точку доступа как было на прошлых версии устройства, сейчас он подключается по BLE и передаёт данные точки доступа, к точке доступа подключается. С помощью wireshark анализировал всё общение Bluetooth и wifi но никаких паролей от устройства он передает, к серверам не коннектится. Вот и получается что в локалке торчит mqtt сервер на dyson, а сделать с ним ничего не могу. Ушёл на внешние датчики и управление по ИК. Теперь думаю о разборе и присоединения esp32 для сбора данных, спасибо за статью.


    1. Didimus
      00.00.0000 00:00

      Может, он где-то через открытый вайфай плохого набрался?


  1. j_larkin
    00.00.0000 00:00

    Спасибо, всегда интересно читать про реверс)) этакий детектив от мира ит