Używanie rekuperatora w zimie ma 1 wadę: do domu zasysane są duże ilości świeżego powietrza. Gdy sąsiad odpala swój węglowy piec, 'świeże powietrze' gryzie w gardło i lepiej byłoby, by zostało sobie na zewnątrz. Stąd pomysł skonstruowania czujnika jakości powietrza, dzięki któremu można by okresowo wyłączać rekuperator.
Postanowiłem zbudować czujnik bazując na module Adafruit Trinket Pro, laserowy czujnik SHARPa GP2Y1010AU0F oraz czujnik MQ135. Poruszam się zupełnie po omacku, bazując na tym, co udało mi się znaleźć w sieci. Jako że RPi nie obsługuje w prosty sposób odczytów analogowych, chciałem rozpoznać bojem świat mikrokontrolerów Arduino. Postanowiłem też, że w możliwie szerokim zakresie bazować będę na komponentach gotowych, by ograniczyć trawienie ścieżek i lutowanie.
Koncepcja jest następująca: Trinekt Pro ma komunikować się z resztą systemu przez sieć 1-wire. Symuluje więc moduły DS2438 i w wartości VAD zwraca odczyty poszczególnych czujników. Co 5 minut zasilany jest czujnik MQ135, który wymaga podgrzania. Po kolejnej minucie uruchamiany jest wentylator, który przepompowuje powietrze przez obudowę czujnika. Po 10 sekundach wentylator jest wyłączany i pomiarów dokonuje czujnik SHARPA i MQ135. Po pomiarach rozpoczyna się oczekiwanie na kolejny cykl.
Każdy z elementów czujnika można omawiać i rozpatrywać osobno. Materiałów w sieci jest mnóstwo, ograniczę się więc do skrótowego przedstawienia:
Jako że czujnik znajdować się ma w dużej odległości od zasilacza, a precyzyjne odczyty wartości czujnika GP2Y1010 wymaga stałego napięcia, jest ono stabilizowane przez step-up/regulator napięcia kupiony na alliexpress za 2 USD
Czujnik ten można kupić za 1,5 USD na alliexpress. Podłączenie jest zupełnie proste: VCC do zasilania (5v), GND to GND i AO to wyjście analogowe, które podłączyć trzeba z AI mikrokontrolera
...kupiony za 10 PLN na allegro. Podłączony jest przez tranzystor P16NF06: lewa noga tranzystora podłączona jest do DO mikrokontrolera, prawa do GND. Wentylator podłączony jest bezpośrednio do VCC (przed regulatorem) oraz go GND przez środkową nogę P16NF06.
Oto, jak całość wyglądała po złożeniu na płytce testowej. Na górze widoczny jest moduł komunikacji NRF24L01, z którego ostatecznie zrezygnowałem (choć działał), jako że implementacja kolejnego sposobu komunikowania się z RPi wydała mi się przerostem i pozostałem przy 1-wire.
Jeszcze zdjęcie płytki prototypowej, obudowy przed złożeniem i czujnika na ścianie:
Czas na kod. By całość działała, konieczne jest dołączenie biblioteki OneWireHub.h
#include "OneWireHub.h" #include "DS2438.h" // Battery Monitor const uint8_t internal_LED = 13; // internal led just to signal when busy const unsigned long int stepTiming[] = {240000, 50000, 10000, 1000, 100}; char currentStep = 0; //for SHARP dust sensor const int dustMeasurePin = 1; // Analog input 1 -> sensor pin 5 const int dustLedPower = 5; // Power the measurement led of the sensor -> sensor pin 3 const int samplingTime = 270; // time after led power-on signal int samples[5]; int maxDustSample = 0; int averageDustSample = 0; //for MQ135 const int gasPower = 6; const int gasMeasurePin = 0; int gasVoltage = 0; //for fan int fanPower = 8; //for timing uint32_t lastAction = 0; // time of the last action/step in ms from millis() uint32_t currTime = 0; // current time om millis() //for 1-wire const uint8_t OneWire_PIN = 9; auto hub = OneWireHub(OneWire_PIN); auto sDustAverage = DS2438(DS2438::family_code, 0x0D, 0x01, 0x0D, 0x01, 0x0D, 0x01); auto sDustMaximum = DS2438(DS2438::family_code, 0x0D, 0x01, 0x0D, 0x01, 0x0D, 0x02); auto sGas = DS2438(DS2438::family_code, 0x0D, 0x01, 0x0D, 0x01, 0x0D, 0x03); void setup() { Serial.begin(115200); Serial.println("Air quality sensor. Config starts..."); pinMode(internal_LED, OUTPUT); pinMode(dustLedPower, OUTPUT); pinMode(gasPower, OUTPUT); pinMode(fanPower, OUTPUT); //attach 1-wire simulated elements hub.attach(sDustAverage); hub.attach(sDustMaximum); hub.attach(sGas); Serial.println("Config done, beginning the cycle..."); lastAction = millis() - stepTiming[currentStep]; } void loop() { // manage 1-wire communication hub.poll(); currTime = millis(); //check if it is time to move on to the next step if (currTime > (lastAction + stepTiming[currentStep])) { lastAction = currTime; currentStep++; switch (currentStep) { case 1: // power up the MQ135 sensor, let it heat! digitalWrite(gasPower, HIGH); Serial.println("1. MQ135 switched on and heating!"); break; case 2: // power up the fan for the air flow digitalWrite(fanPower, HIGH); Serial.println("2. Fan is up, the air is flowing"); break; case 3: // power down the fan digitalWrite(fanPower, LOW); Serial.println("3. Fan is off"); break; case 4: // do the measurements! digitalWrite(internal_LED, HIGH); //show the world we are measurring! Serial.println("4. Start the measurements..."); // measure the dust particles maxDustSample = 0; for (int round = 0; round < 3; round++) { digitalWrite(dustLedPower, LOW); // power on the LED of dust sensor delayMicroseconds(samplingTime); // wait out the time according to specs samples[round] = analogRead(dustMeasurePin); // read the analog input digitalWrite(dustLedPower,HIGH); // turn the LED off Serial.print("-- raw signal values (0-1023): "); Serial.println(samples[round]); if (samples[round] > maxDustSample) maxDustSample = samples[round]; // find the maximum measurment delay(400); } averageDustSample = 0; for (int i = 0; i < 3; i++) { averageDustSample += samples[i]; } averageDustSample = (int)(averageDustSample / 3); Serial.print("--> the maximum sample from dust sensor: "); Serial.println(maxDustSample); Serial.print("--> the average sample from dust sensor: "); Serial.println(averageDustSample); // measure the gases with MQ135 gasVoltage = analogRead(gasMeasurePin); Serial.print("--the reading of MQ135: "); Serial.println(gasVoltage); //power off the MQ135 digitalWrite(gasPower, LOW); Serial.println("5. MQ135 switched off"); digitalWrite(internal_LED, LOW); //set 1-wire values sDustAverage.setVolt(averageDustSample); sDustMaximum.setVolt(maxDustSample); sGas.setVolt(gasVoltage); currentStep = 0; //Finish the round, reset the step counter Serial.print(F("0. Beginging to wait ")); Serial.print(stepTiming[currentStep]); Serial.println(F("ms.")); break; } // end of switch } //end of if (time checking) } // end of loop()
W sieci znalazłem kilka opracowań o pomiarze zapylenia za pomocą czujnika SHARPa. Sam przetestowałem najróżniejsze czasy oczekiwania po włączeniu diody i wielokrotne pomiary. Ostatecznie jednak ustawienia typowe, tj. ze zwłoką 230-280 ms dają w miarę stałe odczyty. Postanowiłem też przesyłać dane maksymalne i średnie, by sprawdzić, które będą właściwsze.
Co do skomplikowanych formuł przeliczania wyników - odczytów napięcia - na konkretne jednostki (np. ppm), stwierdziłem, że na potrzeby tego projektu nie ma to sensu. Nie potrzebuję znać absolutnych wartości zapylenia, a jedynie reagować na nagłe wzrosty odczytów.
Jako że moje RPi i sieć 1-wire funkcjonuje w świecie OWFS, przykładowy kod PHP do odczytu danych wygenerowanych przez czujnik może wyglądać następująco:
<?php $addresses = ["/mnt/1wire/26.0D010D010D01/VAD", "/mnt/1wire/26.0D010D010D02/VAD", "/mnt/1wire/26.0D010D010D03/VAD"]; for ($i = 0; $i < sizeof($addresses); $i++) { $file = fopen($addresses[$i], "r"); if ($file) { $sValue = fgets($file); echo ($sValue * 100).";"; } else { echo "ERROR;"; } }
Aby pokazać, jak działają czujniki, najlepiej spojrzeć na wykresy:
Czujnik SHARPa GP2Y1010 zwraca całkiem stabilne odczyty i może być wykorzystany do wyłapywania nagłych wzrostów zapylenia. Mam jednak wrażenie, że sam pomiar pyłu nie jest wystarczający, by wychwycić momenty zadymiania okolic przez sąsiadów. Nie wiem, jaka substancja powoduje, że powietrze jest kwaśne i ostre. Czy jest to tlenek siarki? Jak go mierzyć?
MQ135, natomiast, to czujnik do zupełnie innych zastosowań. Znawcy tematu mogą powiedzieć, że to sprawa oczywista, ale dla mnie specyfikacje poszczególnych czujników nie są na tyle jednoznaczne, by nie próbować. Jeśli ktoś ma pomysł, który z czujników z serii MQ lub AQ byłby lepszy, chętnie przygarnę taką poradę.
Dla dopełnienia obrazu, oto przykładowy kod do PLC, który można wykorzystać do reagowania na wzrost zapylenia:
VAR rTrigger : R_TRIG; dSensor : DWORD; dData : ARRAY[0..59] OF WORD; dSize : BYTE; dDataCounter : INT; dAverage : REAL; dAcceptedDeviation : BYTE := 50; dAlarm : BOOL := FALSE; dFilled : BOOL := FALSE; (* flag for first table filling *) i : BYTE; END_VAR rTrigger(CLK := HTTPComm.readSuccess); IF rTrigger.Q THEN (* data received *) dSize := SIZEOF(dData) / 2; (*calculate array size dynamically out of laziness*) IF (dSensor < dAverage + dAcceptedDeviation) OR (NOT dFilled) THEN dAlarm := FALSE; dData[dDataCounter] := DWORD_TO_WORD(dSensor); (* calculating average*) dAverage := dData[0]; FOR i := 1 TO (dSize - 1) DO dAverage := dAverage + dData[i]; END_FOR; dAverage := dAverage / dSize; (* increment the array index, INC1 from OSCAT library *) dDataCounter := INC1(dDataCounter, dSize); IF NOT dFilled THEN dFilled := (dDataCounter = (dSize - 1)); END_IF; ELSE dAlarm := TRUE; END_IF; END_IF;