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

Если коротко, у этого проекта две особенности. Первое: стол подключается из Google Smart Home к Heroku с помощью голосовых команд, и второе: Heroku и собственно стол общаются по протоколу Интернета вещей MQTT. MQTT — хорошее решение для Интернета вещей, а также для преодоления некоторых других препятствий, с которыми нам придётся столкнуться.

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

Тот самый столик


Аппаратная часть


Первая и, вероятно, самая трудная часть работы — переделать стол. В прошлой жизни у стола была отсоединяемая ручка, она располагалась у края столешницы. Сначала я подумал о том, чтобы прикрепить что-то к отверстию ручки, не пришлось бы вмешиваться в конструкцию стола. Я приобрёл несколько приводов, чтобы понять, как к столу прикрепить мотор, но безрезультатно. Затем появилась идея: стержень, работающий по длине всего стола, который соединял бы его ножки так, что они опускались и поднимались бы одновременно. Если закрепить подходящий к стержню привод, тогда я смогу использовать ремень для соединения стержня и мотора. Также можно было бы оснастить стол мотором, не сильно вмешиваясь в его конструкцию.

Важность крутящего момента


Заказав нужные привод и ремень, я начал искать на Amazon мотор с высоким крутящим моментом. И — о, чудо! — я нашёл много подходящих двигателей! Или мне так показалось… Купив маленький моторчик, около месяца я ждал его прибытия из Китая. Я был так взволнован, когда моторчик, наконец, приехал! Не мог дождаться выходных, чтобы, наконец, собрать всё воедино и получить мой моторизованный стол.

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

Я включил моторчик, напряжение на своём настольном источнике питания и… ничего не произошло. Несколькими мгновениями позже мотор начал вращаться и скрежетать зубцами приобретённого ремня. Из этого я извлёк два урока: ремень, очевидно, не справляется со своей работой, а надпись «Мотор с высоким крутящим моментом» не означает «Я могу поднять всё на свете». Второй урок: надо смотреть, какого размера моторчик в сравнении с пальцами. Мой оказался крошечным!



Слева на фото мотор и ремень. Вверху справа — прикреплённый к столу мотор (позже вы увидите больше о происходящем). Внизу справа мотор в положении на столе.

Подходящий мотор


Чтобы выбрать подходящий мотор, нужно было рассчитать, какой крутящий момент требуется для поднятия столешницы. Я был удивлён тем, насколько просто сделать это.

$T = F * r$


Крутящий момент — это сила, умноженная на длину плеча рычага.

Что ж, плечо рычага у меня было (это рукоятка стола), нужно было только рассчитать силу, которая легко повернула бы плечо рычага. Я нагрузил стол, привязав к ручке кувшин для молока и постепенно добавлял в кувшин воду, пока рычаг не начал вращаться. Повернув ручку кверху с наполненным кувшином я убедился, что вес легко поворачивает ручку. Я выяснил, что длина плеча рычага составляет 11 см, а требуемая сила — 4 фунта. Подставив эти цифры в формулу, я выяснил, что мотор должен выдавать крутящий момент не менее 19,95 кгс.см. И начал искать его.

Я решил переделать стол необратимым образом. Мне было известно, что проходящий через середину стола стержень полый. Поискав двухвальный мотор, я мог разрезать стержень и пересобрать его с мотором в середине. Купив два мотора с крутящим моментом 20 кгс.см, я гарантировал, что крутящего момента для поднятия стола достаточно.

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


Два верхних фото — полностью установленные на стол моторы. Два нижних фото —- интегрированный стержень, с помощью моторов проходящий по длине стола.

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

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

Обороты важны. Окончательная версия


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

Это было не так сложно. Хотя я не нашёл двухвалового мотора, зато нашёл прямоугольный редуктор, который превращает мотор с одним валом в двухвальный мотор.

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


Последний мотор сам по себе слева, а установленный — справа. Немного аппаратного обеспечения и много программного обеспечения.

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

Bluetooth


Первым решением было добавить к столу Bluetooth. В конце концов, похоже на то, что почти каждое устройство в доме имеет Bluetooth, а телефон представляется удобным интерфейсом управления чего-то подобного моему столу.

Итак, теперь я приобрёл плату контроллера мотора, плату для Bluetooth Nordic NRF52, сенсоры для измерения расстояния и начал возиться с прошивкой контроллера.

