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

Как известно, датчиков у Даджет два — один подключаемый проводом к компьютеру, а к другому можно подключить контроллер(как было сделано тут) для считывания показаний. Меня больше интересовал второй вариант котроллера, так как я хотел, чтобы датчик не был привязан к компьютеру проводом, и его можно было разместить в любом месте квартиры.
Итак, решено: берем монитор CO₂ и прикручиваем к нему WiFi в виде ESP8266.
► WiFi Inside
Пациент готов к операции:

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

И выкрутить винты, скрывающиеся под ними. После этого можно осторожно разъединить две части корпуса(например пластиковой карточкой):

Осторожно — потому что внутри они соединены трубкой, которую надо аккуратно отцепить и засунуть внутрь корпуса, чтобы не мешалась:

На нижней части платы расположены 4 контактных отверстия:

Люди, знакомые с электроникой, поймут их назначение по буквам рядом с ним. Остальные поймут это из моего объяснения: V (voltage) — питание, D(data) — данные, С(clock) — синхронизация, G(ground) — земля.
G, он же GND, он же «земля» — нулевая точка, от которой отсчитывается напряжение питание(то, которое V) и уровни сигналов данных. В обывательской практике можно сказать, что G — это «минус», а V — это «плюс», как в батарейках. Оно даже будет правдой… пока не появится еще одно напряжение, после чего условности вроде «минуса» полетят в тартарары.
С, он же clock, он же тактовый, он же синхросигнал — специальный сигнал, который указывает принимающей стороне, когда надо считывать сигнал с линии данных(которая D).
В отличии асинхронных протоколов(типа UART/RS-232), где такого сигнала нет, и синхронизация строится на точном указании одинаковой частоты(=скорости) на передающей и принимающей стороне(все эти 1200, 9600, 115200 бод), в синхронном протоколе есть отдельная линия, смена уровня на которой означает, что приемник должен измерить состояние линии данных поняв, какой бит передается в текущий момент.
Плюсом синхронного протокола является нечувствительность к разнице тактирования устройств(можно хоть ручками набирать байты, если не ограничений по времени), минусом — необходимость отдельного провода. В принципе, при одинаковой частоте передачи, можно разбирать синхронную передачу и без тактового сигнала, но такими извращениями мы страдать не будем.
Цепляемся осциллографом на контакты и видим посылки с данными:

Приближаем, и вот уже можем разглядеть отдельные биты:

Можем даже расшифровать сообщение, но не будем. Нам важно понять, что он действительно что-то отправляет, чтобы потом не думать «а почему у меня данные не приходят» по причине их отсутствия.
Вместо осциллографа припаиваем и клеим термоклеем платку с ESP8266:

Подключаем ее по UART к компьютеру(на фото два провода, без земли, потому что измеритель питается от того же ноута) и начинаем писать код.

