/*Pocobo 2- Pollution Collecting Bottle 2 * * v1.0 (13.06.2025) * * Devices & Parts: * - Arduino MKR NB 1500 (https://www.arduino.cc/en/Guide/MKRNB1500, https://store.arduino.cc/products/arduino-mkr-nb-1500?_gl=1%2Ajmo3e2%2A_ga%2AMTgyMjQxNTg5MC4xNjM3MTQ1NTIw%2A_ga_NEXN8H46L5%2AMTYzNzc0NjM2My4yLjEuMTYzNzc0NjYwOS4w) * - Sensirion SEN54 PM Sensor (https://www.sensirion.com/products/catalog/SEN54) * - Adafruit Mini GPS PA1010D Module (https://learn.adafruit.com/adafruit-mini-gps-pa1010d-module) * - Dipole Pentaband Waterproff antenna (https://store.arduino.cc/en-at/products/dipole-pentaband-waterproof-antenna?queryID=undefined * - Eckstein 3.7V, 2000mAh Battery (https://eckstein-shop.de/LiPo-Akku-Lithium-Ion-Polymer-Batterie-37V-2000mAh-mit-JST-PHR-2-Stecker-LP803860) * - Adafruit Powerboost 500 Charger (https://www.adafruit.com/product/1944) (needed because the Arduino MKR NB can only charge batteries from 700-1500mAh; see also (scroll down): https://store.arduino.cc/products/arduino-mkr-nb-1500?_gl=1%2Ads0y0w%2A_ga%2AMTgyMjQxNTg5MC4xNjM3MTQ1NTIw%2A_ga_NEXN8H46L5%2AMTYzODQ1MzI5My4xNy4xLjE2Mzg0NTUwMzkuMA..) * - Adafruit RGB On/Off Switch (https://www.adafruit.com/product/3426) * * * * Infos & Libraries: * - Arduino MKR NB Library:https://www.arduino.cc/en/Reference/MKRNB * - Arduino MKR NB Features: https://docs.arduino.cc/hardware/mkr-nb-1500 * - Sensirion SEN54 Library Github: https://github.com/Sensirion/arduino-i2c-sen5x * - Sensirion SEN54 Library Arduino: https://www.arduino.cc/reference/en/libraries/sensirion-i2c-sen5x/ * - Comparison for PM2.5 values: https://kachelmannwetter.com/de/luftqualitaet/wien-umgebung/pm25-feinstaub/20211126-1100z.html * - Comparison for PM10 values: https://kachelmannwetter.com/de/luftqualitaet/wien-umgebung/pm10-feinstaub/20211126-1100z.html * - Available Radio Access Technology: https://www.gsma.com/iot/deployment-map/ * - Sparkfun_I2C_GPS_Arduino_Library: For using TinyGPS++ Library with I2C * - Tiny GPS++ Library: http://arduiniana.org/libraries/tinygpsplus/ * - Switch with Powerboost 500: https://learn.adafruit.com/adafruit-powerboost-500-plus-charger/on-slash-off-switch * - Wiring the Button: https://learn.adafruit.com/ambient-color-controller/build-the-circuit * - Example for lighting the button LED: https://create.arduino.cc/projecthub/102550/rgb-light-control-with-arduino-9979df * - Read Battery voltage on MKR Board: https://docs.arduino.cc/tutorials/mkr-wifi-1010/mkr-battery-app-note * - TinyGPS++ i2c example: https://github.com/sparkfun/SparkFun_GPS_Breakout_XA1110_Qwiic/blob/master/Libraries/SparkFun%20I2C%20GPS/examples/Example2-TinyGPS/Example2-TinyGPS.ino * * WIRING: * Arduino SCL <-> SCL Adafruit Mini GPS, SEN54 (4, yellow) * Arduino SDA <-> SDA Adafruit Mini GPS, SEN54 (3, green) * Arduino VIN <-> 5V * Arduino GND <-> GND * SEN54 (1, white) VCC <-> 5V * SEN54 (2, blue) GND <-> GND * SEN54 (5, black) SEL <-> GND * Adafruit Mini GPS VIN <-> 5V * Adadruit Mini GPS GND <-> GND * Adafruit Power Boost 500 5V <-> 5V * Adafruit Power Boost 500 GND <-> GND * Adafruit Power Boost 500 LBO <-> 5 Arduino //DOES NOT REALLY WORK CHECK WIRING AND FUNCTIONALITY * Adafruit RGB ON/OFF Switch C+ <-> 5V * Adafruit RGB ON/OFF Switch C <-> GND * Adafruit RGB ON/OFF Switch R <-> 6 Arduino * Adafruit RGB ON/OFF Switch G <-> 7 Arduino * Adafruit RGB ON/OFF Switch B <-> 8 Arduino * Adafruit RGB ON/OFF Switch NC <-> EN Adafruit Power Boost 500 * * Wiring Button (Colors) * R = Orange * G = Grey * B = Violett * C+ = White * C = Blue * NC = Yellow * NO (Center) = Green * Wiring 22mm Button * 1 -> EN * 2 -> GND * */ #include #include #include #include #include #include #include // The used commands use up to 48 bytes. On some Arduino's the default buffer // space is not large enough #define MAXBUF_REQUIREMENT 48 #if (defined(I2C_BUFFER_LENGTH) && (I2C_BUFFER_LENGTH >= MAXBUF_REQUIREMENT)) || (defined(BUFFER_LENGTH) && BUFFER_LENGTH >= MAXBUF_REQUIREMENT) #define USE_PRODUCT_INFO #endif #define rLEDPIN 6 // Pin for red Button LED #define gLEDPIN 7 // Pin for green Button LED #define bLEDPIN 8 // Pin for blue Button LED #define batPin 5 // Pin to detect if battery gets low RGBLed ledButton(rLEDPIN, gLEDPIN, bLEDPIN, RGBLed::COMMON_ANODE); byte curR = 0; byte curG = 0; byte curB = 0; SensirionI2CSen5x sen5x; // The TinyGPS++ object I2CGPS myI2CGPS; //Hook object to the library TinyGPSPlus gps; boolean encodeGPSAgain = false; //Connection to DATAhub //NBClient client(false); //Otherwise the NBClient.connect() method waits until the internet connection gets ready, if you explicitly prohibit this it will wait forever. NBClient client(false); GPRS gprs; NB nbAccess; boolean connected = false; //Forwarder address for non SSL: http://forwarder.aml.media.tuwien.ac.at:11313/receive-reading/{token} // URL, path and port (for example: example.org // URL DATAHUB = "http://aml.media.tuwien.ac.at:11312/api/sensordata/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjI0LCJpYXQiOjE3NDY3MDgxOTJ9.pKcHaTF88j6usI55lnWdgdXk9PESK4mGbKn6nf9Vv7A"; char server[] = "aml.media.tuwien.ac.at"; //char server[] = "forwarder.aml.media.tuwien.ac.at"; // IMPORTANT: Place correct source Token here: THIS IS POCOBO 2 - 3 char path[] = "/api/sensordata/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjI0LCJpYXQiOjE3NDY3MDgxOTJ9.pKcHaTF88j6usI55lnWdgdXk9PESK4mGbKn6nf9Vv7A"; int port = 11312; // port 80 is the default for HTTP //Measured Data float lastPM1 = 0; float lastPM25 = 0; float lastPM4 = 0; float lastPM10 = 0; float lastHumidity = 0; float lastTemperature = 0; float lastVocIndex = 0; float lastNoxIndex = 0; //Timing unsigned long currentMillis = 0; uint32_t printTimer = millis(); //Just a timer for printing out values unsigned long sendToDATAhubInterval = 20000; //20sek; Interval for sending data to webservice = Data sampling interval unsigned long lastSendToDATAhub = 0; //String URL = "http://aml.media.tuwien.ac.at:11312/api/sensordata/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjE0LCJpYXQiOjE2MDA4NDY0MDR9.0BPUK7NVTlqi7i9QFx9kcVY55jsn0qskPWDL1PKyKec"; // Sensor Status // used for the LED // 0 = Startup // 1 = Sensor Running / Everything is fine // 2 = Measuring Data (Optional) // 3 = No GPS // 4 = No cell network // 5 = Battery Low // 6 = Sensor Error // 7 = Testoutput byte statusInfo = 0; //int sendcount = 0; /********************************************* SETUP *********************************************************/ void setup() { updateLED(statusInfo); pinMode(batPin, INPUT_PULLUP); Serial.begin(115200); Serial1.begin(9600); //sendcount = 0; //while(!Serial); // UNCOMMENT(remove) // CONNECT Cell network //Serial.println("Starting connection to NB-IOT Service (A1)"); // connection state //updateLED(4); connected = false; // After starting the modem with NB.begin() // attach to the GPRS network with the APN, login and password connectToNetwork(); updateLED(statusInfo); Wire.begin(); //Connect GPS to I2C //updateLED(3); if (myI2CGPS.begin() == false) { //Serial.println("GPS Module failed to respond. Please check wiring."); statusInfo = 3; updateLED(statusInfo); while (1) ; //Freeze! } encodeGPS(); updateLED(statusInfo); // Init SEN54 //updateLED(6); sen5x.begin(Wire); uint16_t error; char errorMessage[256]; error = sen5x.deviceReset(); if (error) { //Serial.print("Error trying to execute deviceReset(): "); errorToString(error, errorMessage, 256); //Serial.println(errorMessage); statusInfo = 6; updateLED(statusInfo); } // set a temperature offset in degrees celsius // Note: supported by SEN54 and SEN55 sensors // By default, the temperature and humidity outputs from the sensor // are compensated for the modules self-heating. If the module is // designed into a device, the temperature compensation might need // to be adapted to incorporate the change in thermal coupling and // self-heating of other device components. // // A guide to achieve optimal performance, including references // to mechanical design-in examples can be found in the app note // “SEN5x – Temperature Compensation Instruction” at www.sensirion.com. // Please refer to those application notes for further information // on the advanced compensation settings used // in `setTemperatureOffsetParameters`, `setWarmStartParameter` and // `setRhtAccelerationMode`. // // Adjust tempOffset to account for additional temperature offsets // exceeding the SEN module's self heating. float tempOffset = 0.0; error = sen5x.setTemperatureOffsetSimple(tempOffset); if (error) { //Serial.print("Error trying to execute setTemperatureOffsetSimple(): "); errorToString(error, errorMessage, 256); //Serial.println(errorMessage); statusInfo = 6; updateLED(statusInfo); } else { //Serial.print("Temperature Offset set to "); //Serial.print(tempOffset); //Serial.println(" deg. Celsius (SEN54/SEN55 only"); } //Do an initial reading readSEN(); updateLED(statusInfo); if (statusInfo == 0) statusInfo = 1; //Init finished without error. Statusinfo is still 0 updateLED(statusInfo); } /********************************************* LOOP *********************************************************/ void loop() { //updateLED(7); //Sending to DATAhub currentMillis = millis(); if ((currentMillis - lastSendToDATAhub >= sendToDATAhubInterval)) { lastSendToDATAhub = currentMillis; updateLED(2); Serial.println("start sending"); //TODO: Do this in a certain time interval //Serial.println("Start checking battery!"); //checkBattery(); //does not really work ??? //Encode GPS //Serial.println("Start encoding gps!"); encodeGPS(); //Read Sensor Data //INFO: Perhaps better to put in a timed interval; Also see sds.setCustomWorkingPeriod //Serial.println("Start reading sensor data!"); readSEN(); //Serial.println("After Sensor read; before DELAY"); delay(1000); //Sending data to DATAhub Webservice //Serial.print("Start sending to DATAhub. statusInfo: "); //Serial.println(statusInfo); /*if (!gps.location.isUpdated()) { Serial.println("ERROR: GPS data not updated (no new signal)"); statusInfo = 3; updateLED(statusInfo); }*/ int satNum = gps.satellites.value(); Serial.println("Satellites: " + String(satNum)); //encodeGPS(); //Encode again if (statusInfo != 3 && statusInfo != 4 && statusInfo != 6) { //no gps, mobile connection and sensor error -> send sendToDATAhub(); } //sendToDATAhub(); updateLED(statusInfo); } } /********************************************* sendToDATAhub *********************************************************/ void sendToDATAhub() { //Serial.println("connecting to DATAhub..."); //Serial.println("Getting geo json datastring"); String dataString = getGeoJSONDataString(); //Serial.println("GOT Geo json datastring"); unsigned int len = dataString.length() + 1; char data[len]; dataString.toCharArray(data, len); //Serial.println("Transformed geo json data string to char array."); Serial.println("Server: " + String(server) + " Port: " + String(port)); // if you get a connection, report back via serial: if (client.connect(server, port)) { Serial.println("connected to server -> YAY I CAME ACROSS THE CONNECTION PART!"); //if (statusInfo != 3 && statusInfo != 4 && statusInfo != 6) { // Make a HTTP request: client.print("POST "); client.print(path); client.println(" HTTP/1.1"); client.print("Host: "); client.println(server); client.println("Content-Type: text/plain"); client.print("Content-Length: "); client.println(strlen(data)); client.println(); client.println(data); client.println("Connection: close"); client.println(); //} Serial.println("...sent to DATAhub!"); if(statusInfo ==2) statusInfo = 1; } else { // if you didn't get a connection to the server: Serial.println("sendToDATAhub: connection failed"); statusInfo = 4; updateLED(statusInfo); connected = false; Serial.println("Shutting down the modem"); nbAccess.shutdown(); //restart the modem delay(5000); connectToNetwork(); } } void connectToNetwork() { // After starting the modem with NB.begin() // attach to the GPRS network with the APN, login and password //&& (gprs.attachGPRS() == GPRS_READY) while (!connected) { Serial.println("Connecting to network!"); if ((nbAccess.begin() == NB_READY)) { Serial.println("Connected to NB-IOT Service (A1)"); connected = true; if(statusInfo ==2) statusInfo = 1; } else { Serial.println("Not connected to NB-IOT Service (A1)"); delay(1000); statusInfo = 4; updateLED(statusInfo); } } } /********************************************* encodeGPS *********************************************************/ void encodeGPS() { unsigned long start = millis(); // For one second we parse GPS data and report some key values for (start; millis() - start < 1000;) { while (myI2CGPS.available()) { //available() returns the number of new bytes available from the GPS module gps.encode(myI2CGPS.read()); //Feed the GPS parser } } //Debugging //if we haven't seen lots of data in 5 seconds, something's wrong. if (start > 5000 && gps.charsProcessed() < 10) { //Serial.println("ERROR: Not getting any GPS data! Encoding again"); statusInfo = 3; updateLED(statusInfo); encodeGPS(); //Encode again } else if (!gps.location.isValid()) { //also add timer for circumstances when there is definately no signal to find Serial.println("ERROR: GPS Data not valid! Encoding again"); statusInfo = 3; updateLED(statusInfo); encodeGPS(); //Encode again } else if (gps.satellites.value() < 3) { Serial.println("ERROR: Not enough GPS satellites"); statusInfo = 3; updateLED(statusInfo); //encodeGPS(); //Encode again } else { if(statusInfo ==2) statusInfo = 1; //no error occured } //printGPSData(); } /********************************************* PrintGPSDATA *********************************************************/ void printGPSData() { //We have new GPS data to deal with! Serial.println(); if (gps.time.isValid()) { Serial.print(F("Date: ")); Serial.print(gps.date.month()); Serial.print(F("/")); Serial.print(gps.date.day()); Serial.print(F("/")); Serial.print(gps.date.year()); Serial.print((" Time: ")); if (gps.time.hour() < 10) Serial.print(F("0")); Serial.print(gps.time.hour()); Serial.print(F(":")); if (gps.time.minute() < 10) Serial.print(F("0")); Serial.print(gps.time.minute()); Serial.print(F(":")); if (gps.time.second() < 10) Serial.print(F("0")); Serial.print(gps.time.second()); Serial.println(); //Done printing time } else { Serial.println(F("Time not yet valid")); } if (gps.location.isValid()) { Serial.print("Location: "); Serial.print(gps.location.lat(), 6); Serial.print(F(", ")); Serial.print(gps.location.lng(), 6); Serial.println(); } else { Serial.println(F("Location not yet valid")); } } /********************************************* readSEN *********************************************************/ void readSEN() { // Start Measurement //TODO: Remove from setup()? uint16_t error; char errorMessage[256]; error = sen5x.startMeasurement(); if (error) { //Serial.print("Error trying to execute startMeasurement(): "); errorToString(error, errorMessage, 256); //Serial.println(errorMessage); statusInfo = 6; updateLED(statusInfo); } error = sen5x.readMeasuredValues( lastPM1, lastPM25, lastPM4, lastPM10, lastHumidity, lastTemperature, lastVocIndex, lastNoxIndex); if (error) { //Serial.print("Error trying to execute readMeasuredValues(): "); errorToString(error, errorMessage, 256); //Serial.println(errorMessage); statusInfo = 6; updateLED(statusInfo); } else { //printSen54Data(); if(statusInfo ==2) statusInfo = 1; } //status = 7; delay(1000); //updateLED(statusInfo); } /********************************************* printSEN54DATA *********************************************************/ void printSen54Data() { Serial.print("PM 1.0: "); Serial.print(lastPM1); Serial.print("\t"); Serial.print("PM 2.5: "); Serial.print(lastPM25); Serial.print("\t"); Serial.print("PM 4.0: "); Serial.print(lastPM4); Serial.print("\t"); Serial.print("PM 10.0: "); Serial.print(lastPM10); Serial.print("\t"); Serial.print("Humidity: "); if (isnan(lastHumidity)) { Serial.print("n/a"); } else { Serial.print(lastHumidity); } Serial.print("\t"); Serial.print("Temperature: "); if (isnan(lastTemperature)) { Serial.print("n/a"); } else { Serial.print(lastTemperature); } Serial.print("\t"); Serial.print("VocIndex: "); if (isnan(lastVocIndex)) { Serial.print("n/a"); } else { Serial.print(lastVocIndex); } Serial.print("\t"); Serial.print("NoxIndex: "); if (isnan(lastNoxIndex)) { Serial.println("n/a"); } else { Serial.println(lastNoxIndex); } } /********************************************* getGEOJSONDataString *********************************************************/ String getGeoJSONDataString() { /* GeoJSON should look like this: { "type": "Feature", "geometry": { "type": "Point", "coordinates": [100.0, 0.0] }, "properties": { "PM 1": [5.5, "myg/m3"], "PM 2.5": [43.5, "myg/m3"], "PM 4": [10.5, "myg/m3"], "PM 10": [21.5, "myg/m3"], "Humidity": [70, "%"], "Temperature": [23,5, "°C"], "VOC": [102, "VOC Index"], "timestamp": "123456723495", } } */ /* String json = "{\"type\":\"Feature\",\"geometry\":{\"type\": \"Point\", \"coordinates\":["+ String(gps.location.lat()) + "," + String(gps.location.lng()) + "]}, \"properties\": {\"PM 1\":[" + String(lastPM1) + ", \"myg/m3\"], \"PM 2.5\":[" + String(lastPM25) + ", \"myg/m3\"], \"PM 4\":[" + String(lastPM4) + ", \"myg/m3\"], \"PM 10\":[" + String(lastPM10) + ", \"myg/m3\"], \"Humidity\":[" + String(lastHumidity) + ", \"%\"], \"Temperature\":[" + String(lastTemperature) + ", \"°C\"], \"VOC\":[" + String(lastVocIndex) + ", \"VOC Index"], \"timestamp\":\"""\"}}"; */ String json = "{\"type\":\"Feature\",\"geometry\":{\"type\": \"Point\", \"coordinates\":[" + String(gps.location.lng(), 6) + "," + String(gps.location.lat(), 6) + "]}, \"properties\": {\"PM 1\":[" + String(lastPM1) + ", \"myg/m3\"], \"PM 2.5\":[" + String(lastPM25) + ", \"myg/m3\"], \"PM 4\":[" + String(lastPM4) + ", \"myg/m3\"], \"PM 10\":[" + String(lastPM10) + ", \"myg/m3\"], \"Humidity\":[" + String(lastHumidity) + ", \"%\"], \"Temperature\":[" + String(lastTemperature) + ", \"°C\"], \"VOC\":[" + String(lastVocIndex) + ", \"VOC Index\"], \"timestamp\":\"""\"}}"; //TODO: print this out! //Serial.println(json); //Serial.println("GeoJSON Datastring built"); return json; } /********************************************* GetGeoJSONTestString *********************************************************/ String getGeoJSONTestString() { /* GeoJSON should look like this: { "type": "Feature", "geometry": { "type": "Point", "coordinates": [100.0, 0.0] }, "properties": { "Temperature": [24.5, "°C"], "Noise": [100.0, "dB"], "timestamp": "123456723495", } } */ //String json = "{\"type\":\"Feature\",\"geometry\":{\"type\": \"Point\", \"coordinates\":[16.791124, 47.865672]}, \"properties\": {\"Temperature\":[35, \"°C\"], \"Noise\":[130, \"dB\"], \"timestamp\":\"\"}}"; String json = "{\"type\":\"Feature\",\"geometry\":{\"type\": \"Point\", \"coordinates\":[16.504511, 48.225247]}, \"properties\": {\"PM 1\":[" + String(lastPM1) + ", \"myg/m3\"], \"PM 2.5\":[" + String(lastPM25) + ", \"myg/m3\"], \"PM 4\":[" + String(lastPM4) + ", \"myg/m3\"], \"PM 10\":[" + String(lastPM10) + ", \"myg/m3\"], \"Humidity\":[" + String(lastHumidity) + ", \"%\"], \"Temperature\":[" + String(lastTemperature) + ", \"°C\"], \"VOC\":[" + String(lastVocIndex) + ", \"VOC Index\"], \"timestamp\":\"" "\"}}"; return json; } /********************************************* checkBattery *********************************************************/ void checkBattery() { //DOES NOT WORK: LOW LED on the Powerboost is active but batPin reading is 1 // reads the pin connect to the LBO output of the powerboost 500 Unit int batState = digitalRead(batPin); if (batState == LOW) { //Sets the LED to red if battery is low statusInfo = 5; updateLED(statusInfo); } //Serial.println("Battery state: " + batState); } /********************************************* updateLED *********************************************************/ void updateLED(byte status) { //Status and LED Colours: //0 = While Setup/Startup: YELLOW (255, 225, 0) //1 = Running & Everything is OK: GREEN (0, 255, 0) //2 = Measuring DATA: GREEN (200, 255, 50) //3 = NO GPS Signal/Position/Fix: BLUE (0, 0, 255) //4 = NO Cell Network/Connection: PINK (255, 0, 255) //5 = Batterry low: RED (255, 0, 0) //6 = Sensor Error: VIOLETT (170, 0, 255) //7 = TESTOUTPUT GREEN/PETROL (3, 252, 177) statusInfo = status; if (status == 0) { ledButton.crossFade(curR, curG, curB, 255, 255, 0, 10, 500); curR = 255; curG = 255; curB = 0; } else if (status == 1) { ledButton.crossFade(curR, curG, curB, 0, 255, 0, 10, 500); curR = 0; curG = 255; curB = 0; //} else if (status == 2) { ledButton.flash(200, 255, 50, 50); } else if (status == 2) { ledButton.crossFade(curR, curG, curB, 200, 255, 50, 10, 500); //ledButton.flash(200, 255, 50, 1000); curR = 200; curG = 255; curB = 50; } else if (status == 3) { ledButton.crossFade(curR, curG, curB, 0, 0, 255, 10, 500); curR = 0; curG = 0; curB = 255; } else if (status == 4) { ledButton.crossFade(curR, curG, curB, 255, 0, 255, 10, 500); curR = 255; curG = 0; curB = 255; } else if (status == 5) { ledButton.crossFade(curR, curG, curB, 255, 0, 0, 10, 500); curR = 255; curG = 0; curB = 0; } else if (status == 6) { ledButton.crossFade(curR, curG, curB, 170, 0, 255, 10, 500); curR = 170; curG = 0; curB = 255; } else if (status == 7) { ledButton.crossFade(curR, curG, curB, 3, 252, 177, 10, 500); curR = 3; curG = 252; curB = 177; //Testoutput } statusInfo = status; }