Подключение датчика CO2 модели MH-Z19B с помощью аналогового выхода Vo / Хабр

Подключение датчика CO2 модели MH-Z19B с помощью аналогового выхода Vo / Хабр Анемометр

Что можно сделать лучше?

Здесь есть еще над чем поработать:1) Решить проблему избыточного тепла в корусе2) Сделать его еще компактнее — разведя плату и избавившись от проводов3) Сделать работу в offline режиме

Что еще можно сделать? Какие у вас идеи? Если кто-то соберется сделать что-то подобное буду рад помочь и ответить на вопросы.

Визуализация

Красивые динамичные графики рисует пакет Grafana.

Установка, тоже последняя версия:

Второй прототип

Плату развёл, как обычно, в Eagle CAD и заказал у JLCPCB. Десяток плат стоит всего $2, что вполне доступно. Изготовление и доставка заняли порядка трёх недель.

Для уменьшения габаритов датчик припаян “вверх ногами”. Под ним располагается преобразователь напряжения на 3.3V, с другой стороны платы – модуль ESP8266 ESP-12e и, по желанию, датчик температуры и влажности DHT22. На свободном месте – трёхцветный светодиодный модуль WS2812b для простой индикации.

Разъема для программирования на плате нет. Я подпаял провода напрямую к ESP8266 и залил туда
минимальную прошивку, которая подключается к домашней сети WiFi и включает режим обновления прошивки по сети.

Железо

Первым делом на eBay были заказаны следующие компоненты:

Arduino Micro ATmega32U4 3.3V

(цена вопроса 5$). Т.к. датчик имеет 3-вольтовую логику, обычные Arduino лучше не использовать.

OLED LCD-дисплей I2C 0.91«128×32

(цена вопроса 7$). Дисплей подключается к стандартным i2c-пинам Arduino.

— Собственно датчик

MH-Z19

(цена вопроса 28$).

— Набор проводов с разъемами для штыревых контактов (цена вопроса 1-2$)


Таким образом, общая стоимость составила ~40$, или 2600р. Фирменный прибор от известной компании стоит примерно вдвое дороже, хотя здесь скорее вопрос не экономии, а технического интереса.

Код для Arduino был позаимствован из вышеприведенной статьи, в него был добавлен вывод данных на дисплей, а для более удобного анализа данных вывод был переделан в формат простой строки с разделителем. Также были добавлены метки времени, каждая соответствует 10 секундам.

Исходный код
#include <SoftwareSerial.h>
#include <Wire.h>

// I2C OLED
#include "SSD1306Ascii.h"
#include "SSD1306AsciiWire.h"
#define I2C_ADDRESS 0x3C
SSD1306AsciiWire oled;

// CO2 sensor:
SoftwareSerial mySerial(8,9); // RX,TX
byte cmd[9] = {0xFF,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79}; 
unsigned char response[9];

void setup() {
  // Serial
  Serial.begin(9600);
  mySerial.begin(9600);

  // OLED
  Wire.begin();         
  oled.begin(&Adafruit128x32, I2C_ADDRESS);
  oled.set400kHz();  
  oled.setFont(ZevvPeep8x16);  

  oled.clear();  
  oled.println("setup::init()");
}

long t = 0;

