Было время, когда все должно было приводиться в действие человеческой или животной силой. Конечно, были исключения, такие как ветряные мельницы, но использовать силу ветра, воды или солнца было непростой задачей.

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

Однако не все устройства могут быть подключены, и цена интеллектуальных устройств часто намного выше, чем альтернативы. Итак, как мы можем превратить наши глупые устройства в умные, не тратя слишком много?

Сегодня я покажу вам, как я подключил свои автоматические внешние ворота к Интернету и связал их с Google Home для полной интеграции с голосовыми командами.

Анализ

Прежде всего, давайте проанализируем ворота: есть блок управления, который отвечает за открытие, закрытие ворот, тайминги, интерпретацию сигнала, полученного фотоэлементами, и получение триггера. Триггер отправляется через портативный пульт дистанционного управления, который посылает определенный сигнал на определенной радиочастоте.

Нам нужно сделать этот радиочастотный триггер умным, и мы можем сделать это одним из двух способов:

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

Я выбрал второе решение, так как оно не требовало модификации блока управления.

Я выбрал ESP8266, недорогой 32-битный MCU со встроенными возможностями WiFi и достаточно хорошими характеристиками вычислительной мощности и памяти. Они стоят около 5 евро каждый, если вы заказываете из Европы, но вы можете получить их по незначительной цене, если заказываете из Китая.

Для передачи сигнала мне понадобился внешний ВЧ-модуль 433,92. Я получил 5 из них всего за 10 евро.

Затем я проанализировал радиочастотный сигнал, передаваемый моим портативным пультом на частоте 433,92 МГц, и расшифровал протокол. В моем конкретном случае, но я думаю, что большинство устройств работают именно так, сигнал модулируется по амплитуде в серию битов, каждый из которых имеет фиксированную продолжительность. Затем эта последовательность битов повторяется до тех пор, пока вы не перестанете нажимать кнопку на пульте с паузой между ними. Поэтому я расшифровал эту серию и преобразовал ее в двоичную последовательность, например. 10011011110101.

Вот как выглядит радиочастотный сигнал пульта:

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

Внутри микроконтроллера эта последовательность битов отправляется в передатчик, управляя выводом DATA. Точно синхронизируя каждый бит и изменяя уровень вывода в соответствии с битовым значением, сигнал отправляется на радиочастотный модуль, который затем транслирует его по воздуху.

Разумное использование

Чтобы разрешить управление этим устройством из Интернета, мне нужно было создать веб-сервер, который открывал бы API и каким-то образом связывался с устройством. Я обрабатывал связь Сервер ↔ Устройство через протокол MQTT, так как он простой, надежный, легкий и поддерживается MCU. Я также добавил аутентификацию в API REST и зашифровал обмен сообщениями MQTT с помощью AES.

Теперь устройством можно управлять из Интернета, но у нас все еще нет хорошей интеграции с Google Home, которая позволяет нам открывать ворота с помощью голосовой команды. Как мы интегрируем наше устройство с Google Home? Оказалось, что Google предоставляет нам удобную панель администрирования, где мы можем создать наше приложение IoT и интегрировать его с нашей собственной службой API, которую мы создали ранее. Вы можете найти консоль Google Actions здесь: https://console.actions.google.com/, а всю информацию об интеграции с Google Home вы можете найти здесь: https://developers.home.google.com. /облако-облако/начать».

Теперь серверу необходимо поддерживать TPA (стороннюю аутентификацию) через OAuth2 и предоставлять конечную точку, чтобы позволить Google контролировать наши устройства.

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

Архитектура всей установки представлена ​​на этой диаграмме:

Давайте подробно рассмотрим каждый компонент этой установки.

Устройство

Как указывалось ранее, устройство представляет собой ESP8266, подключенный к радиочастотному модулю. Альтернатив со встроенным WiFi-модулем не так много, поэтому выбор был между этим и ESP32. Последний более мощный и поддерживает BT/BLE, но нам он не нужен, поэтому мы можем использовать более дешевый 8266.

Прошивка построена на базе платформы Arduino, что ускоряет разработку и позволяет нам использовать сторонние библиотеки для таких задач, как настройка MCU WiFi и реализация протокола MQTT.

Я использовал библиотеку WiFiManager для реализации начального режима AP для конфигурации WiFi, PubSubClient в качестве реализации протокола MQTT и AESLib для шифрования сообщений. расшифровка.

Это код установки, который я использовал для инициализации всех зависимостей:

