Мониторинг влажности растений, комплексный подход (на самом деле, на коленке)

Небольшое предисловие
Этот пост мало того что писался очень долго (около трех месяцев, и все из-за моей лени, естественно), и при этом сильно позже того, как я закончил конструировать и тестировать концепт. Некоторые идеи весьма устарели, код требует доработки, где-то просто сместились приоритеты. Но, все же, я не думаю что имеет смысл “обновлять” статью по мере этих изменений, а лучше каждый следующий этап (если они будут) отражать в новых постах.

Как-то так всегда получилось, что я не был приучен к своевременному поливу цветов, и этим всегда занимался кто-то другой. В какой-то момент, отсутствие внимания к поливу растений стало критичным для их принципиального существования, поэтому мне пришлось вырабатывать у себя соответствующие привычки – проверять землю по приходу домой, обращать внимание на пожухлось листвы и прочее. И уже тогда в моем жидком мозге появился некий пунктик – нужно срочно мониторить сухость растений!

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

Кстати, советую сайт-блог-учебник Easyelectronics, лучшее из того что я нагуглил. Хотя, опережая возможный гнев читателя, безусловно:
А) надо было бы читать школьные учебники!
Б) нужно было нормально учиться в школе!
В) как такого не знать, работая по инженерной специальности!

Таким вот образом, практически незаметно для себя, я набрел на arduino. Хайп относительно DIY на этих микропроцах уже давно прошел, а я вот только созрел. Изучив ютаб, документацию, и несколько чужих проектов, пришло понимание, что собирать поделки на ардуине действительно просто и не требует знаний какого-либо серьезного порядка, при этом позволяет воплощать свои фантазии без серьезных финансовых затрат, или поисков каких-то суперспециальных железяк на алиекспрессе.

В интернете тысяча и одна штука проектов “умных теплиц”, всяких автополивочных и прочего, но я не встретил ни одного варианта интеграции в существующую систему умного дома и, как следствие, проектирования системы с максимально простым и удобным масштабированием. В общем-то, у меня сразу появилось стойкое ощущение что энтузиасты с ютуба делают свои наработки по большей части в стиле proof of concept: свою идею достаточно реализовать в хоть сколько-то работающем виде, показать что она функционирует, после чего интерес к ней угасает и проект разбирается\пылиться на полке. Я для себя решил сразу – чтобы собственный проект считать хоть чего-то стоящим, я должен ответить себе на несколько вопросов:

  • Готов ли я реализовать проект для кого-то другого человека, не опасаясь за то, что придется каждый раз беспокоится за его работу? Не случится ли так, что поддержка такого проекта будет занимать больше сил чем его разработка? Это самый ужаснейший из исходов – когда проклинаешь себя что вообще взялся за создание чего-то подобного.
  • Безопасен ли проект? Если оставить его без присмотра на месяц\год, есть хоть малейшая вероятность пожара\потопа\лягушек с неба? Если да – взрослый адекватный человек (хотелось бы считать себя таковым) таким пользоваться не станет.
  • Можно ли рассчитать MTBF (наработку на отказ)? Срок до “капиталки” должен быть или очень большим или рассчитываемым. Нужно знать, что вот такая-то часть проекта потребует технического обслуживания примерно раз в год. Вот такая вот часть не должна ломаться. Третья часть, теоретически, может выйти из строя при некоторых условиях, но наличие одной запасной детали будет оптимально.

Чего греха таить – на момент написания этих строк, мой проект находится не в том положении – концептуально работает, но ни на один из вышеперечисленных вопросов я не могу ответить утвердительно. Поэтому этот пост – просто затравка, некий фундамент, на котором при должном усердии можно собрать что-то хоть немного полезное.

Итак, первоначальный концепт проекта был таков:

К ардуине подключается максимальное возможное количество датчиков влажности почвы. Показания с датчиков отправляются на устройство способное запускать полноценную ОС, я выбрал для этого Raspberry Pi 2. На ней должен производиться сбор данных, их хранение и отображение. В идеале, должен быть пользовательский интерфейс, через который можно было увидеть все датчики. Каждому датчику можно будет сопоставить растение, со детальным описанием, к которому можно выставить границы предупреждений высыхания, диапазоны “красных”, “жёлтых” и “зелёных” областей, в зависимости от типа растения. Примерно, как-то так.

К сожалению, так просто я не смог подобрать инструментарий для полного осуществления плана (что неудивительно), и начал подозревать что для такого функционала нужны самописные решения. Так как писать приложения я не умею, то у меня было два варианта: таки научиться писать приложения или подобрать некий заменитель. Немного поразмыслив и решив что всему свое время, я выбрал заменитель.