В конце статьи я оставлю ссылки на софт и прошивку, которые написал для проекта. Не стесняйтесь комментировать код: я не занимаюсь прошивками профессионально и хотел бы получить какие-то указания.

В качестве краткого введения: ESP32 написана на C++ с помощью библиотек Arduino для взаимодействия с приложением BLE Terminal на телефоне. Установка и конфигурирование BLE довольно сложны. Для начала нужно создать все характеристики для значений, которые вы хотели бы контролировать через BLE. Думайте о характеристике, как о переменной в вашем коде. BLE оборачивает переменную во множество обработчиков для получения и установки значения этой переменной.

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

Для регулировки высоты столешница имеет определяющий текущую высоту встроенный в нижнюю часть сенсор TFMini-S LiDAR. Это забавный сенсор: он называется LiDAR, хотя на самом деле это лазер. Он использует оптику и светодиод, чтобы определить время полёта ИК-излучения. Так или иначе, сенсор определяет высоту стола. Затем контрольная плата определяет разницу между текущей высотой и запрашиваемой высотой и запускает мотор, который вращается в нужном направлении. Некоторые основные части кода приведены ниже, но вы можете увидеть весь файл здесь.

void setup()
{
   Serial.begin(115200);
   Serial2.begin(TFMINIS_BAUDRATE);
   EEPROM.begin(3); // used for saving the height presets between reboots

   tfminis.begin(&Serial2);
   tfminis.setFrameRate(0);

   ledcSetup(UP_PWM_CHANNEL, PWM_FREQUENCY, PWM_RESOLUTION);
   ledcAttachPin(UP_PWM_PIN, UP_PWM_CHANNEL);

   ledcSetup(DOWN_PWM_CHANNEL, PWM_FREQUENCY, PWM_RESOLUTION);
   ledcAttachPin(DOWN_PWM_PIN, DOWN_PWM_CHANNEL);

   state_machine = new StateMachine();
   state_machine->begin(*t_desk_height, UP_PWM_CHANNEL, DOWN_PWM_CHANNEL);

   BLEDevice::init("ESP32_Desk");
  ...

   BLEServer *p_server = BLEDevice::createServer();
   BLEService *p_service = p_server->createService(BLEUUID(SERVICE_UUID), 20);

   /* ------------------- SET HEIGHT TO PRESET CHARACTERISTIC -------------------------------------- */
   BLECharacteristic *p_set_height_to_preset_characteristic = p_service->createCharacteristic(...);
   p_set_height_to_preset_characteristic->setCallbacks(new SetHeightToPresetCallbacks());
   /* ------------------- MOVE DESK UP CHARACTERISTIC ---------------------------------------------- */
   BLECharacteristic *p_move_desk_up_characteristic = p_service->createCharacteristic(...);
   p_move_desk_up_characteristic->setCallbacks(new MoveDeskUpCallbacks());
   /* ------------------- MOVE DESK UP CHARACTERISTIC ---------------------------------------------- */
   BLECharacteristic *p_move_desk_down_characteristic = p_service->createCharacteristic(...);
   p_move_desk_down_characteristic->setCallbacks(new MoveDeskDownCallbacks());
   /* ------------------- GET/SET HEIGHT 1 CHARACTERISTIC ------------------------------------------ */
   BLECharacteristic *p_get_height_1_characteristic = p_service->createCharacteristic(...);
   p_get_height_1_characteristic->setValue(state_machine->getHeightPreset1(), 1);
   BLECharacteristic *p_save_current_height_as_height_1_characteristic = p_service->createCharacteristic(...);
   p_save_current_height_as_height_1_characteristic->setCallbacks(new SaveCurrentHeightAsHeight1Callbacks());
   /* ------------------- GET/SET HEIGHT 2 CHARACTERISTIC ------------------------------------------ */
  ...
   /* ------------------- GET/SET HEIGHT 3 CHARACTERISTIC ------------------------------------------ */
  ...
   /* ------------------- END CHARACTERISTIC DEFINITIONS ------------------------------------------ */
   p_service->start();

   BLEAdvertising *p_advertising = p_server->getAdvertising();
   p_advertising->start();

   xTaskCreate(
       updateDeskHeight,     // Function that should be called
       "Update Desk Height", // Name of the task (for debugging)
       1024,                 // Stack size
       NULL,                 // Parameter to pass
       5,                    // Task priority
       NULL                  // Task handle
   );
}

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

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