void loop() 
{
  mySerial.write(cmd, 9);
  memset(response, 0, 9);
  mySerial.readBytes(response, 9);
  int i;
  byte crc = 0;
  for (i = 1; i < 8; i  ) crc =response[i];
  crc = 255 - crc;
  crc  ;

  oled.clear();  
  if ( !(response[0] == 0xFF && response[1] == 0x86 && response[8] == crc) ) {
    Serial.println("CRC error: "   String(crc)   " / "  String(response[8]));
    oled.println("Sensor CRC error");
  } else {
    unsigned int responseHigh = (unsigned int) response[2];
    unsigned int responseLow = (unsigned int) response[3];
    unsigned int ppm = (256*responseHigh)   responseLow;
    Serial.print(String(t)); Serial.print(","); Serial.print(ppm); Serial.println(";");
    if (ppm <= 400 || ppm > 4900) {
      oled.println("CO2: no data");          
    } else {
      oled.println("CO2: "   String(ppm)   " ppm"); 
      if (ppm < 450) {   
        oled.println("Very good");
      }
      else if (ppm < 600) {   
        oled.println("Good");
      }
      else if (ppm < 1000) {   
        oled.println("Acceptable");
      }
      else if (ppm < 2500) {   
        oled.println("Bad");
      }
     else {   
        oled.println("Health risk");
      }
    }
  }
  delay(10000);
  t  = 10;
}

Все это было собрано вместе, скетч залит в ардуину, результат выглядит примерно так:

image

Конечно это не верх промышленного дизайна (в планах подыскать какой-то корпус), но для задачи показометра, способного работать как автономно, так и передавать данные по USB, устройство вполне справляется. Для получения данных по USB достаточно открыть в Arduino IDE монитор порта, в нем будут выводиться данные. Текст оттуда можно скопировать и открыть в любой программе, например в Excel.

Измерения


Следующий вопрос: что мы собственно измеряем? Устройство выдает данные в ppm (parts per million, частей на миллион). 1000 ppm = 0,1% содержания СО2. В интернете можно найти следующую таблицу допустимых концентраций:

350 — 450 ppm

: Нормальный уровень на открытом воздухе.

: Приемлемые уровни. Уровень. рекомендованный для спален, детских садов и школ.

600 — 1000 ppm

: Жалобы на несвежий воздух, возможно снижение концентрации внимания.

1000 ppm

Индикация

На плате распаян трехцветный светодиодный модуль WS2812b, а в прошивку была добавлена библиотека WS2812FX для различных цветовых и световых эффектов. Показания датчика сохраняются в массив, по которому высчитывается среднее за последние пару минут значение – это позволяет сглаживать небольшие колебания концентрации.

Пока воздух свежий, устройство медленно и неярко “дышит” зелёным светом. Когда концентрация начинает превышать 1000ppm, то начинает более ярко и настойчиво моргать жёлтым, а при концентрации более 1400ppm резко вспыхивает красным на полной яркости.

Вполне наглядно и удобно. А вот так устройство выглядит на рабочем столе в розетке со встроенным USB блоком питания:

Как собрать подобное устройство?

Для начала нужно купить все необходимые модули, например, на aliexpress. Поскольку конкретные предложения и продавцы постоянно меняются, поэтому в таблице указаны лишь поисковые запросы, по которым найдется то, что нужно.

Корпус

Хотелось сделать с одной стороны устройство из готовых модулей, с другой не хотелось делать его размером с буханку хлеба. Для этого в Tinkercad был нарисован корпус пригодный для 3д-печати. Большая проблема tinkercad в том, что очень тяжело вносить изменения в уже готовые проекты. Если бы я начинал сейчас, то воспользовался бы openSCAD

Про анемометры:  Делаем правильный расчет мощности газового котла отопления

Корпус получился действительно компактным, но провода внутри все равно занимают большую часть места и их пришлось сделать довольно короткими, чтобы коробочка закрылась.

Недостаток такой плотной компановки в том, что тепло от микроконтроллера и dc-dc преобразователя нагревают воздух внутри на ~3 градуса выше реальной температуры, это же вредит точности показаний влажности. С другой стороны это создает конвекцию, которая обновляет воздух в сенсоре CO2 быстрее.

Отчасти эту проблему можно решить, поработав над потреблением микроконтроллера, например реже передавать данные по сети.

Подключение

Электрическая схема очень проста — датчик давления, датчик температуры и влажности и дисплей питаются от 3.3в и подключаются через двухпроводной интерфейс I2C. Он позволяет подключать все устройства параллельно: SDA каждого устройства к ножке D1 (GPIO5), а SCL — к ножке D2 (GPIO4)

