28 сентября 2014 г.

Arduino + Raspberry Pi = термометр + гигрометр с отправкой данных на сервер (часть 1)

А собственно зачем ?

Только недавно начал знакомится с Arduino, и возникла идея сделать что-то не очень сложное, и, собственно, выбор упал на метеостанцию.

Что мне понадобилось

  • Arduino Uno (осуществляет сбор данных с датчиков, отправляет данные на Raspberry Pi и отображает данные на дисплее)
  • Raspberry Pi Model B+ (отправка данных на веб-сайт, ftp-сервер и на OpenWeatherMap, а так же сохранение данных на флешку)
  • датчик температуры DS18B20+ и датчик температуры/влажности DHT11
  • LCD дисплей 16x2
  • макетная плата под пайку (для замены беспаечной макетки)
  • 1 пищевой контейнер
  • провода для соединения экрана и макетки с arduino 
  • несколько метров витой пары и разъемы на 3 провода (такие же, как на корпусных вентиляторах для пк)
  • канистра из-под питьевой воды (для защиты датчиков от дождя)
  • несколько саморезов и прочей мелочи

Как это все собрано

Сразу скажу, что в 1-й части я решил описать все, что связано непосредственно с самим arduino и датчиками, об остальном я подробно расскажу в следующей части.

Итак, начнем. Для удобства сделал схему в Fritzing. Желательно ее скачать.
dht11 ds18b20 схема подключения к arduino термометр гигрометр

Теперь по-порядку, что здесь к чему.

Начнем с того, почему здесь нет беспаечной макетки. Все просто - она не удобна, когда нужно поместить устройство в корпус, и еще не удобней, когда нужно подключать длинные провода, идущие к датчикам. Макетка тут выполняет роль своеобразного коммутатора, то есть у нас подходит один провод +5V от arduino и питает линию контактов (красные провода - +5V, черные - GND, остальные - данные), от которой питаются датчики и экран, аналогично для GND. И у нас есть 2 линии для датчиков, на этой схеме каждому датчику отведена целиком каждая, но если у вас будет макетка другого формата, то это можно будет оптимизировать.

Так же у нас есть 2 подтягивающих резистора по 5 КОм (на схеме 4.7К, потому что нарисовать на 5К не удалось :-/ ), для ds18b20 резистор обязателен, а иначе ничего не будет работать, для dht11 рекомендуется его поставить при длине провода до 20 метров, если нужно длиннее - то нужно подбирать меньшее значение опытным путем. 

Как подключить экран читаем здесь. ВАЖНО!  Внимательно следите за нумерацией контактов на дисплее, на схеме они идут по-порядку от 1 до 16 справа налево. В экране от амперки порядок отличается. Поэтому скачиваем проект и внимательно смотрим номера контактов.

Датчики у меня подключены с помощью двух витых пар, длиной примерно метра 4-5 каждая, никаких проблем с ними не наблюдалось.

А теперь немного фото. Вот так выглядят внутренности:
dht11 ds18b20 схема подключения к arduino термометр гигрометр

Цвета проводов со схемой не совпадают, потому что делал все из того, что было))
Моя макетка отличается от той, что на схеме и разводка сделана очень паршиво, поэтому копировать ее не стоит, делайте сразу как положено.

Штекера на витой паре:
dht11 ds18b20 схема подключения к arduino термометр гигрометр коммутатор

Плату arduino и макетку закрепил саморезами из старых китайский игрушек, дополнительно сделал подкладки из мягкого шланга (резал его вдоль на куски, из-за чего они немного пружинят и нет необходимости сильно притягивать плату саморезами), подобные подкладки сделаны и для дисплея, там лучше подошел разрезанный поперек шланг (около 0.5 см):

Целиком все выглядит вот так (тут датчики подключены без витых пар):
dht11 ds18b20 arduino термометр гигрометр общий вид

В закрытом виде:
lcd дисплей arduino термометр гигрометр

А так выглядят датчики на улице:
dht11 ds18b20 датчики на улице arduino термометр гигрометр

Как это все работает или разбираем скетч

Предварительно стоит сказать, что скетч я старался написать не так, чтобы "лишь бы работало", а старался сделать обработку ошибок, чтобы можно было хотя бы узнать, а почему же вдруг исчезли показания с одного из датчиков. Теперь про отсылку данных - все данные отправляются в формате CSV с интервалом 5 минут, причем опрос датчиков происходит каждые 30 секунд, а отправляемые показания усредняются (на дисплее отображаются текущие показания). А теперь переходим к коду.
#include <dht.h>
#include <LiquidCrystal.h>
#include <OneWire.h>

