Newer
Older
/* =========================================================================================
*
* CS408 Environmental Monitoring Independent of Existing Infrastructure
* Copyright (C) 2021 Callum Inglis
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Contact: Callum.Inglis.2018(at)uni.strath.ac.uk
*
* =========================================================================================
*
* Libraries Used
* ArduinoJson v6 Library - https://arduinojson.org/v6/doc/ (https://github.com/sandeepmistry/arduino-LoRa/blob/master/LICENSE)
* LoRa Library - https://github.com/sandeepmistry/arduino-LoRa/blob/master/API.md (https://github.com/bblanchon/ArduinoJson/blob/6.x/LICENSE.md)
* SHTSensor - https://github.com/Sensirion/arduino-sht (https://github.com/Sensirion/arduino-sht/blob/master/LICENSE)
*
* =========================================================================================
*/
#include <SPI.h>
#include <LoRa.h>
#include <SoftwareSerial.h>
#include <ArduinoJson.h>
// Lora Config
#define ss 16 // Physical Pin 16 = D0 (Default is Physical Pin 5)
#define rst 0
#define dio0 15 // Physical Pin 15 = D8 (Default is Physical Pin 4)
static const int loraSpreadingFactor = 7;
static const int loraSignalBandwidth = 125E3;
static const int loraFrequency = 433E6;
// Serial PMS Config
static const int RXPin = 4, TXPin = 3;
// Temp / Humidity Sensor
SHTSensor sht;
// Particle Sensor Serial
SoftwareSerial pmsSerial(2, 3);
// LoRa Message Tracking
byte localAddress = 0xBB;
byte destination = 0xFF;
// Sensor Values - Temp / Humidity
double temperature;
double humidity;
double avgTemperature = 0;
double avgHumidity = 0;
// Sensor Values - Co2
byte co2;
double avgCo2 = 0;
// Sensor Values - Particles
uint32_t ppm10;
uint32_t ppm25;
uint32_t ppm100;
uint32_t avgPpm10 = 0;
uint32_t avgPpm25 = 0;
uint32_t avgPpm100 = 0;
// Partial Sensor Data
struct pms5003data {
uint16_t framelen;
uint16_t pm10_standard, pm25_standard, pm100_standard;
uint16_t pm10_env, pm25_env, pm100_env;
uint16_t particles_03um, particles_05um, particles_10um, particles_25um, particles_50um, particles_100um;
uint16_t unused;
uint16_t checksum;
};
struct pms5003data data;
int pollingFrequency = 2500; // Polling Frequency, ms
int pollEventCount = 0; // Number of times data has been sampled in this recording period
int sendAfterPolls = 10; // Sample Period, after which Transmit Reading
int reservationTime = 1; // How long do we require use of TX / RX airways for sending packets?
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
/**
* Setup Humidity / Temperature Sensor
*/
bool setupSHT() {
Wire.begin();
if (!sht.init()) {
Serial.println("[-] SHT Init Failed");
return false;
}
sht.setAccuracy(SHTSensor::SHT_ACCURACY_MEDIUM);
return true;
}
double getTemperature() {
double t = -1;
if (sht.readSample()) {
t = sht.getTemperature();
temperature += t;
}
return t; // -1 on error TODO Make this better!
}
double getHumidity() {
double h = -1;
if (sht.readSample()) {
h = sht.getHumidity();
humidity += h;
}
return h; // -1 on error
}
bool setupLoRa() {
LoRa.setPins(ss, rst, dio0);
if (!LoRa.begin(loraFrequency)) {
Serial.println("[-] Fatal. Starting LoRa failed!");
return false;
}
LoRa.setSpreadingFactor(loraSpreadingFactor);
LoRa.setSignalBandwidth(loraSignalBandwidth);
Serial.println("[+] LoRa Initialized OK!");
return true;
}
/*
* Return ESP Module ID, based on MAC address
* Source: https://arduino.stackexchange.com/a/58678
*
* 2021-11-07 - This method proved unreliable, switched to casting ESP.getChipID() as a string instead
String getSensorUID() {
// uint64_t chipid = ESP.getChipId();
// uint16_t chip = (uint16_t)(chipid >> 32);
//
// char sensorID[23];
// snprintf(sensorID, 23, "EMIEI-%04X%08X", chip, (uint32_t)chipid);
String sensorID = "EMIEI-";
sensorID.concat(String(ESP.getChipId()));
return sensorID;
}
/**
* Get data from PMS Partical Sensor
* @param Stream s PMS Serial Connection
* @modifies struct data PMS Sensor Data
* @returns boolean Data Read Success status
*/
boolean readPMSdata(Stream *s) {
if (!s->available()) {
return false;
}
// Read a byte at a time until we get to the special '0x42' start-byte
if (s->peek() != 0x42) {
s->read();
return false;
}
// Now read all 32 bytes
if (s->available() < 32) {
return false;
}
uint8_t buffer[32];
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("Expected: "); Serial.print(sum);
Serial.print(", Got: "); Serial.print(data.checksum);
return false;
}
return true;
}
/* Turn on Reciever,
* listen for messages for [listenDuration] seconds,
* Return true if no messages recieved
*/
boolean clearToSend(int listenDuration) {
int listenUntil = now() + listenDuration;
// Listen until timeout expires
while (listenUntil >= now()) {
int packetSize = LoRa.parsePacket();
if (packetSize) {
return false; // Other message heard on Rx, we can not transmit just now.
}
delay(5);
}
return true; // We didn't hear anything, so continue
}
// Listen of listenDuration seconds
// If we see a response to our txHello packet, and txHello.okToTransmit is True, we can send our packet
boolean listenTxHelloAccept(int listenDuration, int messageID) {
int time_now = now() + listenDuration;
// Listen until timeout expires
Serial.println("[+] Transmit - \"Hello\" - Listening for ack & clear to send");
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
while (time_now >= now()) {
int packetSize = LoRa.parsePacket();
if (packetSize) {
String incoming = "";
char temp;
while (LoRa.available()) {
// TODO - Tidy this up, ensure we only read in valid JSON
temp = (char)LoRa.read();
// Opening {
if (incoming.length() == 0 && temp == '{') {
incoming = "{";
// Closing }
} else if (temp == '}') {
incoming.concat("}");
break;
// Anything else that's valid
} else if (incoming.length() > 0) {
incoming.concat(temp);
}
}
// DEBUG
// Serial.print("in listenTxHelloAccept() Recieved: \n");
// Serial.println(incoming);
// Verify its OK to transmit
StaticJsonDocument<200> doc;
deserializeJson(doc, incoming);
const String authIsForUid = doc["uid"];
const int authIsForMessageID = doc["messageID"];
const String gatewayUid = doc["gatewayUid"];
// Verify txHello.okToTransmit is True & UID Match & Message IDs Match
if (authIsForUid == getSensorUID()) { Serial.println("[+] Transmit - \"Hello\" - Sensor UID Match!"); } else { Serial.println("[-] Transmit - \"Hello\" - Sensor UID Mis-Match! " + String(authIsForUid) + " vs " + String(getSensorUID())); }
if (authIsForMessageID == messageID) { Serial.println("[+] Transmit - \"Hello\" - Message ID Match!"); } else { Serial.println("[-] Transmit - \"Hello\" - MessageID Mis-Match!"); }
return (okToTransmit
&& authIsForUid == getSensorUID()
&& authIsForMessageID == messageID);
Serial.println("[-] Transmit - \"Hello\" - Timeout while waiting for Clear to Send\n\n");
return false; // We didn't hear anything, conside this as meaning "Can't Send"
}
void sendJsonPayloadWithLoRa(DynamicJsonDocument payload) {
LoRa.beginPacket();
serializeJson(payload, LoRa);
LoRa.endPacket();
}
// TODO - Finish Acks & Naks
boolean transmitData(DynamicJsonDocument payload) {
// Listen for other communication
// Rx - Listen for other messages, if no messages heard then continue
if (!clearToSend(0.5)) {
Serial.println("[-] Airways busy, could not send");
delay(random(500, 1250)); // Introduce random delay to avoid another collision
// TODO Refactor
while (!clearToSend(0.5)) {
Serial.println("[-] Airways busy, could not send");
delay(random(500, 1250)); // Introduce random delay to avoid another collision
}
//return false;
}
// Send short "clear to send?" packet
// Tx - "I've got data to send!" + UID
// RX - Continue upon recieving "OK" + UID + TX_Auth_ID
DynamicJsonDocument txHello(2048);
txHello["uid"] = sensorID;
txHello["reservationTime"] = reservationTime; // How long do we require reservation of radio?
sendJsonPayloadWithLoRa(txHello);
if (!listenTxHelloAccept(reservationTime * 1.5, msgCount)) { // Can't transmit just now
Serial.println("[-] Transmit - \"Hello\" - Can Not Transmit At This Time\n");
return false;
Serial.println("[+] Recieved - Clear to Transmit Payload"); // Else we have clear to send
// Transmit Payload
// Tx - Send payload + UID + TX_Auth_ID
// Rx - Listen for Ack/Nack
sendJsonPayloadWithLoRa(payload);
// TODO Await Response Ack/Nak
int ackTimeout = 2; // Seconds
int time_now = now() + ackTimeout;
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
// Listen until timeout expires
Serial.println("[.] Transmit - Payload - Listening for Ack");
while (time_now >= now()) {
int packetSize = LoRa.parsePacket();
if (packetSize) {
String incoming = "";
char temp;
while (LoRa.available()) {
// TODO - Tidy this up, ensure we only read in valid JSON
temp = (char)LoRa.read();
// Opening {
if (incoming.length() == 0 && temp == '{') {
incoming = "{";
// Closing }
} else if (temp == '}') {
incoming.concat("}");
break;
// Anything else that's valid
} else if (incoming.length() > 0) {
incoming.concat(temp);
}
}
// DEBUG
// Serial.print("\nin listedForAck() Recieved: \n");
// Serial.println(incoming);
StaticJsonDocument<200> doc;
deserializeJson(doc, incoming);
const bool ackStatus = doc["ackStatus"];
const String authIsForUid = doc["uid"];
const String gatewayUid = doc["gatewayUid"];
// Verify txHello.okToTransmit is True & UID Match
if (authIsForUid == getSensorUID()) {
Serial.println("[+] Transmit - Payload - Ack Recieved: " + String(ackStatus) + "\n");
if (ackStatus) { return true; } // It all worked :)
// TODO Retransmit, recover, etc
return false;
}
// Else UID Mis-Match so we wait for next message
}
delay(5);
}
// TODO After Timeout we need to deal with!
Serial.println("[-] Transmit - Payload - Ack Timeout Reached - Assuming Message Was Not Delivered");
// TODO Listen for ack/nak
// T+2 Rx OFF
// Handle Ack + UID + Next_Send_Interval
// Handle Nak + UID: GoTo T-3
// Handle No Response: GoTo T-3
// T+3 Tx/Rx Window Expires
//LoRa.write(localAddress);
// LoRa.write(msgCount);
//LoRa.write(outgoing.length());
//LoRa.print(outgoing);
void setup() {
delay(1000);
Serial.begin(115200); // Console Debug
pmsSerial.begin(9600); // Partical Sensor
// Setup Sensors
if (!setupSHT()) { while(1); } // Temp/Humidity - Die on Error
// Setup LoRa
if (!setupLoRa()) { while(1); } // Die on error
}
void loop() {
// TODO Gather Sensor Data
// Error reading PMS Data, ignore on this round
if (!readPMSdata(&pmsSerial)) {
data.pm10_standard = 0;
data.pm25_standard = 0;
data.pm100_standard = 0;
}
pollEventCount++;
Serial.println();
Serial.print("PM 1.0: "); Serial.print(data.pm10_standard);
Serial.print("\t\tPM 2.5: "); Serial.print(data.pm25_standard);
Serial.print("\t\tPM 10: "); Serial.print(data.pm100_standard);
Serial.print("\t\tTemp: "); Serial.print(getTemperature());
Serial.print("\t\tHumidity: "); Serial.print(getHumidity());
// Add to Average
// Standard (_standard) or Environmental (_env)
ppm10 = ppm10 + data.pm10_standard;
ppm25 = ppm25 + data.pm25_standard;
ppm100 = ppm100 + data.pm100_standard;
if (pollEventCount == sendAfterPolls) {
// Average Values over recording period
avgPpm10 = ppm10 / sendAfterPolls;
avgPpm25 = ppm25 / sendAfterPolls;
avgPpm100 = ppm100 / sendAfterPolls;
avgTemperature = temperature / sendAfterPolls;
avgHumidity = humidity / sendAfterPolls;
// TODO Transmit Sensor Data
Serial.print("Avg ppm10: "); Serial.print(avgPpm10);
Serial.print("\t\tAvg ppm25: "); Serial.print(avgPpm25);
Serial.print("\t\tAvg ppm100: "); Serial.print(avgPpm100);
Serial.print("\t\tAvg Temp: "); Serial.print(avgTemperature);
Serial.print("\t\tAvg Humidity: "); Serial.print(avgHumidity);
Serial.print("\t\tChip ID: "); Serial.println(sensorID);
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
Serial.println("");
// LORA SEND
DynamicJsonDocument doc(2048);
JsonObject metadata = doc.createNestedObject("sensorMetadata");
metadata["uid"] = sensorID;
metadata["messageID"] = msgCount;
metadata["samplePeriod"] = sendAfterPolls; // TODO: Multiply by poll duration!
JsonObject data = doc.createNestedObject("data");
JsonObject ppm = data.createNestedObject("ppm"); // Particulates
ppm["p10"] = avgPpm10;
ppm["p25"] = avgPpm25;
ppm["p100"] = avgPpm100;
JsonObject sht = data.createNestedObject("sht"); // Temp, Humidity
sht["temperature"] = avgTemperature;
sht["humidity"] = avgHumidity;
JsonObject co2 = data.createNestedObject("co2"); // Co2
co2["tmp"] = 0;
if (transmitData(doc)) {
Serial.println("Packet Sent\n");
int maxRetries = 10; // TODO Move to Config
int numRetries = 1;
while (!transmitData(doc) && numRetries < maxRetries){
numRetries++;
Serial.println("[-] Failed to send packet, retrying. Attempt " + String(numRetries) + " of " + String(maxRetries) + "\n");
delay(reservationTime + random(1250, 5250)); // Introduce random delay to avoid another collision
if (numRetries >= maxRetries) {
Serial.println("[-] Failed to send packet, max retries reached. Aborting");
// TODO Don't Clear Counters, record more values then try to retransmit
}
}
// Reset Loop Values
ppm10 = 0;
ppm25 = 0;
ppm100 = 0;
temperature = 0;
humidity = 0;
pollEventCount = 0;
Serial.println("---------------+++++-------------------");
}