Датчик CO2 питается от 5в (в нашем случае Vin — питание от USB входа) и подключается через UART. Для этого используются ноги D7 (GPIO13) — RX (который подключается к TX сенсора) и D8 (GPIO15) — TX (который подключается к RX сенсора)

Программа

Программировалось на Ардуино, благо там есть гора уже готовых библиотек, но при помощи PlatformIO – среда разработки гораздо удобнее.

Первым делом в скетч был добавлена возможность загрузки микропрограммы через WiFi – чтобы не переключать постоянно модуль между программатором и датчиком, называется Arduino OTA. Кстати, прошивка загружается так гораздо быстрее чем через UART.

Для получения данных с датчика специальная библиотека тоже есть, но её я не использовал – там и так всего несколько строк:

SoftwareSerialco2(,2);voidsetup(){// ...co2.begin(9600);}intco2ppm(){staticbytecmd[9]={0xFF,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79};staticbyteresponse[9]={};co2.write(cmd,9);co2.readBytes(response,9);unsignedintresponseHigh=(unsignedint)response[2];unsignedintresponseLow=(unsignedint)response[3];return(256*responseHigh) responseLow;}

Сначала данные просто выводились в консоль, но для беспроводного датчика это не самое лучшее решение.

Программная часть

Все файлы проекта были разработаны в среде разработки PlatformIO и лежат на Github-e.

Код прошивки, чтобы не ходить на Github
#include <FS.h>
#include <Arduino.h>
#include <ESP8266WiFi.h>          //https://github.com/esp8266/Arduino

// Wifi Manager
#include <DNSServer.h>
#include <ESP8266WebServer.h>
#include <WiFiManager.h>         //https://github.com/tzapu/WiFiManager

// HTTP requests
#include <ESP8266HTTPClient.h>

// OTA updates
#include <ESP8266httpUpdate.h>
// Blynk
#include <BlynkSimpleEsp8266.h>

// JSON
#include <ArduinoJson.h>          //https://github.com/bblanchon/ArduinoJson

// GPIO Defines
#define I2C_SDA 5 // D1 Orange
#define I2C_SCL 4 // D2 Yellow

// Humidity/Temperature SI7021
#include <SI7021.h>
SI7021 si7021;

#include <Wire.h>

// Pressure and Temperature
#include <Adafruit_BMP085.h>

// Use U8g2 for i2c OLED Lib
#include <SPI.h>
#include <U8g2lib.h>
U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, I2C_SCL, I2C_SDA, U8X8_PIN_NONE);
byte x {0};
byte y {0};

// Handy timers
#include <SimpleTimer.h>

// CO2 SERIAL
#define DEBUG_SERIAL Serial1
#define SENSOR_SERIAL Serial

byte cmd[9] = {0xFF,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79};
unsigned char response[7];

// Pressure and temperature
Adafruit_BMP085 bme;

// Blynk token
char blynk_token[33] {"Blynk token"};
char blynk_server[64] {"blynk-cloud.com"};
const uint16_t blynk_port {8442};

// Device Id
char device_id[17] = "Device ID";
const char fw_ver[17] = "0.1.0";

// Handy timer
SimpleTimer timer;

// Setup Wifi connection
WiFiManager wifiManager;

// Network credentials
String ssid { "ku_"    String(ESP.getChipId())};
String pass {"ku_"   String(ESP.getFlashChipId()) };

//flag for saving data
bool shouldSaveConfig = false;

// Sensors data
int t {-100};
int p {-1};
int h {-1};
int co2 {-1};

char loader[4] {'.'};

//callback notifying the need to save config
void saveConfigCallback() {
        DEBUG_SERIAL.println("Should save config");
        shouldSaveConfig = true;
}

void factoryReset() {
        wifiManager.resetSettings();
        SPIFFS.format();
        ESP.reset();
}