Итак, наконец у меня был стол, который делал всё, что я хотел. Я мог сохранить высоту в пресеты и извлечь высоты из памяти, чтобы установить стол в мои любимые положения. Я применял BLE Terminal на моём телефоне и компьютере, так я мог посылать сообщения в сыром виде моему столу и контролировать его позицию. Это работало, но я знал, что битва с BLE только начинается.


Голый интерфейс bluetooth… Всё, что оставалось на данный момент, — научиться писать приложения под iOS…

После всего этого моя жена сказала кое-что, что изменило весь проект: «А что, если сделать управление твоим голосом?».

Кроме крутости и добавления нового устройства к списку Google Assistant отпала необходимость писать приложение под iOS, чтобы управлять столом. И больше не нужно было доставать телефон, чтобы отрегулировать высоту. Ещё одна маленькая победа!

Добавление Интернета вещей


Теперь поговорим об апгрейде стола до управления голосом через Google Smart Home и как подружить его с Wi-Fi.

Добавить Wi-Fi было достаточно просто. Я заменил микроконтроллер Nordic NRF52 на ESP32 со встроенным WiFi. Большая часть софта была переносимой, потому что была написана на C++, а оба устройства могли программироваться с помощью Platform.IO и библиотек Arduino, включая tfmini-s, написанную мной для измерения текущей высоты стола.

Ниже показана архитектура системы взаимодействия стола с Google Smart Home. Давайте поговорим о взаимодействии между мной и Гуглом.



Итак, Bluetooth был включён. Пришло время выяснить, как взаимодействовать с Google Smart Home. Эта технология контролировала дом с помощью Smart Home Actions. В её действиях интересно то, что сервис действует как сервер OAuth2, а не как клиент. Большая часть проделанной с сервером работы была связана с реализацией приложения OAuth2 Node.js Express, которое добирается до Heroku и взаимодействует как прокси между Google и моим столом.

Мне повезло: была достойная реализация сервера с помощью двух библиотек. Первая библиотека — node-oauth2-server, была найдена здесь. Вторая библиотека express-oauth-server для подключения Express была найдена здесь.

const { Pool } = require("pg");
const crypto = require("crypto");
const pool = new Pool({
   connectionString: process.env.DATABASE_URL
});

module.exports.pool = pool;
module.exports.getAccessToken = (bearerToken) => {...};
module.exports.getClient = (clientId, clientSecret) => {...};
module.exports.getRefreshToken = (bearerToken) => {...};
module.exports.getUser = (email, password) => {...};
module.exports.getUserFromAccessToken = (token) => {...};
module.exports.getDevicesFromUserId = (userId) => {...};
module.exports.getDevicesByUserIdAndIds = (userId, deviceIds) => {...};
module.exports.setDeviceHeight = (userId, deviceId, newCurrentHeight) => {...};
module.exports.createUser = (email, password) => {...};
module.exports.saveToken = (token, client, user) => {...};
module.exports.saveAuthorizationCode = (code, client, user) => {...};
module.exports.getAuthorizationCode = (code) => {...};
module.exports.revokeAuthorizationCode = (code) => {...};
module.exports.revokeToken = (code) => {...};

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

const express = require("express");
const OAuth2Server = require("express-oauth-server");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const flash = require("express-flash-2");
const session = require("express-session");
const pgSession = require("connect-pg-simple")(session);
const morgan = require("morgan");

const { google_actions_app } = require("./google_actions");
const model = require("./model");
const { getVariablesForAuthorization, getQueryStringForLogin } = require("./util");
const port = process.env.PORT || 3000;

// Create an Express application.
const app = express();
app.set("view engine", "pug");
app.use(morgan("dev"));

// Add OAuth server.
app.oauth = new OAuth2Server({
   model,
   debug: true,
});

// Add body parser.
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(express.static("public"));

// initialize cookie-parser to allow us access the cookies stored in the browser.
app.use(cookieParser(process.env.APP_KEY));

// initialize express-session to allow us track the logged-in user across sessions.
app.use(session({...}));

app.use(flash());

// This middleware will check if user's cookie is still saved in browser and user is not set, then automatically log the user out.
// This usually happens when you stop your express server after login, your cookie still remains saved in the browser.
app.use((req, res, next) => {...});

