1 (2025-08-17 14:09:56 отредактировано Karl)

Тема: Измеритель СО2 с управлением вытяжкой

Нашел я проект http://radiopench.blog96.fc2.com/blog- … 01.html?sp .
Весьма продуманная конструкция.
Перевел комментарии.

Решил расширить функционал - добавить управление вытяжкой (конечно в идеале ШИМ, но это я не потяну).
Добавил.

Потом решил добавить возможность принудительного включения кнопкой на 30 минут (в авто режиме включается при уровне 900, выключается при уровне 799).
Вот тут и начались проблемы - если уровень СО2 ниже 799 то не включается.
Сейчас в скетче время установлено 1 минута.

Все строки, которые я добавил в оригинальный скетч я в комментариях добавил знак "+++++". Это что б проще найти мои художества.
Хэлп ми - как сделать что б кнопкой можно было включить вне зависимости от того какой уровень СО2.

#include <Wire.h>              // Библиотека для работы с I2C
#include <Adafruit_GFX.h>      // Библиотека для работы с графикой
#include <Adafruit_SSD1306.h>  // Библиотека для работы с OLED-дисплеем (0.96 дюйма)
#include <SoftwareSerial.h>    // Библиотека для использования программного последовательного порта
#include <MsTimer2.h>          // Библиотека для работы с таймерами
#include <EEPROM.h>            // Библиотека для работы с EEPROM

// Определение пинов
#define MODE_PIN    2
#define ENTER_PIN   3
#define SELECT_PIN  4
#define onPin      6             // Кнопка ручного включения вентилятора++++++++++
#define RX_PIN     10
#define TX_PIN     11
#define LED_PIN    7             // Выход индикатора измерения
#define funPin     5             // Выход управления вентилятором+++++
#define BAUDRATE           9600  // Скорость передачи данных для MH-Z19 (фиксированная)
#define T1                 1000  // Интервал измерений в миллисекундах (минимум 1000 = 1 секунда))
#define DISP_ON_TIME       30000  // Время включения дисплея (время до отключения)
#define DISP_WAKEUP_VALUE  1000  // Уровень CO2, при котором дисплей включается
#define FUN_VALUE_ON        900  // Уровень CO2, при котором вентилятор ВКЛ+++++++++
#define FUN_VALUE_OFF       799  // Уровень CO2, при котором вентилятор ВКЛ++++++++
#define SCREEN_WIDTH 128         // Ширина OLED-дисплея
#define SCREEN_HEIGHT 64         // Высота OLED-дисплея
#define OLED_RESET    -1         // Пин сброса (или -1, если используется общий сброс Arduino)

// Инициализация OLED-дисплея
Adafruit_SSD1306 OLED(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);


// Объявление переменных
unsigned int co2Val;             // Измеренное значение CO2
unsigned int calib;              // Режим калибровки
int dispTimer = DISP_ON_TIME;    // Оставшееся время до выключения дисплея

// Для управления кнопкой
bool funState = false;       // Состояние вентилятора+++++++
unsigned long funOnTime = 0; // Время включения вентилятора+++++++++

byte ReadCO2[9] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79}; // Команда чтения CO2
byte SCalOn[9]  = {0xFF, 0x01, 0x79, 0xA0, 0x00, 0x00, 0x00, 0x00, 0xE6}; // Команда включения калибровки
byte SCalOff[9] = {0xFF, 0x01, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86}; // Команда выключения калибровки
byte RetVal[9];                                                           // Массив для ответа от датчика


// Структура для описания диапазона графиков
struct recipe {     
  int gInterval;   // Интервал графика
  byte scaleV;     // Время на один делитель (30 пикселей)
  char scaleC[2];  // Единицы времени
};

struct recipe hRange[12] = {
  {   1, 30, "s"},  // Диапазон 0: период графика, время на делитель, единицы времени
  {   2,  1, "m"},  //       1
  {   4,  2, "m"},  //       2
  {  10,  5, "m"},  //       3
  {  20, 10, "m"},  //       4
  {  40, 20, "m"},  //       5
  { 120,  1, "h"},  //       6
  { 240,  2, "h"},  //       7
  { 480,  4, "h"},  //       8
  { 960,  8, "h"},  //       9
  {1440, 12, "h"},  //      10
  {2880,  1, "d"},  //      11
};

int dataBuff[91]; // Буфер данных (91 элемент от 0 до 90)
int latestData; // Последние данные CO2
int dataMin = 400; // Минимальное значение для отображения графика
int dataMax = 400; // Максимальное значение для отображения графика
int tCount = 0; // Счетчик времени интервала логирования
unsigned int rNum = 0; // Номер диапазона (rangeNumber) от 0 до 11
char chrBuff[6]; // Буфер символов для строковых данных

volatile boolean timerFlag = false; // Флаг прерывания таймера
boolean entPushed = false; // Флаг нажатия кнопки ENTER

// Инициализация программного последовательного порта для взаимодействия с MH-Z19C
SoftwareSerial mySerial(RX_PIN, TX_PIN);  // MH-Z19C

void setup() {
    pinMode(MODE_PIN, INPUT_PULLUP); // Установка режима авто-калибровки
    pinMode(ENTER_PIN, INPUT_PULLUP); // Установка кнопки ENTER как входа с подтяжкой
    pinMode(SELECT_PIN, INPUT_PULLUP); // Установка кнопки SELECT как входа с подтяжкой
    pinMode(LED_PIN, OUTPUT); // Установка пина LED как выхода
    pinMode(RX_PIN, INPUT); // Установка пина RX как входа для программного порта
    pinMode(TX_PIN, OUTPUT); // Установка пина TX как выхода для программного порта
    pinMode(onPin, INPUT_PULLUP); // Настройка кнопки включения вентиляторв с подтяжкой+++++++
    pinMode(funPin, OUTPUT);           // Настройка пина вентилятора как выход++++++++++
    mySerial.begin(9600); // Инициализация программного последовательного порта с заданной скоростью


  for (int i = 0; i <= 90; i++) {
    dataBuff[i] = -1;                 // Заполнение буфера данных значением -1 (неопределено)
  }

  if (!OLED.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x32
    for (;;);                                    // Остановка программы в случае неудачи инициализации OLED-дисплея
  }
  OLED.clearDisplay();                 // Очистка дисплея перед выводом информации
  OLED.setTextColor(WHITE);            // Установка цвета текста на белый
  if (digitalRead(ENTER_PIN) == LOW) {  
    rangeSet();                        // Если кнопка ENTER нажата при запуске - выполнить настройку диапазона
  }
  rNum = EEPROM.read(0);               // Чтение номера диапазона из EEPROM

  OLED.setCursor(10, 12);
  OLED.print(F("CO2 monitor V0.8"));   // // Вывод стартового сообщения на дисплей
  OLED.display();

  if (digitalRead(MODE_PIN) == LOW) {  
    calib = 0;                         // Если режим LOW - установить ручную калибровку (без авто-калибровки)
  } else {                             
    calib = 1;                         // В противном случае установить авто-калибровку (по умолчанию)
  }
  setCalMode(calib);                   // Установка режима калибровки: 0 - ручной, 1 - автоматический
  measure();                           // Первичное измерение CO2
  MsTimer2::set(T1, timer2_IRQ);       // Запуск таймера
  MsTimer2::start();
}

void loop() {

// Проверяем состояние кнопки (активная низкая)+++++++++++
  if (digitalRead(onPin) == LOW && !funState) {
    funState = true;               // Включаем светодиод
    digitalWrite(funPin, HIGH);    // Включаем светодиод
    funOnTime = millis();          // Запоминаем время включения
  }
  
  // Проверяем, прошло ли 30 минут (1800000 миллисекунд)
  if (funState && (millis() - funOnTime >= 30000)) {
    funState = false;              // Выключаем светодиод
    digitalWrite(funPin, LOW);     // Выключаем светодиод
  }
  
  while (timerFlag != true) {              
    if (digitalRead(ENTER_PIN) == LOW) {   
      entPushed = true;                   // Установка флага нажатия кнопки ENTER в true при нажатии кнопки во время ожидания
    }
  }
  timerFlag = false;                      // Сброс флага таймера для следующего прерывания

  if (entPushed == true) {                 
    if (dispTimer > 1) {                   
      dispTimer = 1;                      // Если дисплей еще включен - выключить его
    } else {                              
      dispTimer = DISP_ON_TIME;           // Если дисплей выключен - вернуть его в исходное состояние (включить)
    }
  }
  entPushed = false;                      // Сброс флага нажатия кнопки ENTER

  digitalWrite(LED_PIN, HIGH);
  measure();                              // Включение светодиода во время измерения CO2
  digitalWrite(LED_PIN, LOW);
  latestData = co2Val;                    // Сохранение последнего измеренного значения CO2
  if (co2Val > DISP_WAKEUP_VALUE) {        
    dispTimer = DISP_ON_TIME;             // Если уровень CO2 превышает заданное значение - сбросить таймер дисплея
  }
  if (co2Val > FUN_VALUE_ON){
    digitalWrite(funPin, HIGH);          // Включить вентилятор если CO2 больше +++++++++++++
    }          
  if (co2Val < FUN_VALUE_OFF){            // Если значение CO2 меньше вентилятор ВЫКЛ++++++++++++++!!!!!!!!!!!!!!!!!!!!!!!
    digitalWrite(funPin, LOW);
  }

  tCount++;                               // Увеличение счетчика интервалов логирования
  if (tCount == hRange[rNum].gInterval) { // Если достигнут заданный интервал логирования - сохранить данные в буфер
    saveBuff();                           // Сброс счетчика интервалов логирования
    tCount = 0;
  }

  dispTimer--; // Декрементирование таймера отображения
    if (dispTimer > 0) { 
        disp(); // Если таймер больше нуля - отобразить данные на OLED-дисплее
    } else {
        OLED.clearDisplay(); // Если таймер равен нулю - очистить OLED-дисплей (выключить его)
        OLED.display(); 
        dispTimer = 1; // Установить значение для следующего отключения дисплея
    }
}

// Чтение значения концентрации CO2 с MH-Z19C

int measure() {                                 // MH-Z19С
  int err;
  mySerial.write(ReadCO2, sizeof ReadCO2);      // Отправка команды измерения
  memset(RetVal, 0x00, sizeof RetVal);          // Очистка буфера приема
  mySerial.readBytes((char *)RetVal, sizeof RetVal); // Получение результатов измерения

  if (RetVal[0] == 0xff && RetVal[1] == 0x86) { // Если чтение прошло успешно
    co2Val = RetVal[2] * 256 + RetVal[3];       // Вычисление концентрации CO2
    err = 0;
  } else {
    co2Val = 399;                               // Если чтение неудачно, вернуть это значение
    err = 1;
  }
  return err;
}

void setCalMode(int m) {                     // Установка режима калибровки
  if (m == 0) {
    mySerial.write(SCalOff, sizeof SCalOff); // Выключение калибровки 
  }
  if (m == 1) {
    mySerial.write(SCalOn, sizeof SCalOn);   // Включение калибровки 
  }
  mySerial.readBytes((char *)RetVal, sizeof RetVal); // Дамми-прием. Без этого не получится прочитать результаты измерений после включения питания
  delay(100);
}

void saveBuff() {                       // Обновление буфера данных и определение максимального и минимального значений
  int d;
  dataMin = 9999;                       // Минимальное значение
  dataMax = 0;                          // Максимальное значение
  for (int i = 90; i >= 1; i--) {       // Сохранение значений в массиве и определение максимального и минимального значений
    d = dataBuff[i - 1];                // Предыдущее значение
    dataBuff[i] = d;                    // Сдвиг массива
    if (d != -1) {                      // Если сдвинутые данные являются допустимыми значениями
      if (d < dataMin) {                // обновить минимальное значение
        dataMin = d;
      }
      if (d > dataMax) {                // обновить максимальное значение
        dataMax = d;
      }
    }
  }
  dataBuff[0] = latestData; // Запись последних данных в начало массива
    if (latestData < dataMin) { // Если последние данные меньше минимального значения
        dataMin = latestData; 
    } 
    if (latestData > dataMax) { // Если последние данные больше максимального значения
        dataMax = latestData; 
    } 
    if (dataMin < 0) { 
        dataMin = 0; // Но нижний предел - 0 
    } 
    if (dataMax > 5000) { 
        dataMax = 5000; // Но если больше 5000, ограничить до 5000 
    } 
}


void disp() { // Отображение на OLED-экране
    OLED.clearDisplay(); // Очистка экрана
    headWrite(); // Запись заголовка
    graphWrite(); // Отображение фона графика
    graphPlot(); // Отображение графика в виде линий
    OLED.display(); // Передача данных и отображение на экране
}

void headWrite() { // Отображение заголовка
    OLED.setCursor(0, 0); 
    OLED.setTextSize(1); // Маленький шрифт для первой строки
    OLED.print(F("CO2")); 
    OLED.setCursor(16 * 6, 0); // Отображение режима калибровки в правом верхнем углу
    if (calib == 0) { 
        OLED.print(F("AZoff")); 
    } else { 
        OLED.print(F("AZon")); 
    } 
    OLED.setCursor(24, 0); 
    OLED.setTextSize(2); // Увеличенный шрифт для значения CO2
    sprintf(chrBuff, "%4d", co2Val); // Форматирование значения CO2 в 4 цифры
    OLED.print(chrBuff); // Отображение значения CO2
    OLED.setCursor(26 + 4 * 6 * 2, 7); // Перемещение курсора для отображения единицы измерения
    OLED.setTextSize(1); 
    OLED.print(F("ppm")); // Отображение единицы измерения
    OLED.print(" "); 
    sprintf(chrBuff, "%4d", (hRange[rNum].gInterval - tCount) - 1); // Остаток времени до обновления графика
    OLED.setCursor(102, 8); 
    OLED.print(chrBuff); 
}


void graphWrite() {                       // Рисование фона графика
  OLED.drawFastVLine(30, 16, 40, WHITE);  // Левый ориентир (вертикальная линия)
  for (int x = 30; x <= 120; x += 4) {
    OLED.drawFastHLine(x, 36, 2, WHITE);  // Рисование пунктирной линии по центру (горизонтальная пунктирная линия)
  }

  for (int x = (120 - 30); x > 40; x -= 30) {
    for (int y = 16; y < 55; y += 4) {
      OLED.drawFastVLine(x, y, 2, WHITE); // Рисование двух пунктирных вертикальных линий
    }
  }
  // Левая шкала (отображение концентрации)
    OLED.drawFastHLine(26, 16, 4, WHITE); // Метка максимального значения
    OLED.drawFastHLine(26, 36, 4, WHITE); // Центр
    OLED.drawFastHLine(26, 55, 4, WHITE); // Минимум
    OLED.setCursor(0, 16); // Отображение максимального значения
    sprintf(chrBuff, "%4d", dataMax); 
    OLED.print(chrBuff); 
    OLED.setCursor(0, 32); // Отображение центрального значения
    sprintf(chrBuff, "%4d", (dataMax + dataMin) / 2); 
    OLED.print(chrBuff); 
    OLED.setCursor(0, 48); // Отображение минимального значения
    sprintf(chrBuff, "%4d", dataMin); 
    OLED.print(chrBuff); 

  // Нижняя шкала (отображение времени)
    OLED.setCursor(19, 57); // Левая шкала времени
    sprintf(chrBuff, "%+3d", (hRange[rNum].scaleV) * -3); // Получение значения из структуры и умножение на -3
    OLED.print(chrBuff); // Передача на OLED
    OLED.print(hRange[rNum].scaleC); // Отображение единицы измерения    
    OLED.setCursor(49, 57); // Центральная шкала времени
    sprintf(chrBuff, "%+3d", (hRange[rNum].scaleV) * -2); // Умножение на -2
    OLED.print(chrBuff); 
    OLED.print(hRange[rNum].scaleC);    
    OLED.setCursor(79, 57); // Правая шкала времени
    sprintf(chrBuff, "%+3d", (hRange[rNum].scaleV) * -1); // Умножение на -1
    OLED.print(chrBuff); 
    OLED.print(hRange[rNum].scaleC);    
    OLED.setCursor(118, 57); // Ноль времени
    OLED.print(F("0")); 
}

void graphPlot() { // Отображение данных в виде линейного графика на основе значений массива
    long y1, y2; 
    for (int i = 0; i <= 89; i++) { 
        if (dataBuff[i + 1] == -1) { // Если данные y2 неопределены (-1), прервать цикл
            break; // Прекращение построения графика
        } 
        y1 = map(dataBuff[i], dataMin, dataMax, 55, 16); // Преобразование для координат графика
        y2 = map(dataBuff[i + 1], dataMin, dataMax, 55, 16); // Преобразование для координат графика
        OLED.drawLine(120 - i, y1, 119 - i, y2, WHITE); // Рисование графика концентрации CO2 линией
    } 
}


void rangeSet() {                              // Установка диапазона записи данных
  unsigned int d;
  d = EEPROM.read(0);
  if (d > 11) {                                // Если номер диапазона вне диапазона,
    d = 0;                                     // установить в 0
  }
  OLED.setCursor(0, 40);                       // Сначала записать маленькие буквы
  OLED.print(F("SEL:change value"));
  OLED.setCursor(0, 50);
  OLED.print(F("ENT:save and exit"));
  OLED.setCursor(0, 0);
  OLED.setTextSize(2);                         // Следующие строки будут отображены шрифтом в 2 раза больше
  OLED.print(F("Range set"));
  OLED.display();
  while (digitalRead(ENTER_PIN) == LOW) {      // Ждать, пока кнопка ENTER не будет отпущена
  }
  delay(30);
  OLED.setCursor(0, 20);
  OLED.print(F("Fs = "));                      // Полный масштаб =
  sprintf(chrBuff, "%2d", 3 * (hRange[d].scaleV)); // Получить значение из определения диапазона и умножить на 3
  OLED.print(chrBuff);                         // Передать на OLED
  OLED.print(hRange[d].scaleC);                // Отображение единицы измерения
  OLED.display();                              // Фактическое отображение
  while (digitalRead(ENTER_PIN) == HIGH) {     // Ждать, пока кнопка ENTER не будет нажата
    if (digitalRead(SELECT_PIN) == LOW) {
      d++;                                     // Увеличить номер диапазона
      if (d > 11) {                            // Если номер диапазона превышает верхний предел
        d = 0;                                 // Вернуться к началу
      }
      OLED.fillRect(0, 20, 128, 16, BLACK);    // Закрасить предыдущие значения черным прямоугольником
      OLED.setCursor(0, 20);
      OLED.print(F("Fs = "));
      sprintf(chrBuff, "%2d", 3 * (hRange[d].scaleV)); // Получить значение из определения диапазона и умножить на 3
      OLED.print(chrBuff);                     // Передать на OLED
      OLED.print(hRange[d].scaleC);            // Отображение единицы измерения
      OLED.display();
      while (digitalRead(SELECT_PIN) == LOW) { // Ждать, пока кнопка SELECT не будет отпущена
      }
      delay(30);
    }
  }
  EEPROM.write(0, d);                          // Сохранить номер диапазона в EEPROM
  OLED.clearDisplay();
  OLED.setCursor(0, 0);
  OLED.setTextSize(1);                 
  OLED.display();
}

void timer2_IRQ() {                        // Прерывание MsTime
  timerFlag = true;                        // Установить флаг, так как произошло прерывание
}

2 (Вчера 11:31:21 отредактировано Karl2233)

Re: Измеритель СО2 с управлением вытяжкой

Решено.
Для таких целей ИИ идеально подходит.
Взял пример который ChatGPT мне написал, и вдумчиво вставил его.
Теперь у меня полностью удовлетворяющее мои хотелки устройство.
Доделаю, выложу фото.