#define DS18B20_ID 0x28
#define DHT11_PIN 2
#define LED 13
#define DS18B20_TRYOUT 5
#define DS18B20_PIN 3

enum DS18_ERR{NOT_FOUND = 1, CRC_ADDR_FAIL, NOT_DS18B20_DEV, CRC_SCRATCH_FAIL};

dht DHT;
LiquidCrystal lcd(4, 5, 9, 10, 11, 12);
OneWire ds(DS18B20_PIN);
char tempS[16], humS[16], ds18_tempS[16];
float ds18_temp;
byte ds18_err;
byte addr[8];
// интервал опроса датчиков
unsigned long sensors_interval = 30000;
// интервал отображения информации с датчиков (поочередно DS18B20+ и DHT11)
unsigned long lcd_interval = 15000;
// интервал отсылки данных на Raspberry Pi (каждые 5 минут)
unsigned long sending_interval = (unsigned long) 5*60*1000;
/* в этих переменных будем хранить значения таймера в момент выполнения действия
Я постарался избежать использования функции delay(), потому что планирую сделать управление через меню или кнопки, а эта функция не позволит этого сделать, подробнее см. ниже
*/
unsigned long prevMillis_sensors = sensors_interval, prevMillis_lcd = lcd_interval, prevMillis_sending = sending_interval;
// с какого датчика будем отображать данные на дисплее (0 - с DHT, 1 - с DS18B20)
byte temp12 = 0;
/* переменные для подсчета средних значений
Подсчитывать будем крайне просто: суммируем значения на каждом считывании, увеличиваем счетчик (сколько раз мы уже считали), а при отправке отправляем сумма_значений/счетчик
*/
float ds18_temp_mid = 0, DHT_temp_mid = 0, humid_mid = 0;
// сколько раз считали данные (так же для подсчета средних значений)
byte ds18_data_read_count = 0;
byte dht_data_read_count = 0;

void setup()
{
  // выводим строку приветствия
  lcd.begin(16, 2);
  lcd.print("MStation v0.1.1");
  lcd.setCursor(0, 1);
  lcd.print("Init...");
  // настраиваем 13-й пин на вывод (это встроенный светодиод)
  pinMode(LED, OUTPUT);
  // настраиваем передачу данных
  Serial.begin(115200);
  // отправляем строку инициализации
  Serial.println("MStationV0.1_Init");
  // ищем датчик DS18B20+
  if (!getAddrDS18B20()) {
    Serial.print("ERR:DS18B20_FATAL_ERROR_");
    Serial.println(ds18_err);
  }
}

void loop()
{
  int chk;
  unsigned long currMillis = millis(); // здесь у нас хранится текущее значение таймера
  
  // ЧИТАЕМ ДАННЫЕ С ДАТЧИКОВ
  if (currMillis - prevMillis_sensors > sensors_interval) {
    prevMillis_sensors = currMillis;
    // ВКЛЮЧАЕМ СВЕТОДИОД
    digitalWrite(LED, HIGH);
    // ЧИТАЕМ С DS18B20+
    if (getTempDS18B20()) {
      // прибавляем 
      ds18_temp_mid += ds18_temp;
      // увеличиваем счетчик
      ds18_data_read_count++;
    } else {
      Serial.print("ERR:DS18B20_ERROR_");
      Serial.println(ds18_err);
    }
    // ЧИТАЕМ С DHT11
    chk = DHT.read11(DHT11_PIN);
    switch (chk)
    {
      case DHTLIB_OK:
        // увеличиваем счетчик и суммируем
        dht_data_read_count++;
        DHT_temp_mid += DHT.temperature;
        humid_mid += DHT.humidity;
        break;
      case DHTLIB_ERROR_CHECKSUM: 
        Serial.println("Checksum_error"); 
        break;
      case DHTLIB_ERROR_TIMEOUT: 
        Serial.println("Time_out_error"); 
        break;
      default: 
        Serial.println("Unknown_error"); 
        break;
    }
    // ВЫКЛЮЧАЕМ СВЕТОДИОД
    digitalWrite(LED, LOW);
  }
  
  // ОТПРАВЛЯЕМ ДАННЫЕ
  if (currMillis - prevMillis_sending > sending_interval) {
    prevMillis_sending = currMillis;
    
    /* Оставил на всякий случай, если вдруг нужно будет отказаться от отсылки средних значений
    Serial.print(ds18_temp, 2);
    Serial.print(',');
    Serial.print((int) DHT.temperature);
    Serial.print(',');
    Serial.println((int) DHT.humidity); */
    
    // Отправляем
    Serial.print((float) ds18_temp_mid/ds18_data_read_count, 2);
    Serial.print(',');
    Serial.print((float) DHT_temp_mid/dht_data_read_count, 2);
    Serial.print(',');
    Serial.println((float) humid_mid/dht_data_read_count, 2);
    
    // обнуляем переменные
    ds18_temp_mid = 0; DHT_temp_mid = 0; humid_mid = 0;
    ds18_data_read_count = 0; dht_data_read_count = 0;
  }
  
  // ВЫВОДИМ ДАННЫЕ НА ДИСПЛЕЙ
  if (currMillis - prevMillis_lcd > lcd_interval) {
    prevMillis_lcd = currMillis;
    // Преобразуем из double в char
    dtostrf(ds18_temp, 3, 2, ds18_tempS);
    // Выводим температуру
    lcd.setCursor(0, 0);
    if (!temp12) {
      lcd.print("Temp1: ");
      lcd.print((int) DHT.temperature);
      lcd.print(" \x99""C    ");
      temp12 = 1;
    } else {
      lcd.print("Temp2: ");
      lcd.print(ds18_tempS);
      lcd.print(" \x99""C");
      temp12 = 0;
    }
    // и влажность
    lcd.setCursor(0, 1);
    lcd.print("Humid: ");
    lcd.print((int) DHT.humidity);
    lcd.print(" %");
  }
}

