Введение
DHT11/DHT22 — одни из самых распространённых, доступных датчиков температуры и влажности для Arduino-проектов. В статье разберём подключение датчика, написание прошивки с использованием готовой библиотеки для чтения показаний, и приём этих данных на стороне Qt-приложения с отображением в простом интерфейсе.
Концепция
DHT-датчики используют собственный однопроводной протокол связи с платой Arduino (не стандартный I2C/SPI), что требует специальной библиотеки для разбора временных интервалов сигнала (готовые библиотеки, такие как DHT sensor library от Adafruit, скрывают эту сложность за простым программным интерфейсом). Расширяя протокол, добавим новый тип сообщения для передачи пары значений (температура и влажность) в одной строке, и соответствующий парсер на стороне Qt, преобразующий текстовую строку в структурированные данные для отображения.
Пример кода
// firmware/src/main.cpp — прошивка с чтением DHT22 и периодической отправкой показаний
#include <DHT.h>
#define DHT_PIN 2
#define DHT_TYPE DHT22
DHT dht(DHT_PIN, DHT_TYPE);
void setup()
{
Serial.begin(9600);
dht.begin();
delay(100);
Serial.println("READY");
}
void loop()
{
static unsigned long lastReadTime = 0;
// DHT22 физически не может выдавать показания чаще, чем раз в ~2 секунды —
// более частый опрос просто вернёт некорректные данные
if (millis() - lastReadTime >= 2000) {
float humidity = dht.readHumidity();
float temperature = dht.readTemperature();
if (isnan(humidity) || isnan(temperature)) {
Serial.println("ERROR:DHT_READ_FAILED");
} else {
Serial.print("DHT:");
Serial.print(temperature);
Serial.print(",");
Serial.println(humidity);
}
lastReadTime = millis();
}
}
// dhtdatareceiver.h — разбор показаний DHT на стороне Qt
#pragma once
#include <QObject>
#include "arduinoconnection.h"
struct DhtReading
{
double temperatureCelsius;
double humidityPercent;
};
class DhtDataReceiver : public QObject
{
Q_OBJECT
public:
explicit DhtDataReceiver(ArduinoConnection *connection, QObject *parent = nullptr)
: QObject(parent)
{
connect(connection, &ArduinoConnection::lineReceived, this, &DhtDataReceiver::onLineReceived);
}
private slots:
void onLineReceived(const QString &line)
{
if (!line.startsWith("DHT:")) {
if (line == "ERROR:DHT_READ_FAILED") {
emit readError();
}
return;
}
const QString payload = line.mid(4); // отрезаем префикс "DHT:"
const QStringList parts = payload.split(',');
if (parts.size() != 2) {
qWarning() << "Некорректный формат показаний DHT:" << line;
return;
}
bool temperatureOk = false, humidityOk = false;
const double temperature = parts[0].toDouble(&temperatureOk);
const double humidity = parts[1].toDouble(&humidityOk);
if (!temperatureOk || !humidityOk) {
qWarning() << "Не удалось разобрать числовые значения из показаний DHT:" << line;
return;
}
emit readingReceived(DhtReading{temperature, humidity});
}
signals:
void readingReceived(const DhtReading &reading);
void readError();
};
// Использование в простом интерфейсе отображения показаний
auto *receiver = new DhtDataReceiver(arduinoConnection, this);
connect(receiver, &DhtDataReceiver::readingReceived, this, [this](const DhtReading &reading) {
temperatureLabel->setText(QString("Температура: %1 °C").arg(reading.temperatureCelsius, 0, 'f', 1));
humidityLabel->setText(QString("Влажность: %1 %").arg(reading.humidityPercent, 0, 'f', 1));
});
Пояснения к коду
Прошивка использует готовую библиотеку DHT.h, инициализированную с указанием пина подключения и конкретной модели датчика (DHT22, отличающейся от DHT11 точностью и поддерживаемым диапазоном) — dht.readHumidity()/dht.readTemperature() скрывают всю сложность разбора протокола датчика за простым вызовом. Проверка isnan() обязательна, поскольку при ошибке чтения (датчик физически не подключён, повреждённый провод, слишком частый опрос) библиотека возвращает специальное значение NaN («не число»), которое нужно явно отличать от валидного показания перед отправкой данных в Qt.
DhtDataReceiver на стороне Qt разбирает строку формата "DHT:23.50,45.20" — отрезает известный префикс, разделяет оставшуюся часть по запятой и преобразует обе части в double через toDouble() с явной проверкой успешности преобразования (через выходной параметр bool). Это защищает от ситуации, когда из-за повреждения данных при передаче или рассинхронизации протокола в поле, ожидаемом как число, окажется нечисловой текст — без такой проверки toDouble() молча вернул бы 0.0 для некорректного ввода, что было бы неотличимо от настоящего нулевого показания температуры.
Подводные камни
- Слишком частый опрос DHT-датчика чаще его физического минимального интервала между измерениями (около 2 секунд для DHT22, иногда больше для DHT11) приводит к регулярным ошибкам чтения (
isnan) — это не недостаток конкретной реализации кода, а аппаратное ограничение самого датчика, которое нужно явно учитывать при проектировании частоты опроса в прошивке. - Использование запятой как разделителя десятичной части числа в зависимости от локали при разборе на стороне Qt. Прошивка Arduino всегда выводит числа с точкой как разделитель десятичной части (стандартное поведение
Serial.print(float)), ноQString::toDouble()без явного указания локали может интерпретировать число согласно текущей системной локали Qt-приложения, где в некоторых локалях (включая русскую) десятичным разделителем ожидается запятая, а не точка — для надёжного разбора чисел, присылаемых Arduino, стоит явно использоватьQLocale::c().toDouble(...)вместо локале-зависимогоtoDouble(). - Физическое расположение датчика, влияющее на точность измерений (близость к источникам тепла, прямой солнечный свет, недостаточная вентиляция) — это не программная, а аппаратная проблема, но она часто проявляется как «необъяснимые» странности в данных, которые разработчик может ошибочно начать искать в коде разбора протокола, тогда как реальная причина — физическое расположение самого датчика.
- Отсутствие фильтрации или усреднения единичных аномальных показаний. Даже исправно работающий датчик иногда выдаёт случайные, явно аномальные единичные значения (резкий скачок на несколько градусов на одно измерение без физической причины) — для более стабильного отображения показаний (особенно при построении графика в реальном времени, как в следующих статьях цикла) часто применяется простое скользящее усреднение нескольких последних показаний вместо прямого отображения каждого отдельного, потенциально шумного измерения.