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