# Как построить надежную IoT-систему на ESP32 и Raspberry Pi?

Источник: https://www.youtube.com/watch?v=eidD14dXW8s
Канал: freeCodeCamp.org
Опубликовано: 02.01.2025

---

Эбенезер Асабре, аспирант Массачусетского университета в Амхерсте, представил подробное практическое руководство по созданию комплексной системы автоматизации дома на базе интернета вещей (IoT). Проект объединяет возможности микроконтроллера ESP32, выполняющего роль клиента для сбора данных и управления актуаторами, и одноплатного компьютера Raspberry Pi, который выступает в качестве центрального сервера. Результатом работы является полноценная отказоустойчивая система с веб-интерфейсом для мониторинга показателей в реальном времени и анализа исторических данных из базы данных.

## 🌐 Архитектура и возможности умного дома
[[JUMP:0:00]]

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

* Температура
* Атмосферное давление
* Качество воздуха
* Интенсивность освещения

По мнению Асабре, протокол MQTT идеально подходит для подобных задач, поскольку он является легковесным и разработан специально для условий с низкой пропускной способностью каналов связи и высокой задержкой сети. Это делает его чрезвычайно популярным в современных IoT-платформах. В рамках данного проекта Raspberry Pi 3 Model B выполняет двойную функцию: работает как центральный MQTT-брокер и одновременно хостит бэкенд-сервер на Node.js.



Информационный обмен построен на двунаправленной связи. Каждые 5 секунд ESP32 отправляет актуальные показания датчиков на сервер. Веб-клиент, используя библиотеку Socket.io, мгновенно отображает эти данные на интерактивных графиках в реальном времени. Кроме того, пользователь может управлять актуаторами прямо из браузера: переключение интерактивных тумблеров отправляет команду обратно на ESP32. Переключатель качества воздуха привязан к сервомотору, а датчики температуры и давления управляют соответствующими светодиодами. Все поступающие данные усредняются и сохраняются в реляционной базе данных PostgreSQL, что позволяет пользователю в любой момент запрашивать исторические срезы за определенные временные интервалы.

## 🛠️ Настройка аппаратного обеспечения и прошивки ESP32
[[JUMP:1:47]]

Аппаратная часть проекта отличается простотой и доступностью для повторения. Для реализации клиентского узла используется плата ESP32. Автор отмечает, что для серверной части подойдет любая модель Raspberry Pi, хотя сам он использует версию 3B. Разработка ПО для микроконтроллера осуществляется в среде Arduino IDE.

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

1.  Открыть Меню `Инструменты` -> `Менеджер плат`, найти и установить пакет поддержки **ESP32**.
2.  Перейти в `Управление библиотеками` и установить библиотеку **PubSubClient** для работы по протоколу MQTT.
3.  Там же найти и добавить библиотеку **ESP32Servo** для точного управления сервоприводом.

В начале исходного кода прошивки подключаются заголовочные файлы **WiFi.h**, **PubSubClient.h**, **HttpClient.h** и **ESP32Servo.h**. Далее задаются учетные данные Wi-Fi-сети и IP-адрес MQTT-брокера, который определяется на Raspberry Pi с помощью команды `ifconfig` в терминале. 

Прошивка содержит функцию `setup_wifi()`, которая удерживает устройство в цикле до тех пор, пока статус подключения не примет значение `WL_CONNECTED`. Поскольку беспроводное соединение может быть нестабильным, Асабре предусмотрел функцию `reconnect()`. Она циклически пытается восстановить подключение к брокеру каждые 5 секунд и автоматически подписывается на командный топик `esp/cmd` после успешной авторизации.

Обработка входящих команд от сервера реализована через callback-функцию. Прилетающий массив байтов (payload) конвертируется в строку формата `имя_датчика,команда`. Функция `parseString()` разделяет эту строку по индексу запятой, извлекая имя датчика и целевое состояние пина (0 или 1). Конструкция `if-else` проверяет имя: если это температура, вызывается метод `digitalWrite()` для отправки сигнала HIGH или LOW на соответствующий пин. Если же команда предназначена для датчика воздуха, вызывается кастомная функция `runServo()`, которая поворачивает вал сервомотора на 180° при включении и возвращает в положение 0° при выключении. Имитация сбора телеметрии реализована генерацией случайных чисел типа `float`, которые упаковываются в строку и публикуются в топик `home/sensors/data` каждые 5 секунд.

## 💻 Развертывание серверной инфраструктуры: Mosquitto и PostgreSQL
[[JUMP:21:36]]