► Исходники
Пишется код в среде Arduino c включенной поддержкой ESP8266(как ее включить, можно прочитать вот тут). Как нажимать кнопочку для прошивки, думаю, разберетесь сами, или с помощью esp8266.ru.Сам проект состоит из трех файлов: файла проекта, и подключенной библиотеки за авторством fedorro, который в свою очередь, использовал наработки отсюда.
#define PIN_CLOCK 14 // Пин, к которому подключен контакт "С" — тактовый сигнал #define PIN_DATA 12 // Пин, к которому подключен контакт "D" — данные #include <ESP8266WiFi.h> #include <ESP8266WebServer.h> #include "mt8060_decoder.h" const char* ssid = "MikroTik-951"; //Имя сети const char* password = "FAKEPASSWORD"; //Пароль сети String co2_value = ""; //Значение содержания углекислоты в ppm String tmp_value = ""; //Значение температуры в градусах цельсия String hum_value = ""; //Значение влажности воздуха в процентах int error_count = 0; //Счетчик ошибок контрольной суммы(кстати, можно не считать, неделями работает без ошибок) ESP8266WebServer server(80); //Создаем обьект сервера void setup() { Serial.begin(115200); //Настраиваем UART WiFi.begin(ssid, password); //Подключаемся к WiFi while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); // Усердно показываем в UART, что мы заняты делом } Serial.println(""); Serial.print("WiFi connected, IP "); Serial.println(WiFi.localIP()); server.on("/co2", co2_show); // Устанавливаем адреса страниц и функции, этим адресам соотвествующие server.on("/tmp", tmp_show); server.on("/hum", hum_show); server.on("/json", json_show); server.onNotFound(NotFound_show); // Главная страница server.begin(); pinMode(PIN_CLOCK, INPUT); //Настраиваем порты на вход pinMode(PIN_DATA, INPUT); //Настраиваем порты на вход attachInterrupt(digitalPinToInterrupt(PIN_CLOCK), interrupt, FALLING); //Включаем прерывание на пине с тактовым сигналом } void interrupt() // По каждому прерыванию начинаем собирать данные { bool dataBit = (digitalRead(PIN_DATA) == HIGH); unsigned long ms = millis(); mt8060_message* message = mt8060_process(ms, dataBit); //Отправляем текущее время и текущий бит в функцию, взамен получаем восхитительное ничего, если битов еще не набралось до полного сообщение, и расшифрованный пакет, если битов достаточно. if (message) { //если в ответ получен пакет if (!message->checksumIsValid) //то проверяем, правильно ли рассчитана контрольная сумма { error_count++; } else // и если правильно... { switch (message->type) //...то в зависимости от типа пакета... { case HUMIDITY: hum_value = String((double)message->value / 100, 0); // сохраняем значение влажности Serial.print("HUM:"); Serial.println(hum_value); // Выводим в UART break; case TEMPERATURE: tmp_value = String((double)message->value / 16 - 273.15, 1); // конвертируем и сохраняем значение температуры Serial.print("TMP:"); // Смотрите-ка! Сиськи! -> (. )(. ) Serial.println(tmp_value); // Выводим в UART break; case CO2_PPM: co2_value = String(message->value, DEC); // сохраняем значение количества CO₂ Serial.print("CO2:"); Serial.println(co2_value); // Выводим в UART break; default: break; } } } } void co2_show() { //Функция, выводящая CO₂ простым текстом server.send(200, "text/plain", co2_value); } void tmp_show() { //Функция, выводящая температуру простым текстом server.send(200, "text/plain", tmp_value); } void hum_show() { //Функция, выводящая влажность простым текстом server.send(200, "text/plain", hum_value); } void NotFound_show() { //Функция, выводящая красивую стартовую страницу с кнопочками String form = "<!DOCTYPE html><html lang=\"en\"><head><meta http-equiv=\"Content-Type\" content=\"text/html\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\"><title>Dadget МТ8060 CO₂ monitor</title><style>.c{text-align: center;} div,input{padding:5px;font-size:1em;} input{width:95%;} body{text-align: center;font-family:verdana;} button{border:0;border-radius:0.3rem;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%;} .q{float: right;width: 64px;text-align: right;}</style><style type=\"text/css\"></style></head><body><div style=\"text-align:left;display:inline-block;min-width:260px;\"><h1>Dadget МТ8060 CO₂ monitor</h1><form action=\"/co2\" method=\"get\"><button>CO₂ value ([co2] ppm now)</button></form><br><form action=\"/tmp\" method=\"get\"><button>Temperature value ([tmp]°С now)</button></form><br><form action=\"/hum\" method=\"get\"><button>Humidity value ([hum]% now)</button></form><br><form action=\"/json\" method=\"post\"><button>JSON(all values)</button></form></div></body></html>"; form.replace("[co2]", co2_value); //подставляем текущие значения form.replace("[hum]", hum_value); form.replace("[tmp]", tmp_value); server.send(200, "text/html", form); //отправляем форму клиенту } void json_show() { //Функция, выводящая все данные в виде JSON String json = "[{\"co2\":"; //Формируем строку c JSON данными json += co2_value; json += ",\"tmp\":"; json += tmp_value; json += ",\"hum\":"; json += hum_value; json += ",\"serial\":"; json += ESP.getChipId(); json += ",\"errors\":"; json += error_count; json += ",\"uptime_min\":"; json += String(millis()/60000); json += "}]"; server.send(200, "application/json", json); //Отправляем } void loop() { server.handleClient(); //Ждем подключения клиентов по HTTP }
Файл второй, mt8060_decoder.cpp:
// Based on https://github.com/fe-c/MT8060-data-read code // All rights for reading code owned https://geektimes.ru/users/fedorro/ // and https://github.com/revspace #include "mt8060_decoder.h" #define MT8060_MAX_MS 2 // Таймаут по которому считаем, что началось новое сообщение #define MT8060_MSG_LEN 5 // В одном полном сообщении 5 байт #define MT8060_MSG_TYPE_BYTE_IDX 0 #define MT8060_MSG_VAL_HIGH_BYTE_IDX 1 #define MT8060_MSG_VAL_LOW_BYTE_IDX 2 #define MT8060_MSG_CHECKSUM_BYTE_IDX 3 #define MT8060_MSG_CR_BYTE_IDX 4 #define BITS_IN_BYTE 8 static uint8_t buffer[MT8060_MSG_LEN]; // Буфер для хранения считанных данных static int num_bits = 0; static unsigned long prev_ms; static mt8060_message _msg; static mt8060_message *msg = &_msg; void mt8060_decode(void) // Декодирует сообщение { uint8_t checksum = buffer[MT8060_MSG_TYPE_BYTE_IDX] + buffer[MT8060_MSG_VAL_HIGH_BYTE_IDX] + buffer[MT8060_MSG_VAL_LOW_BYTE_IDX]; // Вычисление контрольной суммы msg->checksumIsValid = (checksum == buffer[MT8060_MSG_CHECKSUM_BYTE_IDX] && buffer[MT8060_MSG_CR_BYTE_IDX] == 0xD); // Проверка контрольной суммы if (!msg->checksumIsValid) { return; } msg->type = (dataType)buffer[MT8060_MSG_TYPE_BYTE_IDX]; // Получение типа показателя msg->value = buffer[MT8060_MSG_VAL_HIGH_BYTE_IDX] << BITS_IN_BYTE | buffer[MT8060_MSG_VAL_LOW_BYTE_IDX]; // Получение значения показателя } // Вызывается на каждый задний фронт тактового сигнала, возвращает ссылку на структуру сообщения, если оно полностью считано mt8060_message* mt8060_process(unsigned long ms, bool data) { if ((ms - prev_ms) > MT8060_MAX_MS) { num_bits = 0; } prev_ms = ms; if (num_bits < MT8060_MSG_LEN * BITS_IN_BYTE) { int idx = num_bits / BITS_IN_BYTE; buffer[idx] = (buffer[idx] << 1) | (data ? 1 : 0); num_bits++; if (num_bits == MT8060_MSG_LEN * BITS_IN_BYTE) { mt8060_decode(); //Декодируем сообщение return msg; //Возвращаем сообщение } } return nullptr; //Ничего не возвращаем, если сообщение не полное }
Файл третий, mt8060_decoder.h
// Based on https://github.com/fe-c/MT8060-data-read code // All rights for reading code owned https://geektimes.ru/users/fedorro/ // and https://github.com/revspace #include <stdint.h> #include <stdbool.h> typedef enum { HUMIDITY = 0x41, TEMPERATURE = 0x42, CO2_PPM = 0x50, } dataType; typedef struct { dataType type; uint16_t value; bool checksumIsValid; } mt8060_message; mt8060_message* mt8060_process(unsigned long ms, bool data);
Так же, код можно посмотреть на моем GitHub. Пулл-реквесты приветствуются, как и подсказки в комментариях, как можно сделать лучше!
Запускаем, заходим на страничку устройства…

