Команды на поле

Приветствую всех на втором тайме игры в ревёрс‑инжиниринг, в котором мы продолжаем борьбу с очистителем воздуха от Dyson. В предыдущей статье, которую я советую прочитать всем только что подключившимся к «трансляции»,

  • был разобран протокол, используемый при передаче диагностической информации очистителя воздуха;

  • осуществлено подключение модуля ESP32 для сбора и парсинга данных;

  • выполнена первоначальная интеграция с Home Assistant.

По результатам первого тайма часть поставленной задачи была выполнена, наиболее значимые параметры работы dyson»a отображались в Home Assistant (HA). Однако управлять работой очистителя непосредственно из HA мы всё ещё не могли. Решением этой проблемы мы и займёмся в рамках данной статьи.

Напомню, что все данные мы получали через UART разъём, разведённый на управляющей плате очистителя, подключаясь к передающему выводу TX. При этом, очень хотелось использовать соседний принимающий вывод RX для посылки команд непосредственно в управляющий очистителем микроконтроллер STM32. Поскольку для проверки целостности передаваемой информации в протоколе Dyson применяется алгоритм нахождения контрольной суммы СRC32, разбираться с форматом пакетов для отправки в RX без повторения используемой реализации CRC32 — пустая трата времени. Даже, если удастся подобрать правильный формат, без корректной контрольной суммы контроллер отбросит пакет как повреждённый, показав ему красную карточку.

CRC32: атака, удар, промах

Алгоритм CRC32 базируется на свойствах деления с остатком двоичных полиномов. Значение CRC — это остаток от деления полинома, соответствующего входным данным на некий фиксированный порождающий полином. Существует множество стандартизированных и рекомендованных разными организациями порождающих полиномов. Например, полином CRC32 по стандарту IEEE 802.3 выглядит так.

x^{32}+x^{26}+x^{23}+x^{22}+x^{16}+x^{12}+x^{11}+x^{10}+x^8+x^7+x^5+x^4+x^2+x+1 = 0x04C11DB7

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

  1. начальное значение CRC, то есть значения регистров на момент начала вычислений;

  2. флаг RefIn, указывающий на начало и направление вычислений, для обнаружения ошибок должно соответствовать порядку передачи в канале. Существует два варианта: False — начиная со старшего значащего бита (MSB‑first) или True — с младшего (LSB‑first);

  3. флаг RefOut, определяющий, инвертируется ли порядок битов регистра на входе на элемент XOR;

  4. число XorOut, с которым складывается по модулю 2 полученный результат.

Помимо настроек самого алгоритма CRC, нужно знать, как подаётся в алгоритм сам массив данных пакета. Вариантов хватает:

  • в порядке следования байт в UART пакете,

  • в обратном порядке следования,

  • порядок следования байт для многобайтовых переменных внутри пакета изменяется (разворачивается),

  • учитывается ли значение длины пакета при подсчёте CRC,

  • развёрнут ли сам CRC в конце пакета.

Наиболее сложный вариант связан с использование аппаратных модулей расчёта CRC внутри микроконтроллера. Например, в STM32 CRC‑модуль имеет 32-битный регистр. Однобайтные массивы надо упаковывать и подавать пачками по 4 байта. При этом возникнет вопрос, чем заполнить последнюю пачку, если размер массива не кратен четырём. Это могут быть просто нули, а могут быть любые значения, известные только разработчикам. Даже любой дополнительный ноль, полностью изменит контрольную сумму.

Работу по вскрытию CRC я начал с различных онлайн сервисов по расчёту CRC и использованием основных популярных вариантов алгоритма. Подставляя данные из пакета в различном порядке следования в онлайн формы, получал контрольную сумму и сравнивал с значением из пакета. К сожалению, мимо.

Далее, была применена программа Reveng, одной из функций которой является нахождение параметров алгоритма CRC на основе нескольких пакетов данных. При подборе параметров CRC8 программа мне очень помогла. К сожалению, с подбором CRC32 она не справилась, несмотря на то, что я вводил данные в различном порядке.

В итоге, помучавшись с CRC, решил больше не тратить время и менять тактику игры.

План Б. Домашняя заготовка

Единственным регламентированным способом управления очистителем воздуха Dyson TP07 является крошечный инфракрасный (ИК) пульт дистанционного управления (ДУ) с 8-ю кнопками, которые отвечают за следующие действия:

  • Включение\выключение

  • Увеличение и уменьшение скорости вращения вентилятора

  • Изменения направления потока воздуха

  • Автоматический режим

  • Ночной режим

  • Поворот очистителя вокруг вертикальной оси

  • Смена индикации на экране

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

