Подробный разбор датчика качества воздуха на базе ESP8266

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

http://pikabu.ru/story/universalnyiy_datchik_s_esp8288_datch...


Итак, для начала Вам понадобится:

1. ESP8266 Esp-07 - 120 руб

2. Плата-адаптер для ESP - 11 руб

3. Линейный стабилизатор напряжения на 3.3В. В моем случае XC6206 - 2.5 руб

4. Датчик СО2 MH-Z19 - 1500 руб

5. Датчик температуры и влажности DHT22 - 130 руб

6. I2C монохромный дисплей SSD1306 - 180 руб

7. Монтажная плата - 10 руб

8. Резистор 10 КОм - 0.5 руб

Итого: 1954 руб


Цены указаны ориентировочные, состоянием на декабрь-январь 2017 г.


Собираем всё в соответствии со схемой.

Подробный разбор датчика качества воздуха на базе ESP8266 Esp8266, Arduino, Mh-z19 arduino, Своими руками, Умный дом, Автоматизация, Smart home, Длиннопост

Между первым и вторым выводами DHT22 необходимо припаять резистор на 10 КОм. Без него скетч будет выдавать ошибку.

Обязательно обратите внимание на то что ESP8266 питается от 3.3В, а в качестве цепи питания схема использует 5В, для того чтобы ESP не сгрела, необходимо впаять на обратной стороне платы адаптера, линейный стабилизатор на 3.3В и выпаять перемычку на передней части. То что нужно сдлать отмечено на картинке ниже.

Некоторые могут заметить что неплохо было бы поставить делители на входах ESP, однако как показала практика, всё работает стабильно и в оде экспериментов ни одна ESP'шка не пострадала)

Подробный разбор датчика качества воздуха на базе ESP8266 Esp8266, Arduino, Mh-z19 arduino, Своими руками, Умный дом, Автоматизация, Smart home, Длиннопост

После подачи питания 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, в следующем посте могу описать модуль который парсит данные на стороне контроллера.

Если у кого-то будут предложения по оптимизации скетча - с радостью их выслушаю. В комментариях пишите какое еще устройство вы хотели бы увидеть и предлагайте варианты корпусов для текущего (чукча не творческий).