Работает! Приводим провода в порядок, находим для платы свободное место в корпусе:

Закрепляем каплей термоклея, и вот, операция закончена, остается

► Собираем статистику
Однако, пока что ситуации «посмотрел на экран измерителя» и «посмотрел на веб-страничку» отличаются не очень сильно. Чтобы было интереснее, надо либо чем-то управлять, либо собирать статистику.
Конечно, можно еще поковыряться с ESP, использовав ее память под хранение графика, или сделав так, чтобы она управляла каким-нибудь WiFi-реле… Но я не поклонник распределённых систем, и считаю, что у умного дома должен быть как минимум один сервер.
Для того, чтобы сделать из единичных показаний график, я воспользуюсь возможностями Logic Machine — скриптами и трендами. Конечно, все тоже самое можно сделать и на любом компьютере, но раз инструменты есть у меня под рукой, почему бы не воспользоваться.
Создаем новый Sheduled-скрипт(выполняющийся по расписанию), настраиваем его на запуск каждую минуту:

Внутри пишем что-то вроде этого:
local http = require('http') local json = require('json') local raw_data, code = socket.http.request('http://co2meter.lc/json') --Запрашиваем страничку. Говорим спасибо роутеру микротик за внутренние DNS-имена if (code == 200) then --Если код ответа 200.. local data = json.decode(raw_data) --Пытаемся декодировать ответ из json в таблицы lua if (data ~= nil) then --Если ответ являлся валидными json-данными... if (data[1].uptime_min > 2) then --И если это не первые две минуты работы измерителя(после включения происходит "прогрев" и во время него значения могут плавать) grp.update('S_CO2_CO2', data[1].co2) --Записываем в объекты значения CO₂, влажности и температуры grp.update('S_CO2_TMP', data[1].tmp) grp.update('S_CO2_HUM', data[1].hum) end end end
Вуаля! Мы получили первые показания:

Теперь надо их превратить в симпатичные графики. Нет ничего проще(да простит меня Dadget за бессовестную рекламу нашего контроллера, я уже заканчиваю)! Trends logs —> Add new trend log:

Теперь осталось подождать недельку-другую для сбора данных, и вот они, наши графики:

Ну и конечно, самый интересный график:

Оказалось, наблюдать за уровнем CO₂ и проводить параллели между изменениями на графике и своими действиями оказалось очень интересно!
Факт №1: Газовая плита ОЧЕНЬ сильно повышает уровень CO₂.
Факт №2: При отсутствии людей и хотя бы чуть-чуть открытом окне(даже в режиме микропроветривания) уровень CO₂ быстро снижается до фоновых значений.

Факт №3: Люди в квартире(даже спящие) вносят существенный вклад в количество углекислоты. Важно открывать окна(можно в другой части квартиры) на ночь, чтобы не надышать до вредных значений.
Факт №4: Количество CO₂, выделяемое человеком, сильно зависит от его активности. Стоит проснуться и полуспящим походить по квартире, как количество углекислоты начинает расти.

Факт №5: Количество CO₂, выделяемое человеком, ОЧЕНЬ сильно зависит от его активности. И от типа активности.

► Управляем вентилятором
В качестве площадки для тестирования вентиляции, управляемой по уровню CO₂, я выбрал офис. В нем уже настроено управление вентиляцией с контроллера(как именно, смотрите по предыдущей ссылке), так что мне просто оставалось настроить реакцию на повышение уровня CO₂. В LM делается это так:
Создаем новый скрипт типа Event-based(выполняемый при изменении объекта), устанавливаем в качестве объекта мониторинга объект, в который мы записывает текущее значение CO₂:

В коде скрипта пишем несложную логику, которая будет включать вентиляцию при уровне углекислоты выше 1000ppm, и выключать при уровне меньше 800, реализуя гистерезис для предотвращения частого включения-выключения вентиляции:
--Скрипт выполняет при любом изменении объекта value = grp.getvalue("S_CO2_CO2") --Получаем значение обьекта с количеством углекислого газа if (value > 1000) then --Если его больше, чем 1000ppm... grp.write('HP-7.1', true) --..включаем вентиляцию elseif (value < 800) then --Если CO₂ меньше, чем 800ppm... grp.write('HP-7.1', false) --..выключаем вентиляцию end
Таким образом, вентиляция включится при повышении уровня CO₂ до 1000ppm, и не выключится, пока не опустит его значение до 800ppm.
Жужжит!

Ссылки:
Внутренности похожего устройства и описание протокола
Обзор прибора
Разбор прибора
Подключение измерителя к Arduino
Если вам интересны темы интернета устройств и умного дома, добро пожаловать в канал в телеграме: telegram.me/IOTandSmarthome
В течение 14 дней, со дня публикации данной статьи, вы можете приобрести «Монитор качества воздуха» с 10%-й скидкой, используя код GEEKT-MK.