И вот к чему я в итоге пришел.

Общий вид

Базовый механизм таков: арудины заняты тем что собирают информацию с датчиков, которые запрашивает raspberry по протоколу i2c.

i2c (Inter-Integrated Circuit) – протокол обмена данными между устройствами на одной шине, использует две линии передачи: линия данных (SDA) и линия тактов (SCL). Он достаточно хитро работает, я ограничился прочтением статьи на педивикии, но даже она оказалась для меня не очень простой.

Схема подключения

На картиночке мы можем наблюдать основные элементы системы. На самом деле, ими являются только три электронных компоненты – raspberry, arduino и гигрометры (датчики влажности) для почвы. Помимо них, на схеме присутствуют rgb светодиод, и кнопка. Они нужны для дебага, но об этом подробнее позже.

Также, в данной схеме используется arduino nano – как самая маленькая и дешевая (из популярных) версия ардуины. Но использовать можно абсолютно любую – я лично тестов не проводил, но логично было бы посоветовать arduino Micro, у них больше аналоговых пинов при тех же размерах платы, а SDA и SCL занимают цифровые выходы. Но плата гораздо менее распространенная, поэтому проблемой может быть не только ее приобрести, но и заставить правильно работать. Если все растения находятся в непосредственной близости друг к другу, то можно обратить внимание и на arduino Mega (16 аналоговых пинов), хотя, вероятно, дешевле будет воспользоваться платой-экстендером или мультиплексором, хотя для этого придется видоизменять код. Вариантов очень много, и подобрать наиболее подходящую плату – задача выходящая за рамки стадии сборки концепта.

Также, из того что я смог попробовать из подручных средств – увеличил шину i2c по длине до 2,5 метров, каких-то изменений не обнаружил, данные не начали “гулять” из-за наводок.

Настройка ардуины

Стандартный датчик влажности представляет собой анод и катод, с минимальной электрической обвязкой. Ток идёт от одного зубца к другому, и делает это наиболее хорошо в максимально проводимой среде. На полное замыкание (проводом, к примеру) он не рассчитан, и предполагает что электрическая проводимость между зубьями всегда будет достаточно низкая.

У датчика который я купил (самый дешёвый и популярный вариант), есть 4 контакта: плюс, земля, цифровой выход и аналоговый. Работает он от 5 вольт (от 3,3 в принципе тоже), а полярность не имеет значения. Примерное энергопотребление 3 миллиампера, что, учитывая максимальных выходной ток пинов на арудине в 40 мА, дает возможность питать 10 таких датчиков с небольшим запасом.

Если цифровому выходу сложно придумать применение, то с аналогового можно получать значения в 10 бит, от 0 до 1023, где (если подключать через INPUT_PULLUP) 1023 это полная сухость. При опускании в воду оба моих датчика показывают значения около 400.
В целом, этого уже достаточно чтобы собрать простой тестовый стенд – подключаем датчик к ардуине, плюсы и землю ясно куда, а аналоговый пин датчика к любому аналоговому пину на плате. Переводим этот пин в INPUT_PULLUP (питание через резистор, позволяет избегать наводок и паразитных токов) и снимаем значения в Serial.println.

На самом деле, отдельный подтягивающий резистор все равно не помешает – возможно, в будущем, придется переделать под него код (а лучше с переменной, в которой можно будет выставить работу с резистором\без оного).

Также, я попробовал удлинить провода от “обвязки” где установлен подстроечный резистор до самой “вилки” до ~20 см и не заметил изменений. Но удлинение проводов от обвязки до платы ардуины вызвало странные флуктуации показателей с датчика. Хотя это не являлось корректным экспериментом, и требует качественной проверки.

Но, как я говорил, тестовый стенд очень сильно далек от “промышленного” варианта, и не представляет собой ничего стоящего. Все сложности начинаются тогда, когда появляется потребность организовать хорошую, надежную и адекватную работу.

Итак, коротко о логике работы программы. Ниже представлена абы-какая блок-схема, которая, скорее всего, не соответствует нормативам, но должна отразить суть:

В первую очередь стоит обратить внимание на то, что основа программы – три “ожидания”. Два из них последовательно выполняются в основном цикле void loop, это настриваемый таймер и нажатие кнопки. А одно идет параллельно модулем Wire, активируясь при получении запроса по i2c.
Таким образом, при включении ардуины, без получения управляющего сигнала, она ничего делать вообще не будет. И есть два варианта как ее можно активировать: нажать кнопочку в течении секунды, которая активирует нулевой датчик (подключенный к A0), либо отправить по i2c запрос с номером датчика, информацию с которого хотелось бы получить. В этом случае, этот датчик будет помечен как активный внутренней логикой программы, и, с цикличностью равной period, с него будут собираться показатели.