Главная плата. Верхний правый угол. Зелёный провод припаян.
Главная плата. Верхний правый угол. Зелёный провод припаян.

Первым делом на плате была обнаружена микросхема приёмника ИК сигнала. Одна из её ног соединяется с управляющим микроконтроллером STM32. Эта линия подтянута к питанию 3.3V через резистор, поступающие импульсы тянут линию к «земле». Это следует учесть при написании управляющей программы имитатора ИК команд.

Для начала определим применённую микросхему приёмника сигнала. Так как на ней нет никакой маркировки, определяем модель по внешнему виду. Походящая по разводке микросхема носит наименование TSOP753xx, где xx — значение несущей частоты. Трейтья нога (Vs) микросхемы — наша искомая. По ней текут нужные биты информации.

Аккуратно подпаиваемся к тестовой контактной точке на печатной плате, провод выводим наружу вместе с остальными через технологическое отверстие напротив диагностического UART разъёма. Подключаем к проводу логический анализатор, записываем последовательность поступающих импульсов при нажатии кнопок на пульте ДУ для последующего анализа.

Мяч на нашей стороне

Существует целый ряд протоколов, описывающих формат команд при передаче через ИК порт, применяемые длительности импульсов, модуляции, кодирование. Многие протоколы описаны на сайте энтузиаста и инженера Сэма Бергманса https://www.sbprojects.net/, который собрал информацию о наиболее популярных из них. Несмотря на то, что используемый в очистителе воздуха ИК протокол является нестандартным и в точности не походит под описанные на сайте, изучение всей номенклатуры позволило выделить примененные подходы к формированию кодирующих информацию импульсов.

Короткий таймаут

Небольшое отступление для описания принципа работы современной инфракрасной связи. Синтезатор генерирует короткие импульсы на несущей частоте. Для повышения помехозащищенности сигнал модулируется по определенным в выбранном стандарте правилам. Приёмник должен быть настроен на несущую частоту. Полученный сигнал усиливается, фильтруется, де‑модулируется и направляется в интегрирующий блок и компаратор. На выходе компаратора мы поочерёдно получаем низкие или высокие логические уровни, соответствующие используемой в передатчике модуляции.

В данный момент мы абстрагируемся от нюансов ИК передачи и рассматриваем уже полученный на выходе микросхемы сигнал.

Для передачи одного бита информации могут применяться различные способы кодирования, такие как:

  • кодирование длиной импульса,

  • кодирование длиной паузы,

  • би-фазное кодирование (Manchester code).

Подробнее вы можете почитать, перейдя по ссылкам в конце статьи.

В данном случае используется кодирование длиной паузы. При этом длина паузы после импульса определяет, передаётся ноль или единица. Приступим к описанию ИК протокола от Dyson.

Основные характеристики ИК протокола Dyson:

  • длительность импульса = 750 микросекунд,

  • бит “0” – длительность паузы = 720 микросекунд,

  • бит “1” – длительность паузы = 1450 микросекунд,

  • несущая частота = 38 кГц,

  • 8 бит на адрес и 8 бит на команду,

  • порядок следования бит – MSB-first (most significant bit).

Структурное изображение ИК пакета
Структурное изображение ИК пакета

В начале ИК пакета посылается заголовок (header), имеющий следующую структуру:

  1. Импульс длительностью 2220 микросекунд

  2. Пауза длительностью 720 микросекунд

  3. Бит заголовка: 0 — новая команда, 1 — повтор предыдущей команды

За заголовком пакета следует поле для адреса (address) длиной 8 бит. По всей видимости, в адресе указывается ID устройства, для которого предназначена команда. В моём случае адрес для всех команд равняется 000010012 = 910. Вероятно, 9 — это номер для всех очистителей воздуха. Получив посылку, устройство сравнивает число из поля address со своим номером, при несовпадении обработка команды прерывается.

Следующим идёт поле с номером команды (command). При выборе длины этого поля пришлось изрядно подумать, сопоставить все за и против. У нас есть два варианта длины: 8 бит команда + 2 бита для контрольной суммы (checksum) или все 10 бит на команду без контрольной суммы. Чтобы сделать выбор запишем команды со всех 8-ми кнопок пульта в одну таблицу.

Перечень ИК-команд
Перечень ИК-команд

