Утром форточка закрыта, солнце поднимается, а вы заняты на другом конце участка. Хочется, чтобы теплица проветривалась по температуре. Начнём с простого опыта: датчик DHT22, ESP32 и небольшой позиционный сервопривод. На столе будет видно, как изменение показаний превращается в движение.
Это лабораторная сборка для знакомства с автоматикой. Перед переносом на настоящую форточку подбирают питание, тягу и защиту от отказов. Проценты в программе задают положение привода; реальное положение створки проверяют отдельно. Начнём со снятой тяги, чтобы спокойно разобраться с направлением и ходом.
Что положить на стол
- ESP32 DevKit на классическом ESP32; номера выводов нужно сверить со своей платой.
- DHT22 и библиотека DHT sensor library от Adafruit. SHT31 не является программной заменой: ему нужны другая библиотека и подключение.
- Позиционный RC-сервопривод, подходящий по усилию, ходу, питанию и уровню управляющего сигнала. Модель выбирают по механике, а не только по массе створки.
- Отдельное питание сервопривода, рассчитанное в том числе на пуск и заклинивание, защита цепи питания, клеммы и провода соответствующего сечения.
- Тяга, механические ограничители, возможность ручного освобождения створки и защищённый корпус электроники.
Пример рассчитан на позиционный RC-сервопривод. Для линейного привода на 12 В понадобится другой узел: драйвер направления, концевики и ограничитель времени движения. Выберите тип механики до покупки деталей — от него зависит и схема управления.
Подключаем датчик и привод
ESP32 3V3 → DHT22 VCC
ESP32 GND → DHT22 GND
ESP32 GPIO4 → DHT22 DATA
Подтяжка DATA → 3V3, по документации конкретного датчика/модуля
ESP32 GPIO18 → управляющий вход совместимого сервопривода
Отдельный источник питания → питание сервопривода
GND источника сервопривода → ESP32 GND
Переключатель «Открыть» → между GPIO25 и GND
Кнопка «Подтвердить восстановление» → между GPIO27 и GND
Плату ESP32 на стенде можно питать от USB. Не подавайте питание сервопривода на её вывод 3V3 и не соединяйте положительные выходы независимых источников. Если вход сервопривода не совместим с логикой 3,3 В, нужен согласователь уровня. Для DHT22 не подтягивайте DATA к 5 В.
Первое включение сделайте со снятой тягой. Посмотрите, куда поворачивается вал при запуске: библиотека может выдать начальный импульс ещё до первой команды. Затем подберите диапазон импульсов и крайние углы для своего привода.
Почему форточка не должна дёргаться на каждом измерении
Для первого опыта заданы такие точки: при 23 °C и ниже — закрыто, от 23 до 30 °C открытие растёт от 0 до 100%, при 30 °C — полностью открыто. У 35 °C есть отдельная приоритетная проверка. Так удобно проследить работу алгоритма; рабочие температуры потом выбирают под культуру и режим теплицы.
Попробуйте медленно менять температуру. Программа сравнивает её с показанием, при котором последний раз меняла цель. Накопился 1 °C — пора передвинуть привод. Так маленькие шаги складываются в заметное изменение, а лёгкие колебания возле одной температуры оставляют вал в покое.
При ошибке датчика контроллер запоминает неисправность и задаёт открытие на 100%. Проверьте этот переход на столе. Для настоящей теплицы аварийное положение выбирают с учётом мороза и ветра; вместе с ним предусматривают независимую защиту и возможность освободить створку вручную.
После запуска или ошибки дождитесь допустимого показания и нажмите подтверждение. Это ваш сигнал контроллеру: причину проверили, можно продолжать. Переключатель ручного открытия позволяет отдельно испытать полный ход. Температурный алгоритм работает одинаково днём и ночью.
Загружаем скетч
Для воспроизводимой сборки: Arduino-ESP32 3.3.12, ESP32Servo 3.2.1, DHT sensor library 1.4.7 и Adafruit Unified Sensor 1.1.15. Выберите плату ESP32 Dev Module. Перед подключением механики настройте CLOSED_ANGLE, OPEN_ANGLE, диапазон импульсов и FAULT_PERCENT под свой стенд.
#include <Arduino.h>
#include <DHT.h>
#include <ESP32Servo.h>
#include <math.h>
constexpr int DHT_PIN = 4, SERVO_PIN = 18;
constexpr int MANUAL_OPEN_PIN = 25, FAULT_RESET_PIN = 27;
constexpr int CLOSED_ANGLE = 20, OPEN_ANGLE = 120;
constexpr int FAULT_PERCENT = 100; // Выбрать по анализу отказов конкретной теплицы.
constexpr float T_CLOSED = 23.0f, T_OPEN = 30.0f;
constexpr float T_EMERGENCY = 35.0f, HYST = 1.0f;
constexpr uint32_t SAMPLE_MS = 10000, STEP_MS = 40;
DHT dht(DHT_PIN, DHT22);
Servo vent;
int commanded = FAULT_PERCENT, target = FAULT_PERCENT;
float acceptedTemp = NAN;
bool fault = true, valid = false, haveSample = false;
float temperature = NAN;
uint32_t sampledAt = 0, movedAt = 0;
int targetFor(float t) {
if (t <= T_CLOSED) return 0;
if (t >= T_OPEN) return 100;
return int((t - T_CLOSED) * 100.0f / (T_OPEN - T_CLOSED) + 0.5f);
}
int angleFor(int percent) {
return CLOSED_ANGLE + (OPEN_ANGLE - CLOSED_ANGLE) * percent / 100;
}
void acceptReading(float t, float h) {
haveSample = true;
valid = isfinite(t) && isfinite(h) && t >= -40 && t <= 80 && h >= 0 && h <= 100;
if (!valid) {
fault = true;
acceptedTemp = NAN;
Serial.println("FAULT: sensor; inspect and press RESET button");
return;
}
temperature = t;
// fault остаётся установленным до ручного подтверждения.
}
void decide(bool manualOpen, bool resetPressed) {
if (resetPressed && haveSample && valid) {
fault = false;
acceptedTemp = NAN;
}
if (manualOpen) { target = 100; acceptedTemp = NAN; return; }
if (fault) { target = FAULT_PERCENT; return; }
if (temperature >= T_EMERGENCY) {
target = 100; acceptedTemp = temperature; return;
}
// Сравниваем с температурой последнего принятого решения, а не прошлого опроса.
if (!isfinite(acceptedTemp) || fabsf(temperature - acceptedTemp) >= HYST ||
temperature <= T_CLOSED || temperature >= T_OPEN) {
target = targetFor(temperature);
acceptedTemp = temperature;
}
}
void moveOneStep(uint32_t now) {
if (uint32_t(now - movedAt) < STEP_MS || commanded == target) return;
movedAt = now;
if (commanded < target) commanded = min(commanded + 2, target);
else commanded = max(commanded - 2, target);
vent.write(angleFor(commanded));
}
void setup() {
Serial.begin(115200);
pinMode(MANUAL_OPEN_PIN, INPUT_PULLUP);
pinMode(FAULT_RESET_PIN, INPUT_PULLUP);
dht.begin();
vent.setPeriodHertz(50);
vent.attach(SERVO_PIN, 500, 2400);
if (!vent.attached()) {
Serial.println("FAULT: servo attach failed; manual intervention required");
while (true) delay(1000);
}
vent.write(angleFor(commanded)); // Первая команда действительно отправляется.
movedAt = millis();
}
void loop() {
const uint32_t now = millis();
if (!haveSample || uint32_t(now - sampledAt) >= SAMPLE_MS) {
sampledAt = now;
acceptReading(dht.readTemperature(), dht.readHumidity());
Serial.printf("T=%.1f valid=%d fault=%d\n", temperature, valid, fault);
}
static bool previousReset = false;
bool reset = digitalRead(FAULT_RESET_PIN) == LOW;
decide(digitalRead(MANUAL_OPEN_PIN) == LOW, reset && !previousReset);
previousReset = reset;
moveOneStep(now);
}
Датчик читается раз в десять секунд. Пока вы наблюдаете за приводом, откройте последовательный порт: туда выводятся ошибки. Плавное движение организовано по таймеру, поэтому программа продолжает выполнять остальные действия между изменениями команды.
Первые опыты с форточкой
- Без тяги проверить запуск, направление движения и крайние положения. Убедиться, что сброс питания не вызывает недопустимого перемещения.
- Подать последовательность температур с небольшим шагом вверх и вниз. Проверить достижение полного открытия и закрытия, а также подавление малых колебаний.
- Отключить датчик: ошибка должна зафиксироваться. После восстановления показаний движение по температуре должно возобновиться только после подтверждения.
- Проверить переключатель ручного открытия и возврат к автоматическому режиму.
- С подключённой механикой проверить ток, нагрев, ограничители и возможность ручного освобождения створки. Программа без обратной связи не обнаруживает заклинивание.
- Проверить сценарии холода, жары, ветра и пропадания питания. Для хозяйства предусмотреть независимую защиту и оповещение, соответствующие последствиям отказа.
Когда поведение на столе стало понятным, подключите механику и повторите опыты. Особенно интересно проверить медленный нагрев: форточка должна постепенно доходить до нужного положения, а не ждать резкого скачка. Запишите, где появляются люфт, нагрузка и задержка, — эти наблюдения помогут довести конструкцию.
Что попробовать дальше
Интерфейс управления приводом описан в документации ESP32Servo, чтение DHT22 — в библиотеке Adafruit DHT, настройка кнопок — в документации GPIO Arduino-ESP32.
Измерение температуры и влажности в теплице поможет выбрать место датчика и увидеть, как меняются условия после проветривания.
Полив с учётом прогноза погоды — следующий проект, если захочется связать наблюдения за теплицей с другими задачами.