Помимо запросов по датчикам, управляющий элемент (raspberry pi) может отправлять и другие запросы. Любой запрос, отличный от номера датчика, считается служебным и должен обрабатываться в соответствии с этим, например менять на ходу переменные (типа period) или сбрасывать флаги. Этот механизм предусмотрен, но я его еще не реализовал :).

Дальше я хочу выложить код арудины, донести его логику и попутно объяснить те или иные решения. Сразу нужно оговориться – я нисколько не программист, поэтому у Вас наверняка будет бомбить как от содержательной части кода так и его форматирования (по большей части его просто нет).

Код ардуины
/*
Bydlo code for moisture sensors

*/
#include <Wire.h> //i2c module
#include <JC_Button.h> //easy button module

//#define HardwareSerial_h //disable serial port, significally reduces code size
#define buttonPin 3 //button pin
#define ledRedPin 9 //led pins for testing
#define ledGreenPin 10
#define ledBluePin 11
#define SLAVE_ADDRESS 0x04 // arduino will have that i2c address in slave mode
#define powerPin 5


//timers
unsigned long timing; //timer for collecting cycle
unsigned long fadeTiming; //timer for collecting cycle
unsigned long period; //period for collector in seconds
//data
byte reqType; //request data, input from raspberry
byte countCollects; //number of collects for median calculation
int* dataArray; //data storage for sensors
int* collectArray; //pointer for dynamic array of sensor values creation
bool* sensorArray; //store flags if sensor is requested and must be used in collect function
//pins
const uint8_t analog_pins[] = {A0,A1,A2,A3,A6};
byte sensorPins; //count of sensors calculating from analog_pins, 7 analog pins on nano (used in loops, so starts with zero) minus 2 for i2c (see above)
//debug
bool debugSerial = false; //change to true to enable serial port messages. !!WARNING makes i2c proto almost unusable (dont know why)
//LED
byte ledBrightness; //from 0 to 10
Button ledButton(buttonPin,50); //set button type on buttonPin, with 50 debounce time, input_pullup (JC_button functionality)
byte ledActive; //active led var
byte fadeBrightness = 0; // how bright the LED is
byte fadeAmount = 5; // how many points to fade the LED by
bool fadeOn; // does fading to be used

void setup() {
//set default values:
ledBrightness = 1;
countCollects = 5;
period = 60;
//initialization
Wire.begin(SLAVE_ADDRESS); // I2C start on hard-coded address
Wire.onReceive(receive); //When raspberry sends data, do receive function
Wire.onRequest(send); //response function for answer
ledButton.begin(); //button module init
sensorPins = sizeof(analog_pins); //count of sensor pins calculation
ledActive = sensorPins; //default status for led is off (pins starts with 0, so last pin is (sizeof(analog_pins) - 1).
pinMode(ledRedPin, OUTPUT); // color led init
pinMode(ledGreenPin, OUTPUT);
pinMode(ledBluePin, OUTPUT);
pinMode(powerPin, OUTPUT); //Enable Power pin mode
//creating dynamic arrays:
dataArray = new int [sensorPins]; //creating dynamic array for data storage
collectArray = new int [countCollects]; //creating dynamic array for median calculating
sensorArray = new bool [sensorPins]; //creating dynamic array for flags
//zeroing arrays:
for (byte i = 0; i < sensorPins; i++) { //or i <= 4
pinMode(analog_pins[i],INPUT_PULLUP);
dataArray[i] = 0;
sensorArray[i] = 0;
}
if (debugSerial == true) {
Serial.begin(9600);
Serial.println("Ready!");
}
}

void loop() {
ledButton.read();
if (millis() - timing > (period * 1000 )){ // period of collecting data
timing = millis();
delay(500); //warming sensor for some time
for (byte i = 0; i < sensorPins; i++) {
digitalWrite(powerPin, HIGH);
if (sensorArray[i] == 1 ) { //check array of flags if sensor is used
collect(i); //send to function what sensor need to be processed
if (debugSerial == true) {
Serial.print ("sensorPins = ");
Serial.println (sensorPins);
Serial.print ("Sensor ");
Serial.print (i);
Serial.print (" collected data = ");
Serial.println (dataArray[i]); //debug
Serial.print ("sensorFlag = ");
Serial.println (sensorArray[i]);
}
}
digitalWrite(powerPin, LOW); //collect ended. power off
}
}
//wait for input from button
if (ledButton.wasReleased()) {
if (ledActive < sensorPins) {
ledActive++;
led();
}
else {
ledActive = 0;
led();
}
}
else if (ledButton.pressedFor(1000))
if (digitalRead(buttonPin) == LOW) { //"low" because of pullup
sensorArray[0] = 1;
digitalWrite(powerPin, HIGH);
collect(0); //collecting 0 button for test
delay(500);
digitalWrite(powerPin, LOW); //collect ended. power off
}
if (debugSerial == true) {
Serial.print ("ledActive = ");
Serial.println (ledActive);
Serial.print (" collected data = ");
Serial.println (dataArray[ledActive]); //debug
}
fader();
}
void receive() {
while (Wire.available()) {
reqType = Wire.read();
if (debugSerial == true) {
Serial.print("Get request, type = ");
Serial.println(reqType);
Serial.println(sensorArray[reqType]);
}
if (reqType <= sensorPins) { //check request if it lower than number of maximum available sensors, this is sensor number. Another is service codes
if (sensorArray[reqType] == 0) { //check if flag not set
sensorArray[reqType] = 1; //set flag of sensors array (enable sensor)
if (debugSerial == true) {
Serial.print("Sensor ");
Serial.print(reqType);
Serial.println(" activated");
}
collect(reqType); //collect data from newly activated sensor *!! COMMENT THIS LINE on debug process it is not working with enabled serial port, dont know why*
}
}
else {
if (debugSerial == true) {
Serial.println("Service code received");
}
}
}
}

int collect(byte sensNumber) {
//Here goes loop for calculating median of value
for (byte i = 0; i < countCollects; i++) { //do countCollect number of sensor requests
collectArray[i] = analogRead(sensNumber);
// Serial.println(collectArray[i]); //detailed debug
delay(150); //delay between requests
}
sortArray();
if ( (countCollects % 2) == 0) { //even number of elements
dataArray[sensNumber] = ((collectArray[countCollects/2] + collectArray[(countCollects/2)+1]) / 2);
}
else { //odd number of elements
dataArray[sensNumber] = collectArray[countCollects/2];
}

//send();
led();
}


void send () {
byte metric;
if (debugSerial == true) {
Serial.print("Raw data for sensor ");
Serial.print(reqType);
Serial.print(" is ");
Serial.println(dataArray[reqType]);
}
metric = map(dataArray[reqType], 0, 1023, 0, 255);
metric = constrain(metric, 0, 255);
Wire.write(int(metric));
if (debugSerial == true) {
Serial.print("Sensor ");
Serial.print(reqType);
Serial.print(" data sent = ");
Serial.println(metric);
}
}

void sortArray() {
int out, in, swapper;
for(out=0 ; out < countCollects; out++) { // outer loop
for(in=out; in<(countCollects-1); in++) { // inner loop
if( collectArray[in] > collectArray[in+1] ) { // out of order?
// swap them:
swapper = collectArray[in];
collectArray[in] = collectArray[in+1];
collectArray[in+1] = swapper;
}
}
}
}


void led () {
if (ledActive == sensorPins) {
fadeOn = 0;
analogWrite(ledRedPin, 0);
analogWrite(ledGreenPin, 0);
analogWrite(ledBluePin, 0);
}
else if (dataArray[ledActive] > 900 ) {
fadeOn = 0;
analogWrite(ledRedPin, 25 * ledBrightness);
//Serial.println(50 * ledBrightness);
analogWrite(ledGreenPin, 0);
analogWrite(ledBluePin, 0);
}
else if ((dataArray[ledActive] < 900) && (dataArray[ledActive] > 750)) {
fadeOn = 0;
analogWrite(ledRedPin, 25 * ledBrightness);
analogWrite(ledGreenPin, 15 * ledBrightness);
analogWrite(ledBluePin, 0);
}
else if (dataArray[ledActive] < 750 && (dataArray[ledActive] > 15)) {
fadeOn = 0;
analogWrite(ledRedPin, 0);
analogWrite(ledGreenPin, 25 * ledBrightness);
analogWrite(ledBluePin, 0);
}
else if (dataArray[ledActive] < 15) {
fadeOn = 1;
analogWrite(ledGreenPin, 0);
analogWrite(ledBluePin, 0);
}
}

void fader() {
if ( fadeOn == 1 ) {
if (millis() - fadeTiming > 10 ) { // wait for 30 milliseconds to see the dimming effect
fadeTiming = millis();
analogWrite(ledRedPin, fadeBrightness);
// change the brightness for next time through the loop:
fadeBrightness = fadeBrightness + fadeAmount;
// reverse the direction of the fading at the ends of the fade:
if (fadeBrightness <= 0 || fadeBrightness >= 255) {
fadeAmount = -fadeAmount;
}
}
}
}

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

На второй странице будет исключительно разбор кода ардуины, если он вам неинтересен, можно сразу переходить на 3-ю.