Автор демонстрирует процесс настройки сервера на базе macOS, подчеркивая, что для Linux-систем команды аналогичны, а пользователи Windows могут использовать официальные инсталляторы. Для управления пакетами применяется менеджер Homebrew.

Установка и базовая конфигурация брокера сообщений выполняются с помощью команды `brew install mosquito`. После успешного завершения процесса требуется отредактировать конфигурационный файл **mosquitto.conf** для обеспечения сетевой доступности:

* Разрешить анонимные подключения, удалив или закомментировав ограничение `allow_anonymous`.
* Добавить слушатель на стандартный порт: `listener 1883 0.0.0.0`. IP-адрес `0.0.0.0` указывает, что брокер обязан принимать входящие соединения от любых устройств в локальной домашней сети.

После этого служба Mosquitto перезапускается. СУБД разворачивается командой `brew install postgresql`.



Разработка серверного приложения начинается с инициализации проекта Node.js командой `npm init`. Для сборки бэкенда Асабре устанавливает стек необходимых npm-пакетов:

* **express** — для организации HTTP-маршрутизации.
* **http** — для создания базового сервера.
* **pg** — официальный клиент для интеграции с PostgreSQL.
* **socket.io** — для реализации постоянного двунаправленного веб-сокета.
* **mqtt** — для взаимодействия с брокером сообщений Mosquitto.

Проект организуется по строгой модульной структуре. В корне создается файл **index.js** (точка входа) и директория **public** для статических веб-ресурсов. Бизнес-логика выносится в папку **source**, разделенную на подкаталоги: **config** (параметры подключения), **models** (SQL-запросы и структура), **services** (промежуточная логика), **controllers** (обработка HTTP-запросов) и **routes** (эндпоинты API).

## 🗄️ Проектирование слоя данных и бизнес-логики (MVC)
[[JUMP:28:24]]

В файле **source/config/db.config.js** описывается и экспортируется объект с конфигурацией подключения к СУБД. В нем жестко задаются имя пользователя (`postgresql`), хост, порт (`5432`), имя базы данных (`home`) и пароль. В файле **source/models/db.js** создается экземпляр пула соединений `new Pool()` из пакета **pg**, который экспортируется для дальнейшего использования.

Файл **sensors.model.js** инкапсулирует в себе схему данных и методы прямого взаимодействия с таблицами. При запуске приложения автоматически триггерится асинхронная функция `createSensorDataTable()`, выполняющая SQL-запрос `CREATE TABLE IF NOT EXISTS sensor_data`. Таблица содержит следующие поля:

* `id` — первичный ключ с автоинкрементом (`PRIMARY KEY`).
* `timestamp` — временная метка со значением по умолчанию `DEFAULT NOW()`.
* `temperature`, `pressure`, `air_quality`, `light_intensity` — числовые поля типа `NUMERIC` для хранения физических показателей.

Для выборки всей имеющейся информации создается функция `getAllSensorData()`, отправляющая запрос `SELECT

*   FROM sensor_data`. Поиск конкретной записи по идентификатору реализуется через параметризированный запрос `SELECT
*   FROM sensor_data WHERE id = $1`.

Особое внимание уделено функции выборки данных за диапазон времени `getSensorDataWithinRange(time)`. Она принимает объект с параметрами `timeStart` и `timeEnd` в часах и строит SQL-запрос с фильтрацией по временному интервалу: `WHERE timestamp BETWEEN NOW() - INTERVAL '$1 hours' AND NOW() - INTERVAL '$2 hours'`. Также в модели определены функции создания записи `createSensorData(data)` через `INSERT INTO` и удаления `deleteSensorData(id)`.

Слой сервисов (**sensors.services.js**) выступает абстракцией над моделями, напрямую транслируя вызовы к методам `sensorModel`. Контроллеры (**sensors.controller.js**) берут на себя обработку объектов `request` и `response`. Например, метод `getAllSensorData` асинхронно ждет массив данных от сервиса и отправляет клиенту статус `200 OK` вместе с телом JSON. В случае сбоя возвращается ошибка со статусом `500`. Завершает построение MVC-архитектуры файл **sensors.routes.js**, который маппит HTTP-методы (GET, POST, DELETE) и URL-пути на соответствующие методы контроллеров.

## ⚡ Серверное ядро: Интеграция MQTT и реального времени через Socket.io
[[JUMP:48:04]]

Файл **index.js** связывает воедино все компоненты системы. Для обеспечения возможности рендеринга динамических данных на стороне сервера Асабре доустанавливает пакет шаблонизатора **EJS** (`npm install ejs`). В коде инициализируется Express-приложение, настраивается HTTP-сервер, а также создается инстанс Socket.io, привязанный к этому серверу. Настройки приложения включают установку движка представлений `view engine` в режим `ejs` и определение статической папки `public` для отдачи HTML-страниц.

