Тема: Измеритель СО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; // Установить флаг, так как произошло прерывание
}