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


#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

В блоке определений (#define) классически указываем позиции уникальные для каждого устройства (на всякий случай: #define это не переменная, это просто подстановка одних значений вместо других для упрощения кода, не жертвуя памятью устройства). Так как наша схема подразумевает отношение “много ардуин <=> одна raspberry” то каждая ардуинка должна быть каким-то параметром уникальна. В этой роли выступает slave i2c-адрес. Поэтому перед прошивкой нужно быть уверенным что он уникальный во всей собранной схеме. activator pin – пин к которому подключена кнопка, led* это пины RGB-светодиода по цветам.

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


//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

Следующим идет блок глобальных переменных, в нем объявляются хранилища времени (для организации задержек), массивы для данных, флаги и прочее. Есть возможность включить отладку программы, при которой результаты всех промежуточных итераций выводятся в serial-порт (почему-то это ломает i2c). Обратите внимание на переменную reqType – это байт, который приходит от raspberry и представляет собой запрос, в зависимости от которого будет выполнятся программа. Массив sensorArray с размерностью равной количеству пинов указанных в прошлом блоке – это хранилище флагов, где флаг – состояние датчика, нужно его опрашивать, или нет. Сами показатели с датчиков будут лежать в dataArray.
Fade-производные переменные нужны для корректной “пульсации” светодиодом когда он отображает состояние неподключенного датчика.
Для других моделей arduino важно корректно указать список аналоговых пинов, в таком же формате:

const uint8_t analog_pins[] = {A0,A1,A2,A3,A6}


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!");
  }
}

В setup находится код, выполняющийся при включении устройства. Здесь прописаны остальные значения по умолчанию, которые можно изменить перед компиляцией. В виде переменных они сделаны неспроста – это “задел на будущее” – логично иметь возможность переписывать эти значения по требованию с головного устройства. Протокол i2c (насколько я понял) не подразумевает возможности slave-устройствам обращаться с запросами к master, поэтому для начального функционирования нужны некоторые стандартные значения. Возможно, целесообразным будет сообразить какой-нибудь EEPROM для хранения этих значений, чтобы они не сбрасывались после перезагрузки.

Кстати, как я позже выяснил, EEPROM не нужно соображать, он есть и работает и в нём вполне можно хранить данные. Что ж, очередное to-do.


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();
}

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

  • Срабатывание таймера, в котором проверяется условие наступления времени с указанным периодом (берется из переменной period)
  • Нажатие кнопки (два вида)

Когда срабатывает таймер, дается небольшая временная заминка для “прогрева” датчика, после чего, для каждого пина с выставленным флагом выполняется функция collect.

По короткому нажатию кнопки инкрементируется ledActive и вызывается функция led.

По длинному нажатию выставляется флаг 1 для нулевого датчика и собираются с него показатели.

Также в цикле выполняется функция 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");
		  }   
	  }
   }
}

Процедура receive активируется при получении запроса по i2c. Она считывает полученный байт, проверяет является ли число в этом байте номером датчика и, если да, отправляет этот байт процедуре collect. Соответственно, можно к условию дописать else и обрабатывать уже сервисные запросы.


int collect(int sensNumber) {
    //Here goes loop for calculating median of value
    for (int 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];
    }
    led();
  }
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;
      }
    }
  }
}

Процедура сбора показателей с датчиков сделана с учетом возможных кратковременных сбоев и глюков, и выглядит следующим образом: в переменной countCollect хранится количество запросов к датчику, из которых затем вычисляется медиана. С отбивочкой в 150 миллисекунд (впрочем, можно переделать в переменное значение), собирается по умолчанию 5 значений и складывается в массив. Затем массив сортируется (процедура sortArray) и берется среднее значение из этого массива (смысл медианы).


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);
        }
}

На i2c – запрос, если он содержит номер датчика, нужно же как-то ответить, и в процедуре send мы отправляем данные из массива-хранилища соответствующие запрошенному датчику. Единственное, что нужно учесть – потому что я глуп и ленив, я не стал разбираться с механизмом отправки и получения “фрагментированных” данных – отправлять большие значения побайтово, а на принимающей стороне их собирать снова в исходные. Поэтому, выбрав легкий путь, я просто сделал линейное отображение десяти битов в восемь (интервала 0-1023 в интервал 0-255), немножко потеряв в точности. Кстати, сделано это именно здесь, перед отправкой, чтобы на самой ардуине хранить все же изначальные показатели влажности, на всякий случай (хотя это неэкономно).


void led () {
  ....
  много однообразного кода
  ....
}
void fader () {
  ....
  еще вспомогательный код
  ....
}

Процедура led() оставлена скорее как небольшая заделка на будущее, а также для дебага и headless режима (если нету распберри и нечем опрашивать арудину). То есть, в принципе световой индикатор бывает и не лишним, правда в текущем виде он имеет захардкоженые интервалы цвета свечения в зависимости от показателей в массиве данных. Также, переключение работает недостаточно точно – так как в коде присутствуют команды delay(), то во время их выполнения нажатие на кнопку не отрабатывает (решается введением interrupts, но очередной раз напомню – я ленивый).

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


Вот такой вот быдлокодинг. Но это он еще не закончился.