Аргументами за выбор варианта 8+2 бит стали следующие.

  • Во-первых, 8 бит даёт нам 256 различных вариантов команд, что видится достаточным для разнообразных пультов ДУ.

  • Во-вторых, команда включения (On/off) увлажнителя при использовании 8 бит будет 000000012, что представляется достаточно логичным.

  • В-третьих, номера команд для повышения и понижения скорости вращения вентилятора оказываются рядом 010101102=8610 и 010110002=8810 соответственно.

Основным аргументом против 8-ми битной команды стал фейл с подбором алгоритма подсчёта контрольной суммы. Перепробовал кучу вариантов: проверки на чётность (обычные и двухмерные), разные варианты операции XOR (через один бит, дуплетами), суммирование по модулю 4, подобие кодов Хемминга. Наличие на руках большего количества устройств от Dyson с ИК управлением помогло бы с подбором алгоритма. Но имеем то, что имеем.

Последним идёт поле Seq# размером 2 бита - номер отправленного пакета. Варианты 0..3 перебираются циклически. Каждое отдельное нажатие на кнопку пульта увеличивает значение номера пакета. Честно говоря, я не понял, зачем включили данное поле в протокол. Оно могло бы помочь отличить частые нажатия кнопки от длительного её удержания. Однако, для кодирования удержания кнопки в ИК протоколе от Dyson предусмотрена отправка короткого пакета, состоящего только из заголовка с установленным в ”1” битом. Пакет повтора команды отправляется через 50 мс после основного пакета.

Последовательность импульсов при длительном нажатии кнопки на пульте ДУ
Последовательность импульсов при длительном нажатии кнопки на пульте ДУ

И все-таки, я склоняюсь к варианту 8+2 даже, если сейчас разгадка поля checksum не найдена. Кнопок на пульте у нас всего 8, сделаем своеобразный дамп и запишем в память нашего ESP32 всю последовательность импульсов.

Переходим в атаку

Теперь у нас есть все данные для написания имитатора ИК команд. Модуль ESP32 может быть дополнен новым функционалом, и станет генерировать импульсы аналогичные формируемым микросхемой TSOP753xx. Надо только помнить, что линия связи ИК приёмника и STM32 подтянута к питанию, и все импульсы тянут линию к земле.

Для реализации всего задуманного была написана маленькая библиотека IRimitator.h без привязки к конкретному микроконтроллеру. Функционал сводится к подготовке структуры, содержащей длительности импульсов и пауз между ними на основании полученной команды. По завершении вызывается функция запуска последовательности импульсов, аргументом которой является ссылка на подготовленный объект с последовательностью импульсов. Функция является слабой (weak), требуется написать её пользовательскую реализацию, где будет находится функционал управления применённым микроконтролером.

__attribute__((weak)) void StartCommandSequence(IRcommand_t* command)
{
    printf("WARNING: weak func is called. Function should be user reimplemented\n");
    if(command)
        free(command);

}

Вывод D4 микроконтроллера мы подсоединяем к припаянному ранее проводу через токоограничивающий резистор (100 - 500 Ом). Резистор опциональный, нужен для защиты портов как ESP32, так и STM32, от чрезмерной токовой нагрузки (на всякий случай).

Формирование импульсов

Первоначально, для управления длительностями импульсов, я использовал функционал программных таймеров из состава ESP IDF. Оказалось, что очиститель Dyson откликается на команду с имитатора через раз, а то и того меньше. Программные таймеры частенько срабатывают с задержкой, если требуется выполнить какую-либо более приоритетную задачу в операционке FreeRTOS. Их можно применять только для длительностей более 10 мс, где ошибка в 10% не столь критична, у нас же счёт на микросекунды.

Нарушение временной последовательности при использовании программных таймеров. Импульс длиной 1720 мкс вместо 750 мкс.
Нарушение временной последовательности при использовании программных таймеров. Импульс длиной 1720 мкс вместо 750 мкс.

Переходим на встроенные в ESP32 хардверные таймеры. Они отрабатывают достаточно чётко в соответствии с заложенными настройками. На временной диаграмме представлено сравнение двух одинаковых команд на включение, одна из которых поступила от ИК пульта, вторая сформирована нашим аппаратно-программным имитатором. Можно заметить различие в последних битах. Имитатор оставляет их нулевыми, поле Seq# никак не влияет на работоспособность устройства, очиститель без вопросов распознаёт команду.

Сравнение ИК имитатора и реальной ИК команды
Сравнение ИК имитатора и реальной ИК команды