void printString(String str) {
        DEBUG_SERIAL.println(str);
}

void readCO2() {
        // CO2
        bool header_found {false};
        char tries {0};

        SENSOR_SERIAL.write(cmd, 9);
        memset(response, 0, 7);

        // Looking for packet start
        while(SENSOR_SERIAL.available() && (!header_found)) {
                if(SENSOR_SERIAL.read() == 0xff ) {
                        if(SENSOR_SERIAL.read() == 0x86 ) header_found = true;
                }
        }

        if (header_found) {
                SENSOR_SERIAL.readBytes(response, 7);

                byte crc = 0x86;
                for (char i = 0; i < 6; i  ) {
                        crc =response[i];
                }
                crc = 0xff - crc;
                crc  ;

                if ( !(response[6] == crc) ) {
                        DEBUG_SERIAL.println("CO2: CRC error: "   String(crc)   " / "  String(response[6]));
                } else {
                        unsigned int responseHigh = (unsigned int) response[0];
                        unsigned int responseLow = (unsigned int) response[1];
                        unsigned int ppm = (256*responseHigh)   responseLow;
                        co2 = ppm;
                        DEBUG_SERIAL.println("CO2:"   String(co2));
                }
        } else {
                DEBUG_SERIAL.println("CO2: Header not found");
        }

}

void sendMeasurements() {
        // Read data
        // Temperature
        float tf = si7021.getCelsiusHundredths() / 100.0;
        float t2f =bme.readTemperature();
        t = static_cast<int>((tf   t2f) / 2);

        // Humidity
        h = si7021.getHumidityPercent();

        // Pressure (in mmHg)
        p = static_cast<int>(bme.readPressure() * 760.0 / 101325);

        // CO2
        readCO2();

        // Send to server
        Blynk.virtualWrite(V1, t);
        Blynk.virtualWrite(V2, h);
        Blynk.virtualWrite(V4, p);
        Blynk.virtualWrite(V5, co2);

        // Write to debug console
        printString("H: "   String(h)   "%");
        printString("T: "   String(t)   "C");
        printString("P: "   String(p)   "mmHg");
        printString("CO2: "   String(co2)   "ppm");
}

void loading() {
        long unsigned int count {(millis() / 500) % 4};
        memset(loader, '.', count);
        memset(&loader[count], 0, 1);
}

void draw() {
        u8g2.clearBuffer();

        // CO2
        if (co2 > -1) {
                char co2a [5];
                sprintf (co2a, "%i", co2);

                u8g2.setFont(u8g2_font_inb19_mf);
                x = (128 - u8g2.getStrWidth(co2a))/2;
                y = u8g2.getAscent() - u8g2.getDescent();
                u8g2.drawStr(x, y, co2a);

                const char ppm[] {"ppm CO2"};
                u8g2.setFont(u8g2_font_6x12_mf);
                x = (128 - u8g2.getStrWidth(ppm)) / 2;
                y = y   2   u8g2.getAscent() - u8g2.getDescent();
                u8g2.drawStr(x, y, ppm);
        } else {
                loading();
                u8g2.setFont(u8g2_font_inb19_mf);
                x = (128 - u8g2.getStrWidth(loader)) / 2;
                y = u8g2.getAscent() - u8g2.getDescent();
                u8g2.drawStr(x, y, loader);
        }

        // Cycle other meauserments
        String measurement {"..."};
        const char degree {176};

        // Switch every 3 seconds
        switch((millis() / 3000) % 3) {
        case 0:
                if (t > -100) { measurement = "T: "   String(t)   degree   "C"; }
                break;
        case 1:
                if (h > -1) { measurement = "H: "   String(h)   "%"; }
                break;
        default:
                if (p > -1) { measurement =  "P: "   String(p)   " mmHg"; }
        }

        char measurementa [12];
        measurement.toCharArray(measurementa, 12);

        u8g2.setFont(u8g2_font_9x18_mf);
        x = (128 - u8g2.getStrWidth(measurementa))/2;
        y = 64   u8g2.getDescent();
        u8g2.drawStr(x, y, measurementa);

        u8g2.sendBuffer();
}

