После подачи питания ESP моргнёт синим огоньком, говоря о том что она включилась и зажжет красный индикатор питания. Если это произошло - перейдем к скетчу, если нет - проверьте правильность сборки и подаваемое питание. Зачастую ESP капризничает при подключении китайских блоков питания.
Далее нам нужно подключить USB to TTL конвертер к TX, RX и GND выводам ESP.
Не забываем что TX RX подключаются зеркально, т.е. TX ESP к RX конвертера и наоборот.
При загрузке скетча GPIO0 должен быть подключен к земле, это переводит ESP в режим загрузки.
Перейдем к скетчу.
Подключение библиотек:
extern "C" { //Это нужно для работы таймера
#include "user_interface.h"
}
#include <DHT.h> //Библиотека для работы с датчиком температуры/влажности
#include <SoftwareSerial.h> //Программный UART для MH-Z19
#include <ESP8266WiFi.h> //Работа с Wi-Fi
#include <WiFiUdp.h> //Работа с UDP пакетами
#include <EEPROM.h> //Эмуляция EEPROM на флешке для хранения статистики
#include <Wire.h> //I2C шина для подключения дисплея
#include "SSD1306.h" //Библиотека для работы с дисплеем
#define DHTPIN 13 //Говорим на какой ноге будет висеть сигнальная нога DHT22
Объявление переменных:
SoftwareSerial mySerial(14, 12); //Говорим библиотеке на каких ногах будет висеть программный UART
IPAddress ipServidor(192, 168, 1, 1); //Адрес шлюза (в моем случае роутера)
IPAddress ipCliente(192, 168, 1, 5); //Адрес нашего устройства
IPAddress Subnet(255, 255, 255, 0); //Подсеть
IPAddress broadcastIp(192, 168, 1, 255); //Адрес для широковещатльных сообщений
Здесь стоит отметить что стандартный broadcast адрес 255.255.255.255 на ESP'шке не работает.
WiFiUDP Udp; //Говорим что будем использовать UDP
SSD1306 display(0x3c, 4, 5); //Объявляем ноги на которых будет висеть дисплей
DHT dht(DHTPIN, DHT22); //Берем из define ногу для DHT22
byte cmd[9] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79}; // команда запроса данных у MH-Z19
Для запроса данных на скорости 9600 (8 bit, stop — 1, parity — none) нужно отправить следующие девять байт:
• 0xFF — начало любой команды
• 0x01 — первый сенсор (он всего один)
• 0x86 — команда
• 0x00, 0x00, 0x00, 0x00, 0x00 — данные
• 0x79 — контрольная сумма.
В ответ придет что-то такое:
• 0xFF — начало любого ответа
• 0x86 — команда
• 0x01, 0xC1 — старшее и младшее значение (256 * 0x01 + 0xC1 = 449)
• 0x3C, 0x04, 0x3C, 0xC1 — в документации сказано, что должно приходить что-то типа 0x47, 0x00, 0x00, 0x00, но на деле приходит непонятно что.
• 0x7B — контрольная сумма.
Контрольная сумма считается следующим образом: берутся 7 байт ответа (все кроме первого и последнего), складываются, инвертируются, увеличиваются на 1: 0x86 + 0x01… + 0xC1 = 0x85, 0x85 xor 0xFF = 0x7A, 0x7A + 1 = 0x7B.
Согласно документации сенсору требуется около трех минут, чтобы выйти на рабочий режим. Первое время после включения он будет выдавать или 5000ppm, или 400ppm. После особо усердной пайки может приходить в себя несколько часов.
(скопировал с гиктаймса у Hellsy22)
unsigned char response[9]; //Сюда пишем ответ MH-Z19
unsigned int ppm = 0; //Текущее значение уровня СО2
float hum; //Текущее значение уровня влажности
float temp; //Текущее значение уровня температуры
unsigned int coCurrentHour = 0; //Значение СО2 в текущем часе
unsigned int tempCurrentHour = 0; //Значение тмпературы в текущем часе
unsigned int humidityCurrentHour = 0; //Значение влажности в текущем часе
const char *ssid = "имя_вашей_сети_в_кавычках";
const char *password = "пароль_от_вашей_сети_в_кавычках";
unsigned int localPort = 1900; //Порт по которому мы будем рассылать данные
long t = 0; //Счётчик
int displayClk = 0; //Счётчик
Для того чтобы хранить статистику мы должны записывать данные за последние 24 часа по каждому параметру в ОЗУ и один раз в час обновлять значения в енергонезависимой памяти. Так как энергонезависимая память хранит данные побайтово, а у нас есть значения типа float и int которые занимают боьше одного байта, мы разбиваем каждое значение на старший и младший байт. При чтении из памяти производим обратную процедуру.
unsigned int coHour[250] = {0}; //Масив с суточной статистикой СО2
unsigned int tempHour[250] = {0}; //Масив с суточной статистикой температуры
unsigned int humidityHour[250] = {0}; //Масив с суточной статистикой влажностиbyte coHigh[26]; //Массив старших байтов значения СО2
byte coLow[26]; //Массив младших байтов значения СО2
byte tempHigh[26]; //Массив старших байтов значения температуры
byte tempLow[26]; //Массив младших байтов значения температуры
byte humidityHigh[26]; //Массив старших байтов значения влажности
byte humidityLow[26]; //Массив младших байтов значения влажности
Блок Setup это что-то вроде автозагрузки в ПК, весь хранящийся здесь код выполняется один раз при загрузке устройства.
void setup() {
Serial.begin(9600); //Запускаем аппаратный UART
mySerial.begin(9600); //Запускаем программный UART
WiFi.begin(ssid, password); //Задаем параметры работы Wi-Fi
WiFi.mode(WIFI_STA);
WiFi.config(ipCliente, ipServidor, Subnet);
Udp.begin(localPort); //Запускаем UDP
EEPROM.begin(512); //Задаем размерность энергонезависимой памяти
display.init(); //Инициализируем дисплей
display.flipScreenVertically(); //Переворачиваем координатную сетку дисплея
dht.begin(); //Подключаем датчик температуры и влажности
delay(2000); //Ждём
В случае с ESP8266 иногда нужно намеренно делать задержки в выполнении программы чтобы успел отработать Wi-Fi стек и все фоновые процессы
for (unsigned int h = 0; h != 24; h++) { //Читаем данные из EEPROM
coHigh[h] = EEPROM.read(h);
delay(5);
coLow[h] = EEPROM.read(h + 24);
delay(5);
tempLow[h] = EEPROM.read(h + 48);
delay(5);
tempHigh[h] = 0;
humidityLow[h] = EEPROM.read(h + 72);
delay(5);
humidityHigh[h] = 0;
coHour[h] = word(coHigh[h], coLow[h]); //Собираем данные из отдельных байтов
tempHour[h] = word(tempHigh[h], tempLow[h]);
humidityHour[h] = word(humidityHigh[h], humidityLow[h]);
Udp.beginPacket(broadcastIp, localPort); //Отправляем статистику за последние сутки в UART и в сеть всем желающим
Serial.print("C"); Serial.print(h); Serial.print(":"); Serial.print(coHour[h]); Serial.print("|"); Serial.print(tempHour[h]); Serial.print("|"); Serial.print(humidityHour[h]); Serial.println("|");
Udp.write("C"); Udp.print(h); Udp.write(":"); Udp.print(coHour[h]); Udp.write("|"); Udp.print(tempHour[h]); Udp.write("|"); Udp.print(humidityHour[h]); Udp.write("|");
Udp.endPacket();
}
}
WiFiClient client;
Блок Loop представляет собой бесконечный цикл
void loop()
{
display.clear(); //Готовим дисплей к выводу текста и говорим что всё ок, но мы ждем
display.setTextAlignment(TEXT_ALIGN_LEFT);
display.setFont(ArialMT_Plain_16);
display.drawString(0, 40, "LOADING....");
display.display();
delay(5000);
mySerial.write(cmd, 9); //Запрашиваем данные у MH-Z19
memset(response, 0, 9); //Чистим переменную от предыдущих значений
mySerial.readBytes(response, 9); //Записываем свежий ответ от MH-Z19
unsigned int i;
byte crc = 0;//Ниже магия контрольной суммы
for (i = 1; i < 8; i++) crc += response[i];
crc = 255 - crc;
crc++;
String stringBr;
float prevHum = hum;
float prevTemp = temp;
hum = dht.readHumidity(); //Получаем текущую влажность
temp = dht.readTemperature(); //Получаем текущуютемпературу
int intHum = hum; //Переводим значения в int для упрощения обработки
int intTemp = temp;
//Проверяем контрольную сумму и если она не сходится - перезагружаем модуль
if ( !(response[0] == 0xFF && response[1] == 0x86 && response[8] == crc) ) {
Serial.println("CRC error: " + String(crc) + " / " + String(response[8]));
ESP.restart();
}
else {
unsigned int responseHigh = (unsigned int) response[2];
unsigned int responseLow = (unsigned int) response[3];
ppm = (256 * responseHigh) + responseLow;
Serial.print("Time: " + String(t) + " sec\t" + "CO2: " + String(ppm) + " ppm\t"); //Выводим данные на UART для отладки
Udp.beginPacket(broadcastIp, localPort); //Отправляем данные в сеть
Udp.print(t); Udp.write("["); Udp.print(ppm); Udp.write("]");
if (isnan(hum) || isnan(temp)) { //Проверяем получили ли данные температуры и влажности
Serial.println(" Data reading error!"); //Если получена ошибка то отправляем предыдущее значение в сеть
Udp.write("["); Udp.print(prevHum); Udp.write("]"); Udp.write("["); Udp.print(prevTemp); Udp.write("]");
display.setTextAlignment(TEXT_ALIGN_LEFT);
display.setFont(ArialMT_Plain_16);
display.drawString(64, 20, "DHT Data Error!"); //Выводим сообщение об ошибке на дисплей
}
else
{
//Если всё ок - выводим данные на UART и отправляем в сеть
Serial.println(" Temperature: " + String(temp) + " *C " + "Humidity: " + String(hum) + " %\t");
Udp.write("["); Udp.print(hum); Udp.write("]"); Udp.write("["); Udp.print(temp); Udp.write("]");
tempCurrentHour = (tempCurrentHour + intTemp) / 2;
humidityCurrentHour = (humidityCurrentHour + intHum) / 2;
}
Udp.endPacket();
delay(100);
}
//Считаем статистику
t = t + 2;
coCurrentHour = (coCurrentHour + ppm) / 2;
if (t == 720) {
//Если пришло время обновлять статистику, запускаем цикл и сдвигаем все значения на одно
for (unsigned int d = 0; d != 24; d++) {
if (d == 23) {
coHour[d] = coCurrentHour;
tempHour[d] = tempCurrentHour;
humidityHour[d] = humidityCurrentHour;
Serial.print("C"); Serial.print(String(d)); Serial.print(":"); Serial.print(String(coHour[d])); Serial.print("|"); Serial.print(String(tempHour[d])); Serial.print("|"); Serial.print(String(humidityHour[d])); Serial.println("|");
Udp.beginPacket(broadcastIp, localPort);
Udp.write("C"); Udp.print(d); Udp.write(":"); Udp.print(coHour[d]); Udp.write("|"); Udp.print(tempHour[d]); Udp.write("|"); Udp.print(humidityHour[d]); Udp.write("|");
Udp.endPacket();
delay(100);
coCurrentHour = ppm;
tempCurrentHour = intTemp;
humidityCurrentHour = intHum;
t = 0;
}
else {
coHour[d] = coHour[d + 1];
tempHour[d] = tempHour[d + 1];
humidityHour[d] = humidityHour[d + 1];
Serial.print("C"); Serial.print(String(d)); Serial.print(":"); Serial.print(String(coHour[d])); Serial.print("|"); Serial.print(String(tempHour[d])); Serial.print("|"); Serial.print(String(humidityHour[d])); Serial.println("|");
Udp.beginPacket(broadcastIp, localPort);
Udp.write("C"); Udp.print(d); Udp.write(":"); Udp.print(coHour[d]); Udp.write("|"); Udp.print(tempHour[d]); Udp.write("|"); Udp.print(humidityHour[d]); Udp.write("|");
Udp.endPacket();
delay(100);
}
coHigh[d] = highByte(coHour[d]);
coLow[d] = lowByte(coHour[d]);
tempHigh[d] = highByte(tempHour[d]);
tempLow[d] = lowByte(tempHour[d]);
humidityHigh[d] = highByte(humidityHour[d]);
humidityLow[d] = lowByte(humidityHour[d]);
//Записываем обновленные данные в EEPROM
EEPROM.write(d, coHigh[d]);
delay(50);
EEPROM.write(d + 24, coLow[d]);
delay(50);
EEPROM.write(d + 48, tempLow[d]);
delay(50);
EEPROM.write(d + 72, humidityLow[d]);
delay(50);
EEPROM.commit();
}
}
//Проверяем значение счётчика и выводим на дисплей текущие показания
if (displayClk == 1) {
display.clear();
display.setFont(ArialMT_Plain_24);
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.drawString(64, 0, "CO2");
display.drawString(64, 35, String(ppm) + " ppm");
}
if (displayClk == 2) {
display.clear();
display.setFont(ArialMT_Plain_24);
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.drawString(64, 0, "Temp");
display.drawString(64, 35, String(temp) + " °C");
}
if (displayClk == 3) {
display.clear();
display.setFont(ArialMT_Plain_24);
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.drawString(64, 0, "Humidity");
display.drawString(64, 35, String(hum) + " %");
displayClk = 0;
}
display.display();
На этом пожалуй и закончу пост. Код можно скопировать загрузить в ESP, я ничего не вырезал.
Если у кого-то есть Crestron, в следующем посте могу описать модуль который парсит данные на стороне контроллера.
Если у кого-то будут предложения по оптимизации скетча - с радостью их выслушаю. В комментариях пишите какое еще устройство вы хотели бы увидеть и предлагайте варианты корпусов для текущего (чукча не творческий).