Интеграция с Home Assistant продолжается

Мы подошли к моменту, когда уже возможно расширить и углубить интеграцию нашего устройства (читать голосом Михал Сергеича) в среду Home Assistant. В прошлый раз для получения данных о состоянии очистителя мы отправляли GET запрос и получали соответствующий ответ. Теперь для включения и изменения режимов работы увлажнителя мы будем отправлять POST запрос необходимой командой в качестве аргумента.

В среде HA для включения/выключения очистителя  HA создаём переключатель (switch), при нажатии на ESP32 будет направлен соответствующий post запрос по адресу:
http//<ip адрес ESP32>/api/controls.

В payload запроса кладём json структуру вида “attributes” : “on_off”}, где on_off – имя команды для исполнения. 

Перечень команд инициализируется при запуске устройства, имя команды связывается с переменной формата uint32_t, последовательность бит, которая будет передана через ИК имитатор. Для установки конкретной скорости вентилятора используется запрос вида {“set_fan” : 5}, где 5 – желаемая скорость от 1 до 10.

Так как не существует отдельной ИК-команды на включение определённой скорости, установка конкретного значения осуществляется ступенчато, серией последовательных команд vent_up и vent_down. За это отвечает ESP32.

Возникает задача: как контролировать состояние очистителя после отправки команды, да и в любой момент времени. Ведь, управлять устройством можно не только через HA, а всё ещё с пульта ДУ. Такие параметры, как состояние вкл\выкл и скорость вентилятора контролировать легко, данные о состоянии передаются очистителем через диагностический порт. С остальными настойками всё сложнее. Очиститель контролирует своё состояние полностью, но не делится с нами всей информацией, держит оборону. Для однозначного включения режима AUTO последовательно посылается три ИК-команды: vent_up, vent_down, auto_mode. Для выключения две команды: vent_up, vent_down.

С ночным режимом, направлением потока воздуха, режимом углового вращения всё хуже. Изменять режимы можно, а проконтролировать нельзя. Максимум, что можно сделать, сохранять настройки текущих режимов во flash памяти ESP32, и молиться, чтобы другие члены семьи не пользовались пультом ДУ. Последнее звучит нереально, правда?

Настройки для configuration.yaml
#Add the following lines to the end of configuration.yaml
input_text:
  post_url:
    initial: "http://<ESP32_IP_ADDR>/api/controls"
rest_command:
  fan_level_change:
    url: "{{states('input_text.post_url')}}"
    method: POST
    content_type: 'application/json'
    payload: '{"set_fan": {{ fan_level }}}'
  auto_mode_on:
    url: "{{states('input_text.post_url')}}"
    method: POST
    content_type: 'application/json'
    payload: '{"attributes": "auto_mode_on"}'
  auto_mode_off:
    url: "{{states('input_text.post_url')}}"
    method: POST
    content_type: 'application/json'
    payload: '{"attributes": "auto_mode_off"}'
  night_mode:
    url: "{{states('input_text.post_url')}}"
    method: POST
    content_type: 'application/json'
    payload: '{"attributes": "night_mode"}'
  flow_direction:
    url: "{{states('input_text.post_url')}}"
    method: POST
    content_type: 'application/json'
    payload: '{"attributes": "direction"}'
  rotation_mode:
    url: "{{states('input_text.post_url')}}"
    method: POST
    content_type: 'application/json'
    payload: '{"attributes": "sw_rotation"}'
  info_change:
    url: "{{states('input_text.post_url')}}"
    method: POST
    content_type: 'application/json'
    payload: '{"attributes": "info"}'
  led_change:
    url: "{{states('input_text.post_url')}}"
    method: POST
    content_type: 'application/json'
    payload: '{"set_led_mode": {{ led_mode }}}'
#my sensor
switch:
  - platform: rest
    resource: http://<ESP32_IP_ADDR>/api/controls
    method: post
    name: Dysonswitch
    body_on: >
      {% if is_state('switch.dysonswitch','off') %}
      {"attributes": "on_off"}
      {% endif %}
    body_off: >
      {% if is_state('switch.dysonswitch','on') %}
      {"attributes": "on_off"}
      {% endif %}
    state_resource: http://<ESP32_IP_ADDR>/api/states/sensors
    is_on_template: >-
      {% if value_json.state == 1 %} True
      {% else %} False
      {% endif %}
    scan_interval: 5
  - platform: template
    switches:
      dysons_witch:
        friendly_name: "Air purifier power"
        value_template: "{{is_state('switch.dysonswitch','on')}}"
        turn_on:
          service: switch.turn_on
          target:
            entity_id: switch.dysonswitch
        turn_off:
          service: switch.turn_off
          target:
            entity_id: switch.dysonswitch
      dyson_auto_mode:
        friendly_name: "Auto mode"
        turn_on:
          service: rest_command.auto_mode_on
        turn_off:
          service: rest_command.auto_mode_off

