diff --git a/ESP8266_Transmitter/ESP8266_Transmitter.ino b/ESP8266_Transmitter/ESP8266_Transmitter.ino index d80a17c3e9c88201b4e2f89df48dea1af3db196f..cbf84bc8e144fd2d6654893f69a0ad641575aee5 100644 --- a/ESP8266_Transmitter/ESP8266_Transmitter.ino +++ b/ESP8266_Transmitter/ESP8266_Transmitter.ino @@ -42,245 +42,44 @@ #include "SHTSensor.h" #include <CCS811.h> -// Debug / Polling / Transmitting Config -#define DEBUG 2 -#define MIN_RAND_DELAY 500 // ms -#define MAX_RAND_DELAY 1250 // ms -#define MAX_TRANSMISSION_RETRIES 5 // No. Retries before recordings new values then retry -#define TX_RESERVATION_TIME 1 // How long do we require use of TX for sending packets? (Seconds) - -// Poor mans memory-contrained hashmap alternative. Store index of polled count for each sensor in pollEventCountPerSensor -#define IDX_PPM10 0 -#define IDX_PPM25 1 -#define IDX_PPM100 2 -#define IDX_TEMPERATURE 3 -#define IDX_HUMIDITY 4 -#define IDX_CO2 5 - -// Variable Values - May be updated by Gateway -int TX_AFTER_N_READINGS = 4; // No. of samples, after which Transmit Avg Readings -int POLLING_FREQUENCY = 30000; // ms Take value every POLLING_FREQUENCY ms +// Definitions, helper files, etc +#include "config.h" +#include "_sht.h" +#include "_co2.h" +#include "_lora.h" +#include "_pms.h" +#include "_transmission.h" int pollEventCount = 0; // Number of times data has been sampled in this recording period -int pollEventCountPerSensor[6] = {0}; // Number of times each sensor has been polled in this recording period (Used for averaging values) - -// 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; - -// LoRa Message Tracking -String sensorID = ""; -byte localAddress = 0xBB; -byte destination = 0xFF; -uint32_t msgCount = 0; +int pollEventCountPerSensor[NUM_SENSORS] = {0}; // Number of times each sensor has been polled in this recording period (Used for averaging values) +double totalValuePerSensor[NUM_SENSORS] = {0}; // Store reading from each sensor here. We will then average later on // Sensor Config -static const int RXPin = 4, TXPin = 3; // Serial PMS Config -SHTSensor sht; // Temp / Humidity Sensor -SoftwareSerial pmsSerial(2, 3); // Particle Sensor Serial -CCS811 co2Sensor; // CO2 Sensor - -// Immediate Sensor Data -double temperature; // Sensor Values - Temp -double humidity; // Sensor Values - Humidity -double co2; // Sensor Values - Co2 -int32_t ppm10; // Sensor Values - Particulate -int32_t ppm25; // Sensor Values - Particulate -int32_t ppm100; // Sensor Values - Particulate - -// Partial Sensor Data -struct pms5003data { - uint16_t framelen; - int32_t pm10_standard, pm25_standard, pm100_standard; - int32_t pm10_env, pm25_env, pm100_env; - int32_t particles_03um, particles_05um, particles_10um, particles_25um, particles_50um, particles_100um; - uint16_t unused; - uint16_t checksum; -}; - -struct pms5003data data; +SHTSensor sht; // Temp / Humidity (SHT) Sensor [ic2] +SoftwareSerial pmsSerial(2, 3); // Particle (PMS) Sensor [Serial] +CCS811 co2Sensor; // CO2 Sensor [i2c] -/** - * 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(); - } - - return t; // -1 on error -} - -double getHumidity() { - double h = -1; - - if (sht.readSample()) { - h = sht.getHumidity(); - } - - return h; // -1 on error -} - -/** - * Setup CO2 Sensor - */ - bool setupCO2() { - if (co2Sensor.begin() != 0) { - Serial.println("[-] CO2 Init Failed"); - return false; - } - - /** - * @brief Set measurement cycle - * @param cycle:in typedef enum{ - * eClosed, //Idle (Measurements are disabled in this mode) - * eCycle_1s, //Constant power mode, IAQ measurement every second - * eCycle_10s, //Pulse heating mode IAQ measurement every 10 seconds - * eCycle_60s, //Low power pulse heating mode IAQ measurement every 60 seconds - * eCycle_250ms //Constant power mode, sensor measurement every 250ms - * }eCycle_t; - */ - co2Sensor.setMeasCycle(co2Sensor.eCycle_250ms); - return true; -} - -/** - * Get CO2 reading, returns -1 on error - */ -double getCo2() { - double c = -1; - - co2Sensor.writeBaseLine(0x847B); - - if(co2Sensor.checkDataReady() == true){ - c = co2Sensor.getCO2PPM(); - } - - return c; // -1 on error -} +struct pms5003data pmsData; // Struct for PMS Sensor Data - -// TODO DOC +// Reset poll counters void resetCounters() { - ppm10 = 0; - ppm25 = 0; - ppm100 = 0; - temperature = 0; - humidity = 0; pollEventCount = 0; -} -bool setupLoRa() { - LoRa.setPins(ss, rst, dio0); - if (!LoRa.begin(loraFrequency)) { - Serial.println("[-] Fatal. Starting LoRa failed!"); - return false; + for (int i = 0; i < NUM_SENSORS; i++) { + pollEventCountPerSensor[i] = 0; + totalValuePerSensor[i] = 0; } - - LoRa.setSpreadingFactor(loraSpreadingFactor); - LoRa.setSignalBandwidth(loraSignalBandwidth); - LoRa.setTxPower(20); - - 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() { - String sensorID = "EMIEI-"; // [E]nviornmental [M]onitoring [I]ndepenet of [E]xisting [I]nfrastructure - 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; -} /** * Provided sensor data, construct JSON object ready for transmission, Averaged over numSamples - * - * @param messageID + * + * @param messageID Number of messages sent since startup * @param numSamples Numer of samples averages - * @param avgPpm10 Average Particuate readings - * @param avgPpm25 Average Particuate readings - * @param avgPpm100 Average Particuate readings - * @param avgTemperature Average Temperature - * @param avgHumidity Average Humidity - * @param avgCo2 Average Co2 - * * @return DynamicJsonDocument sensorData */ -DynamicJsonDocument prepareSensorData(int messageID, int numSamples, uint32_t avgPpm10, uint32_t avgPpm25, uint32_t avgPpm100, double avgTemperature, double avgHumidity, double avgCo2) { +DynamicJsonDocument prepareSensorData(int messageID, int numSamples) { DynamicJsonDocument doc(2048); JsonObject metadata = doc.createNestedObject("sensorMetadata"); @@ -291,300 +90,44 @@ DynamicJsonDocument prepareSensorData(int messageID, int numSamples, uint32_t av JsonObject data = doc.createNestedObject("data"); JsonObject ppm = data.createNestedObject("ppm"); // Particulates - ppm["p10"] = avgPpm10; - ppm["p25"] = avgPpm25; - ppm["p100"] = avgPpm100; + ppm["p10"] = getAverageValue(IDX_PPM10); + ppm["p25"] = getAverageValue(IDX_PPM25); + ppm["p100"] = getAverageValue(IDX_PPM100); JsonObject sht = data.createNestedObject("sht"); // Temp, Humidity - sht["temperature"] = avgTemperature; - sht["humidity"] = avgHumidity; + sht["temperature"] = getAverageValue(IDX_TEMPERATURE); + sht["humidity"] = getAverageValue(IDX_HUMIDITY); - JsonObject co2 = data.createNestedObject("co2"); // TODO Co2 - co2["tmp"] = avgCo2; + JsonObject co2 = data.createNestedObject("co2"); // Co2 + co2["co2"] = getAverageValue(IDX_CO2); return doc; } -/** - * Listen on Lora for other messages, returns true if no messages detected within [listenDuration] seconds - * Use prior to transmissions to avoid interruption of other messages - * - * @param int listenDuration How long to listen on Lora RX in Seconds - * @returns boolean If messages detected within listenDuration - */ -boolean clearToSend(int listenDuration) { - int listenUntil = now() + listenDuration; - - // Listen until timeout expires - while (listenUntil >= now()) { - int packetSize = LoRa.parsePacket(); - - if (packetSize) { - Serial.println("[-] TX Busy - Not Clear To Send"); - return false; // Other message heard on Rx, infer that we can not transmit just now. - } - - delay(5); - } - - return true; // We didn't hear anything, so continue -} - -// Introduce random delay to avoid another collision -void waitRandomDelay(int minSeconds = 0) { - int randDelay = (minSeconds * 1000) + random(MIN_RAND_DELAY, MAX_RAND_DELAY); - if (DEBUG){ - Serial.println("[i] Delay for " + String(randDelay) + "ms"); - } - delay(randDelay); -} - -/** - * Send short "clear to send?" packet - * Tx - "I've got data to send!" + UID - * @param messageID ID of this message / sample period - */ -void sendTxHello(int messageID) { - Serial.println("[+] Transmit - \"Hello\""); - - DynamicJsonDocument txHello(2048); - txHello["uid"] = getSensorUID(); - txHello["reservationTime"] = TX_RESERVATION_TIME; - txHello["messageID"] = messageID; - - sendJsonPayloadWithLoRa(txHello); -} - -/** - * Send Payload - * Tx - Sensor Payload - * @param payload JSON Payload to be sent - */ -void sendTxPayload(DynamicJsonDocument payload) { - Serial.println("[+] Transmit - Payload"); - sendJsonPayloadWithLoRa(payload); -} - -/** - * Listen for messages on TX, expecting JSON - * - * @param listenDuration How long to listen on Lora RX in Seconds - * @return StaticJsonDocument|null RX Payload in JSON, null on timeout reached or error - */ -StaticJsonDocument<1024> listenForAndConsumeMessage(int listenDuration) { - int listenUntil = now() + listenDuration; - StaticJsonDocument<1024> json; - - // Listen until timeout expires - while (listenUntil >= now()) { - int packetSize = LoRa.parsePacket(); - - if (!packetSize) { - delay(3); - continue; - } - - // Read in packet, ensure we only bring in anything after { and before } inclusive - String incoming = ""; - char temp; - - while (LoRa.available()) { - temp = (char)LoRa.read(); - - if (incoming.length() == 0 && temp == '{') { // Opening { - incoming = "{"; - - } else if (temp == '}') { // Closing } - incoming.concat("}"); - break; - - } else if (incoming.length() > 0) { // Anything else that's valid - incoming.concat(temp); - } - } - - Serial.println(incoming); - // Deserialize - TODO Error Handling https://arduinojson.org/v6/api/json/deserializejson/ - deserializeJson(json, incoming); - break; - } - - return json; -} - -/** - * Listen for response to TxHello for [listenDuration] seconds, check Sensor UID & Message ID match and return true if we have clear to transmit payload. - * @param listenDuration How long to listen on Lora RX in Seconds - * @param messageID ID of message we are expecting to receive - * @return boolean Clear to Transmit - */ -boolean listenForTxHelloAccept(int listenDuration, int messageID) { - Serial.println("[+] Transmit - \"Hello\" - Listening for \"Hello\" Ack & Clear to Send"); - - StaticJsonDocument<1024> json = listenForAndConsumeMessage(listenDuration); - - // Timeout, likely TX Hello was not recieved or ack got lost - if (json.isNull()) { - Serial.println("[-] Transmit - \"Hello\" - Timeout while waiting for Clear to Send\n\n"); - return false; - } - - const bool okToTransmit = json["okTransmit"]; - const String authIsForUid = json["uid"]; - const int authIsForMessageID = json["messageID"]; - const String gatewayUid = json["gatewayUid"]; - - // Verify txHello.okTransmit is True and Sensor UID & Message IDs Match - if (DEBUG) { - 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!"); - } - } - - // Ok To Trasmit, Sensor UID Match & Message ID Match - bool clearToSend = (okToTransmit - && authIsForUid == getSensorUID() - && authIsForMessageID == messageID); - - if (DEBUG) { - if (clearToSend) { - Serial.println("[+] Transmit - \"Hello\" - Recieved \"Clear to Transmit\" Payload"); - } else { - Serial.println("[-] Transmit - \"Hello\" - Can Not Transmit At This Time"); - } - } - - return clearToSend; -} - -// TODO DOC -boolean listenForTxPayloadAccept(int listenDuration, int messageID) { - Serial.println("[.] Transmit - Payload - Listening for Ack"); - - StaticJsonDocument<1024> json = listenForAndConsumeMessage(listenDuration); - - // Timeout, likely TX Payload was not recieved or ack got lost. - if (json.isNull()) { - Serial.println("[-] Transmit - Payload - Ack Timeout Reached - Assuming Message Was Not Delivered\n\n"); - return false; - } - - const bool ackStatus = json["ackStatus"]; - const String authIsForUid = json["uid"]; - const int authIsForMessageID = json["replyMsgID"]; - const String gatewayUid = json["gatewayUid"]; - - - // Verify Sensor UID Match - if (authIsForUid == getSensorUID() - && authIsForMessageID == messageID) { - - Serial.println("[+] Transmit - Payload - Ack Recieved: " + String(ackStatus) + "\n"); - - if (ackStatus) { // It all worked :) - // Check for any updated config values - const int newTxAfterNReadings = json["txAfterNReadings"]; - if (newTxAfterNReadings != NULL && newTxAfterNReadings != TX_AFTER_N_READINGS) { - TX_AFTER_N_READINGS = newTxAfterNReadings; - Serial.println("[+] Tx After N Readings Updated, now: " + String(TX_AFTER_N_READINGS) + " samples"); - } - - const int newPollingFrequency = json["pollingFrequency"]; - if (newPollingFrequency != NULL && newPollingFrequency != POLLING_FREQUENCY) { - POLLING_FREQUENCY = newPollingFrequency; - Serial.println("[+] Polling Frequency Updated, now: " + String(POLLING_FREQUENCY) + "ms"); - } - - return true; - } - - // TODO Retransmit, recover, etc - Serial.println("[-] Transmit - Payload - Ack Failed - TODO Setup Retransmission"); - return false; - } - - // TODO Else UID Mis-Match so we wait for next message - Serial.println("[-] Transmit - Payload - Ack Message ID or Sensor ID Mis-Match"); - return false; -} - -/** - * Send JSON Payload over LoRa - * @param payload JSON Payload to be send - */ -void sendJsonPayloadWithLoRa(DynamicJsonDocument payload) { - LoRa.beginPacket(); - serializeJson(payload, LoRa); - LoRa.endPacket(); -} - -// TODO - Finish Acks & Naks -boolean transmitData(DynamicJsonDocument payload) { - - // TODO MAX_RETRIES - - // Listen for other communication - // Rx - Listen for other messages, if no messages heard then continue - if (!clearToSend(0.5)) { - waitRandomDelay(); // Wait for short time before retrying - while (!clearToSend(0.5)) { waitRandomDelay(); } // TODO MAX_TIMEOUT - } - - // Send TX Hello - sendTxHello(msgCount); - - // Await TX Hello Auth - Expect: Timeout | Not Auth | Accept + Clear To Transmit - if (!listenForTxHelloAccept(TX_RESERVATION_TIME * 4, msgCount)) { - return false; // Can't transmit just now, we will retry - } - - // Send TX Payload - sendTxPayload(payload); - - // Await TX Payload Ack - Expect: Timeout | Nack | Accept + Data Match - if (!listenForTxPayloadAccept(2, msgCount)) { - return false; // TODO Ack Failed Setup a retry here! - } - - // TODO Update Sensor Config - - // TODO Clear values & Continue - resetCounters(); - Serial.println("Packet Sent Succesfully\n"); - Serial.println("---------------+++++-------------------"); - msgCount++; - - return true; -} /** * Add recorded values to the total, do not store false/failed/invalid values, e.g. -1 */ -double addValueToTotal(double avgValue, double newValue, int idx_sensor_count) { +void addValueToTotal(double newValue, int idx_sensor_count) { if (newValue == -1) { - return avgValue; + return; } pollEventCountPerSensor[idx_sensor_count] += 1; - return avgValue + newValue; + totalValuePerSensor[idx_sensor_count] += newValue; } -double averageValueByNumberPolls(double value, int idx_sensor_count) { - // Ensure we do not divide by 0 - if (pollEventCountPerSensor[idx_sensor_count] == 0) { +/** + * Get average value for a particular sensor, exluding any error values + * If there are no valid values, return -1 to indicate error + */ +double getAverageValue(int idx_sensor_count) { + if (pollEventCountPerSensor[idx_sensor_count] == 0) { // Ensure we do not divide by 0 (e.g. 0 valid values) return -1; } // Otherwise divide total by number of times we sampled this value - return value / pollEventCountPerSensor[idx_sensor_count]; + return totalValuePerSensor[idx_sensor_count] / pollEventCountPerSensor[idx_sensor_count]; } void setup() { @@ -596,8 +139,8 @@ void setup() { pmsSerial.begin(9600); // Partical Sensor // Setup Hardware TODO Handle broken sensor! - if (!setupSHT()) { /*while(1);*/ } // Temp/Humidity - Die on Error - if (!setupCO2()) { /*while(1);*/ } + if (!setupSHT(sht)) { /*while(1);*/ } // Temp/Humidity - Die on Error + if (!setupCO2(co2Sensor)) { /*while(1);*/ } if (!setupLoRa()) { /*while(1);*/ } // Die on error } @@ -606,52 +149,44 @@ void loop() { pollEventCount++; // Get values from sensors - double instantTemperature = getTemperature(); // Prevent polling of sensors too frequently when debugging - double instantHumidity = getHumidity(); - double instantCo2 = getCo2(); + double instantTemperature = getTemperature(sht); // Prevent polling of sensors too frequently when debugging + double instantHumidity = getHumidity(sht); + double instantCo2 = getCo2(co2Sensor); int instantPPM10, instantPPM25, instantPPM100; - if (!readPMSdata(&pmsSerial)) { // Error reading PMS Data, ignore on this round + if (!readPMSdata(&pmsSerial, &pmsData)) { // Error reading PMS Data, ignore on this round instantPPM10 = instantPPM25 = instantPPM100 = -1; } else { - instantPPM10 = data.pm10_standard; // Standard (_standard) or Environmental (_env) readings for Particulate Data - instantPPM25 = data.pm25_standard; - instantPPM100 = data.pm100_standard; + instantPPM10 = pmsData.pm10_standard; // Standard (_standard) or Environmental (_env) readings for Particulate Data + instantPPM25 = pmsData.pm25_standard; + instantPPM100 = pmsData.pm100_standard; } - if (DEBUG) { + if (DEBUG) { // Print instantanious values Serial.println(); Serial.print(String(pollEventCount) + ") "); Serial.print("PM 1.0: "); Serial.print(instantPPM10); Serial.print("\t\tPM 2.5: "); Serial.print(instantPPM25); Serial.print("\t\tPM 10: "); Serial.print(instantPPM100); Serial.print("\t\tTemp: "); Serial.print(instantTemperature); Serial.print("\t\tHumidity: "); Serial.print(instantHumidity); Serial.print("\t\tCo2: "); Serial.print(instantCo2); } // Add to running total, we will divide by the number of times samples later on to get an average over sample period - temperature = addValueToTotal(temperature, instantTemperature, IDX_TEMPERATURE); - humidity = addValueToTotal(humidity, instantHumidity, IDX_HUMIDITY); - co2 = addValueToTotal(co2, instantCo2, IDX_CO2); - ppm10 = addValueToTotal(ppm10, instantPPM10, IDX_PPM10); - ppm25 = addValueToTotal(ppm25, instantPPM25, IDX_PPM25); - ppm100 = addValueToTotal(ppm100, instantPPM100, IDX_PPM100); + addValueToTotal(instantTemperature, IDX_TEMPERATURE); + addValueToTotal(instantHumidity, IDX_HUMIDITY); + addValueToTotal(instantCo2, IDX_CO2); + addValueToTotal(instantPPM10, IDX_PPM10); + addValueToTotal(instantPPM25, IDX_PPM25); + addValueToTotal(instantPPM100, IDX_PPM100); // If we should now transmit if (pollEventCount >= TX_AFTER_N_READINGS) { - // Average Values over recording period - double avgTemperature = averageValueByNumberPolls(temperature, IDX_TEMPERATURE); - double avgHumidity = averageValueByNumberPolls(humidity, IDX_HUMIDITY); - double avgCo2 = averageValueByNumberPolls(co2, IDX_CO2); - int avgPpm10 = averageValueByNumberPolls(ppm10, IDX_PPM10); - int avgPpm25 = averageValueByNumberPolls(ppm25, IDX_PPM25); - int avgPpm100 = averageValueByNumberPolls(ppm100, IDX_PPM100); - - if (DEBUG) { - 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\tAvg Co2: "); Serial.print(avgCo2); + if (DEBUG) { // Print average values over this period + Serial.println(""); Serial.print("Avg ppm10: "); Serial.print(getAverageValue(IDX_PPM10)); Serial.print("\t\tAvg ppm25: "); Serial.print(getAverageValue(IDX_PPM25)); Serial.print("\t\tAvg ppm100: "); Serial.print(getAverageValue(IDX_PPM100)); + Serial.print("\t\tAvg Temp: "); Serial.print(getAverageValue(IDX_TEMPERATURE)); Serial.print("\t\tAvg Humidity: "); Serial.print(getAverageValue(IDX_HUMIDITY)); Serial.print("\t\tAvg Co2: "); Serial.print(getAverageValue(IDX_CO2)); Serial.print("\t\tChip ID: "); Serial.println(getSensorUID()); Serial.println(""); } // Prepare Data For Send - DynamicJsonDocument sensorData = prepareSensorData(msgCount, pollEventCount, avgPpm10, avgPpm25, avgPpm100, avgTemperature, avgHumidity, avgCo2); + DynamicJsonDocument sensorData = prepareSensorData(msgCount, pollEventCount); // Transmit if (transmitData(sensorData)) { diff --git a/ESP8266_Transmitter/_co2.h b/ESP8266_Transmitter/_co2.h new file mode 100644 index 0000000000000000000000000000000000000000..f6939a905bec84351c480c763e1f943701ae92c0 --- /dev/null +++ b/ESP8266_Transmitter/_co2.h @@ -0,0 +1,37 @@ +/** + * Setup CO2 Sensor + */ + bool setupCO2(CCS811 co2Sensor) { + if (co2Sensor.begin() != 0) { + Serial.println("[-] CO2 Init Failed"); + return false; + } + + /** + * @brief Set measurement cycle + * @param cycle:in typedef enum{ + * eClosed, //Idle (Measurements are disabled in this mode) + * eCycle_1s, //Constant power mode, IAQ measurement every second + * eCycle_10s, //Pulse heating mode IAQ measurement every 10 seconds + * eCycle_60s, //Low power pulse heating mode IAQ measurement every 60 seconds + * eCycle_250ms //Constant power mode, sensor measurement every 250ms + * }eCycle_t; + */ + co2Sensor.setMeasCycle(co2Sensor.eCycle_250ms); + return true; +} + +/** + * Get CO2 reading, returns -1 on error + */ +double getCo2(CCS811 co2Sensor) { + double c = -1; + + co2Sensor.writeBaseLine(0x847B); + + if(co2Sensor.checkDataReady() == true){ + c = co2Sensor.getCO2PPM(); + } + + return c; // -1 on error +} diff --git a/ESP8266_Transmitter/_lora.h b/ESP8266_Transmitter/_lora.h new file mode 100644 index 0000000000000000000000000000000000000000..253bcb9e81fca79d831550e4abc71223fc6f01f8 --- /dev/null +++ b/ESP8266_Transmitter/_lora.h @@ -0,0 +1,14 @@ +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); + LoRa.setTxPower(20); + + Serial.println("[+] LoRa Initialized OK!"); + return true; +} diff --git a/ESP8266_Transmitter/_pms.h b/ESP8266_Transmitter/_pms.h new file mode 100644 index 0000000000000000000000000000000000000000..3e1107583ca054296c10468e625df288f57ed1a5 --- /dev/null +++ b/ESP8266_Transmitter/_pms.h @@ -0,0 +1,51 @@ +/** + * Get data from PMS Partical Sensor + * @param Stream s PMS Serial Connection + * @param pms5003data data struct data PMS sensor Data + * @modifies struct data PMS Sensor Data + * @returns boolean Data Read Success status + */ +boolean readPMSdata(Stream *s, struct pms5003data *data) { + 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; +} diff --git a/ESP8266_Transmitter/_sht.h b/ESP8266_Transmitter/_sht.h new file mode 100644 index 0000000000000000000000000000000000000000..3b69420b7e3605abc68e30e73eed1ce47497a029 --- /dev/null +++ b/ESP8266_Transmitter/_sht.h @@ -0,0 +1,34 @@ +/** + * Setup Humidity / Temperature Sensor + */ +bool setupSHT(SHTSensor sht) { + Wire.begin(); + + if (!sht.init()) { + Serial.println("[-] SHT Init Failed"); + return false; + } + + sht.setAccuracy(SHTSensor::SHT_ACCURACY_MEDIUM); + return true; +} + +double getTemperature(SHTSensor sht) { + double t = -1; + + if (sht.readSample()) { + t = sht.getTemperature(); + } + + return t; // -1 on error +} + +double getHumidity(SHTSensor sht) { + double h = -1; + + if (sht.readSample()) { + h = sht.getHumidity(); + } + + return h; // -1 on error +} diff --git a/ESP8266_Transmitter/_transmission.h b/ESP8266_Transmitter/_transmission.h new file mode 100644 index 0000000000000000000000000000000000000000..768c5b4105707a35c7ec74bfd26049c5e4976a25 --- /dev/null +++ b/ESP8266_Transmitter/_transmission.h @@ -0,0 +1,350 @@ +/******************************************** + * Data Prep / Validation * + ********************************************/ + +/** + * 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 + * + * @return Unique ID for sensor + */ +String getSensorUID() { + String sensorID = "EMIEI-"; // [E]nviornmental [M]onitoring [I]ndepenet of [E]xisting [I]nfrastructure + sensorID.concat(String(ESP.getChipId())); + return sensorID; +} + + + +/******************************************** + * Timings * + ********************************************/ + +/** + * Introduce random delay, with optional minimum delay time + * + * @param minSecods Min number of secods to wait, default 0 (i.e.) any random delay between MIN and MAX + */ +void waitRandomDelay(int minSeconds = 0) { + int randDelay = (minSeconds * 1000) + random(MIN_RAND_DELAY, MAX_RAND_DELAY); + if (DEBUG){ Serial.println("[i] Delay for " + String(randDelay) + "ms"); } + delay(randDelay); +} + + + +/******************************************** + * Transmission * + ********************************************/ + +/** + * Send JSON Payload over LoRa + * + * @param payload JSON Payload to be send + */ +void sendJsonPayloadWithLoRa(DynamicJsonDocument payload) { + Serial.println("[+] Transmit - Payload"); + LoRa.beginPacket(); + serializeJson(payload, LoRa); + LoRa.endPacket(); +} + + + +/******************************************** + * Recieving * + ********************************************/ + +/** + * Listen on Lora for other messages, returns true if no messages detected within [listenDuration] seconds + * Use prior to transmissions to avoid interruption of other messages + * + * @param int listenDuration How long to listen on Lora RX in Seconds + * @returns boolean If messages detected within listenDuration + */ +boolean clearToSend(int listenDuration) { + int listenUntil = now() + listenDuration; + + // Listen until timeout expires + while (listenUntil >= now()) { + int packetSize = LoRa.parsePacket(); + + if (packetSize) { + Serial.println("[-] TX Busy - Not Clear To Send"); + return false; // Other message heard on Rx, infer that we can not transmit just now. + } + + delay(5); + } + + return true; // We didn't hear anything, so continue +} + +/** + * Listen for messages on TX, expecting JSON + * + * @param listenDuration How long to listen on Lora RX in Seconds + * @return StaticJsonDocument|null RX Payload in JSON, null on timeout reached or error + */ +StaticJsonDocument<1024> listenForAndConsumeMessage(int listenDuration) { + int listenUntil = now() + listenDuration; + StaticJsonDocument<1024> json; + + // Listen until timeout expires + while (listenUntil >= now()) { + int packetSize = LoRa.parsePacket(); + + if (!packetSize) { + delay(3); + continue; + } + + // Read in packet, ensure we only bring in anything after { and before } inclusive + String incoming = ""; + char temp; + + while (LoRa.available()) { + temp = (char)LoRa.read(); + + if (incoming.length() == 0 && temp == '{') { // Opening { + incoming = "{"; + + } else if (temp == '}') { // Closing } + incoming.concat("}"); + break; + + } else if (incoming.length() > 0) { // Anything else that's valid + incoming.concat(temp); + } + } + + Serial.println(incoming); + // Deserialize - TODO Error Handling https://arduinojson.org/v6/api/json/deserializejson/ + deserializeJson(json, incoming); + break; + } + + return json; +} + +// TODO DOC +boolean listenForTxPayloadAccept(int listenDuration, int messageID) { + Serial.println("[.] Transmit - Payload - Listening for Ack"); + + StaticJsonDocument<1024> json = listenForAndConsumeMessage(listenDuration); + + // Timeout, likely TX Payload was not recieved or ack got lost. + if (json.isNull()) { + Serial.println("[-] Transmit - Payload - Ack Timeout Reached - Assuming Message Was Not Delivered\n\n"); + return false; + } + + const bool ackStatus = json["ackStatus"]; + const String authIsForUid = json["uid"]; + const int authIsForMessageID = json["replyMsgID"]; + const String gatewayUid = json["gatewayUid"]; + + + // Verify Sensor UID Match + if (authIsForUid == getSensorUID() + && authIsForMessageID == messageID) { + + Serial.println("[+] Transmit - Payload - Ack Recieved: " + String(ackStatus) + "\n"); + + if (ackStatus) { // It all worked :) + // Check for any updated config values + const int newTxAfterNReadings = json["txAfterNReadings"]; + if (newTxAfterNReadings != NULL && newTxAfterNReadings != TX_AFTER_N_READINGS) { + TX_AFTER_N_READINGS = newTxAfterNReadings; + Serial.println("[+] Tx After N Readings Updated, now: " + String(TX_AFTER_N_READINGS) + " samples"); + } + + const int newPollingFrequency = json["pollingFrequency"]; + if (newPollingFrequency != NULL && newPollingFrequency != POLLING_FREQUENCY) { + POLLING_FREQUENCY = newPollingFrequency; + Serial.println("[+] Polling Frequency Updated, now: " + String(POLLING_FREQUENCY) + "ms"); + } + + return true; + } + + // TODO Retransmit, recover, etc + Serial.println("[-] Transmit - Payload - Ack Failed - TODO Setup Retransmission"); + return false; + } + + // TODO Else UID Mis-Match so we wait for next message + Serial.println("[-] Transmit - Payload - Ack Message ID or Sensor ID Mis-Match"); + return false; +} + + + +/******************************************** + * TX Hello * + ********************************************/ + + /** + * Send short "clear to send?" packet via LoRa + * Tx - "I've got data to send!" + UID + how long to reserve radio + * Note: Gateway has final say over TX_RESERVATION_TIME and may override requested value + * + * @param messageID ID of this message + */ +void sendTxHello(int messageID) { + if (DEBUG){ Serial.println("[+] Transmit - \"Hello\""); } + + DynamicJsonDocument txHello(2048); + txHello["uid"] = getSensorUID(); + txHello["reservationTime"] = TX_RESERVATION_TIME; + txHello["messageID"] = messageID; + + sendJsonPayloadWithLoRa(txHello); +} + +/** + * Listen for response to TxHello for [listenDuration] seconds. + * Upon recieving response, check Sensor UID & Message ID match and return true if we have clear to transmit payload. + * + * @param listenDuration How long to listen on Lora RX (Seconds) + * @param messageID ID of message we are expecting to receive + * @return Clear to Transmit (Y/N) + */ +boolean listenForTxHelloAccept(int listenDuration, int messageID) { + if (DEBUG){ Serial.println("[+] Transmit - \"Hello\" - Listening for \"Hello\" Ack & Clear to Send"); } + + StaticJsonDocument<1024> json = listenForAndConsumeMessage(listenDuration); + + // Timeout, likely TX Hello was not recieved or ack got lost + if (json.isNull()) { + Serial.println("[-] Transmit - \"Hello\" - Timeout while waiting for Clear to Send\n\n"); + return false; + } + + const bool okToTransmit = json["okTransmit"]; + const String authIsForUid = json["uid"]; + const int authIsForMessageID = json["messageID"]; + const String gatewayUid = json["gatewayUid"]; + + // Verify txHello.okTransmit is True and Sensor UID & Message IDs Match + if (DEBUG) { + 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!"); + } + } + + // Ok To Trasmit, Sensor UID Match & Message ID Match + bool clearToSend = (okToTransmit + && authIsForUid == getSensorUID() + && authIsForMessageID == messageID); + + if (DEBUG) { + if (clearToSend) { + Serial.println("[+] Transmit - \"Hello\" - Recieved \"Clear to Transmit\" Payload"); + } else { + Serial.println("[-] Transmit - \"Hello\" - Can Not Transmit At This Time"); + } + } + + return clearToSend; +} + + + + + + + + + + + + + + + + + +// TODO - Finish Acks & Naks +boolean transmitData(DynamicJsonDocument payload) { + + // TODO MAX_RETRIES + + // Listen for other communication + // Rx - Listen for other messages, if no messages heard then continue + if (!clearToSend(0.5)) { + waitRandomDelay(); // Wait for short time before retrying + while (!clearToSend(0.5)) { waitRandomDelay(); } // TODO MAX_TIMEOUT + } + + // Send TX Hello + sendTxHello(msgCount); + + // Await TX Hello Auth - Expect: Timeout | Not Auth | Accept + Clear To Transmit + if (!listenForTxHelloAccept(TX_RESERVATION_TIME * 4, msgCount)) { + return false; // Can't transmit just now, we will retry + } + + // Send TX Payload + sendJsonPayloadWithLoRa(payload); + + // Await TX Payload Ack - Expect: Timeout | Nack | Accept + Data Match + if (!listenForTxPayloadAccept(2, msgCount)) { + return false; // TODO Ack Failed Setup a retry here! + } + + // TODO Update Sensor Config + + // TODO Clear values & Continue + //resetCounters(); TODO + Serial.println("Packet Sent Succesfully\n"); + Serial.println("---------------+++++-------------------"); + msgCount++; + + return true; +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ESP8266_Transmitter/config.h b/ESP8266_Transmitter/config.h new file mode 100644 index 0000000000000000000000000000000000000000..c97f320358844eb4cd9a038216a5b38b7938d8f9 --- /dev/null +++ b/ESP8266_Transmitter/config.h @@ -0,0 +1,44 @@ +// Debug / Polling / Transmitting Config +#define DEBUG 2 +#define MIN_RAND_DELAY 500 // ms +#define MAX_RAND_DELAY 1250 // ms +#define MAX_TRANSMISSION_RETRIES 5 // No. Retries before recordings new values then retry +#define TX_RESERVATION_TIME 1 // How long do we require use of TX for sending packets? (Seconds) + +#define NUM_SENSORS 6 + +// Poor mans memory-contrained hashmap alternative. Store index of polled count for each sensor in pollEventCountPerSensor +#define IDX_PPM10 0 +#define IDX_PPM25 1 +#define IDX_PPM100 2 +#define IDX_TEMPERATURE 3 +#define IDX_HUMIDITY 4 +#define IDX_CO2 5 + +// Variable Values - May be updated by Gateway +int TX_AFTER_N_READINGS = 10; // No. of samples, after which Transmit Avg Readings +int POLLING_FREQUENCY = 10000; // ms Take value every POLLING_FREQUENCY ms + +// 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; + +// LoRa Message Tracking +String sensorID = ""; +byte localAddress = 0xBB; +byte destination = 0xFF; +uint32_t msgCount = 0; // Number of messages sent since startup + +// Partial Sensor Data +struct pms5003data { + uint16_t framelen; + int32_t pm10_standard, pm25_standard, pm100_standard; + int32_t pm10_env, pm25_env, pm100_env; + int32_t particles_03um, particles_05um, particles_10um, particles_25um, particles_50um, particles_100um; + uint16_t unused; + uint16_t checksum; +}; \ No newline at end of file