/******************************************** * 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; }