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

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

GIMP = пакетная обработка изображений с помощью BIMP

Возникла такая задача, нужно было обрезать и подогнать под стандартный размер несколько фоток. Можно конечно вручную, но как-то это медленно и нудно. Раньше приходилось сталкиваться с таким, но тогда нужно было только изменить размер, и тогда я воспользовался плагином David's Batch Processor. Штука удобная, но умеет только простейшие функции. Пришлось погуглить, и почти сразу наткнулся на плагин BIMP, который привлек внимание несколько иным набором функционала, и, что самое интересное, есть возможность вызывать функции самого GIMP-a (к сожалению далеко не все, только те, которые работают как процедуры, то есть ничего не возвращают).

Устанавливается он просто. Сначала поставим необходимые пакеты:

sudo apt-get install libgimp2.0-dev libpcre3-dev

Потом скачиваем BIMP, распаковываем и переходим в папку, в которой запускаем компиляцию:

cd ~/bimp
make
make install

Для установки под Windows ничего не нужно компилировать, достаточно только скопировать содержимое папки bin\win32 в папку с плагинами GIMP-a. Для этого заходим в "Мои документы", там ищем папку .gimp2.6 или .gimp2.8, в зависимости от версии, и находим папку plug-ins. И копируем туда все, после чего запускаем GIMP.

Теперь запускаем GIMP и плагин должен в нем появиться:

Запускаем его и теперь можно задавать последовательность нужных нам действий. Мне нужно было автокадрирование и изменение размера. В базовых функциях есть обрезка, но она не умеет автоматическое кадрирование, поэтому выбираем пункт с процедурами GIMP:

Далее ищем нужную процедуру:

Добавляем Resize (изменение размера):

Теперь выбираем изображения для обработки, попутно я добавил переименование уже обработанных фоток по шаблону:

Запускаем на обработку и готово. Так же есть возможность сохранить последовательность действий, что на мой взгляд очень удобно, не нужно будет по несколько раз вбивать одно и то же:

Из крайне полезных функций так же стоит отметить возможность предосмотра результата для каждой картинки. Для этого выбираем нужную в списке:

Жмем на Click for preview и смотрим что должно получиться:

На этом и закончим обзор, с остальными функциями думаю будет легко разобраться. И могу сказать, что этот удобный плагин поможет многим сэкономить драгоценное время.

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

Asus 1015BX + Ubuntu 14.04 = регулировка яркости подсветки

После обновления на Ubuntu 14.04 обнаружилась неприятная вещь - регулировка яркости подсветки стала работать как-то странно, с большими скачками, неудобно и тп. Пришлось погуглить решение проблемы, и вроде бы удалось найти рабочее.
Для этого открываем файл с настройками grub:

sudo nano /etc/default/grub

Там находим строчку:
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"

И добавляем в нее:
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash acpi_osi= acpi_backlight=vendor"

Обновляем настройки:
sudo update-grub

Теперь перезапускаем нетбук. После перезагрузки подсветка должна начать регулироваться как положено, но, к сожалению, не будет уведомления об изменении яркости.
В конце скажу, что данный способ работает с установленным Catalyst (он же fglrx, проверено на версии Catalyst 14.4)

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

Raspberry Pi + Raspbian = автозапуск программы при помощи upstart после синхронизации с NTP

В чем собственно проблема ?

Наверное у многих возникал вопрос, как настроить запуск программы при загрузке. Как известно, на raspberry pi отсутствуют RTC, то есть часы реального времени, поэтому при запуске можно увидеть дату примерного такого вида:

1 января 1970 00:01:00

что само собой не очень хорошо, если вам нужно точно знать время (неважно для чего). В Raspbian уже есть настроенный ntpd, который синхронизирует часы через интернет, все хорошо, НО, это происходит не моментально, и мы вполне можем получить абсолютно неверную дату и время (у меня считывались данные о температуре и влажности с метеостанции на базе Arduino), и получить данные образца 1970 года было совсем и совсем не круто). Поэтому и пришлось искать решение проблемы. Сегодня я расскажу как настроить автозапуск программы без лишних хлопот уже после синхронизации с серверами NTP

Что будем делать

В первую очередь установим Upstart (ВНИМАНИЕ! Лучше не делать этого на рабочей системе, во избежании так скажем, сделайте резервную копию образа системы !!!) Для установки upstart вводим в терминале:

sudo apt-get install upstart