// Post token.
app.post("/oauth/token", app.oauth.token());

// Get authorization.
app.get("/oauth/authorize", (req, res, next) => {...}, app.oauth.authorize({...}));

// Post authorization.
app.post("/oauth/authorize", function (req, res) {...});
app.get("/log-in", (req, res) => {...});
app.post("/log-in", async (req, res) => {...});
app.get("/log-out", (req, res) => {...});
app.get("/sign-up", async (req, res) => {...});
app.post("/sign-up", async (req, res) => {...});
app.post("/gaction/fulfillment", app.oauth.authenticate(), google_actions_app);
app.get('/healthz', ((req, res) => {...}));
app.listen(port, () => {
   console.log(`Example app listening at port ${port}`);
});

Кода довольно много, но я объясню основные моменты. Два используемых для сервера маршрута OAuth2, — это /oauth/token и /oauth/authorize. Они применяются для получения нового токена или обновления истекших токенов. Далее нужно заставить сервер реагировать на действие Google. Вы заметите, что конечная точка /gaction/fulfillment указывает на объект google_actions_app.

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

Конечная точка /gaction/fulfillment использует промежуточное ПО под названием app.oauth.authenticate(), тяжёлая работа по обеспечению работы сервера OAuth2 была направлена на то, чтобы работало это промежуточное ПО. Оно проверяет, что токен-носитель, предоставленный нам Google, ссылается на существующего пользователя и не истёк. Далее маршрут отправляет запрос и ответ объекту google_actions_app.

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

const { smarthome } = require('actions-on-google');
const mqtt = require('mqtt');
const mqtt_client = mqtt.connect(process.env.CLOUDMQTT_URL);

const model = require('./model');
const { getTokenFromHeader } = require('./util');

mqtt_client.on('connect', () => {
   console.log('Connected to mqtt');
});

const updateHeight = {
   "preset one": (deviceId) => {
       mqtt_client.publish(`/esp32_iot_desk/${deviceId}/command`, "1");
   },
   "preset two": (deviceId) => {
       mqtt_client.publish(`/esp32_iot_desk/${deviceId}/command`, "2");
   },
   "preset three": (deviceId) => {
       mqtt_client.publish(`/esp32_iot_desk/${deviceId}/command`, "3");
   },
};

const google_actions_app = smarthome({...});
google_actions_app.onSync(async (body, headers) => {...});
google_actions_app.onQuery(async (body, headers) => {...});
google_actions_app.onExecute(async (body, headers) => {...});
module.exports = { google_actions_app };

Когда вы добавите интеллектуальное действие в свой аккаунт Google, Google выполнит запрос на синхронизацию. Этот запрос позволяет узнать, какие устройства доступны из аккаунта. Далее происходит опрашивающий запрос: Google запрашивает ваши устройства, чтобы определить их текущее состояние.

Когда вы впервые добавляете действие Google в свой аккаунт Smart Home, вы заметите, что Google сначала отправляет запрос синхронизации, а затем опрашивающий запрос, чтобы получить целостное представление о ваших устройствах. Последний — запрос это запрос на выполнение, который Google сообщает вашим устройствам, чтобы они что-то делали.

«Особенности» (trait) устройства Google Smart Home


Google использует особенности устройства для предоставления элементов пользовательского интерфейса управления вашими устройствами в Google, а также для создания коммуникационных шаблонов голосового управления. Некоторые из особенностей включают в себя следующие настройки: ColorSetting, Modes, OnOff, and StartStop. Мне потребовалось некоторое время, чтобы решить, какая особенность будет лучше всего работать в моём приложения, но позже я выбрал режимы.

Вы можете думать о режимах как о выпадающем списке, где выбирается одно из N предопределённых значений или, в моём случае, предустановки высоты. Я назвал свой режим «высота», а возможные значения — «предустановка один», «предустановка два» и «предустановка три». Это позволяет мне управлять своим столом, говоря: «Эй, Google, установите высоту моего стола в предустановку один», — и Google отправит соответствующий запрос на выполнение в мою систему. Вы можете прочитать больше об особенностях устройств Google здесь.

Проект в деле


