Skip to content
Snippets Groups Projects
Commit bc0d0766 authored by Callum Inglis's avatar Callum Inglis
Browse files

Sensor - The Great Refactor

parent 0bb8ba06
No related branches found
No related tags found
3 merge requests!12CO2 Sensor, Refactoring, CAD Files, Update Config from API,!7Update sensor config from api; CO2 Sensor; Refactor & Tidy-Up,!6Update sensor config from api; CO2 Sensor; Refactor & Tidy-Up
This commit is part of merge request !6. Comments created here will be created in the context of that merge request.
...@@ -42,245 +42,44 @@ ...@@ -42,245 +42,44 @@
#include "SHTSensor.h" #include "SHTSensor.h"
#include <CCS811.h> #include <CCS811.h>
// Debug / Polling / Transmitting Config // Definitions, helper files, etc
#define DEBUG 2 #include "config.h"
#define MIN_RAND_DELAY 500 // ms #include "_sht.h"
#define MAX_RAND_DELAY 1250 // ms #include "_co2.h"
#define MAX_TRANSMISSION_RETRIES 5 // No. Retries before recordings new values then retry #include "_lora.h"
#define TX_RESERVATION_TIME 1 // How long do we require use of TX for sending packets? (Seconds) #include "_pms.h"
#include "_transmission.h"
// 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
int pollEventCount = 0; // Number of times data has been sampled in this recording period 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) 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
// 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;
// Sensor Config // Sensor Config
static const int RXPin = 4, TXPin = 3; // Serial PMS Config SHTSensor sht; // Temp / Humidity (SHT) Sensor [ic2]
SHTSensor sht; // Temp / Humidity Sensor SoftwareSerial pmsSerial(2, 3); // Particle (PMS) Sensor [Serial]
SoftwareSerial pmsSerial(2, 3); // Particle Sensor Serial CCS811 co2Sensor; // CO2 Sensor [i2c]
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;
/** struct pms5003data pmsData; // Struct for PMS Sensor Data
* 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
}
// Reset poll counters
// TODO DOC
void resetCounters() { void resetCounters() {
ppm10 = 0;
ppm25 = 0;
ppm100 = 0;
temperature = 0;
humidity = 0;
pollEventCount = 0; pollEventCount = 0;
}
bool setupLoRa() { for (int i = 0; i < NUM_SENSORS; i++) {
LoRa.setPins(ss, rst, dio0); pollEventCountPerSensor[i] = 0;
if (!LoRa.begin(loraFrequency)) { totalValuePerSensor[i] = 0;
Serial.println("[-] Fatal. Starting LoRa failed!");
return false;
} }
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 * 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 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 * @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); DynamicJsonDocument doc(2048);
JsonObject metadata = doc.createNestedObject("sensorMetadata"); JsonObject metadata = doc.createNestedObject("sensorMetadata");
...@@ -291,300 +90,44 @@ DynamicJsonDocument prepareSensorData(int messageID, int numSamples, uint32_t av ...@@ -291,300 +90,44 @@ DynamicJsonDocument prepareSensorData(int messageID, int numSamples, uint32_t av
JsonObject data = doc.createNestedObject("data"); JsonObject data = doc.createNestedObject("data");
JsonObject ppm = data.createNestedObject("ppm"); // Particulates JsonObject ppm = data.createNestedObject("ppm"); // Particulates
ppm["p10"] = avgPpm10; ppm["p10"] = getAverageValue(IDX_PPM10);
ppm["p25"] = avgPpm25; ppm["p25"] = getAverageValue(IDX_PPM25);
ppm["p100"] = avgPpm100; ppm["p100"] = getAverageValue(IDX_PPM100);
JsonObject sht = data.createNestedObject("sht"); // Temp, Humidity JsonObject sht = data.createNestedObject("sht"); // Temp, Humidity
sht["temperature"] = avgTemperature; sht["temperature"] = getAverageValue(IDX_TEMPERATURE);
sht["humidity"] = avgHumidity; sht["humidity"] = getAverageValue(IDX_HUMIDITY);
JsonObject co2 = data.createNestedObject("co2"); // TODO Co2 JsonObject co2 = data.createNestedObject("co2"); // Co2
co2["tmp"] = avgCo2; co2["co2"] = getAverageValue(IDX_CO2);
return doc; 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 * 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) { if (newValue == -1) {
return avgValue; return;
} }
pollEventCountPerSensor[idx_sensor_count] += 1; 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 * Get average value for a particular sensor, exluding any error values
if (pollEventCountPerSensor[idx_sensor_count] == 0) { * 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; return -1;
} }
// Otherwise divide total by number of times we sampled this value // 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() { void setup() {
...@@ -596,8 +139,8 @@ void setup() { ...@@ -596,8 +139,8 @@ void setup() {
pmsSerial.begin(9600); // Partical Sensor pmsSerial.begin(9600); // Partical Sensor
// Setup Hardware TODO Handle broken sensor! // Setup Hardware TODO Handle broken sensor!
if (!setupSHT()) { /*while(1);*/ } // Temp/Humidity - Die on Error if (!setupSHT(sht)) { /*while(1);*/ } // Temp/Humidity - Die on Error
if (!setupCO2()) { /*while(1);*/ } if (!setupCO2(co2Sensor)) { /*while(1);*/ }
if (!setupLoRa()) { /*while(1);*/ } // Die on error if (!setupLoRa()) { /*while(1);*/ } // Die on error
} }
...@@ -606,52 +149,44 @@ void loop() { ...@@ -606,52 +149,44 @@ void loop() {
pollEventCount++; pollEventCount++;
// Get values from sensors // Get values from sensors
double instantTemperature = getTemperature(); // Prevent polling of sensors too frequently when debugging double instantTemperature = getTemperature(sht); // Prevent polling of sensors too frequently when debugging
double instantHumidity = getHumidity(); double instantHumidity = getHumidity(sht);
double instantCo2 = getCo2(); double instantCo2 = getCo2(co2Sensor);
int instantPPM10, instantPPM25, instantPPM100; 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; instantPPM10 = instantPPM25 = instantPPM100 = -1;
} else { } else {
instantPPM10 = data.pm10_standard; // Standard (_standard) or Environmental (_env) readings for Particulate Data instantPPM10 = pmsData.pm10_standard; // Standard (_standard) or Environmental (_env) readings for Particulate Data
instantPPM25 = data.pm25_standard; instantPPM25 = pmsData.pm25_standard;
instantPPM100 = data.pm100_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.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); 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 // 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); addValueToTotal(instantTemperature, IDX_TEMPERATURE);
humidity = addValueToTotal(humidity, instantHumidity, IDX_HUMIDITY); addValueToTotal(instantHumidity, IDX_HUMIDITY);
co2 = addValueToTotal(co2, instantCo2, IDX_CO2); addValueToTotal(instantCo2, IDX_CO2);
ppm10 = addValueToTotal(ppm10, instantPPM10, IDX_PPM10); addValueToTotal(instantPPM10, IDX_PPM10);
ppm25 = addValueToTotal(ppm25, instantPPM25, IDX_PPM25); addValueToTotal(instantPPM25, IDX_PPM25);
ppm100 = addValueToTotal(ppm100, instantPPM100, IDX_PPM100); addValueToTotal(instantPPM100, IDX_PPM100);
// If we should now transmit // If we should now transmit
if (pollEventCount >= TX_AFTER_N_READINGS) { if (pollEventCount >= TX_AFTER_N_READINGS) {
// Average Values over recording period if (DEBUG) { // Print average values over this period
double avgTemperature = averageValueByNumberPolls(temperature, IDX_TEMPERATURE); 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));
double avgHumidity = averageValueByNumberPolls(humidity, IDX_HUMIDITY); 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));
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);
Serial.print("\t\tChip ID: "); Serial.println(getSensorUID()); Serial.println(""); Serial.print("\t\tChip ID: "); Serial.println(getSensorUID()); Serial.println("");
} }
// Prepare Data For Send // Prepare Data For Send
DynamicJsonDocument sensorData = prepareSensorData(msgCount, pollEventCount, avgPpm10, avgPpm25, avgPpm100, avgTemperature, avgHumidity, avgCo2); DynamicJsonDocument sensorData = prepareSensorData(msgCount, pollEventCount);
// Transmit // Transmit
if (transmitData(sensorData)) { if (transmitData(sensorData)) {
......
/**
* 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
}
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;
}
/**
* 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;
}
/**
* 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
}
/********************************************
* 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;
}
// 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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment