Метеостанция на Arduino Nano. Часть 2.

Я в прошлой статье написал как сделать метеостанцию на коленке. Метеостанция получилось хорошая. Однако, для моей задачи, определить динамику изменения влажности в ванной после душа, не очень подходила. Показания приходилось записывать вручную, а это не очень удобно. Поэтому я решил использовать встроенную энергонезависимую память микроконтроллера EEPROM для записи показаний влажности. А затем скидывать эти данные на компьютер.

Свою покупную метеостанцию я, кстати, нашёл! Но она теперь, по возможностям, и в подмётки не годиться моей новой. Её ни перепрограммировать под свои нужды, ни скинуть данные на компьютер нельзя. Новая куда лучше!

Так что же я сделал? Всё очень просто. Я взял упомянутую в прошлой статье метеостанцию, и немного ее перепрограммировал. Код привожу ниже.

Посмотреть код
// meteo 2.0.0 Меняем библиотеку dht.h на TroykaDHT.h
// meteo 2.0.1 Добавляем отсчет времени
// meteo 2.0.2 Добавляем работу с памятью EEPROM

// Подключаем библиотеки:
#include <TroykaDHT.h>      // библиотека для работы с датчиками серии DHT (https://github.com/amperka/TroykaDHT)
#include <LiquidCrystal.h>  // библиотека для работы с ЖК-дисплеем
#include <TimeLib.h>        // библиотека отображает время в человеческом виде (https://github.com/PaulStoffregen/Time)
#include <EEPROM.h>         // библиотека для работы с внутренней памятью микроконтроллера EEPROM

// Объявляем константы (буквенные обозначения пинов Ардруино):
const int rs = 12, en = 11, d4 = 5, d5 = 4, d6 = 3, d7 = 2, d8 = 8;

// создаём объект класса DHT
// передаём номер пина к которому подключён датчик и тип датчика
// типы сенсоров: DHT11, DHT21, DHT22
DHT dht(d8, DHT11);

// initialize the library by associating any needed LCD interface pin
// with the arduino pin number it is connected to
LiquidCrystal lcd(rs, en, d4, d5, d6, d7);

struct Record {
  unsigned long time;
  byte temperature;
  byte humidity;
};

Record rec;

boolean isOutput = false; //Определяет режим работы выводить данные на COM порт или записывать показания счётчика
unsigned long time = 0; 
unsigned long lastTime = 0;
unsigned int eeAddress = 0;   //Location we want the data to be put.

void setup() {

  lcd.begin(16, 2);
  lcd.print("hello, world!");
  
  setTime(0, 0, 0, 1, 1, 2019);
  
  Serial.begin(9600);

  Serial.println("Последовательный порт включен..."); // #DEBUG# Строка для целей отладки. После отладки её нужно закомментировать.
  
/*  if (Serial){
    isOutput = true;  
  }
  else {  
    // Подключение датчика
    dht.begin();
    delay(1000);
    
    // Считывание показаний с датчика
    dht.read();
    lastTime = millis();
  
    // Проверка показаний датчика
    if (dht.getState() == DHT_OK){
      
      // Если показания считались, то записываем их в EEPROM 
      rec.time = 0;
      rec.temperature = dht.getTemperatureC();
      rec.humidity = dht.getHumidity();
    
      // Получаем адрес ячейки памяти для сохранения показаний датчика
      EEPROM.get(0, eeAddress);
      if (eeAddress < 4 && eeAddress > 1024-sizeof(rec) && !(eeAddress + 2)%6 != 0){
        eeAddress = 4;     
      }    
      // Записываем первые показания датчика в EEPROM
      writeDHT(); 
    }
    else {
      // Если не удалось снять показания, то метеостанция будет работать в режиме чтения EEPROM.
      lcd.setCursor(0, 0);
      lcd.print("  ERROR DHT!!!  ");
      lcd.setCursor(0, 1);
      lcd.print(" Serial output! ");
      isOutput = true;
      delay(5000);    
    }
  } /**/

  // Подключение датчика
  dht.begin();
  delay(1000);
  
  // Считывание показаний с датчика
  dht.read();
  lastTime = millis();

  // Проверка показаний датчика
  if (dht.getState() == DHT_OK){
    
    // Если показания считались, то записываем их в EEPROM 
    rec.time = 0;
    rec.temperature = dht.getTemperatureC();
    rec.humidity = dht.getHumidity();
  
    // Получаем адрес ячейки памяти для сохранения показаний датчика
    EEPROM.get(0, eeAddress);
    if (eeAddress < 4 && eeAddress > 1024-sizeof(rec) && !(eeAddress + 2)%6 != 0){
      eeAddress = 4;     
    }    
    // Записываем первые показания датчика в EEPROM
    writeDHT(); 
  }
  else {
    // Если не удалось снять показания, то метеостанция будет работать в режиме чтения EEPROM.
    lcd.setCursor(0, 0);
    lcd.print("  ERROR DHT!!!  ");
    lcd.setCursor(0, 1);
    lcd.print(" Serial output! ");
    isOutput = true;
    delay(5000);    
  } /**/
  
  lcd.clear();

  if (isOutput) {
    Serial.println("Начало вывода показаний датчика: ");
    Serial.println("eeAddress; time; temperature; humidity");
    EEPROM.get(0, eeAddress);
    Serial.print(eeAddress);
    Serial.println(";;;");
    for (eeAddress = 4; eeAddress <= 1024-sizeof(rec); eeAddress += sizeof(rec)){
      EEPROM.get(eeAddress, rec);
      Serial.print(eeAddress);
      Serial.print(";");
      Serial.print(rec.time);
      Serial.print(";");
      Serial.print(rec.temperature);
      Serial.print(";");
      Serial.println(rec.humidity);
    }
  }
}


void loop() {
  if (!isOutput){
    digitalDHTDisplay();
    digitalClockDisplay();

    // Опрос датчика через каждые 2000 миллисекунды
    if((millis() - lastTime) > 2000){
      
      // считывание данных с датчика
      dht.read();
      lastTime = millis();

      // Проверка состояния датчика
      if(dht.getState() == DHT_OK){
        
        // Если данные с датчика получены, то убираем отметку 'E' на дисплее
        lcd.setCursor(15, 0);
        lcd.print(" "); 

        // Проверяем, изменились ли показания датчика abs(rec.temperature - dht.getTemperatureC()) + abs(rec.humidity - dht.getHumidity()) > 1
        if ( haveChanged(1)){

          // Если показания датчика изменились, то записываем их в EEPROM
          rec.time = millis()/1000;
          rec.temperature = dht.getTemperatureC();
          rec.humidity = dht.getHumidity();
        
          // Записываем показания датчика в EEPROM
          writeDHT(); 
        }
      }
      else {
        // Если показания с датчика не получены, то отмечаем ошибку
        lcd.setCursor(15, 0);
        lcd.print("E");
      }
    }
  }
  
  delay(1000);
}

boolean haveChanged(int inaccuracy){
//  return (abs(rec.temperature - dht.getTemperatureC()) + abs(rec.humidity - dht.getHumidity())) > inaccuracy;
  return abs(rec.temperature - dht.getTemperatureC()) > inaccuracy || abs(rec.humidity - dht.getHumidity()) > inaccuracy;
}


void digitalClockDisplay(){
  // показываем цифровые часы:
  lcd.setCursor(4,1);
  if (hour() < 10) lcd.print('0');
  lcd.print(hour());
  printDigits(minute());
  printDigits(second());
}
 
void printDigits(int digits){
  // вспомогательная функция для печати данных о времени 
  // на монитор порта; добавляет в начале двоеточие и ноль:
  lcd.print(":");
  if(digits < 10)
    lcd.print('0');
  lcd.print(digits);
}

void writeDHT(){
  // Запись показаний датчика в память EEPROM
  Serial.println("Записываем показания датчика:"); Serial.println(String(eeAddress) + ":   " + String(rec.time) + " - " + String(rec.temperature) + " - " + String(rec.humidity)); // #DEBUG# Строка для целей отладки. После отладки её нужно закомментировать.
  EEPROM.put(0, eeAddress + sizeof(rec));
  EEPROM.put(eeAddress, rec);
  eeAddress += sizeof(rec); 
  if (eeAddress > 1024-sizeof(rec)) eeAddress = 4; 
  Serial.print("Показания записаны... Адрес для следующей записи: "); Serial.println(eeAddress); // #DEBUG# Строка для целей отладки. После отладки её нужно закомментировать
}

void digitalDHTDisplay(){
  lcd.setCursor(4, 0);
  lcd.print(String(int(dht.getTemperatureC())) + "C  " + String(int(dht.getHumidity())) + "%");
}

В целом, я сделал следующее.

Во-первых, поменял библиотеку для работы с датчиком на более современную и надёжную. 

Во-вторых, добавил отсчёт времени на экране. В микроконтроллере нет встроенных часов. Он отсчитывает время с начала включения. Отображать это время на экране необходимо, чтобы была возможность зафиксировать время, когда закончил мыться.

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

В-третьих, я добавил запись показаний счётчика во внутреннюю память EEPROM. Память оказалось довольно мало, всего 1 килобайт. Поэтому я решил записывать не через равный промежуток времени (что первое пришло в голову), а записывать только изменения показаний.

Ну и наконец, добавил вывод из памяти на компьютер. Долго не думал, как дать знать устройству, что пора выводить данные. Поэтому просто отключаю датчик и подсоединяю устройство к компьютеру. Вывод производиться в формате csv-файла. Потом я его открываю с помощью Excel, редактирую и строю график.

Вот что получается в результате. Красная вертикальная линия обозначает окончание приёма душа. На графике хорошо видно, что после отдергивание шторки влага еще 25 минут распространяется по всему объему помещения, а затем начинает падать. 

Первый вариант графика (который на обложке) получился более красочным, но менее информативным.

Если кому-то пригодилась данная статья и программный код, напишите в комментарии. Я с программным кодом, кстати, сильно не заморачивался, поэтому, он, скорее всего, получился аппаратно зависимым, т.е. на другом микроконтроллере (не AT mega 328) может на работать.  Если кому интересно, напишите, могу сделать аппаратно независимым.