void drawBoot(String msg = "Loading...") {
        u8g2.clearBuffer();
        u8g2.setFont(u8g2_font_9x18_mf);
        x = (128 - u8g2.getStrWidth(msg.c_str())) / 2;
        y = 32   u8g2.getAscent() / 2;
        u8g2.drawStr(x, y, msg.c_str());
        u8g2.sendBuffer();
}

void drawConnectionDetails(String ssid, String pass, String url) {
        String msg {""};
        u8g2.clearBuffer();

        msg = "Connect to WiFi:";
        u8g2.setFont(u8g2_font_7x13_mf);
        x = (128 - u8g2.getStrWidth(msg.c_str())) / 2;
        y = u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, msg.c_str());

        msg = "net: "   ssid;
        x = (128 - u8g2.getStrWidth(msg.c_str())) / 2;
        y = y   1   u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, msg.c_str());

        msg = "pw: "  pass;
        x = (128 - u8g2.getStrWidth(msg.c_str())) / 2;
        y = y   1   u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, msg.c_str());

        msg = "Open browser:";
        x = (128 - u8g2.getStrWidth(msg.c_str())) / 2;
        y = y   1   u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, msg.c_str());

        // URL
        // u8g2.setFont(u8g2_font_6x12_mf);
        x = (128 - u8g2.getStrWidth(url.c_str())) / 2;
        y = y   1   u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, url.c_str());

        u8g2.sendBuffer();
}

bool loadConfig() {
        File configFile = SPIFFS.open("/config.json", "r");
        if (!configFile) {
                DEBUG_SERIAL.println("Failed to open config file");
                return false;
        }

        size_t size = configFile.size();
        if (size > 1024) {
                DEBUG_SERIAL.println("Config file size is too large");
                return false;
        }

        // Allocate a buffer to store contents of the file.
        std::unique_ptr<char[]> buf(new char[size]);

        // We don't use String here because ArduinoJson library requires the input
        // buffer to be mutable. If you don't use ArduinoJson, you may as well
        // use configFile.readString instead.
        configFile.readBytes(buf.get(), size);

        StaticJsonBuffer<200> jsonBuffer;
        JsonObject &json = jsonBuffer.parseObject(buf.get());

        if (!json.success()) {
                DEBUG_SERIAL.println("Failed to parse config file");
                return false;
        }

        // Save parameters
        strcpy(device_id, json["device_id"]);
        strcpy(blynk_token, json["blynk_token"]);
}

void configModeCallback (WiFiManager *wifiManager) {
        String url {"http://192.168.4.1"};
        printString("Connect to WiFi:");
        printString("net: "   ssid);
        printString("pw: "  pass);
        printString("Open browser:");
        printString(url);
        printString("to setup device");

        drawConnectionDetails(ssid, pass, url);
}