void setup() {
  Serial.begin(115200);
  pinMode(GPIO_RF, OUTPUT);

  WiFi.mode(WIFI_STA);

  WiFiManager wm;
  // Wait 15 seconds before switching to ap mode
  wm.setConnectTimeout(15);
  // Keep the device in AP mode for 10 minutes, then try connecting with the previous credentials
  wm.setConfigPortalTimeout(600);

  bool res = wm.autoConnect("DoorRemote");

  if (!res) {
    Serial.println("Failed to initialize WiFiManager");
  }

  // Increase MQTT message limit to 1KB
  psClient.setBufferSize(1024);
  psClient.setServer(MQTT_REMOTE_HOST, MQTT_REMOTE_PORT);
  psClient.setCallback(mqttCallback);

  crypto::aesInit();
}

Серийный номер, выходной PIN-код для радиочастотного модуля инициализируются. Затем настраивается Wi-Fi через библиотеку WiFiManager и, наконец, настраивается клиент MQTT. Последняя строка кода — это функция, которая инициализирует библиотеку AES.

Функция mqttCallback получает пакет от сервера MQTT, проверяет соответствие темы, расшифровывает его и передает.

void mqttCallback(char *topic, byte *payload, unsigned int length) {
  Serial.print("Message received on topic ");
  Serial.print(topic);
  Serial.print(". Length: ");
  Serial.println(length);

  if (strcmp(topic, MQTT_REMOTE_TOPIC) == 0) {
    std::vector<uint8_t> decrypted = crypto::aesDecryptBuffer(payload, length);

    if(decrypted.size() > sizeof(code_packet_t)) {
      code_packet_t packet = {};
      memcpy(&packet, decrypted.data(), sizeof(code_packet_t));

      utils::swapEndianness(&packet.bitDurationUs);
      utils::swapEndianness(&packet.signalBitsCount);
      utils::swapEndianness(&packet.signalPauseUs);
      utils::swapEndianness(&packet.signalRepeatCount);

      Serial.printf("Decoded: bit duration = %u, pause = %u, repeat count = %u, bits count = %u, pause state = %s, logic type = %d\r\n", 
        packet.bitDurationUs, packet.signalPauseUs, packet.signalRepeatCount, packet.signalBitsCount, packet.pauseState == 1 ? "HIGH" : "LOW",
        packet.logicType);

      if(decrypted.size() == sizeof(code_packet_t) + ceil(packet.signalBitsCount / 8.0f)) {
        std::string decodedSignal = utils::expandBits(decrypted.data() + sizeof(code_packet_t), decrypted.size() - sizeof(code_packet_t), packet.signalBitsCount);
        SignalData signal;
        
        signal.bitDurationUs = packet.bitDurationUs;
        signal.signalPauseUs = packet.signalPauseUs;
        signal.signalRepeatCount = packet.signalRepeatCount;
        signal.signal = decodedSignal;
        signal.pauseState = packet.pauseState == 1 ? HIGH : LOW;
        signal.logicType = packet.logicType;

        Serial.println("Payload OK. Triggering...");
        Serial.printf("Code: %s\r\n", decodedSignal.c_str());

        transmitSignal(&signal);
      } else {
        Serial.println("Couldn't parse the signal protocol.");
      }
    } else {
      Serial.println("The decryption failed");
    }
  }
}

Обратный вызов расшифрует пакет, при необходимости поменяет порядок следования байтов (пакеты передаются в обратном порядке) и проверит, соответствует ли размер данных сигнала количеству битов. Затем сигнал распаковывается с помощью функции expandBits, а структура SignalData строится и передается с помощью служебного метода transmitSignal.

Пакет сообщений имеет следующую структуру:

typedef struct {
  uint32_t bitDurationUs;
  uint32_t signalPauseUs;
  uint32_t signalRepeatCount;
  uint32_t signalBitsCount;
  unsigned char pauseState;
  unsigned char logicType;
} code_packet_t;

Эта структура отправляется сервером и содержит все необходимые данные для передачи сигнала:

  • bitDurationUs — это временная длина бита в микросекундах.
  • signalPauseUs — пауза в микросекундах между каждым повторением сигнала.
  • signalRepeatCount — количество повторений сигнала.
  • signalBitsCount — количество битов, содержащихся в сигнале.
  • pauseState — логический уровень сигнала паузы (может быть низким или высоким).
  • logicType — это флаг, указывающий, является ли логика нормальной (1 — высокий, 0 — низкий) или инвертированной (0 — высокий, 1 — низкий).

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

Это функция, передающая сигнал:

void transmitSignal(const SignalData* signal) {
  unsigned char offState = LOW;
  unsigned char onState = HIGH;

  if(signal->logicType != 0) {
    offState = HIGH;
    onState = LOW;
  }

  for(int i = 0; i < signal->signalRepeatCount; i++) {
    for (char c : signal->signal) {
      switch (c) {
        case '1':
          digitalWrite(GPIO_RF, onState);
          break;
        case '0':
          digitalWrite(GPIO_RF, offState);
          break;
      }
      delayMicroseconds(signal->bitDurationUs);
    }
    digitalWrite(GPIO_RF, signal->pauseState);
    delayMicroseconds(signal->signalPauseUs);
  }
}