Наконец, Google Smart Home и мой компьютер начали общаться. До этого для локального запуска сервера Express я использовал ngrok. Теперь, когда мой сервер наконец заработал достаточно хорошо, пришло время сделать его доступным для Google в любое время. Значит, нужно было разместить приложение на Heroku — это поставщик PaaS, упрощающий развёртывание и управление приложениями.

Одно из главных преимуществ Heroku — режим дополнений. С дополнениями очень просто добавить CloudMQTT и сервер Postgres для приложения. Ещё одно преимущество использования Heroku — простота сборки и развёртывания. Heroku автоматически определяет, какой код вы используете, и создаёт/развёртывает его для вас. Вы можете найти более подробную информацию об этом, прочитав о Heroku Buildpacks. В моём случае всякий раз, когда я отправляю код в git remote Heroku, он устанавливает все мои пакеты, удаляет все зависимости разработки и развёртывает приложение, и это всё простой командой «git push heroku main».

Всего в несколько кликов CloudMQTT и Postgres стали доступны моему приложению, и мне нужно было использовать только несколько переменных среды, чтобы интегрировать эти сервисы с моим приложением. Heroku не потребовал денег. Однако CloudMQTT — сторонним дополнение за $5 в месяц.

Я считаю, что необходимость в Postgres не нуждается в комментариях, но CloudMQTT заслуживает больше внимания.

Из Интернета в частную сеть. Сложный способ


Есть несколько способов предоставить доступ к приложению или, в моём случае, устройству Интернета вещей. Первый — открыть порт в моей домашней сети, чтобы вывести устройство в Интернет. В этом случае моё приложение Heroku Express отправит запрос на моё устройство, используя публичный IP-адрес. Это потребовало бы от меня иметь публичный статический IP-адрес, а также статический IP-адрес для ESP32. ESP32 также должен был бы действовать как HTTP-сервер и всё время слушать инструкции от Heroku. Это большие накладные расходы для устройства, получающего инструкции несколько раз в день.

Второй способ называется «дырокол». С ним вы можете задействовать сторонний внешний сервер для доступа устройства в Интернет без необходимости переадресации портов. Ваше устройство в основном подключается к серверу, который устанавливает открытый порт. Затем другая служба может подключиться непосредственно к вашему внутреннему устройству, получив открытый порт от внешнего сервера. Наконец, он подключается непосредственно к устройству, используя этот открытый порт. Подход может быть правильным или не совсем правильным: я прочитал о нём только часть статьи.

Внутри «дырокола» происходит многое, и я не до конца понимаю происходящее. Однако, если вам интересно, есть несколько интересных статей, объясняющих больше. Вот две статьи, которые я прочитал, чтобы лучше понять «дырокол»: Википедия и статья из MIT, написанная Брайаном Фордом и другими.

Из Интернета в частную сеть через IoT


Я не был очень доволен этими решениями. Я подключил много смарт-устройств в дом, и мне никогда не приходилось открывать порт на маршрутизаторе, так что переадресации портов не было. Кроме того, пробивка дыр кажется гораздо более сложной, чем то, что я ищу, и лучше подходит для сетей P2P. В результате дальнейших исследований я обнаружил MQTT и узнал, что это протокол для IoT. Он обладает некоторыми преимуществами, такими как низкое энергопотребление, настраиваемая отказоустойчивость, и не требует переадресации портов. MQTT — протокол типа издатель/подписчик, это означает, что стол — подписчик определённого топика, а приложение Heroku — издатель этого топика.

Таким образом, Google связывается с Heroku, этот запрос анализируется, чтобы определить запрошенное устройство и его новое состояние или режим. Затем приложение Heroku публикует сообщение на сервере CloudMQTT, развёрнутом как надстройка на Heroku, с указанием столу перейти к новой предустановке. Наконец, стол подписывается на топик и получает сообщение, опубликованное приложением Heroku, наконец, стол настраивает свою высоту в соответствии с запросом! В файле googleactionsapp вы заметите, что есть функция updateHeight, которая публикует один номер в топике MQTT для определённого идентификатора устройства. Вот так приложение Heroku публикует в MQTT запрос на перемещение стола.

Последний шаг ё получение сообщения на ESP32 и перемещение стола. Я покажу некоторые основные моменты кода для стола ниже, а весь исходный код находится здесь.

