Мониторинг влажности растений, комплексный подход (на самом деле, на коленке)
#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() нужен для моргания светодиодом, когда в хранилище данных по выбранному датчику хранятся аномальные низкие значения – что подразумевает неподключенный датчик.
Вот такой вот быдлокодинг. Но это он еще не закончился.