void setupWiFi() {
        //set config save notify callback
        wifiManager.setSaveConfigCallback(saveConfigCallback);

        // Custom parameters
        WiFiManagerParameter custom_device_id("device_id", "Device name", device_id, 16);
        WiFiManagerParameter custom_blynk_server("blynk_server", "Blynk server", blynk_server, 64);
        WiFiManagerParameter custom_blynk_token("blynk_token", "Blynk token", blynk_token, 34);
        wifiManager.addParameter(&custom_blynk_server);
        wifiManager.addParameter(&custom_blynk_token);
        wifiManager.addParameter(&custom_device_id);

        // wifiManager.setTimeout(180);
        wifiManager.setAPCallback(configModeCallback);

        if (!wifiManager.autoConnect(ssid.c_str(), pass.c_str())) {
                DEBUG_SERIAL.println("failed to connect and hit timeout");
        }

        //save the custom parameters to FS
        if (shouldSaveConfig) {
                DEBUG_SERIAL.println("saving config");
                DynamicJsonBuffer jsonBuffer;
                JsonObject &json = jsonBuffer.createObject();
                json["device_id"] = custom_device_id.getValue();
                json["blynk_server"] = custom_blynk_server.getValue();
                json["blynk_token"] = custom_blynk_token.getValue();

                File configFile = SPIFFS.open("/config.json", "w");
                if (!configFile) {
                        DEBUG_SERIAL.println("failed to open config file for writing");
                }

                json.printTo(DEBUG_SERIAL);
                json.printTo(configFile);
                configFile.close();
                //end save
        }

        //if you get here you have connected to the WiFi
        DEBUG_SERIAL.println("WiFi connected");

        DEBUG_SERIAL.print("IP address: ");
        DEBUG_SERIAL.println(WiFi.localIP());
}

// Virtual pin update FW
BLYNK_WRITE(V22) {
        if (param.asInt() == 1) {
                DEBUG_SERIAL.println("Got a FW update request");

                char full_version[34] {""};
                strcat(full_version, device_id);
                strcat(full_version, "::");
                strcat(full_version, fw_ver);

                t_httpUpdate_return ret = ESPhttpUpdate.update("http://romfrom.space/get", full_version);
                switch (ret) {
                case HTTP_UPDATE_FAILED:
                        DEBUG_SERIAL.println("[update] Update failed.");
                        break;
                case HTTP_UPDATE_NO_UPDATES:
                        DEBUG_SERIAL.println("[update] Update no Update.");
                        break;
                case HTTP_UPDATE_OK:
                        DEBUG_SERIAL.println("[update] Update ok.");
                        break;
                }

        }
}

// Virtual pin reset settings
BLYNK_WRITE(V23) {
        factoryReset();
}

void setup() {
        // Init serial ports
        DEBUG_SERIAL.begin(115200);

        SENSOR_SERIAL.begin(9600);
        SENSOR_SERIAL.swap();  // GPIO15 (TX) and GPIO13 (RX)

        // Init I2C interface
        Wire.begin(I2C_SDA, I2C_SCL);

        // Init display
        u8g2.begin();
        drawBoot();

        // Init Humidity/Temperature sensor
        si7021.begin(I2C_SDA, I2C_SCL);

        // Init Pressure/Temperature sensor
        if (!bme.begin()) {
                DEBUG_SERIAL.println("Could not find a valid BMP085 sensor, check wiring!");
        }

        // Init filesystem
        if (!SPIFFS.begin()) {
                DEBUG_SERIAL.println("Failed to mount file system");
                ESP.reset();
        }

        // Setup WiFi
        setupWiFi();

        // Load config
        drawBoot();
        if (!loadConfig()) {
                DEBUG_SERIAL.println("Failed to load config");
                factoryReset();
        } else {
                DEBUG_SERIAL.println("Config loaded");
        }

        // Start blynk
        Blynk.config(blynk_token, blynk_server, blynk_port);

        // Setup a function to be called every 10 second
        timer.setInterval(10000L, sendMeasurements);

        sendMeasurements();
}

void loop() {
        Blynk.run();
        timer.run();
        draw();
}

Разработка сильно упростилась благодаря наличию хороших примеров и наличию функциональных библиотек:

Про анемометры:  Нормы воздухообмена на человека в помещениях различного назначения

Для работы с CO2 датчиком был использован чутка улучшенный пример из datasheet-а.

Прототип

В качестве “мозга” устройства будет полюбившийся мне в последнее время ESP8266. Для его подключения достаточно всего четырёх проводов, значит можно использовать самую простую версию – ESP-01. В качестве основы всей конструкции – макетная плата, несколько разъёмов и немного провода.