На что система спросит, действительно ли мы хотим это сделать, вводим фразу целиком, которую просит apt. После установки перезагружаемся командой:

sudo reboot

Система должна загрузится без каких-либо проблем. Теперь надо сказать, что конфиги upstart хранятся в папке /etc/init и имеют формат .conf
Создадим в этой папке конфиг для нашей программы:

sudo nano /etc/init/your_serv.conf

Теперь набираем наш конфиг:

description "Program name"
author "Your name"

start on runlevel [2345]
stop on runlevel [!2345]

respawn
respawn limit 10 120

pre-start script
 until ntpq -np | grep -q '^\*';
 do
  echo "NTP not ready... wait"
  sleep 30
 done
end script

exec /usr/local/your_serv.py

post-start script
 echo "daemon started"
 date
end script

Теперь распишу конфиг подробно. description и author указывают, собственно, кто автор конфига и для чего оно надо, эти секции не обязательны, но лучше добавить описание, чтобы не забыть, для чего это вообще было нужно.

start on указывает, когда начинать запуск сервиса (в терминологии upstart), у нас указано, что запуск нужно производить на всех уровнях (про значения чисел и что такое уровни выполнения читаем тут)

stop on - на каких уровнях нужно останавливать сервис (восклицательный знак означает отрицание, то есть не на 2345, как в си)

Стоит наверное сказать, что в этих двух секциях не обязательно должны быть указаны уровни выполнения, upstart предоставляет возможность останавливать и запускать сервис после запуска, остановки, во время работы других сервисов, что позволяет довольно гибко выбирать что и когда у нас должно запускаться и останавливаться.

respawn и respawn limit указывают, что сервис должен перезапускаться при ошибке основной программы, respawn limit меняет значения по-умолчанию, в данном случае будет 10 попыток перезапуска с интервалами в 120 секунд.

pre-start указывает, что нам нужно делать перед запуском основной программы, собственно в этом месте мы и проверяем, синхронизировалась ли наша малинка с NTP.

Связка script ... end script используется когда нам нужно выполнить несколько команд, команды вводятся одна за другой как в обычном терминале, так же можно писать bash-скрипты (что в нашем случае и сделано). Нужно добавить, что после секций типа pre-start и им подобных должно обязательно следовать script или exec (используется если нужно запустить только одну команду).

exec указывает, что нам нужно запускать как основную программу, это так же может быть script ... end script. Но важно отметить, что в конфиге обязательно должен присутствовать exec или script, иначе все остальное просто бессмысленно. То есть данная секция строго обязательна.

post-script указывает, что делать после запуска основной программы. В нашем случае пишем, что запуск произошел и выводим дату как бонус.

Весь вывод, который делает стартовый конфиг или сама программа в консоль, хранится в файле /var/log/upstart/your_serv.log

Теперь запускаем наш сервис командой: sudo service your_serv start
Для остановки используем: sudo service your_serv stop 
Для перезапуска меняем stop на restart
Для проверки работает ли наша программа пишем: service your_serv status

Стоит сказать, что здесь мы использовали далеко не все возможности upstart и не все возможные настройки в конфиге. За подробностями идем на сайт upstart или просто вводим в терминале man 5 init и знакомимся с документацией в полном объеме (upstart должен быть предварительно установлен)

В конце объясню, как происходит проверка, что синхронизация с NTP успешна. Для этого используем ntpq, опции -np указывают что выводим сервера, с которыми происходит синхронизация, в качестве адресов выводи ip
Выглядит это примерно так:
edgar@edgar-desktop:~$ ntpq -pn
     remote           refid      st t when poll reach   delay   offset  jitter
==============================================================================
+91.122.42.73    193.79.237.14    2 u    1   64   77   12.440   56.259  66.320
*79.165.63.245   .GPS.            1 u   63   64   37    2.492   23.173  62.735
+31.131.249.26   89.175.22.41     2 u   62   64   37   11.419   21.781  62.520
+85.159.224.52   89.109.251.21    2 u   63   64   37   32.024   23.979  61.772
 91.189.94.4     131.188.3.220    2 u   60   64   37   56.996   18.162  71.548

Собственно в скрипте вывод передается утилите grep, которая по шаблону ищет в начале каждой строки "*", которая указывает на тот сервер, с которым произошла синхронизация. А grep возвращает истину, если такая строка найдена. Подробнее про шаблоны и grep читаем здесь

Вот собственно и все, если возникнут проблемы или вопросы, пишем в комментариях или мне в личку в контакте сюда