boolean getTempDS18B20()
{
  byte i;
  byte data[12];
  
  ds.reset();
  ds.select(addr);
  ds.write(0x44, 0); // start conversion, without parasite power
  delay(1000); // delay must be >750ms
  
  ds.reset();
  ds.select(addr);
  ds.write(0xBE); //read scratchpad
  
  for (i = 0; i < 9; i++) {
    data[i] = ds.read();
  }
  // check CRC
  if (OneWire::crc8(data, 8) != data[8]) {
    ds18_err = CRC_SCRATCH_FAIL;
    return false;
  }
    
  ds18_temp = ((data[1] << 8) + data[0])*0.0625; // convert temperaturen
  
  return true;
}

boolean getAddrDS18B20()
{
  byte i;
  i = 0;
  while(!ds.search(addr)) {
    ds.reset_search();
    i++;
    if (i == DS18B20_TRYOUT) {
      ds18_err = NOT_FOUND;
      return false;
    }
    delay(250);
  }
  
  if (OneWire::crc8(addr, 7) != addr[7]) {
    ds18_err = CRC_ADDR_FAIL;
    return false;
  }
  
  if (addr[0] != DS18B20_ID) {
    ds18_err = NOT_DS18B20_DEV;
    return false;
  }
  
  return true;
}
Я сознательно не стал расписывать две последние функции, потому что пришлось бы расписать очень много, и это выходит уже за рамки статьи. Теперь об отказе от delay(). Эта функция во время работы полностью блокирует arduino, то есть если мы указали задержку в 5 минут, то эти 5 минут контроллер будет простаивать, и мы не сможем обработать например нажатие кнопки или тп. Поэтому делаем по-другому. 

Работать будем по очень простому принципу: у нас есть 2 переменные curr и prev, а так же есть функция millis(), которая возвращает значение таймера в миллисекундах. В prev у нас будет храниться значение таймера в момент выполнения действия, а в curr - текущее значение (обновляем эту переменную на каждой итерации главного цикла loop() ). На каждой итерации будем проверять разницу между этими переменными, примерно вот так:
unsigned long interv = 500; // сделаем интервал в 500мс
unsigned long prev = interv; // = interv нужно для того, чтобы выполнить нужное действие сразу

void setup() {
}

void loop() {
  unsigned long curr = millis(); // сохраняем текущее

  // высчитываем разницу, сравниваем ее с интервалом
  if (curr - prev >= interv) {
    // делаем что-то
    prev = curr; // сохраняем время выполнения действия
  }
}
Ну, пожалуй, по коду самое основное я рассказал. На этом можно и остановиться, все остальное (как обрабатываются данные на raspberry, отсылаются на севера и тд) я расскажу в следующих частях.
Написал вторую часть статьи про сбор данных с ардуины на raspberry pi: http://blog.edtex.ru/2015/10/arduino-raspberry-pi-2.html
Для тех, кто хочет разобраться в деталях, выкладываю даташиты для датчиков и дисплея, скачиваем тут.
Комплект исходников можно скачать на GitHub-е.
Данные с метеостанции можно посмотреть тут: edtex.ru

Комментариев нет:

Отправить комментарий