/* ========================================================================================= * * CS408 Environmental Monitoring Independent of Existing Infrastructure * Copyright (C) 2021 Callum Inglis * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * Contact: Callum.Inglis.2018(at)uni.strath.ac.uk * * ========================================================================================= * * Libraries Used * ArduinoJson v6 Library - https://arduinojson.org/v6/doc/ (https://github.com/sandeepmistry/arduino-LoRa/blob/master/LICENSE) * LoRa Library - https://github.com/sandeepmistry/arduino-LoRa/blob/master/API.md (https://github.com/bblanchon/ArduinoJson/blob/6.x/LICENSE.md) * SHTSensor - https://github.com/Sensirion/arduino-sht (https://github.com/Sensirion/arduino-sht/blob/master/LICENSE) * * ========================================================================================= */ #include <TimeLib.h> #include <SPI.h> #include <Wire.h> #include <LoRa.h> #include <SoftwareSerial.h> #include <ArduinoJson.h> #include <base64.h> #include "SHTSensor.h" // Lora Config #define ss 16 // Physical Pin 16 = D0 (Default is Physical Pin 5) #define rst 0 #define dio0 15 // Physical Pin 15 = D8 (Default is Physical Pin 4) static const int loraSpreadingFactor = 7; static const int loraSignalBandwidth = 125E3; static const int loraFrequency = 433E6; // Serial PMS Config static const int RXPin = 4, TXPin = 3; // Temp / Humidity Sensor SHTSensor sht; // Particle Sensor Serial SoftwareSerial pmsSerial(2, 3); // LoRa Message Tracking String sensorID = ""; byte localAddress = 0xBB; byte destination = 0xFF; uint32_t msgCount = 0; // Sensor Values - Temp / Humidity double temperature; double humidity; double avgTemperature = 0; double avgHumidity = 0; // Sensor Values - Co2 byte co2; double avgCo2 = 0; // Sensor Values - Particles uint32_t ppm10; uint32_t ppm25; uint32_t ppm100; uint32_t avgPpm10 = 0; uint32_t avgPpm25 = 0; uint32_t avgPpm100 = 0; // Partial Sensor Data struct pms5003data { uint16_t framelen; uint16_t pm10_standard, pm25_standard, pm100_standard; uint16_t pm10_env, pm25_env, pm100_env; uint16_t particles_03um, particles_05um, particles_10um, particles_25um, particles_50um, particles_100um; uint16_t unused; uint16_t checksum; }; struct pms5003data data; // Polling / Transmitting Config int pollingFrequency = 2500; // Polling Frequency, ms int pollEventCount = 0; // Number of times data has been sampled in this recording period int sendAfterPolls = 10; // Sample Period, after which Transmit Reading int reservationTime = 1; // How long do we require use of TX / RX airways for sending packets? /** * Setup Humidity / Temperature Sensor */ bool setupSHT() { Wire.begin(); if (!sht.init()) { Serial.println("[-] SHT Init Failed"); return false; } sht.setAccuracy(SHTSensor::SHT_ACCURACY_MEDIUM); return true; } double getTemperature() { double t = -1; if (sht.readSample()) { t = sht.getTemperature(); temperature += t; } return t; // -1 on error TODO Make this better! } double getHumidity() { double h = -1; if (sht.readSample()) { h = sht.getHumidity(); humidity += h; } return h; // -1 on error } bool setupLoRa() { LoRa.setPins(ss, rst, dio0); if (!LoRa.begin(loraFrequency)) { Serial.println("[-] Fatal. Starting LoRa failed!"); return false; } LoRa.setSpreadingFactor(loraSpreadingFactor); LoRa.setSignalBandwidth(loraSignalBandwidth); Serial.println("[+] LoRa Initialized OK!"); return true; } /* * Return ESP Module ID, based on MAC address * Source: https://arduino.stackexchange.com/a/58678 * * 2021-11-07 - This method proved unreliable, switched to casting ESP.getChipID() as a string instead */ String getSensorUID() { // uint64_t chipid = ESP.getChipId(); // uint16_t chip = (uint16_t)(chipid >> 32); // // char sensorID[23]; // snprintf(sensorID, 23, "EMIEI-%04X%08X", chip, (uint32_t)chipid); String sensorID = "EMIEI-"; sensorID.concat(String(ESP.getChipId())); return sensorID; } /** * Get data from PMS Partical Sensor * @param Stream s PMS Serial Connection * @modifies struct data PMS Sensor Data * @returns boolean Data Read Success status */ boolean readPMSdata(Stream *s) { if (!s->available()) { return false; } // Read a byte at a time until we get to the special '0x42' start-byte if (s->peek() != 0x42) { s->read(); return false; } // Now read all 32 bytes if (s->available() < 32) { return false; } uint8_t buffer[32]; uint32_t sum = 0; s->readBytes(buffer, 32); // get checksum ready for (uint8_t i=0; i<30; i++) { sum += buffer[i]; } // The data comes in endian'd, this solves it so it works on all platforms uint16_t buffer_u16[15]; for (uint8_t i=0; i<15; i++) { buffer_u16[i] = buffer[2 + i*2 + 1]; buffer_u16[i] += (buffer[2 + i*2] << 8); } // Struct it memcpy((void *)&data, (void *)buffer_u16, 30); if (sum != data.checksum) { Serial.print("\n Checksum failure... "); Serial.print("Expected: "); Serial.print(sum); Serial.print(", Got: "); Serial.print(data.checksum); return false; } return true; } /* Turn on Reciever, * listen for messages for [listenDuration] seconds, * Return true if no messages recieved */ boolean clearToSend(int listenDuration) { int listenUntil = now() + listenDuration; // Listen until timeout expires while (listenUntil >= now()) { int packetSize = LoRa.parsePacket(); if (packetSize) { return false; // Other message heard on Rx, we can not transmit just now. } delay(5); } return true; // We didn't hear anything, so continue } // Listen of listenDuration seconds // If we see a response to our txHello packet, and txHello.okToTransmit is True, we can send our packet boolean listenTxHelloAccept(int listenDuration, int messageID) { int time_now = now() + listenDuration; // Listen until timeout expires Serial.println("[+] Transmit - \"Hello\" - Listening for ack & clear to send"); while (time_now >= now()) { int packetSize = LoRa.parsePacket(); if (packetSize) { String incoming = ""; char temp; while (LoRa.available()) { // TODO - Tidy this up, ensure we only read in valid JSON temp = (char)LoRa.read(); // Opening { if (incoming.length() == 0 && temp == '{') { incoming = "{"; // Closing } } else if (temp == '}') { incoming.concat("}"); break; // Anything else that's valid } else if (incoming.length() > 0) { incoming.concat(temp); } } // DEBUG // Serial.print("in listenTxHelloAccept() Recieved: \n"); // Serial.println(incoming); // Verify its OK to transmit StaticJsonDocument<200> doc; deserializeJson(doc, incoming); const bool okToTransmit = doc["okTransmit"]; const String authIsForUid = doc["uid"]; const int authIsForMessageID = doc["messageID"]; const String gatewayUid = doc["gatewayUid"]; // Verify txHello.okToTransmit is True & UID Match & Message IDs Match if (authIsForUid == getSensorUID()) { Serial.println("[+] Transmit - \"Hello\" - Sensor UID Match!"); } else { Serial.println("[-] Transmit - \"Hello\" - Sensor UID Mis-Match! " + String(authIsForUid) + " vs " + String(getSensorUID())); } if (authIsForMessageID == messageID) { Serial.println("[+] Transmit - \"Hello\" - Message ID Match!"); } else { Serial.println("[-] Transmit - \"Hello\" - MessageID Mis-Match!"); } return (okToTransmit && authIsForUid == getSensorUID() && authIsForMessageID == messageID); } delay(3); } Serial.println("[-] Transmit - \"Hello\" - Timeout while waiting for Clear to Send\n\n"); return false; // We didn't hear anything, conside this as meaning "Can't Send" } void sendJsonPayloadWithLoRa(DynamicJsonDocument payload) { LoRa.beginPacket(); serializeJson(payload, LoRa); LoRa.endPacket(); } // TODO - Finish Acks & Naks boolean transmitData(DynamicJsonDocument payload) { // Listen for other communication // Rx - Listen for other messages, if no messages heard then continue if (!clearToSend(0.5)) { Serial.println("[-] Airways busy, could not send"); delay(random(500, 1250)); // Introduce random delay to avoid another collision // TODO Refactor while (!clearToSend(0.5)) { Serial.println("[-] Airways busy, could not send"); delay(random(500, 1250)); // Introduce random delay to avoid another collision } //return false; } Serial.println("[+] Transmit - \"Hello\""); // Send short "clear to send?" packet // Tx - "I've got data to send!" + UID // RX - Continue upon recieving "OK" + UID + TX_Auth_ID DynamicJsonDocument txHello(2048); txHello["uid"] = sensorID; txHello["reservationTime"] = reservationTime; // How long do we require reservation of radio? txHello["messageID"] = msgCount; sendJsonPayloadWithLoRa(txHello); if (!listenTxHelloAccept(reservationTime * 1.5, msgCount)) { // Can't transmit just now Serial.println("[-] Transmit - \"Hello\" - Can Not Transmit At This Time\n"); return false; } Serial.println("[+] Recieved - Clear to Transmit Payload"); // Else we have clear to send // Transmit Payload // Tx - Send payload + UID + TX_Auth_ID // Rx - Listen for Ack/Nack Serial.println("[+] Transmit - Payload"); sendJsonPayloadWithLoRa(payload); // TODO Await Response Ack/Nak int ackTimeout = 2; // Seconds int time_now = now() + ackTimeout; // Listen until timeout expires Serial.println("[.] Transmit - Payload - Listening for Ack"); while (time_now >= now()) { int packetSize = LoRa.parsePacket(); if (packetSize) { String incoming = ""; char temp; while (LoRa.available()) { // TODO - Tidy this up, ensure we only read in valid JSON temp = (char)LoRa.read(); // Opening { if (incoming.length() == 0 && temp == '{') { incoming = "{"; // Closing } } else if (temp == '}') { incoming.concat("}"); break; // Anything else that's valid } else if (incoming.length() > 0) { incoming.concat(temp); } } // DEBUG // Serial.print("\nin listedForAck() Recieved: \n"); // Serial.println(incoming); StaticJsonDocument<200> doc; deserializeJson(doc, incoming); const bool ackStatus = doc["ackStatus"]; const String authIsForUid = doc["uid"]; const String gatewayUid = doc["gatewayUid"]; // Verify txHello.okToTransmit is True & UID Match if (authIsForUid == getSensorUID()) { Serial.println("[+] Transmit - Payload - Ack Recieved: " + String(ackStatus) + "\n"); if (ackStatus) { return true; } // It all worked :) // TODO Retransmit, recover, etc return false; } // Else UID Mis-Match so we wait for next message } delay(5); } // TODO After Timeout we need to deal with! Serial.println("[-] Transmit - Payload - Ack Timeout Reached - Assuming Message Was Not Delivered"); // TODO Listen for ack/nak // T+2 Rx OFF // Handle Ack + UID + Next_Send_Interval // Handle Nak + UID: GoTo T-3 // Handle No Response: GoTo T-3 // T+3 Tx/Rx Window Expires //LoRa.write(localAddress); // LoRa.write(msgCount); //LoRa.write(outgoing.length()); //LoRa.print(outgoing); return true; } void setup() { delay(1000); Serial.begin(115200); // Console Debug Serial.println("\n\n[+] Transmitter Node"); sensorID = getSensorUID(); pmsSerial.begin(9600); // Partical Sensor // Setup Sensors if (!setupSHT()) { while(1); } // Temp/Humidity - Die on Error // Setup LoRa if (!setupLoRa()) { while(1); } // Die on error } void loop() { // TODO Gather Sensor Data // Error reading PMS Data, ignore on this round if (!readPMSdata(&pmsSerial)) { data.pm10_standard = 0; data.pm25_standard = 0; data.pm100_standard = 0; } pollEventCount++; Serial.println(); Serial.print(String(pollEventCount) + ") "); Serial.print("PM 1.0: "); Serial.print(data.pm10_standard); Serial.print("\t\tPM 2.5: "); Serial.print(data.pm25_standard); Serial.print("\t\tPM 10: "); Serial.print(data.pm100_standard); Serial.print("\t\tTemp: "); Serial.print(getTemperature()); Serial.print("\t\tHumidity: "); Serial.print(getHumidity()); // Add to Average // Standard (_standard) or Environmental (_env) ppm10 = ppm10 + data.pm10_standard; ppm25 = ppm25 + data.pm25_standard; ppm100 = ppm100 + data.pm100_standard; if (pollEventCount == sendAfterPolls) { // Average Values over recording period avgPpm10 = ppm10 / sendAfterPolls; avgPpm25 = ppm25 / sendAfterPolls; avgPpm100 = ppm100 / sendAfterPolls; avgTemperature = temperature / sendAfterPolls; avgHumidity = humidity / sendAfterPolls; // TODO Transmit Sensor Data Serial.println(""); Serial.print("Avg ppm10: "); Serial.print(avgPpm10); Serial.print("\t\tAvg ppm25: "); Serial.print(avgPpm25); Serial.print("\t\tAvg ppm100: "); Serial.print(avgPpm100); Serial.print("\t\tAvg Temp: "); Serial.print(avgTemperature); Serial.print("\t\tAvg Humidity: "); Serial.print(avgHumidity); Serial.print("\t\tChip ID: "); Serial.println(sensorID); Serial.println(""); // LORA SEND DynamicJsonDocument doc(2048); JsonObject metadata = doc.createNestedObject("sensorMetadata"); metadata["uid"] = sensorID; metadata["messageID"] = msgCount; metadata["samplePeriod"] = sendAfterPolls; // TODO: Multiply by poll duration! JsonObject data = doc.createNestedObject("data"); JsonObject ppm = data.createNestedObject("ppm"); // Particulates ppm["p10"] = avgPpm10; ppm["p25"] = avgPpm25; ppm["p100"] = avgPpm100; JsonObject sht = data.createNestedObject("sht"); // Temp, Humidity sht["temperature"] = avgTemperature; sht["humidity"] = avgHumidity; JsonObject co2 = data.createNestedObject("co2"); // Co2 co2["tmp"] = 0; if (transmitData(doc)) { Serial.println("Packet Sent\n"); } else { int maxRetries = 10; // TODO Move to Config int numRetries = 1; while (!transmitData(doc) && numRetries < maxRetries){ numRetries++; Serial.println("[-] Failed to send packet, retrying. Attempt " + String(numRetries) + " of " + String(maxRetries) + "\n"); delay(reservationTime + random(1250, 5250)); // Introduce random delay to avoid another collision } if (numRetries >= maxRetries) { Serial.println("[-] Failed to send packet, max retries reached. Aborting"); // TODO Don't Clear Counters, record more values then try to retransmit } } // Reset Loop Values ppm10 = 0; ppm25 = 0; ppm100 = 0; temperature = 0; humidity = 0; pollEventCount = 0; Serial.println("---------------+++++-------------------"); msgCount++; } delay(pollingFrequency); }