Как мы видим, передача осуществляется путем переключения состояния контакта, к которому подключен контакт DATA передатчика, через точно установленные интервалы времени. Также есть поддержка инвертирования логики и логического уровня паузы путем изменения параметров, отправляемых в MCU.

Веб-сервер

Веб-сервер написан на nodeJS с использованием express и typescript. Он подключен к базе данных MySQL, где хранятся данные аутентификации, устройства и виртуальные пульты.

Логическая структура, которую я придумал, такова:

виртуальные пульты — это, по сути, передаваемые сигналы. Каждый виртуальный пульт связан с одним или несколькими устройствами, поэтому при срабатывании виртуального пульта сигнал передается на все связанные с ним устройства.

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

Когда пользователь запускает пульт через REST API или через интеграцию с Google Home, данные сигнала шифруются и отправляются на устройство по протоколу MQTT. Устройство получает его, расшифровывает и передает через RF-модуль. Ворота в этот момент получают сигнал «виртуального пульта» и открываются.

Пакет кода на веб-сервере сериализуется через тип данных Buffer:

export default class CodePacket {
  bitDurationUs: number = 0;
  signalPauseUs: number = 0;
  signalRepeatCount: number = 0;
  signalCode: Uint8Array = new Uint8Array();
  signalBitsCount: number = 0;
  pauseState: number = 0;
  logicType: number = 0;

  serialize(): Buffer {
    const bufSize = 4 * 5 + this.signalCode.length;
    const result = Buffer.alloc(bufSize);

    let of = result.writeUInt32BE(this.bitDurationUs, 0);
    of = result.writeUInt32BE(this.signalPauseUs, of);
    of = result.writeUInt32BE(this.signalRepeatCount, of);
    of = result.writeUInt32BE(this.signalBitsCount, of);

    of = result.writeUInt8(this.pauseState, of);
    of = result.writeUInt8(this.logicType, of);
    of = result.writeUInt8(0, of);
    of = result.writeUInt8(0, of);

    for (let i = 0; i < this.signalCode.length; i++) {
      of = result.writeUInt8(this.signalCode[i], of);
    }

    return result;
  }
}

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

Интеграция с Google Home осуществляется одним и тем же сервером nodeJS через одну конечную точку, которая обрабатывает 3 намерения. Я использовал Google SmartHome SDK, чтобы обернуть конечные точки и получить информацию о типах данных запроса и ответа. Вы можете найти всю информацию об интеграции Google Home здесь: https://developers.home.google.com/cloud-to-cloud/get-started.

Действия, которые Google Home отправляет нам:

  • цель SYNC используется Google для перечисления доступных устройств и их возможностей.
  • цель QUERY отправляется Google для получения подробной информации об одном или нескольких устройствах. Сюда возвращается такая информация, как возможности устройства и его состояние (например, ОТКРЫТО или ЗАКРЫТО).
  • намерение EXECUTE отправляется Google, когда пользователь выполняет действие на устройстве, например, когда вы спрашиваете помощника: «Окей, Google, открой ворота», Google переводит эту голосовую команду в намерение EXECUTE. чтобы открыть ворота, которые вы ранее зарегистрировали в Google Home

Нам нужно обработать все действия, и в частности действие EXECUTE — это то, которое попросит нас открыть ворота. Когда это действие выполняется, Google отправляет нам конкретный виртуальный пульт, который должен быть запущен, через идентификатор, который мы отправили ему в конечной точке SYNC. Мы выбираем удаленное устройство через этот идентификатор, а затем запускаем связанные с ним устройства, публикуя сообщение в нужных темах MQTT.

Управление виртуальными пультами и устройствами

Для управления виртуальными пультами и связанными с ними устройствами я создал приложение для Android, которое взаимодействует с REST API сервера nodeJS. Приложение перечисляет все зарегистрированные нами виртуальные пульты, чтобы мы могли изменять их, создавать новые или удалять. Мы также можем привязать или отвязать устройство от виртуального пульта, чтобы мы могли решить, на какие устройства будет передаваться сигнал.

Последние мысли

В целом, этот проект было очень весело строить. Требовалось настроить и разработать множество компонентов: облачный сервер, интеграцию с Google Home, прошивку MCU, сигнальный пакет и т. д.

Несмотря на множество задействованных компонентов, связи между частями очень просты, и было несложно соединить все части вместе и заставить их работать.

Помимо того, что это весело, я использую его каждый день и могу оценить преимущества домашней автоматизации и домашних помощников. Это область, где относительно легко экспериментировать и создавать новые вещи из нескольких дешевых компонентов, поэтому я призываю всех начать свой собственный проект и создавать невероятные вещи. И, если хотите, сообщите мне об удивительных вещах, которые вы строите!