void setup()
{
 Serial.begin(115200);
...
 tfminis.begin(&Serial2);
 tfminis.setFrameRate(0);

...

 state_machine = new StateMachine();
 state_machine->begin(*t_desk_height, UP_PWM_CHANNEL, DOWN_PWM_CHANNEL);

 setup_wifi();

 client.setServer(MQTT_SERVER_DOMAIN, MQTT_SERVER_PORT);
 client.setCallback(callback);
...
}

Когда стол загружается, мы сначала запускаем связь между TFMini-S — датчиком расстояния — чтобы получить текущую высоту стола. Затем мы настраиваем конечный автомат для движения стола. Конечный автомат получает команды через MQTT, а затем отвечает за согласование запроса пользователя с фактической высотой стола, считываемой датчиком расстояния. Наконец, мы подключаемся к сети Wi-Fi, подключаемся к серверу MQTT и настраиваем обратный вызов для любых данных, получаемых по теме MQTT, на которую мы подписаны. Ниже я покажу функцию обратного вызова.

void callback(char *topic, byte *message, unsigned int length)
{
 ...

 String messageTemp;

 for (int i = 0; i < length; i++)
 {
   messageTemp += (char)message[i];
 }

 if (messageTemp == "1") {
   state_machine->requestStateChange(ADJUST_TO_PRESET_1_HEIGHT_STATE);
 }

 if (messageTemp == "2") {
   state_machine->requestStateChange(ADJUST_TO_PRESET_2_HEIGHT_STATE);
 }

 if (messageTemp == "3") {
   state_machine->requestStateChange(ADJUST_TO_PRESET_3_HEIGHT_STATE);
 }
...
}

Конечный автомат регистрирует изменение состояния, полученное в теме MQTT. Затем он в основном цикле обрабатывает новое состояние.

void loop()
{
 if (!client.connected())
 {
   reconnect();
 }
 client.loop();
 state_machine->processCurrentState();
}

Основной цикл выполняет несколько задач: во-первых, он повторно подключается к серверу MQTT, если еще не подключён. Затем обрабатывает все данные, полученные через топик MQTT. Наконец, код отрабатывает, перемещая столешницу в нужное место, запрошенное в топике MQTT.

Вот и всё! Стол полностью управляется голосом и обменивается данными с Google для получения команд!

Последние заметки


Последняя конечная точка, о которой я не упоминал, — конечная точка / healthz. Это связано с тем, что Google ожидает, довольно быстрого ответа, и загрузка приложения Heroku при каждом запросе в моём случае не работает. Я установил службу ping для проверки связи с конечной точкой /healthz каждую минуту, чтобы служба оставалась работоспособной и была готова ответить. Если вы планируете сделать что-то подобное, помните, что на это будут израсходованы все бесплатные часы на стенде. Сейчас всё нормально: это единственное используемое на Heroku приложение. Кроме того, за 7 долларов в месяц вы можете перейти на тарифный план Heroku’s Hobby с поддержкой постоянной работы приложения.

Создание устройства IoT связано с большими накладными расходами в начале. Я сконструировал оборудование, построил схему управления, настроил сервер MQTT, написал сервер Express OAuth2 и научился взаимодействовать с Google Smart Home через действия. Первоначальные накладные расходы были огромными, но я чувствую, что многого добился! Не говоря уже о том, что сервер MQTT, сервер приложений Express OAuth2 и Google Smart Home Actions можно использовать для другого проекта. Умные дома мне интересны, и я могу попытаться расширить свой репертуар IoT-устройств, включив в него датчики, отслеживающие происходящее вокруг моего дома и сообщающее об этом через MQTT. Датчики для мониторинга почвы, температуры и датчики света будет очень интересно мониторить и анализировать.

Что дальше?


Высота столешницы сейчас измеряется в лучшем случае ненадёжно. Я использую в целом работающим инфракрасным датчиком расстояния TFMini-S. Замечено, что высота стола немного меняется в течение дня, когда меняется окружающее освещение в комнате. Я заказал датчик угла поворота, чтобы подсчитать обороты стержня, проходящего через стол. Это должно дать мне движения точнее в любое время дня. У меня также есть доступ к серверу, который я размещаю в подвале. На нём я могу исследовать собственный сервер Mosquitto MQTT, Node-RED и Express-приложения OAuth2, если захочу хостить что-то сам. Наконец, сейчас вся электроника лежит прямо на моём столе. Я планирую организовать устройства так, чтобы все было красиво и аккуратно!


image