sensor:
  - platform: rest
    name: DysonDevice
    json_attributes:
    - state
    - temperature
    - humidity
    - vent_level
    - pm25_level
    - pm10_level
    resource: http://<ESP32_IP_ADDR>/api/states/sensors
    value_template: "{{ value_json}}"
    scan_interval: 5
  - 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³"

input_number:
  number1:
    name: "Fan level"
    initial: 1
    min: 1
    max: 10
    step: 1
input_button:
  direction_btn:
    name: Flow direction
    icon: mdi:waves-arrow-right
  night_btn:
    name: Night mode
    icon: mdi:weather-night
  rotation_btn:
    name: Rotation
    icon: mdi:axis-z-rotate-counterclockwise

input_select:
  led_mode:
    name: Onboard Led Mode
    options:
      - "Off"
      - "On"
      - "Blink"
    initial: "Blink"
    icon: mdi:led-on

Настройки для automations.yaml
#Add the following lines to the end of automations.yaml
- id: '233222334333'
  alias: Update Sensor
  trigger:
    platform: state
    entity_id: switch.dysons_witch
  action:
  - service: homeassistant.update_entity
    target:
      entity_id:
      - sensor.state
      - sensor.fan_level
      - sensor.dysondevice
    delay: 1
- id: '111222344443'
  alias: Fan level change
  trigger:
  - platform: state
    entity_id: input_number.number1
  condition: []
  action:
  - service: rest_command.fan_level_change
    data:
      fan_level: '{{ states(''input_number.number1'') | int }}'
  mode: single
- id: '1682662058414'
  alias: Fan Level Slider Update
  description: ''
  trigger:
  - platform: state
    entity_id:
    - sensor.fan_level
  condition: []
  action:
  - service: input_number.set_value
    data:
      value: '{{states(''sensor.fan_level'')|int}}'
    target:
      entity_id: input_number.number1
  mode: single
- id: '1682704871943'
  alias: Flow direction change
  description: ''
  trigger:
  - platform: state
    entity_id:
    - input_button.direction_btn
  condition: []
  action:
  - service: rest_command.flow_direction
    data: {}
  mode: single
- id: '168270487242'
  alias: Night mode change
  description: ''
  trigger:
  - platform: state
    entity_id:
    - input_button.night_btn
  condition: []
  action:
  - service: rest_command.night_mode
    data: {}
  mode: single
- id: '1682704872422'
  alias: Rotation change
  description: ''
  trigger:
  - platform: state
    entity_id:
    - input_button.rotation_btn
  condition: []
  action:
  - service: rest_command.rotation_mode
    data: {}
  mode: single
- id: '168270487287422'
  alias: Set Led Mode
  trigger:
  - platform: state
    entity_id: input_select.led_mode
  action:
  - service: rest_command.led_change
    data:
      led_mode: '{% if states(''input_select.led_mode'') == "Off" %}0 {% else %} {%
        if states(''input_select.led_mode'') == "On" %}1 {% else %} {% if states(''input_select.led_mode'')
        == "Blink" %}2 {% endif %} {% endif %} {% endif %}'

Все настройки HA приведены выше. Нужно отредактировать два конфигурационных файла configuration.yaml и automations.yaml, после чего на dashboard можно вывести нужные элементы. Например, как показано ниже.

Послематчевая пресс-конференция

Пришло время подвести некоторые итоги. По результатам двух таймов был

  • разобран диагностический протокол от Dyson,

  • описан формат ИК-протокола от него же,

  • создано устройство для общения с очистителем воздуха,

  • выполнена интеграция с Home Assistant.

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

Спасибо, всем за внимание. Матч окончен победой над очистителем воздуха, правда, не разгромной.

  1. Wiki про CRC

  2. С.В. Клименко, В.В. Яковлев, Е.А. Благовещенская. Исследование реализаций алгоритмов контрольной суммы CRC32

  3. Кодирование длительностью паузы

  4. Кодирование длительностью импульса

  5. Би-фазное кодирование

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