From bc0d07668ba587ba30a145779b5e7b9b5ee21e4e Mon Sep 17 00:00:00 2001
From: Callum Inglis <callum.inglis@4oh4.co.uk>
Date: Sun, 6 Feb 2022 17:19:56 +0000
Subject: [PATCH] Sensor - The Great Refactor

---
 ESP8266_Transmitter/ESP8266_Transmitter.ino | 579 ++------------------
 ESP8266_Transmitter/_co2.h                  |  37 ++
 ESP8266_Transmitter/_lora.h                 |  14 +
 ESP8266_Transmitter/_pms.h                  |  51 ++
 ESP8266_Transmitter/_sht.h                  |  34 ++
 ESP8266_Transmitter/_transmission.h         | 350 ++++++++++++
 ESP8266_Transmitter/config.h                |  44 ++
 7 files changed, 587 insertions(+), 522 deletions(-)
 create mode 100644 ESP8266_Transmitter/_co2.h
 create mode 100644 ESP8266_Transmitter/_lora.h
 create mode 100644 ESP8266_Transmitter/_pms.h
 create mode 100644 ESP8266_Transmitter/_sht.h
 create mode 100644 ESP8266_Transmitter/_transmission.h
 create mode 100644 ESP8266_Transmitter/config.h

diff --git a/ESP8266_Transmitter/ESP8266_Transmitter.ino b/ESP8266_Transmitter/ESP8266_Transmitter.ino
index d80a17c..cbf84bc 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 0000000..f6939a9
--- /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 0000000..253bcb9
--- /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 0000000..3e11075
--- /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 0000000..3b69420
--- /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 0000000..768c5b4
--- /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 0000000..c97f320
--- /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
-- 
GitLab