///////////////////////////////////////////////////////////////////////
/// © tykhon, 2019
///////////////////////////////////////////////////////////////////////
/*
MH-Z19
TX: 3<->5 A0
RX: 3<->5 A1
PWM: 12
Gnd: Gnd
Vin: +5v
MH-Z14A
1_+5v: +5v
2_Gnd: Gnd
6_Pwm:
11_RX: 3<->5 7
10_TX: 3<->5 6
S8:
G+: +5v
G0: Gnd
Rx: 3<->5 A3
Tx: 3<->5 A2
LGAQS-HT01
SCL: A5
SDA: A4
Gnd: Gnd
VDD: 3.3v
BMP280
SCL: A5
SDA: A4
Gnd: Gnd
VDD: 3.3v
DS3231
SCL: A5
SDA: A4
Gnd: Gnd
VDD: 5v
TFT
Led: -> 150 Ohm -> 5v
SCK: -> 1 KOhm -> 13
SDA: -> 1 KOhm -> 11
A0: -> 1 KOhm -> 8
Reset: -> 1 KOhm -> 9
CS: -> 1 KOhm -> 10
Gnd: Gnd
Vcc: 5v
*/
#include <Adafruit_GFX.h> // for LCD Core graphics library
#include <Adafruit_ST7735.h> // for LCD Hardware-specific library
#include <CCS811.h> // for TVOC sensor
#include <Adafruit_Sensor.h> // for bme280
#include <Adafruit_BME280.h> // for bme280
#include <SoftwareSerial.h>
#include "Wire.h"
#define TFT_CS 10
#define TFT_RST 9
#define TFT_DC 8
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);
#define SEALEVELPRESSURE_HPA (1013.25) // for bme280
Adafruit_BME280 bme; // for bme280 I2C
#define CCS811_ADDR 0x5A // for LGAQS-HT01 or ccs811
#define SI7021_ADDR 0x40 // for LGAQS-HT01 or si7021
#define WAKE_PIN 5 // for LGAQS-HT01 or ccs811
#define DS1307_ADDR 0x68 // RTC address
CCS811 sensor;
float si7021_h = 0;
float si7021_t = 0;
unsigned int data[2];
int ccs188_co2 = 0;
int ccs188_tvoc = 0;
int z14_co2;
int z19_co2;
int z19_t;
int z19_ss;
int ccs188_co2_last;
int ccs188_tvoc_last;
int z14_co2_last;
int z19_co2_last;
unsigned long s8_co2_sum = 0;
unsigned long z14_co2_sum = 0;
unsigned long z19_co2_sum = 0;
unsigned long ccs188_co2_sum = 0;
unsigned long ccs188_tvoc_sum = 0;
unsigned long bme_t_sum = 0;
unsigned long bme_h_sum = 0;
unsigned long bme_p_mm_sum = 0;
unsigned int polls = 0;
unsigned long s8_co2 = 0;
unsigned long s8_co2_last = 0;
int bme_t;
int bme_h;
float bme_p;
int bme_p_mm;
const float hpa2mm = 133.3224;
SoftwareSerial mySerial_z19(A0, A1);
SoftwareSerial mySerial_s8(A2, A3);
SoftwareSerial mySerial_z14(7, 6);
byte cmd_z14[9] = {0xFF,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79};
unsigned char response_z14[9];
String ppmString = " ";
byte cmd_z19[9] = {0xFF,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79};
byte cmd_s8[7] = {0xFE,0x44,0x00,0x08,0x02,0x9F,0x25};
unsigned char response_z19[9];
byte response_s8[] = {0,0,0,0,0,0,0};
unsigned int current_minute, lastminute;
int second_delay = 30000; // delay between measurements, ms
int work_period = 3; // period for recording data, min
//////////////////////////////////////// functions //////////////////////////////////////////////
byte bcdToDec(byte val) {
return ( (val/16*10) + (val%16) );
}
String getdate(){
Wire.beginTransmission(DS1307_ADDR); // Reset the register pointer
byte zero = 0x00;
Wire.write(zero);
Wire.endTransmission();
Wire.requestFrom(DS1307_ADDR, 7);
int secondint = bcdToDec(Wire.read());
int minuteint = bcdToDec(Wire.read());
current_minute = minuteint;
int hour = bcdToDec(Wire.read() & 0b111111); //24 hour time
int weekDay = bcdToDec(Wire.read()); //0-6 -> sunday - Saturday
int monthDay = bcdToDec(Wire.read());
int month = bcdToDec(Wire.read());
int year = bcdToDec(Wire.read());
String second = String(secondint); if (secondint < 10) {second ="0"+second;};
String minute = String(minuteint); if (minuteint < 10) {minute ="0"+minute;};
return String(hour)+":"+minute+":"+second+" "+String(monthDay)+"/"+String(month)+"/"+String(year);
}
void sendRequest_s8(byte packet[]){
mySerial_s8.begin(9600);
while(!mySerial_s8.available()){ //keep sending request until we start to get a response
mySerial_s8.write(cmd_s8,7);
delay(50);
}
int timeout=0; //set a timeout counter
while(mySerial_s8.available() < 7 ) { //Wait to get a 7 byte response
timeout++;
if(timeout > 10) { //if it takes to long there was probably an error
while(mySerial_s8.available()) //flush whatever we have
mySerial_s8.read();
break; //exit and try again
}
delay(50);
}
for (int i=0; i < 7; i++) {
response_s8[i] = mySerial_s8.read();
}
mySerial_s8.end();
}
unsigned long getValue_s8(byte packet[])
{
int high = packet[3]; //high byte for value is 4th byte in packet in the packet
int low = packet[4]; //low byte for value is 5th byte in the packet
unsigned long val = high*256 + low; //Combine high byte and low byte with this formula to get value
return val;
}
void sendRequest_SI7021(){
Wire.beginTransmission(SI7021_ADDR);
Wire.endTransmission();
delay(500);
Wire.requestFrom(SI7021_ADDR, 2); // Request 2 bytes of data
if(Wire.available() == 2) // Read 2 bytes of data to get humidity
{
data[0] = Wire.read();
data[1] = Wire.read();
}
float hum = ((data[0] * 256.0) + data[1]); // Convert the data
si7021_h = ((125 * hum) / 65536.0) - 6;
Wire.begin();
Wire.beginTransmission(SI7021_ADDR); // Send temperature measurement command
Wire.write(0xF3);
Wire.endTransmission();
delay(500);
Wire.requestFrom(SI7021_ADDR, 2); // Request 2 bytes of data
if(Wire.available() == 2) // Read 2 bytes of data for temperature
{
data[0] = Wire.read();
data[1] = Wire.read();
}
float temp = ((data[0] * 256.0) + data[1]); // Convert the data
si7021_t = ((175.72 * temp) / 65536.0) - 46.85; //
}
void sendRequest_ccs188(){
sensor.compensate(si7021_t, si7021_h); // replace with t and rh values from sensor
sensor.getData();
ccs188_co2 = sensor.readCO2();
ccs188_tvoc = sensor.readTVOC();
}
void sendRequest_z14(){
mySerial_z14.begin(9600);
mySerial_z14.write(cmd_z14, 9);
memset(response_z14, 0, 9);
mySerial_z14.readBytes(response_z14, 9);
int i;
byte crc = 0;
for (i = 1; i < 8; i++) crc+=response_z14[i];
crc = 255 - crc;
crc++;
if ( !(response_z14[0] == 0xFF && response_z14[1] == 0x86 && response_z14[8] == crc) ) {
Serial.println("CRC error z14: " + String(crc) + " / "+ String(response_z14[8]));
} else {
unsigned int responseHigh = (unsigned int) response_z14[2];
unsigned int responseLow = (unsigned int) response_z14[3];
z14_co2 = (256*responseHigh) + responseLow;
};
mySerial_z14.end();
}
void sendRequest_z19(){
mySerial_z19.begin(9600);
mySerial_z19.write(cmd_z19, 9);
memset(response_z19, 0, 9);
mySerial_z19.readBytes(response_z19, 9);
int i;
byte crc = 0;
for (i = 1; i < 8; i++) crc+=response_z19[i];
crc = 255 - crc;
crc++;
if (response_z19[0] != 0xFF) {Serial.println("CRC error z19: response_z19[0] != 0xFF: " + String(response_z19[0]));};
if (response_z19[1] != 0x86) {Serial.println("CRC error z19: response_z19[1] != 0x86: " + String(response_z19[1]));};
if (response_z19[8] != crc) {Serial.println("CRC error z19: response_z19[8] != crc: " + String(response_z19[8]) + " / "+ String(crc));};
unsigned int responseHigh = (unsigned int) response_z19[2];
unsigned int responseLow = (unsigned int) response_z19[3];
unsigned int responseTT = (unsigned int) response_z19[4];
unsigned int responseSS = (unsigned int) response_z19[5];
z19_t = responseTT-40;
z19_ss = responseSS;
z19_co2 = (256*responseHigh) + responseLow;
mySerial_z19.end();
}
void tft_form(){
tft.setTextSize(2);
tft.setCursor(0, 4);
tft.print("ccs: ");
tft.setCursor(0, 30);
tft.print("z19: ");
tft.setCursor(0, 60);
tft.print("z14: ");
tft.setCursor(0, 90);
tft.print("s8: ");
}
void tft_output(){
tft.setTextSize(2);
tft.setTextColor(ST7735_BLACK);
tft.setCursor(60, 4);
tft.print(String(ccs188_co2_last));
tft.print(String("/"));
tft.print(String(ccs188_tvoc_last));
tft.setTextColor(ST7735_WHITE);
tft.setCursor(60, 4);
tft.print(String(ccs188_co2));
tft.print(String("/"));
tft.print(String(ccs188_tvoc));
ccs188_co2_last = ccs188_co2;
ccs188_tvoc_last = ccs188_tvoc;
tft.setCursor(60, 30);
tft.setTextColor(ST7735_BLACK);
tft.print(String(z19_co2_last));
tft.setCursor(60, 30);
tft.setTextColor(ST7735_WHITE);
tft.print(String(z19_co2));
z19_co2_last = z19_co2;
tft.setCursor(60, 60);
tft.setTextColor(ST7735_BLACK);
tft.print(String(z14_co2_last));
tft.setCursor(60, 60);
tft.setTextColor(ST7735_WHITE);
tft.print(String(z14_co2));
z14_co2_last = z14_co2;
tft.setCursor(60, 90);
tft.setTextColor(ST7735_BLACK);
tft.print(String(s8_co2_last));
tft.setCursor(60, 90);
tft.setTextColor(ST7735_WHITE);
tft.print(String(s8_co2));
s8_co2_last = s8_co2;
tft.setTextSize(1);
tft.fillRect(0, 117, 160, 120, ST7735_BLACK);
tft.setCursor(0, 117);
tft.print(String(getdate()));
}
/////////////////////////////////////// setup /////////////////////////////////////////
void setup(void) {
Serial.begin(9600);
bool status;
status = bme.begin();
if (!status) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
delay(100); // let sensor boot up
tft.initR(INITR_BLACKTAB); // initialize a ST7735S chip, black tab
Serial.println("init");
if(!sensor.begin(uint8_t(CCS811_ADDR), uint8_t(WAKE_PIN)))
Serial.println("Init fail");
tft.setTextWrap(false); // Allow text to run off right edge
tft.setRotation(3);
tft.fillScreen(ST7735_BLACK);
tft_form();
}
//////////////////////////////////////////////// loop /////////////////////////////////////////////////////
void loop() {
sendRequest_SI7021();
sendRequest_ccs188();
sendRequest_z14();
sendRequest_z19();
sendRequest_s8(cmd_s8);
s8_co2 = getValue_s8(response_s8);
bme_p = bme.readPressure();
bme_p_mm = int(bme_p/hpa2mm);
bme_h = bme.readHumidity();
bme_t = bme.readTemperature();
s8_co2_sum += s8_co2;
z14_co2_sum += z14_co2;
z19_co2_sum += z19_co2;
ccs188_co2_sum += ccs188_co2;
ccs188_tvoc_sum += ccs188_tvoc;
bme_t_sum += bme_t;
bme_h_sum += bme_h;
bme_p_mm_sum += bme_p_mm;
polls ++;
getdate();
tft_output();
if (((current_minute)%work_period == 0)&&(lastminute != current_minute)) {
s8_co2 = s8_co2_sum/polls;
z14_co2 = z14_co2_sum/polls;
z19_co2 = z19_co2_sum/polls;
ccs188_co2 = ccs188_co2_sum/polls;
ccs188_tvoc = ccs188_tvoc_sum/polls;
bme_t = bme_t_sum/polls;
bme_h = bme_h_sum/polls;
bme_p_mm = bme_p_mm_sum/polls;
String dataString = getdate()+" s8: "+String(s8_co2)+" z14: "+String(z14_co2)+" z19: "+String(z19_co2)+" z19_t: "+String(z19_t)+" ccs: "+String(ccs188_co2)+ " TVOC: "+String(ccs188_tvoc)+" ccs_t: "+String(si7021_t)+" ccs_h: "+String(si7021_h)+" bme_t: "+String(bme_t)+" bme_h: "+String(bme_h);
dataString.replace(".",",");
dataString.replace(" ","\t");
Serial.println(dataString);
lastminute = current_minute;
polls = 0;
s8_co2_sum = 0;
z14_co2_sum = 0;
z19_co2_sum = 0;
ccs188_co2_sum = 0;
ccs188_tvoc_sum = 0;
bme_t_sum = 0;
bme_h_sum = 0;
bme_p_mm_sum = 0;
};
delay(second_delay);
}
+127 |
2072
100
|
Кмк проще сунуть датчик на проводах внутрь воздушного шарика и надуть гелием. Горловину перетянуть и откалибровать.
Даже простой шарик даст достаточно времени (для гелия стенки обрабатывают чтобы не травил).
За 400ppm а чистый лес идти)
Калибровку в диапазоне измерения проводят калибровочными газами с прецизионной концентрацией.
Но это в медицине.
Обозреваемые датчики имеют другую область использования и возможно описанные алгоритмы калибровки имеют право на жизнь, но я в них не уверен, imho
Но если уж очень хочется кустарно — то можно обойтись и без поисков жидкого азота или разговором со сварщиком на шиномонтаже. Достаточно пойти в магазин и купить брендовый баллончик газа для заправки зажигалок, или для туристических плиток. Там тоже углекислоты нет, пропан-бутан, довольно чистый…
Мне кажется такой лучше
P.S. Майнкрафт не люблю :)
Да и в коменте скорее всего разрешение дисплея выше и, соответственно, намутить шрифт можно посглаженнее.
Сам размышляю над этой темой и хотелось бы узнать какие управляемые заслонки кто использует.
Меня крайне одолевают сомнения, что в комнате возможно явное расслоение.
Можете сделать измерения CO2 на полу, в центре и под потолком? Для статистической верности — несколько раз, но в одинаковых условиях. Скажем, минут по 5 на каждом уровне и два-три-четыре измерения в течение дня — чтобы невозможно было списать на случайность и погрешности.
Мне просто не удалось нагуглить чётких данных. Все ограничиваются пространными рассуждениями про молекулярную массу или конвекцию воздуха. А циферок-то нет.
Как по мне — эти измерения на статью на Хабр потянули бы.
habr.com/ru/post/395755/
habr.com/ru/post/187210/
habr.com/ru/post/301296/
и ещё несколько там же можно нарыть.
www.youtube.com/user/Consulbud
Настолько системного и объёмного разжёвывания всяких строительных тонкостей я во всём рунете не встречал. Тем не менее, некоторое количество спорных тезисов есть и у него.
Ну а про молекулярный вес углекислого газа тут уже ответили. Это из школьного учебника — можете Рабиновича не трогать.
в медицине устройство приточно-вытяжной вентиляции предусматривает приток сверху, вытяжку снизу на высоте 0,3 метра от пола.
Дальше. Оптимальное место для приточки ещё и от температур зависит. У нас ведь сезоны разные — зимой мы подогреваем входящий холодный воздух, а летом — охлаждаем входящий с улицы тёплый. И тут либо делать полностью воздушное отопление-кондиционирование как у америкосов, либо соответствующим образом располагать дырки под приток.
Ну т.е. в типовой квартире с центральным отоплением приточка для зимы делается сразу над батареей, а приточка для лета — сразу под кондишеном.
Впрочем, кондишен и так агрессивно перемешивает воздух, значит можно для всех сезонов ограничиться подоконной приточкой.
А вот где забирать воздух при принудительной вентиляции зависит от того, где он «грязнее» — наверху из-за выдыхаемого пара или внизу из-за выдыхаемого CO2.
Поэтому и попросил сделать замеры на разных высотах. Ибо строить теоретические выкладки можно до посинения.
Это что, прям щели на улицу сделаны чтоль? =)
Если будет подпор, то система будет неустойчивой — может случиться картина, когда давление нагнетания будет равно разрежению вытяжки — тогда у вас в щели под окнами вообще перестанет поступать воздух — два вентилятора будут пахать исключительно друг на друга.
Тогда один общий вентилятор-вытяжка на кухне будет создавать общее разрежение на всю квартиру. Причём, скорость вентилятора (тобишь создаваемое разрежение) должно равняться степени открытия всех задвижек суммарно.
И в каждом помещении свой датчик СО2. Где больше нагазовано — там задвижка сильнее открывается (автоматикой) на улицу. А вентилятор тем сильнее крутится, чем больше задвижек открыто и чем они сильнее открыты.
В этой схеме исключено передавливание-перетягивание — когда несколько вентиляторов могут мешать друг другу и перенаправлять поток не так как надо.
Всё — ИМХО от диванного инженера, хоть и с дипломом :) Сам пока только планирую себе подобную схему — как там оно на практике встанет — посмотрим.
Вот бы обзор с фоточками.
Хотя бы уже реализованных узлов, в частности вот этой околобатарейной вентиляции.
И ещё такой вопрос интересует: а разве это легально, вентилировать прям на батарею?
Там наверняка какие-то нормативы же есть по кол-ву отнимаемого у соседей по стояку тепла.
Я, просто, хотел себе обдув батарей сделать, чтобы тёплый с холодным воздух перемешивался лучше, и чтобы теплее было дома.
Статистика по датчикам что говорит, критично это или норм?
Но вообще фильтр ведь для того и сделан, чтобы на нём что-то оседало.
Всё на пальцах — конкретных цифр и измерений не видел. Реальное воздействие нужно подкреплять обширными исследованиями. Только кому оно надо? Фирмачам — нафиг. Юзерам — да, но не потянут. Ждём может институт какой озабодится проблемой.
например, вполне легальный выжиматель Climer_sx_25 или, скажем, наоборот, Стеновой_клапан_Домвент тот же.
Ну, или когда помещение не имеет окон вообще, находится внутри здания.
+ Если людей больше 1, то и понятие душно у каждого может юыть разное.
P.S.
Для меня, к примеру, «душно» — это когда у большинства других это уже перебор. Дкмаю это зависит не только от самой предрасположенности к CO2, но и то как ты дышишь (частота/глуюина дыхания).
За обзор спасибо.
А то от жидкого-то, датчик кони двинет))
пс. Подскажите, где можно почитать про логгер «OpenLog»? Интересная штука.
судя по гуглу, это разработка sparkfun, так что плясать надо от его github'а.
Но станет очень холодным газом, что не отменяет возможного песца сенсору.
Плюс опять-же это будет не чистый азот, а смесь с воздухом, с ненормируемым содержанием других газов, что не позволит правильно откалибровать ноль/нуль.
Спасибо за ссылочку!
а вот фиг его знает, что будет с данным объектом, если он даже временно заморозится. Зависит от веществ, которые входят в его состав.
На состав этого азота — скорее всего, т.к. судя по вики точка кипения жидкого азота -195,75 °C. Сомневаюсь, что там азот при температуре ниже этой => какая смесь газов.
я делаю метеостанцию, взял 5000, только потому что в исходном проекте было озвучено что датчик используется 5000. Т.е я смогу переключить диапазон на 2000?
Вообщем он периодически сбивается на 500-1000 пунктов. И сам же возвращается назад…
Попробуйте проведите длительный тест в пару-тройку недель.
Вторая проблема этих датчиков была описана на хабре — предельный диапазон на цифре и аналоге может быть и 5000ppm и 2000ppm и узнать это без образцового прибора можно лишь косвенно, анализируя показания чистого воздуха и не очень.
зы: Только приехал такой, с индексом «B»…
Имею несколько MH-Z19B и несколько S8. Если MH-Z19B откалибровать и отключить автокалибровку — показывает ПОЛНОСТЬЮ идентичные данные с S8 (в рамках заявленной погрешности). Проверенно опытами в несколько месяцев непрерывной работы в квартире с показаниями 400-2500 (газовая плита).
Зеленая линия — MH-Z19, красная — MH-Z14, синяя — S8.
Глядя на график, можно лишь предположить, что Z19 откалиброван неверно — за 400 ppm он принимает некоторое большее значение (700?) и всё, что меньше него, обрубает. То же касается электрохимического. Z14 вообще оч странно колбасит, не очень похоже на калибровку. Возможно, глюки. S8 тоже один раз скакнул. Почему? Непонятно.
В общем, для начала надо откалибровать, а потом уже приводить графики.
У меня почему-то показания идут в диапазоне 1000 до 2000, меньше 1000 не хочет показывать. Версия датчика 0053.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.