Разъёма питания на макетке не предусмотрено, но в закромах нашлась старая плата с припаянными Micro USB разъемом и
регулятором напряжения на 3.3в. То, что надо. Отпилил всё лишнее и прикрутил к макетке двумя винтами М2.

Схема подключения тривиальная. Если нигде не напутать, то заработает сразу.

Резисторы по 10К, конденсаторы на питание я проигнорировал – прототип, все-таки.

Все началось с того что я работаю в офисе, где как водится нет нормальной вентиляции, зато есть много народу половине которого все время жарко, а второй половине отчаянно дует.

Понятно что одного термометра для контроля воздуха в помещении недостаточно. Даже с кондиционером часто бывает прохладно, но душно. Спертый воздух. Оказалось на это больше всего влияет концентрация со2. Когда я узнал стоимость готовых приборов хотел от этой идеи отказаться. Но случайно попал на обзор оптического датчика концентрации со2. Цена конечно тоже не маленькая, но все-таки близко к разумным пределам. И руки давно чесались по паяльнику. В качестве контролера использовать решил ESP8266. Во первых дешево, во вторых что бы передавать информацию на компьютер, свой и любого желающего в комнате. После того как собрал и оттестировал первый вариант, решил добавить экран. Во первых это красиво:) Во вторых во многих случаях удобно.

Подключение датчика CO2 модели MH-Z19B с помощью аналогового выхода Vo / Хабр

Устройство построено на модуле ESP8266 NodeMcu Lua wi-fi

Сперва я подключил датчики температуры, в комнате и на улице. ds18b20
Подключение датчика CO2 модели MH-Z19B с помощью аналогового выхода Vo / Хабр

Затем собственно датчик углекислого газа. MH-Z19
Подключение датчика CO2 модели MH-Z19B с помощью аналогового выхода Vo / Хабр

И под конец дисплей
Подключение датчика CO2 модели MH-Z19B с помощью аналогового выхода Vo / Хабр

написал вот такую программу под винды

Подключение датчика CO2 модели MH-Z19B с помощью аналогового выхода Vo / Хабр

После того как я вставил прибор в корпус он начал тупить, так как воздух слабо проникал внутрь. Что бы он поживее реагировал на окружающую атмосферу, я вставил бушный вентилятор от какой-то простенькой видеокарты. Он 12 вольтовый и при подключении к 5 вольтам крутиться медленно и печально, но это именно то что в данном случае требовалось.

Про анемометры:  Простейший измеритель CO2 за 2000 рублей и полчаса / Хабр

А теперь собственно впечатления.
Прибор оказался намного полезней и интереснее чем я ожидал. Во первых больше нет проблем с проветриванием, никто не возмущается, так как у всех выскакивает предупреждение и они довольно хорошо согласуются с личными ощущениями.
Ну и просто наблюдения показали что в солнечный день уровень со2 значительно ниже чем в пасмурный и дождливый, утром загрязненность воздуха выше чем днем. С первого взгляда можно понять что делать, проветривать, если зашкаливает со2, или включить кондиционер, если повышенная температура. Датчик температуры на улице хорошо показывает получится ли охладить проветриванием, или только кондиционер. Стало очевидным и понятным, что творится в комнате и как в этом жить. И дома прибор оказался очень полезным, особенно в детской комнате, тем более что за показаниями можно следить удаленно через WiFi.

В результате получилось что-то вроде:

Тут видно сам прибор с экраном и программа на компьютере которая принимает информацию через WIFI.

Дополнение про датчик.
Почему именно такой датчик?
Есть дешевый MQ-135. Но я не встретил в откликах ни одного доброго слова об этом датчике. Он реагирует на все подряд, температуру, влажность, питание. Напротив датчики аналогичной конструкции какой применил я используются везе в том числе в профессиональной аппаратуре.

Тут описан принцип работы датчика. Называется «инфракрасный оптический анализ» capnography.narod.ru/princips.html