Для предотвращения переполнения базы данных частыми записями (каждые 5 секунд) автор внедряет промежуточный буфер данных `let sensorBuffer = []`. Логика работы с буфером устроена следующим образом:

1.  Приложение подключается к локальному брокеру по адресу `mqtt://127.0.0.1:1883` (или IP вашей платы Raspberry) и подписывается на топик `home/sensors/data`.
2.  При получении сообщения срабатывает событие `MQTT client.on('message')`, которое переводит буфер байтов в строку и парсит её в валидный JavaScript-объект функцией `convertPayloadStringToObject()`.
3.  Полученный объект мгновенно транслируется всем подключенным веб-клиентам через веб-сокет командой `io.emit('sensor data', sensorData)`.
4.  Затем объект добавляется в `sensorBuffer` с помощью метода `push()`.
5.  Поскольку данные приходят каждые 5 секунд, за 5 минут накапливается ровно 60 образцов телеметрии. При достижении `sensorBuffer.length >= 60` запускается процедура усреднения показателей.
6.  С помощью встроенной функции `reduce()` вычисляется среднее арифметическое для температуры, давления, качества воздуха и освещенности. Результаты округляются до двух знаков после запятой с помощью `.toFixed(2)`, формируется итоговый объект и записывается в PostgreSQL через метод модели `createSensorData()`. После успешной транзакции массив `sensorBuffer` полностью очищается.

Помимо приема данных, сервер обрабатывает входящие события от веб-интерфейса через сокет-соединение `io.on('connection')`. Когда пользователь переключает тумблер на сайте, фронтенд генерирует событие `checkbox data`. Сервер перехватывает его и перенаправляет команду в MQTT-топик `esp/cmd` для физического переключения реле или светодиодов на ESP32. Аналогично обрабатывается событие `search time range`: сервер принимает от клиента интервал, делает асинхронный запрос к БД и возвращает исторический массив через сокет-канал `received range`.

## 📊 Разработка клиентской части: Интерактивные графики и отчеты
[[JUMP:1:04:42]]

Фронтенд-часть состоит из двух ключевых файлов, размещенных в папке **public**: **graph.html** (страница мониторинга) и **detail.ejs** (страница отчетов). Для визуального оформления используется легковесный CSS-фреймворк **W3CSS**, предоставляющий готовые адаптивные стили. 

На главной странице мониторинга размещены четыре информационных блока, окрашенных в разные цвета, каждый из которых выделен под конкретный тип датчика. Ниже находятся контейнеры для графиков. Построение интерактивных графиков реализовано посредством JS-библиотеки **Plotly**, которая значительно упрощает рендеринг холста. Для каждого графика задаются оси: по оси X откладывается текущее системное время, по оси Y — пришедшее значение физической величины.

Динамическое обновление интерфейса обеспечивает функция `updatePlot(data)`. Чтобы графики оставались читаемыми и не перегружали браузер, разработчик устанавливает жесткий лимит — на экране одновременно отображается не более 20 точек. Как только количество точек превышает этот порог, график начинает плавно смещаться влево, удаляя старые значения.

Интерактивное управление реализовано через обработчик изменения состояния чекбоксов `handleCheckboxChange()`. При клике на тумблер генерируется событие, отправляющее сокет-сообщение на бэкенд. Слушатель `socket.on('sensor data')` непрерывно ожидает новые пакеты от сервера, обновляя текстовые поля элементов по их уникальным `ID` и перерисовывая графики Plotly.

Второй файл, **detail.ejs**, отвечает за вывод табличной информации. Сверху страницы расположены переключатели (radio buttons), позволяющие выбрать гранулярность исторических данных: минуты, часы, дни или годы. Рядом находится текстовое поле ввода диапазона и кнопка поиска. При нажатии на кнопку кастомный скрипт считывает введенную пользователем строку (например, `1,5`), разбивает её методом `split()` на две переменные — `timeStart` и `timeEnd` — и отсылает этот объект на сервер по каналу `search time range`. Сервер возвращает массив строк, который перебирается циклом во фронтенд-функции `addSensorContent()`. Скрипт динамически генерирует новые HTML-элементы строк таблицы `<tr>` и ячеек `<td>`, наполняет их данными (временная метка, температура, давление, качество воздуха) и вставляет в тело таблицы `<tbody>`, формируя наглядный отчет.