Что касается ШИМ и UART. Я изначально решил использовать ШИМ так как это было удобнее в данной конструкции, решил попробовать, и если бы результат был не удовлетворительный, переделал бы на UART. По факту оказалось что ШИМ ничем не уступает. Точный надежный, ни каких плясок и сбоев. Так и оставил.

Тут можно прочитать про со2. Зачем его измерять? И как это влияет на наше самочувствие. www.pvsm.ru/moskva/108944

Таким получается готовое изделие
Подключение датчика CO2 модели MH-Z19B с помощью аналогового выхода Vo / Хабр
Подключение датчика CO2 модели MH-Z19B с помощью аналогового выхода Vo / Хабр

Последняя реинкарнация данного прибора

Обратите внимание чихаю и кашляю в кадре не я. Это сам прибор при превышении нормы СО2 чихает, а если уровень становится опасным кашляет.

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

Результаты прототипирования

Сначала датчик показывал какие-то дикие значения, но на следующий день самостоятельно откалибровался и цифры стали более-менее вменяемыми. Самостоятельная калибровка заключается в том, что минимальное значение за прошедшие сутки принимается за 400ppm.

Где-то неделю я наблюдал за графиками. Показания, может и не точные, но представление о свежести воздуха дают. При закрытых окнах концентрация медленно растет и за несколько часов добирается до 1000ppm, что, действительно, субъективно ощущается как чувство усталости и потери внимания. Если проветрить – опять опускается до 400-500ppm и становится ощутимо легче.

Результаты работы прототипа мне понравились и я решил собрать устройство более качественно – на нормальной печатной плате.

Сбор данных

Для сбора данных у меня сейчас используется валявшийся ранее без дела микрокомпьютер Raspberry PI. На него
был установлен брокер протокола MQTT mosquitto с конфигурацией по умолчанию. Внутри локальной домашней сети это достаточно безопасно, да и ничего секретного там все равно нет.

Установка элементарная:

Все настройки – по умолчанию, этого достаточно для домашней сети. Единственный важный момент – надо настроить маршрутизатор так, чтобы у малинки был статический IP адрес, а если можно – то и имя прописать в DNS.

После этого малинка готова к приему циферок от датчика. Надо только установить библиотеку PubSubClient.

В программу добавилось следующее:

Данные отправляются в формате InfluxDB – это пригодится на следующем шаге.

Хранение истории для анализа

Хранятся данные в InfluxDB. Это специальная база данных для записей,
упорядоченных по времени. Самое то для истории показаний разных датчиков.

Установка немножко более замороченная, потому что хочется последнюю версию от разработчиков:

Все настройки тоже по умолчанию, только нужно добавить конфигурацию для импорта данных из mosquitto в InfluxDB:

Файл /etc/telegraf/telegraf.d/sensors.conf

Получаем данные из mosquitto, отправляем в influxdb.

Таким образом в базе данных sensors появятся показания нашего датчика.

Выводы

Устройство оказалось довольно-таки интересным, и особенно актуальным для работающих дома за компьютером. Так например, за время написания этой статьи уровень co2 вырос в помещении с 500 до 770ppm. Поглядывание на экран заставляет либо чаще открывать окно, либо наконец-таки задуматься об устройстве в доме нормальной вентиляции (наверное режим микропроветривания в окне был бы не лишним, а лучше какая-то вытяжка). Если бы я сейчас покупал новые окна, наверно задумался бы о более-менее качественной модели с нормальным проветриванием.

Также важно отметить актуальность хорошей вентиляции на кухне: как показывает график, даже за 10 минут одна газовая горелка может „выжечь“ весь запас кислорода, доведя концентрацию CO2 до весьма высокой. Измерения в спальне показали, что в плане вентиляции тоже все не очень хорошо: к утру концентрация co2 составляет более 1000ppm, а для умственного труда хороший сон это весьма актуально.

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

Оцените статью
Анемометры
Добавить комментарий

Adblock
detector