diff --git a/libraries/ESP8266WiFiMesh/examples/HelloMesh/HelloMesh.ino b/libraries/ESP8266WiFiMesh/examples/HelloMesh/HelloMesh.ino index cdd3e6d5c..aedc75871 100644 --- a/libraries/ESP8266WiFiMesh/examples/HelloMesh/HelloMesh.ino +++ b/libraries/ESP8266WiFiMesh/examples/HelloMesh/HelloMesh.ino @@ -1,7 +1,7 @@ #include -#include #include #include +#include /** NOTE: Although we could define the strings below as normal String variables, @@ -14,84 +14,94 @@ https://github.com/esp8266/Arduino/issues/1143 https://arduino-esp8266.readthedocs.io/en/latest/PROGMEM.html */ -const char exampleMeshName[] PROGMEM = "MeshNode_"; -const char exampleWiFiPassword[] PROGMEM = "ChangeThisWiFiPassword_TODO"; +const char exampleMeshName[] PROGMEM = "MeshNode_"; // The name of the mesh network. Used as prefix for the node SSID and to find other network nodes in the example networkFilter and broadcastFilter functions below. +const char exampleWiFiPassword[] PROGMEM = "ChangeThisWiFiPassword_TODO"; // The password has to be min 8 and max 64 characters long, otherwise an AP which uses it will not be found during scans. -unsigned int requestNumber = 0; -unsigned int responseNumber = 0; +// A custom encryption key is required when using encrypted ESP-NOW transmissions. There is always a default Kok set, but it can be replaced if desired. +// All ESP-NOW keys below must match in an encrypted connection pair for encrypted communication to be possible. +uint8_t espnowEncryptionKey[16] = {0x33, 0x44, 0x33, 0x44, 0x33, 0x44, 0x33, 0x44, // This is the key for encrypting transmissions. + 0x33, 0x44, 0x33, 0x44, 0x33, 0x44, 0x32, 0x11 + }; +uint8_t espnowHashKey[16] = {0xEF, 0x44, 0x33, 0x0C, 0x33, 0x44, 0xFE, 0x44, // This is the secret key used for HMAC during encrypted connection requests. + 0x33, 0x44, 0x33, 0xB0, 0x33, 0x44, 0x32, 0xAD + }; -String manageRequest(const String &request, ESP8266WiFiMesh &meshInstance); -transmission_status_t manageResponse(const String &response, ESP8266WiFiMesh &meshInstance); -void networkFilter(int numberOfNetworks, ESP8266WiFiMesh &meshInstance); +bool meshMessageHandler(String &message, FloodingMesh &meshInstance); /* Create the mesh node object */ -ESP8266WiFiMesh meshNode = ESP8266WiFiMesh(manageRequest, manageResponse, networkFilter, FPSTR(exampleWiFiPassword), FPSTR(exampleMeshName), "", true); +FloodingMesh floodingMesh = FloodingMesh(meshMessageHandler, FPSTR(exampleWiFiPassword), espnowEncryptionKey, espnowHashKey, FPSTR(exampleMeshName), uint64ToString(ESP.getChipId()), true); + +bool theOne = true; +String theOneMac = ""; + +bool useLED = false; // Change this to true if you wish the onboard LED to mark The One. /** - Callback for when other nodes send you a request + Callback for when a message is received from the mesh network. - @param request The request string received from another node in the mesh - @param meshInstance The ESP8266WiFiMesh instance that called the function. - @returns The string to send back to the other node + @param message The message String received from the mesh. + Modifications to this String are passed on when the message is forwarded from this node to other nodes. + However, the forwarded message will still use the same messageID. + Thus it will not be sent to nodes that have already received this messageID. + If you want to send a new message to the whole network, use a new broadcast from within the loop() instead. + @param meshInstance The FloodingMesh instance that received the message. + @return True if this node should forward the received message to other nodes. False otherwise. */ -String manageRequest(const String &request, ESP8266WiFiMesh &meshInstance) { - // We do not store strings in flash (via F()) in this function. - // The reason is that the other node will be waiting for our response, - // so keeping the strings in RAM will give a (small) improvement in response time. - // Of course, it is advised to adjust this approach based on RAM requirements. +bool meshMessageHandler(String &message, FloodingMesh &meshInstance) { + int32_t delimiterIndex = message.indexOf(meshInstance.broadcastMetadataDelimiter()); + if (delimiterIndex == 0) { + Serial.print("Message received from STA " + meshInstance.getEspnowMeshBackend().getSenderMac() + ": "); + Serial.println(message.substring(1, 101)); - /* Print out received message */ - Serial.print("Request received: "); - Serial.println(request); + String potentialMac = message.substring(1, 13); - /* return a string to send back */ - return ("Hello world response #" + String(responseNumber++) + " from " + meshInstance.getMeshName() + meshInstance.getNodeID() + "."); -} + if (potentialMac > theOneMac) { + if (theOne) { + if (useLED) { + digitalWrite(LED_BUILTIN, HIGH); // Turn LED off (LED is active low) + } -/** - Callback for when you get a response from other nodes + theOne = false; + } - @param response The response string received from another node in the mesh - @param meshInstance The ESP8266WiFiMesh instance that called the function. - @returns The status code resulting from the response, as an int -*/ -transmission_status_t manageResponse(const String &response, ESP8266WiFiMesh &meshInstance) { - transmission_status_t statusCode = TS_TRANSMISSION_COMPLETE; + theOneMac = potentialMac; - /* Print out received message */ - Serial.print(F("Request sent: ")); - Serial.println(meshInstance.getMessage()); - Serial.print(F("Response received: ")); - Serial.println(response); + return true; + } else { + return false; + } + } else if (delimiterIndex > 0) { + if (meshInstance.getOriginMac() == theOneMac) { + uint32_t totalBroadcasts = strtoul(message.c_str(), nullptr, 0); // strtoul stops reading input when an invalid character is discovered. - // Our last request got a response, so time to create a new request. - meshInstance.setMessage(String(F("Hello world request #")) + String(++requestNumber) + String(F(" from ")) - + meshInstance.getMeshName() + meshInstance.getNodeID() + String(F("."))); + // Static variables are only initialized once. + static uint32_t firstBroadcast = totalBroadcasts; - // (void)meshInstance; // This is useful to remove a "unused parameter" compiler warning. Does nothing else. - return statusCode; -} + if (totalBroadcasts - firstBroadcast >= 100) { // Wait a little to avoid start-up glitches + static uint32_t missedBroadcasts = 1; // Starting at one to compensate for initial -1 below. + static uint32_t previousTotalBroadcasts = totalBroadcasts; + static uint32_t totalReceivedBroadcasts = 0; + totalReceivedBroadcasts++; -/** - Callback used to decide which networks to connect to once a WiFi scan has been completed. + missedBroadcasts += totalBroadcasts - previousTotalBroadcasts - 1; // We expect an increment by 1. + previousTotalBroadcasts = totalBroadcasts; - @param numberOfNetworks The number of networks found in the WiFi scan. - @param meshInstance The ESP8266WiFiMesh instance that called the function. -*/ -void networkFilter(int numberOfNetworks, ESP8266WiFiMesh &meshInstance) { - for (int networkIndex = 0; networkIndex < numberOfNetworks; ++networkIndex) { - String currentSSID = WiFi.SSID(networkIndex); - int meshNameIndex = currentSSID.indexOf(meshInstance.getMeshName()); - - /* Connect to any _suitable_ APs which contain meshInstance.getMeshName() */ - if (meshNameIndex >= 0) { - uint64_t targetNodeID = stringToUint64(currentSSID.substring(meshNameIndex + meshInstance.getMeshName().length())); - - if (targetNodeID < stringToUint64(meshInstance.getNodeID())) { - ESP8266WiFiMesh::connectionQueue.push_back(NetworkInfo(networkIndex)); + if (totalReceivedBroadcasts % 50 == 0) { + Serial.println("missed/total: " + String(missedBroadcasts) + '/' + String(totalReceivedBroadcasts)); + } + if (totalReceivedBroadcasts % 500 == 0) { + Serial.println("Benchmark message: " + message.substring(0, 100)); + } } } + } else { + // Only show first 100 characters because printing a large String takes a lot of time, which is a bad thing for a callback function. + // If you need to print the whole String it is better to store it and print it in the loop() later. + Serial.print("Message with origin " + meshInstance.getOriginMac() + " received: "); + Serial.println(message.substring(0, 100)); } + + return true; } void setup() { @@ -102,7 +112,7 @@ void setup() { Serial.begin(115200); delay(50); // Wait for Serial. - //yield(); // Use this if you don't want to wait for Serial. + //yield(); // Use this if you don't want to wait for Serial, but not with the ESP-NOW backend (yield() causes crashes with ESP-NOW). // The WiFi.disconnect() ensures that the WiFi is working correctly. If this is not done before receiving WiFi connections, // those WiFi connections will take a long time to make or sometimes will not work at all. @@ -111,52 +121,52 @@ void setup() { Serial.println(); Serial.println(); - Serial.println(F("Note that this library can use static IP:s for the nodes to speed up connection times.\n" - "Use the setStaticIP method as shown in this example to enable this.\n" - "Ensure that nodes connecting to the same AP have distinct static IP:s.\n" - "Also, remember to change the default mesh network password!\n\n")); + Serial.println(F("If you have an onboard LED on your ESP8266 it is recommended that you change the useLED variable to true.\n" + "That way you will get instant confirmation of the mesh communication.\n" + "Also, remember to change the default mesh network password and ESP-NOW keys!\n")); Serial.println(F("Setting up mesh node...")); - /* Initialise the mesh node */ - meshNode.begin(); - meshNode.activateAP(); // Each AP requires a separate server port. - meshNode.setStaticIP(IPAddress(192, 168, 4, 22)); // Activate static IP mode to speed up connection times. + floodingMesh.begin(); + + uint8_t apMacArray[6] {0}; + theOneMac = macToString(WiFi.softAPmacAddress(apMacArray)); + + if (useLED) { + pinMode(LED_BUILTIN, OUTPUT); // Initialize the LED_BUILTIN pin as an output + digitalWrite(LED_BUILTIN, LOW); // Turn LED on (LED is active low) + } + + floodingMeshDelay(5000); // Give some time for user to start the nodes } -int32_t timeOfLastScan = -10000; +int32_t timeOfLastProclamation = -10000; void loop() { - if (millis() - timeOfLastScan > 3000 // Give other nodes some time to connect between data transfers. - || (WiFi.status() != WL_CONNECTED && millis() - timeOfLastScan > 2000)) { // Scan for networks with two second intervals when not already connected. - String request = String(F("Hello world request #")) + String(requestNumber) + String(F(" from ")) + meshNode.getMeshName() + meshNode.getNodeID() + String(F(".")); - meshNode.attemptTransmission(request, false); - timeOfLastScan = millis(); + static uint32_t benchmarkCount = 0; + static uint32_t loopStart = millis(); - // One way to check how attemptTransmission worked out - if (ESP8266WiFiMesh::latestTransmissionSuccessful()) { - Serial.println(F("Transmission successful.")); + // The floodingMeshDelay() method performs all the background operations for the FloodingMesh (via FloodingMesh::performMeshMaintainance()). + // It is recommended to place one of these methods in the beginning of the loop(), unless there is a need to put them elsewhere. + // Among other things, the method cleans up old ESP-NOW log entries (freeing up RAM) and forwards received mesh messages. + // Note that depending on the amount of messages to forward and their length, this method can take tens or even hundreds of milliseconds to complete. + // More intense transmission activity and less frequent calls to performMeshMaintainance will likely cause the method to take longer to complete, so plan accordingly. + floodingMeshDelay(1); + + if (theOne) { + if (millis() - timeOfLastProclamation > 10000) { + uint32_t startTime = millis(); + floodingMesh.broadcast(String(floodingMesh.broadcastMetadataDelimiter()) + theOneMac + " is The One."); + Serial.println("Proclamation broadcast done in " + String(millis() - startTime) + " ms."); + + timeOfLastProclamation = millis(); + floodingMeshDelay(20); } - // Another way to check how attemptTransmission worked out - if (ESP8266WiFiMesh::latestTransmissionOutcomes.empty()) { - Serial.println(F("No mesh AP found.")); - } else { - for (TransmissionResult &transmissionResult : ESP8266WiFiMesh::latestTransmissionOutcomes) { - if (transmissionResult.transmissionStatus == TS_TRANSMISSION_FAILED) { - Serial.println(String(F("Transmission failed to mesh AP ")) + transmissionResult.SSID); - } else if (transmissionResult.transmissionStatus == TS_CONNECTION_FAILED) { - Serial.println(String(F("Connection failed to mesh AP ")) + transmissionResult.SSID); - } else if (transmissionResult.transmissionStatus == TS_TRANSMISSION_COMPLETE) { - // No need to do anything, transmission was successful. - } else { - Serial.println(String(F("Invalid transmission status for ")) + transmissionResult.SSID + String(F("!"))); - assert(F("Invalid transmission status returned from responseHandler!") && false); - } - } + if (millis() - loopStart > 23000) { // Start benchmarking the mesh once three proclamations have been made + uint32_t startTime = millis(); + floodingMesh.broadcast(String(benchmarkCount++) + String(floodingMesh.broadcastMetadataDelimiter()) + ": Not a spoon in sight."); + Serial.println("Benchmark broadcast done in " + String(millis() - startTime) + " ms."); + floodingMeshDelay(20); } - Serial.println(); - } else { - /* Accept any incoming connections */ - meshNode.acceptRequest(); } } diff --git a/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.cpp b/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.cpp index 42bc01463..20c39e254 100644 --- a/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.cpp +++ b/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.cpp @@ -753,14 +753,19 @@ void EspnowMeshBackend::espnowReceiveCallback(uint8_t *macaddr, uint8_t *dataArr { if(messageType == 'B') { + auto key = std::make_pair(macAndType, messageID); + if(receivedEspnowTransmissions.find(key) != receivedEspnowTransmissions.end()) + return; // Should not call BroadcastFilter more than once for an accepted message + String message = espnowGetMessageContent(dataArray, len); setSenderMac(macaddr); + espnowGetTransmissionMac(dataArray, _senderAPMac); setReceivedEncryptedMessage(usesEncryption(messageID)); bool acceptBroadcast = getBroadcastFilter()(message, *this); if(acceptBroadcast) { // Does nothing if key already in receivedEspnowTransmissions - receivedEspnowTransmissions.insert(std::make_pair(std::make_pair(macAndType, messageID), MessageData(message, espnowGetTransmissionsRemaining(dataArray)))); + receivedEspnowTransmissions.insert(std::make_pair(key, MessageData(message, espnowGetTransmissionsRemaining(dataArray)))); } else { @@ -822,6 +827,7 @@ void EspnowMeshBackend::espnowReceiveCallback(uint8_t *macaddr, uint8_t *dataArr //Serial.println("methodStart request stored " + String(millis() - methodStart)); setSenderMac(macaddr); + espnowGetTransmissionMac(dataArray, _senderAPMac); setReceivedEncryptedMessage(usesEncryption(messageID)); String response = getRequestHandler()(totalMessage, *this); //Serial.println("methodStart response acquired " + String(millis() - methodStart)); @@ -847,6 +853,7 @@ void EspnowMeshBackend::espnowReceiveCallback(uint8_t *macaddr, uint8_t *dataArr } setSenderMac(macaddr); + espnowGetTransmissionMac(dataArray, _senderAPMac); setReceivedEncryptedMessage(usesEncryption(messageID)); getResponseHandler()(totalMessage, *this); } @@ -1135,39 +1142,46 @@ transmission_status_t EspnowMeshBackend::espnowSendToNodeUnsynchronized(const St ////// Transmit ////// - _espnowSendConfirmed = false; - uint32_t transmissionStartTime = millis(); - - while(!_espnowSendConfirmed && millis() - transmissionStartTime < getEspnowTransmissionTimeout()) + uint32_t retransmissions = 0; + if(messageType == 'B') + retransmissions = espnowInstance->getBroadcastTransmissionRedundancy(); + + for(uint32_t i = 0; i <= retransmissions; i++) { - if(esp_now_send(_transmissionTargetBSSID, transmission, transmissionSize) == 0) // == 0 => Success + _espnowSendConfirmed = false; + uint32_t transmissionStartTime = millis(); + + while(!_espnowSendConfirmed && millis() - transmissionStartTime < getEspnowTransmissionTimeout()) { - uint32_t transmissionAttemptStart = millis(); - while(!_espnowSendConfirmed - && (millis() - transmissionAttemptStart < getEspnowRetransmissionInterval()) - && (millis() - transmissionStartTime < getEspnowTransmissionTimeout())) - { - delay(1); // Note that callbacks can be called during delay time, so it is possible to receive a transmission during this delay. + if(esp_now_send(_transmissionTargetBSSID, transmission, transmissionSize) == 0) // == 0 => Success + { + uint32_t transmissionAttemptStart = millis(); + while(!_espnowSendConfirmed + && (millis() - transmissionAttemptStart < getEspnowRetransmissionInterval()) + && (millis() - transmissionStartTime < getEspnowTransmissionTimeout())) + { + delay(1); // Note that callbacks can be called during delay time, so it is possible to receive a transmission during this delay. + } + } + + if(_espnowSendConfirmed) + { + if(messageStart) + { + if(encryptedConnection && !usesConstantSessionKey(messageType) && encryptedConnection->getOwnSessionKey() == messageID) + { + encryptedConnection->setDesync(false); + encryptedConnection->incrementOwnSessionKey(); + } + + messageStart = false; + } + + break; } } - - if(_espnowSendConfirmed) - { - if(messageStart) - { - if(encryptedConnection && !usesConstantSessionKey(messageType) && encryptedConnection->getOwnSessionKey() == messageID) - { - encryptedConnection->setDesync(false); - encryptedConnection->incrementOwnSessionKey(); - } - - messageStart = false; - } - - break; - } } - + if(!_espnowSendConfirmed) { _transmissionsFailed++; @@ -1395,6 +1409,18 @@ uint8_t *EspnowMeshBackend::getSenderMac(uint8_t *macArray) return macArray; } +void EspnowMeshBackend::setSenderAPMac(uint8_t *macArray) +{ + std::copy_n(macArray, 6, _senderAPMac); +} + +String EspnowMeshBackend::getSenderAPMac() {return macToString(_senderAPMac);} +uint8_t *EspnowMeshBackend::getSenderAPMac(uint8_t *macArray) +{ + std::copy_n(_senderAPMac, 6, macArray); + return macArray; +} + void EspnowMeshBackend::setReceivedEncryptedMessage(bool receivedEncryptedMessage) { _receivedEncryptedMessage = receivedEncryptedMessage; } bool EspnowMeshBackend::receivedEncryptedMessage() {return _receivedEncryptedMessage;} @@ -2241,6 +2267,9 @@ void EspnowMeshBackend::broadcast(const String &message) espnowSendToNode(message, broadcastMac, 'B', this); } +void EspnowMeshBackend::setBroadcastTransmissionRedundancy(uint8_t redundancy) { _broadcastTransmissionRedundancy = redundancy; } +uint8_t EspnowMeshBackend::getBroadcastTransmissionRedundancy() { return _broadcastTransmissionRedundancy; } + void EspnowMeshBackend::sendStoredEspnowMessages(const ExpiringTimeTracker *estimatedMaxDurationTracker) { sendPeerRequestConfirmations(estimatedMaxDurationTracker); @@ -2532,7 +2561,7 @@ String EspnowMeshBackend::serializeUnencryptedConnection() { using namespace JsonTranslator; - // Returns: {"connectionState":{"uMessageID":"123"}} + // Returns: {"connectionState":{"unencMsgID":"123"}} return jsonConnectionState + createJsonEndPair(jsonUnencryptedMessageID, String(_unencryptedMessageID)); } diff --git a/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.h b/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.h index ec935d9c6..ad1b20a91 100644 --- a/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.h +++ b/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.h @@ -132,7 +132,7 @@ protected: public: /** - * WiFiMesh Constructor method. Creates a WiFi Mesh Node, ready to be initialised. + * ESP-NOW constructor method. Creates an ESP-NOW node, ready to be initialised. * * @param requestHandler The callback handler for dealing with received requests. Takes a string as an argument which * is the request string received from another node and returns the string to send back. @@ -141,6 +141,8 @@ public: * @param networkFilter The callback handler for deciding which WiFi networks to connect to. * @param broadcastFilter The callback handler for deciding which ESP-NOW broadcasts to accept. * @param meshPassword The WiFi password for the mesh network. + * @param espnowEncryptionKey An uint8_t array containing the key used by this EspnowMeshBackend instance for creating encrypted ESP-NOW connections. + * @param espnowHashKey An uint8_t array containing the secret key used by this EspnowMeshBackend to generate HMACs for encrypted ESP-NOW connections. * @param ssidPrefix The prefix (first part) of the node SSID. * @param ssidSuffix The suffix (last part) of the node SSID. * @param verboseMode Determines if we should print the events occurring in the library to Serial. Off by default. This setting is shared by all EspnowMeshBackend instances. @@ -293,6 +295,15 @@ public: */ void broadcast(const String &message); + /** + * Set the number of redundant transmissions that will be made for every broadcast. + * A greater number increases the likelihood that the broadcast is received, but also means it takes longer time to send. + * + * @param redundancy The number of extra transmissions to make of each broadcast. Defaults to 1. + */ + void setBroadcastTransmissionRedundancy(uint8_t redundancy); + uint8_t getBroadcastTransmissionRedundancy(); + /** * Set the EspnowMeshBackend instance responsible for handling incoming requests. The requestHandler of the instance will be called upon receiving ESP-NOW requests. * @@ -518,6 +529,23 @@ public: */ uint8_t *getSenderMac(uint8_t *macArray); + /** + * Get the AP MAC address of the sender of the most recently received ESP-NOW request, response or broadcast to this EspnowMeshBackend instance. + * Returns a String. + * + * @return A String filled with a hexadecimal representation of the AP MAC, without delimiters. + */ + String getSenderAPMac(); + + /** + * Get the AP MAC address of the sender of the most recently received ESP-NOW request, response or broadcast to this EspnowMeshBackend instance. + * Returns a uint8_t array. + * + * @param macArray The array that should store the MAC address. Must be at least 6 bytes. + * @return macArray filled with the sender AP MAC. + */ + uint8_t *getSenderAPMac(uint8_t *macArray); + /** * Get whether the ESP-NOW request, response or broadcast which was most recently received by this EspnowMeshBackend instance was encrypted or not. * @@ -714,6 +742,13 @@ protected: */ void setSenderMac(uint8_t *macArray); + /** + * Set the MAC address considered to be the AP MAC of the sender of the most recently received ESP-NOW request, response or broadcast. + * + * @param macArray An uint8_t array which contains the MAC address to store. The method will store the first 6 bytes of the array. + */ + void setSenderAPMac(uint8_t *macArray); + /** * Set whether the most recently received ESP-NOW request, response or broadcast is presented as having been encrypted or not. * @@ -875,6 +910,8 @@ private: broadcastFilterType _broadcastFilter; + uint8_t _broadcastTransmissionRedundancy = 1; + static String _ongoingPeerRequestNonce; static uint8_t _ongoingPeerRequestMac[6]; static EspnowMeshBackend *_ongoingPeerRequester; @@ -896,6 +933,7 @@ private: static uint32_t _unencryptedMessageID; uint8_t _senderMac[6] = {0}; + uint8_t _senderAPMac[6] = {0}; bool _receivedEncryptedMessage = false; static bool _espnowSendToNodeMutex; diff --git a/libraries/ESP8266WiFiMesh/src/FloodingMesh.cpp b/libraries/ESP8266WiFiMesh/src/FloodingMesh.cpp new file mode 100644 index 000000000..c501417dd --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/FloodingMesh.cpp @@ -0,0 +1,503 @@ +/* + * Copyright (C) 2019 Anders Löfgren + * + * License (MIT license): + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "FloodingMesh.h" +#include "TypeConversionFunctions.h" +#include "JsonTranslator.h" + +std::set FloodingMesh::availableFloodingMeshes = {}; + +char FloodingMesh::_broadcastMetadataDelimiter = 23; + +void floodingMeshDelay(uint32_t durationMs) +{ + uint32_t startingTime = millis(); + + while(millis() - startingTime < durationMs) + { + delay(1); + FloodingMesh::performMeshMaintainance(); + } +} + +FloodingMesh::FloodingMesh(messageHandlerType messageHandler, const String &meshPassword, const uint8_t espnowEncryptionKey[EspnowProtocolInterpreter::espnowEncryptionKeyLength], + const uint8_t espnowHashKey[EspnowProtocolInterpreter::espnowHashKeyLength], const String &ssidPrefix, + const String &ssidSuffix, bool verboseMode, uint8 meshWiFiChannel) + : _espnowBackend( + [this](const String &request, MeshBackendBase &meshInstance){ return _defaultRequestHandler(request, meshInstance); }, + [this](const String &response, MeshBackendBase &meshInstance){ return _defaultResponseHandler(response, meshInstance); }, + [this](int numberOfNetworks, MeshBackendBase &meshInstance){ return _defaultNetworkFilter(numberOfNetworks, meshInstance); }, + [this](String &firstTransmission, EspnowMeshBackend &meshInstance){ return _defaultBroadcastFilter(firstTransmission, meshInstance); }, + meshPassword, espnowEncryptionKey, espnowHashKey, ssidPrefix, ssidSuffix, verboseMode, meshWiFiChannel) +{ + setMessageHandler(messageHandler); + restoreDefaultTransmissionOutcomesUpdateHook(); +} + +FloodingMesh::FloodingMesh(const String &serializedMeshState, messageHandlerType messageHandler, const String &meshPassword, + const uint8_t espnowEncryptionKey[EspnowProtocolInterpreter::espnowEncryptionKeyLength], + const uint8_t espnowHashKey[EspnowProtocolInterpreter::espnowHashKeyLength], const String &ssidPrefix, + const String &ssidSuffix, bool verboseMode, uint8 meshWiFiChannel) + : FloodingMesh(messageHandler, meshPassword, espnowEncryptionKey, espnowHashKey, ssidPrefix, ssidSuffix, verboseMode, meshWiFiChannel) +{ + loadMeshState(serializedMeshState); +} + +FloodingMesh::~FloodingMesh() +{ + availableFloodingMeshes.erase(this); +} + +void FloodingMesh::begin() +{ + // Initialise the mesh node + getEspnowMeshBackend().begin(); + + // Makes it possible to find the node through scans, and also makes it possible to recover from an encrypted ESP-NOW connection where only the other node is encrypted. + // Note that only one AP can be active at a time in total, and this will always be the one which was last activated. + // Thus the AP is shared by all backends. + getEspnowMeshBackend().activateAP(); + + availableFloodingMeshes.insert(this); // Returns std::pair +} + +void FloodingMesh::performMeshMaintainance() +{ + for(FloodingMesh *meshInstance : availableFloodingMeshes) + { + meshInstance->performMeshInstanceMaintainance(); + } +} + +void FloodingMesh::performMeshInstanceMaintainance() +{ + EspnowMeshBackend::performEspnowMaintainance(); + + for(std::list>::iterator backlogIterator = _forwardingBacklog.begin(); backlogIterator != _forwardingBacklog.end(); ) + { + std::pair &messageData = *backlogIterator; + if(messageData.second) // message encrypted + { + _macIgnoreList = messageData.first.substring(0, 12) + ','; // The message should contain the messageID first + encryptedBroadcastKernel(messageData.first); + _macIgnoreList = ""; + } + else + { + broadcastKernel(messageData.first); + } + + backlogIterator = _forwardingBacklog.erase(backlogIterator); + + EspnowMeshBackend::performEspnowMaintainance(); // It is best to performEspnowMaintainance frequently to keep the Espnow backend responsive. Especially if each encryptedBroadcast takes a lot of time. + } +} + +String FloodingMesh::serializeMeshState() +{ + using namespace JsonTranslator; + + // Returns: {"meshState":{"connectionState":{"unencMsgID":"123"},"meshMsgCount":"123"}} + + String connectionState = getEspnowMeshBackend().serializeUnencryptedConnection(); + + return + "{\"meshState\":{" + + connectionState.substring(1, connectionState.length() - 1) + "," + + createJsonEndPair(jsonMeshMessageCount, String(_messageCount)); +} + +void FloodingMesh::loadMeshState(const String &serializedMeshState) +{ + using namespace JsonTranslator; + + if(!getMeshMessageCount(serializedMeshState, _messageCount)) + getEspnowMeshBackend().warningPrint("WARNING! serializedMeshState did not contain MeshMessageCount. Using default instead."); + + String connectionState = ""; + if(!getConnectionState(serializedMeshState, connectionState) || !getEspnowMeshBackend().addUnencryptedConnection(connectionState)) + { + getEspnowMeshBackend().warningPrint("WARNING! serializedMeshState did not contain unencryptedMessageID. Using default instead."); + } +} + +String FloodingMesh::generateMessageID() +{ + char messageCountArray[2] = { 0 }; + sprintf(messageCountArray, "%04X", _messageCount++); + uint8_t apMac[6] {0}; + return macToString(WiFi.softAPmacAddress(apMac)) + String(messageCountArray); // We use the AP MAC address as ID since it is what shows up during WiFi scans +} + +void FloodingMesh::broadcast(const String &message) +{ + assert(message.length() <= maxUnencryptedMessageSize()); + + String messageID = generateMessageID(); + + // Remove getEspnowMeshBackend().getMeshName() from the broadcastMetadata below to broadcast to all ESP-NOW nodes regardless of MeshName. + String targetMeshName = getEspnowMeshBackend().getMeshName(); + + broadcastKernel(targetMeshName + String(broadcastMetadataDelimiter()) + messageID + String(broadcastMetadataDelimiter()) + message); +} + +void FloodingMesh::broadcastKernel(const String &message) +{ + getEspnowMeshBackend().broadcast(message); +} + +void FloodingMesh::setBroadcastReceptionRedundancy(uint8_t redundancy) +{ + assert(redundancy < 255); + _broadcastReceptionRedundancy = redundancy; +} +uint8_t FloodingMesh::getBroadcastReceptionRedundancy() { return _broadcastReceptionRedundancy; } + +void FloodingMesh::encryptedBroadcast(const String &message) +{ + assert(message.length() <= maxEncryptedMessageSize()); + + String messageID = generateMessageID(); + + encryptedBroadcastKernel(messageID + String(broadcastMetadataDelimiter()) + message); +} + +void FloodingMesh::encryptedBroadcastKernel(const String &message) +{ + getEspnowMeshBackend().attemptAutoEncryptingTransmission(message); +} + +void FloodingMesh::clearMessageLogs() +{ + _messageIDs.clear(); + std::queue().swap(_messageIdOrder); +} + +void FloodingMesh::clearForwardingBacklog() +{ + _forwardingBacklog.clear(); +} + +void FloodingMesh::setMessageHandler(messageHandlerType messageHandler) { _messageHandler = messageHandler; } +FloodingMesh::messageHandlerType FloodingMesh::getMessageHandler() { return _messageHandler; } + +void FloodingMesh::setOriginMac(uint8_t *macArray) +{ + std::copy_n(macArray, 6, _originMac); +} + +String FloodingMesh::getOriginMac() { return macToString(_originMac); } +uint8_t *FloodingMesh::getOriginMac(uint8_t *macArray) +{ + std::copy_n(_originMac, 6, macArray); + return macArray; +} + +uint32_t FloodingMesh::maxUnencryptedMessageSize() +{ + return getEspnowMeshBackend().getMaxMessageLength() - MESSAGE_ID_LENGTH - (getEspnowMeshBackend().getMeshName().length() + 1); // Need room for mesh name + delimiter +} + +uint32_t FloodingMesh::maxEncryptedMessageSize() +{ + // Need 1 extra delimiter character for maximum metadata efficiency (makes it possible to store exactly 18 MACs in metadata by adding an extra transmission) + return getEspnowMeshBackend().getMaxMessageLength() - MESSAGE_ID_LENGTH - 1; +} + +void FloodingMesh::setMessageLogSize(uint16_t messageLogSize) +{ + assert(messageLogSize >= 1); + _messageLogSize = messageLogSize; +} +uint16_t FloodingMesh::messageLogSize() { return _messageLogSize; } + +void FloodingMesh::setBroadcastMetadataDelimiter(char broadcastMetadataDelimiter) +{ + // Using HEX number characters as a delimiter is a bad idea regardless of broadcast type, since they are always in the broadcast metadata + assert(broadcastMetadataDelimiter < 48 || 57 < broadcastMetadataDelimiter); + assert(broadcastMetadataDelimiter < 65 || 70 < broadcastMetadataDelimiter); + + _broadcastMetadataDelimiter = broadcastMetadataDelimiter; +} +char FloodingMesh::broadcastMetadataDelimiter() { return _broadcastMetadataDelimiter; } + +EspnowMeshBackend &FloodingMesh::getEspnowMeshBackend() +{ + return _espnowBackend; +} + +bool FloodingMesh::insertPreliminaryMessageID(uint64_t messageID) +{ + uint8_t apMacArray[6] = { 0 }; + if(messageID >> 16 == macToUint64(WiFi.softAPmacAddress(apMacArray))) + return false; // The node should not receive its own messages. + + auto insertionResult = _messageIDs.emplace(messageID, 0); // Returns std::pair + + if(insertionResult.second) // Insertion succeeded. + updateMessageQueue(insertionResult.first); + else if(insertionResult.first->second < getBroadcastReceptionRedundancy()) // messageID exists but not with desired redundancy + insertionResult.first->second++; + else + return false; // messageID already existed in _messageIDs with desired redundancy + + return true; +} + +bool FloodingMesh::insertCompletedMessageID(uint64_t messageID) +{ + uint8_t apMacArray[6] = { 0 }; + if(messageID >> 16 == macToUint64(WiFi.softAPmacAddress(apMacArray))) + return false; // The node should not receive its own messages. + + auto insertionResult = _messageIDs.emplace(messageID, MESSAGE_COMPLETE); // Returns std::pair + + if(insertionResult.second) // Insertion succeeded. + updateMessageQueue(insertionResult.first); + else if(insertionResult.first->second < MESSAGE_COMPLETE) // messageID exists but is not complete + insertionResult.first->second = MESSAGE_COMPLETE; + else + return false; // messageID already existed in _messageIDs and is complete + + return true; +} + +void FloodingMesh::updateMessageQueue(messageQueueElementType messageIterator) +{ + _messageIdOrder.emplace(messageIterator); + + if(_messageIDs.size() > messageLogSize()) + { + _messageIDs.erase(_messageIdOrder.front()); + _messageIdOrder.pop(); + assert(_messageIDs.size() == messageLogSize()); // If this is false we either have too many elements in messageIDs or we deleted too many elements. + assert(_messageIDs.size() == _messageIdOrder.size()); // The containers should always be in sync + } +} + +void FloodingMesh::restoreDefaultRequestHandler() +{ + getEspnowMeshBackend().setRequestHandler([this](const String &request, MeshBackendBase &meshInstance){ return _defaultRequestHandler(request, meshInstance); }); +} + +void FloodingMesh::restoreDefaultResponseHandler() +{ + getEspnowMeshBackend().setResponseHandler([this](const String &response, MeshBackendBase &meshInstance){ return _defaultResponseHandler(response, meshInstance); }); +} + +void FloodingMesh::restoreDefaultNetworkFilter() +{ + getEspnowMeshBackend().setNetworkFilter([this](int numberOfNetworks, MeshBackendBase &meshInstance){ return _defaultNetworkFilter(numberOfNetworks, meshInstance); }); +} + +void FloodingMesh::restoreDefaultBroadcastFilter() +{ + getEspnowMeshBackend().setBroadcastFilter([this](String &firstTransmission, EspnowMeshBackend &meshInstance){ return _defaultBroadcastFilter(firstTransmission, meshInstance); }); +} + +void FloodingMesh::restoreDefaultTransmissionOutcomesUpdateHook() +{ + /* Optional way of doing things. Lambda is supposedly better https://stackoverflow.com/a/36596295 . + + using namespace std::placeholders; + + getEspnowMeshBackend().setTransmissionOutcomesUpdateHook(std::bind(&FloodingMesh::_defaultTransmissionOutcomesUpdateHook, this, _1)); + */ + + getEspnowMeshBackend().setTransmissionOutcomesUpdateHook([this](MeshBackendBase &meshInstance){ return _defaultTransmissionOutcomesUpdateHook(meshInstance); }); +} + +/** + * Callback for when other nodes send you a request + * + * @param request The request string received from another node in the mesh + * @param meshInstance The MeshBackendBase instance that called the function. + * @return The string to send back to the other node. For ESP-NOW, return an empy string ("") if no response should be sent. + */ +String FloodingMesh::_defaultRequestHandler(const String &request, MeshBackendBase &meshInstance) +{ + (void)meshInstance; // This is useful to remove a "unused parameter" compiler warning. Does nothing else. + + String broadcastTarget = ""; + String remainingRequest = ""; + + if(request.charAt(0) == broadcastMetadataDelimiter()) + { + int32_t broadcastTargetEndIndex = request.indexOf(broadcastMetadataDelimiter(), 1); + + if(broadcastTargetEndIndex == -1) + return ""; // broadcastMetadataDelimiter not found + + broadcastTarget = request.substring(1, broadcastTargetEndIndex + 1); // Include delimiter + remainingRequest = request.substring(broadcastTargetEndIndex + 1); + } + else + { + remainingRequest = request; + } + + int32_t messageIDEndIndex = remainingRequest.indexOf(broadcastMetadataDelimiter()); + + if(messageIDEndIndex == -1) + return ""; // broadcastMetadataDelimiter not found + + uint64_t messageID = stringToUint64(remainingRequest.substring(0, messageIDEndIndex)); + + if(insertCompletedMessageID(messageID)) + { + uint8_t originMacArray[6] = { 0 }; + setOriginMac(uint64ToMac(messageID >> 16, originMacArray)); + + String message = remainingRequest.substring(messageIDEndIndex + 1); + + if(getMessageHandler()(message, *this)) + { + message = broadcastTarget + remainingRequest.substring(0, messageIDEndIndex + 1) + message; + assert(message.length() <= _espnowBackend.getMaxMessageLength()); + _forwardingBacklog.emplace_back(message, getEspnowMeshBackend().receivedEncryptedMessage()); + } + } + + return ""; +} + +/** + * Callback for when you get a response from other nodes + * + * @param response The response string received from another node in the mesh + * @param meshInstance The MeshBackendBase instance that called the function. + * @return The status code resulting from the response, as an int + */ +transmission_status_t FloodingMesh::_defaultResponseHandler(const String &response, MeshBackendBase &meshInstance) +{ + transmission_status_t statusCode = TS_TRANSMISSION_COMPLETE; + + getEspnowMeshBackend().warningPrint("WARNING! Response to FloodingMesh broadcast received, but none is expected!"); + + (void)response; // This is useful to remove a "unused parameter" compiler warning. Does nothing else. + (void)meshInstance; // This is useful to remove a "unused parameter" compiler warning. Does nothing else. + + return statusCode; +} + +/** + * Callback used to decide which networks to connect to once a WiFi scan has been completed. + * + * @param numberOfNetworks The number of networks found in the WiFi scan. + * @param meshInstance The MeshBackendBase instance that called the function. + */ +void FloodingMesh::_defaultNetworkFilter(int numberOfNetworks, MeshBackendBase &meshInstance) +{ + // Note that the network index of a given node may change whenever a new scan is done. + for (int networkIndex = 0; networkIndex < numberOfNetworks; ++networkIndex) + { + String currentSSID = WiFi.SSID(networkIndex); + int meshNameIndex = currentSSID.indexOf(meshInstance.getMeshName()); + + // Connect to any APs which contain meshInstance.getMeshName() + if(meshNameIndex >= 0) + { + if(_macIgnoreList.indexOf(macToString(WiFi.BSSID(networkIndex))) == -1) // If the BSSID is not in the ignore list + { + if(EspnowMeshBackend *espnowInstance = meshBackendCast(&meshInstance)) + { + espnowInstance->connectionQueue().push_back(networkIndex); + } + else + { + Serial.println(String(F("Invalid mesh backend!"))); + } + } + } + } +} + +/** + * Callback used to decide which broadcast messages to accept. Only called for the first transmission in each broadcast. + * If true is returned from this callback, the first broadcast transmission is saved until the entire broadcast message has been received. + * The complete broadcast message will then be sent to the requestHandler. + * If false is returned from this callback, the broadcast message is discarded. + * Note that the BroadcastFilter may be called multiple times for messages that are discarded in this way, but is only called once for accepted messages. + * + * @param firstTransmission The first transmission of the broadcast. + * @param meshInstance The EspnowMeshBackend instance that called the function. + * + * @return True if the broadcast should be accepted. False otherwise. + */ +bool FloodingMesh::_defaultBroadcastFilter(String &firstTransmission, EspnowMeshBackend &meshInstance) +{ + // This broadcastFilter will accept a transmission if it contains the broadcastMetadataDelimiter + // and as metaData either no targetMeshName or a targetMeshName that matches the MeshName of meshInstance + // and insertPreliminaryMessageID(messageID) returns true. + + // Broadcast firstTransmission String structure: targetMeshName+messageID+message. + + int32_t metadataEndIndex = firstTransmission.indexOf(broadcastMetadataDelimiter()); + + if(metadataEndIndex == -1) + return false; // broadcastMetadataDelimiter not found + + String targetMeshName = firstTransmission.substring(0, metadataEndIndex); + + if(targetMeshName != "" && meshInstance.getMeshName() != targetMeshName) + { + return false; // Broadcast is for another mesh network + } + else + { + int32_t messageIDEndIndex = firstTransmission.indexOf(broadcastMetadataDelimiter(), metadataEndIndex + 1); + + if(messageIDEndIndex == -1) + return false; // broadcastMetadataDelimiter not found + + uint64_t messageID = stringToUint64(firstTransmission.substring(metadataEndIndex + 1, messageIDEndIndex)); + + if(insertPreliminaryMessageID(messageID)) + { + // Add broadcast identifier to stored message and mark as accepted broadcast. + firstTransmission = String(broadcastMetadataDelimiter()) + firstTransmission; + return true; + } + else + { + return false; // Broadcast has already been received the maximum number of times + } + } +} + +/** + * Once passed to the setTransmissionOutcomesUpdateHook method of the ESP-NOW backend, + * this function will be called after each update of the latestTransmissionOutcomes vector during attemptTransmission. + * (which happens after each individual transmission has finished) + * + * @param meshInstance The MeshBackendBase instance that called the function. + * + * @return True if attemptTransmission should continue with the next entry in the connectionQueue. False if attemptTransmission should stop. + */ +bool FloodingMesh::_defaultTransmissionOutcomesUpdateHook(MeshBackendBase &meshInstance) +{ + (void)meshInstance; // This is useful to remove a "unused parameter" compiler warning. Does nothing else. + + return true; +} diff --git a/libraries/ESP8266WiFiMesh/src/FloodingMesh.h b/libraries/ESP8266WiFiMesh/src/FloodingMesh.h new file mode 100644 index 000000000..797ceebdd --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/FloodingMesh.h @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2019 Anders Löfgren + * + * License (MIT license): + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef __FLOODINGMESH_H__ +#define __FLOODINGMESH_H__ + +#include "EspnowMeshBackend.h" +#include +#include +#include + +/** + * An alternative to standard delay(). Will continuously call performMeshMaintainance() during the waiting time, so that the FloodingMesh node remains responsive. + * Note that if there is a lot of FloodingMesh transmission activity to the node during the floodingMeshDelay, the desired duration may be overshot by several ms. + * Thus, if precise timing is required, use standard delay() instead. + * + * Should not be used inside callbacks since performMeshMaintainance() can alter the ESP-NOW state. + * + * @param durationMs The shortest allowed delay duration, in milliseconds. + */ +void floodingMeshDelay(uint32_t durationMs); + +class FloodingMesh { + +protected: + + typedef std::function messageHandlerType; + typedef std::unordered_map::iterator messageQueueElementType; + +public: + + /** + * FloodingMesh constructor method. Creates a FloodingMesh node, ready to be initialised. + * + * @param messageHandler The callback handler responsible for dealing with messages received from the mesh. + * @param meshPassword The WiFi password for the mesh network. + * @param espnowEncryptionKey An uint8_t array containing the key used by the EspnowMeshBackend instance for creating encrypted ESP-NOW connections. + * @param espnowHashKey An uint8_t array containing the secret key used by the EspnowMeshBackend instance to generate HMACs for encrypted ESP-NOW connections. + * @param ssidPrefix The prefix (first part) of the node SSID. + * @param ssidSuffix The suffix (last part) of the node SSID. + * @param verboseMode Determines if we should print the events occurring in the library to Serial. Off by default. This setting is shared by all EspnowMeshBackend instances. + * @param meshWiFiChannel The WiFi channel used by the mesh network. Valid values are integers from 1 to 13. Defaults to 1. + * WARNING: The ESP8266 has only one WiFi channel, and the the station/client mode is always prioritized for channel selection. + * This can cause problems if several mesh instances exist on the same ESP8266 and use different WiFi channels. + * In such a case, whenever the station of one mesh instance connects to an AP, it will silently force the + * WiFi channel of any active AP on the ESP8266 to match that of the station. This will cause disconnects and possibly + * make it impossible for other stations to detect the APs whose WiFi channels have changed. + * + */ + FloodingMesh(messageHandlerType messageHandler, const String &meshPassword, const uint8_t espnowEncryptionKey[EspnowProtocolInterpreter::espnowEncryptionKeyLength], + const uint8_t espnowHashKey[EspnowProtocolInterpreter::espnowHashKeyLength], const String &ssidPrefix, + const String &ssidSuffix, bool verboseMode = false, uint8 meshWiFiChannel = 1); + + /** + * This constructor should be used in combination with serializeMeshState() when the node has gone to sleep while other nodes stayed awake. + * Otherwise the message ID will be reset after sleep, which means that the nodes that stayed awake may ignore new broadcasts for a while. + * + * @param serializedMeshState A String with a serialized mesh node state that the node should use. + */ + FloodingMesh(const String &serializedMeshState, messageHandlerType messageHandler, const String &meshPassword, + const uint8_t espnowEncryptionKey[EspnowProtocolInterpreter::espnowEncryptionKeyLength], + const uint8_t espnowHashKey[EspnowProtocolInterpreter::espnowHashKeyLength], const String &ssidPrefix, + const String &ssidSuffix, bool verboseMode = false, uint8 meshWiFiChannel = 1); + + virtual ~FloodingMesh(); + + /** + * The method responsible for initialising this FloodingMesh instance. + * + * Since there is only one WiFi radio on the ESP8266, only the FloodingMesh instance that was the last to begin() will be visible to surrounding nodes. + * All FloodingMesh instances can still broadcast messages though, even if their AP is not visible. + */ + void begin(); + + /** + * Performs maintainance for all available Flooding Mesh instances + */ + static void performMeshMaintainance(); + + /** + * Performs maintainance for this particular Flooding Mesh instance + */ + void performMeshInstanceMaintainance(); + + /** + * Serialize the current mesh node state. Useful to save a state before the node goes to sleep. + * Note that this saves the current state only, so if a broadcast is made after this, the stored state is invalid. + * + * @return A string with the serialized current mesh node state. + */ + String serializeMeshState(); + + /** + * Make an unencrypted broadcast to the entire mesh network. + * + * It is recommended that there is at most one new message transmitted in the mesh every 10, 20, 30 ms for messages of length maxUnencryptedMessageSize()*n, + * where n is (roughly, depending on mesh name length) 1/4, 3/5 and 1 respectively. If transmissions are more frequent than this, message loss will increase. + * + * @param message The message to broadcast. Maximum message length is given by maxUnencryptedMessageSize(). The longer the message, the longer the transmission time. + */ + void broadcast(const String &message); + + /** + * Set the maximum number of redundant copies that will be received of every broadcast. (from different senders) + * A greater number increases the likelihood that at least one of the copies is received successfully, but will also use more RAM. + * + * @param redundancy The maximum number of extra copies that will be accepted. Defaults to 2. Valid values are 0 to 254. + */ + void setBroadcastReceptionRedundancy(uint8_t redundancy); + uint8_t getBroadcastReceptionRedundancy(); + + /** + * Make an encrypted broadcast to the entire mesh network. + * + * ########## WARNING! This an experimental feature. API may change at any time. Only use if you like it when things break. ########## + * Will be very slow compared to unencrypted broadcasts. Probably works OK in a small mesh with a maximum of one new message transmitted in the mesh every second. + * Because of the throughput difference, mixing encypted and unencrypted broadcasts is not recommended if there are frequent mesh broadcasts (multiple per second), + * since a lot of unencrypted broadcasts can build up while a single encrypted broadcast is sent. + * + * It is recommended that verboseMode is turned off if using this, to avoid slowdowns due to excessive Serial printing. + * + * @param message The message to broadcast. Maximum message length is given by maxEncryptedMessageSize(). The longer the message, the longer the transmission time. + */ + void encryptedBroadcast(const String &message); + + void clearMessageLogs(); + void clearForwardingBacklog(); + + /** + * Set the callback handler responsible for dealing with messages received from the mesh. + * + * @param messageHandler The message handler callback function to use. + */ + void setMessageHandler(messageHandlerType messageHandler); + messageHandlerType getMessageHandler(); + + /** + * Get the origin AP MAC address of the most recently received mesh message. + * Returns a String. + * + * @return A String filled with a hexadecimal representation of the MAC, without delimiters. + */ + String getOriginMac(); + + /** + * Get the origin AP MAC address of the most recently received mesh message. + * Returns a uint8_t array. + * + * @param macArray The array that should store the MAC address. Must be at least 6 bytes. + * @return macArray filled with the origin MAC. + */ + uint8_t *getOriginMac(uint8_t *macArray); + + /** + * The number of received messageID:s that will be stored by the node. Used to remember which messages have been received. + * Setting this too low will cause the same message to be received many times. + * Setting this too high will cause the node to run out of RAM. + * In practice, setting this value to more than 1337 is probably a bad idea since the node will run out of RAM quickly and crash as a result. + * + * Defaults to 100. + * + * @param messageLogSize The size of the message log for this FloodingMesh instance. Valid values are 1 to 65535 (uint16_t_max). + * If a value close to the maximum is chosen, there is a high risk the node will ignore transmissions on messageID rollover if they are sent only by one node + * (especially if some transmissions are missed), since the messageID also uses uint16_t. + */ + void setMessageLogSize(uint16_t messageLogSize); + uint16_t messageLogSize(); + + /** + * Hint: Use String.length() to get the ASCII length of a String. + * + * @return The maximum length in bytes an unencrypted ASCII message is allowed to be when broadcasted by this node. + * Note that non-ASCII characters usually require at least two bytes each. + * Also note that for unencrypted messages the maximum size will depend on getEspnowMeshBackend().getMeshName().length() + */ + uint32_t maxUnencryptedMessageSize(); + + /** + * Hint: Use String.length() to get the ASCII length of a String. + * + * @return The maximum length in bytes an encrypted ASCII message is allowed to be when broadcasted by this node. + * Note that non-ASCII characters usually require at least two bytes each. + */ + uint32_t maxEncryptedMessageSize(); + + /** + * Set the delimiter character used for metadata by every FloodingMesh instance. + * Using characters found in the mesh name or in HEX numbers is unwise, as is using ','. + * + * @param broadcastMetadataDelimiter The metadata delimiter character to use. + * Defaults to 23 = End-of-Transmission-Block (ETB) control character in ASCII + */ + static void setBroadcastMetadataDelimiter(char broadcastMetadataDelimiter); + static char broadcastMetadataDelimiter(); + + /* + * Gives you access to the EspnowMeshBackend used by the mesh node. + * The backend handles all mesh communication, and modifying it allows you to change every aspect of the mesh behaviour. + * Random interactions with the backend have a high chance of breaking the mesh network, + * and so are discouraged for those who prefer it when things just work. + */ + EspnowMeshBackend &getEspnowMeshBackend(); + + void restoreDefaultRequestHandler(); + void restoreDefaultResponseHandler(); + void restoreDefaultNetworkFilter(); + void restoreDefaultBroadcastFilter(); + void restoreDefaultTransmissionOutcomesUpdateHook(); + +protected: + + static std::set availableFloodingMeshes; + + String generateMessageID(); + + void broadcastKernel(const String &message); + + void encryptedBroadcastKernel(const String &message); + + bool insertPreliminaryMessageID(uint64_t messageID); + bool insertCompletedMessageID(uint64_t messageID); + void updateMessageQueue(messageQueueElementType messageIterator); + + void loadMeshState(const String &serializedMeshState); + + /** + * Set the MAC address considered to be the origin AP MAC address of the most recently received mesh message. + * + * @param macArray An uint8_t array which contains the MAC address to store. The method will store the first 6 bytes of the array. + */ + void setOriginMac(uint8_t *macArray); + +private: + + static const uint8_t MESSAGE_ID_LENGTH = 17; // 16 characters and one delimiter + static const uint8_t MESSAGE_COMPLETE = 255; + + EspnowMeshBackend _espnowBackend; + + messageHandlerType _messageHandler; + + uint16_t _messageCount = 0; + uint16_t _messageLogSize = 100; + + uint8_t _broadcastReceptionRedundancy = 2; + + static char _broadcastMetadataDelimiter; // Defaults to 23 = End-of-Transmission-Block (ETB) control character in ASCII + + uint8_t _originMac[6] = {0}; + + std::unordered_map _messageIDs = {}; + std::queue _messageIdOrder = {}; + std::list> _forwardingBacklog = {}; + + String _macIgnoreList = ""; + + String _defaultRequestHandler(const String &request, MeshBackendBase &meshInstance); + transmission_status_t _defaultResponseHandler(const String &response, MeshBackendBase &meshInstance); + void _defaultNetworkFilter(int numberOfNetworks, MeshBackendBase &meshInstance); + bool _defaultBroadcastFilter(String &firstTransmission, EspnowMeshBackend &meshInstance); + bool _defaultTransmissionOutcomesUpdateHook(MeshBackendBase &meshInstance); +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/JsonTranslator.h b/libraries/ESP8266WiFiMesh/src/JsonTranslator.h index 8ff320e4d..93aadeb57 100644 --- a/libraries/ESP8266WiFiMesh/src/JsonTranslator.h +++ b/libraries/ESP8266WiFiMesh/src/JsonTranslator.h @@ -40,8 +40,8 @@ namespace JsonTranslator const String jsonNonce = "\"nonce\":"; const String jsonHmac = "\"hmac\":"; const String jsonDesync = "\"desync\":"; - const String jsonUnencryptedMessageID = "\"uMessageID\":"; - const String jsonMeshMessageCount = "\"mMessageCount\":"; + const String jsonUnencryptedMessageID = "\"unencMsgID\":"; + const String jsonMeshMessageCount = "\"meshMsgCount\":"; String createJsonPair(const String &valueIdentifier, const String &value); String createJsonEndPair(const String &valueIdentifier, const String &value); diff --git a/libraries/ESP8266WiFiMesh/src/MessageData.cpp b/libraries/ESP8266WiFiMesh/src/MessageData.cpp index 89508bd43..721792433 100644 --- a/libraries/ESP8266WiFiMesh/src/MessageData.cpp +++ b/libraries/ESP8266WiFiMesh/src/MessageData.cpp @@ -31,7 +31,6 @@ MessageData::MessageData(String &message, uint8_t transmissionsRemaining, uint32 TimeTracker(creationTimeMs) { _transmissionsExpected = transmissionsRemaining + 1; - assert(message.length() <= EspnowMeshBackend::getMaxMessageBytesPerTransmission()); // Should catch some cases where transmission is not null terminated. _totalMessage += message; _transmissionsReceived++; } diff --git a/libraries/ESP8266WiFiMesh/src/TcpIpMeshBackend.h b/libraries/ESP8266WiFiMesh/src/TcpIpMeshBackend.h index b0929c7a6..632bd075d 100644 --- a/libraries/ESP8266WiFiMesh/src/TcpIpMeshBackend.h +++ b/libraries/ESP8266WiFiMesh/src/TcpIpMeshBackend.h @@ -38,7 +38,7 @@ class TcpIpMeshBackend : public MeshBackendBase { public: /** - * WiFiMesh Constructor method. Creates a WiFi Mesh Node, ready to be initialised. + * TCP/IP constructor method. Creates a TCP/IP node, ready to be initialised. * * @param requestHandler The callback handler for dealing with received requests. Takes a string as an argument which * is the request string received from another node and returns the string to send back. diff --git a/libraries/ESP8266WiFiMesh/src/TypeConversionFunctions.h b/libraries/ESP8266WiFiMesh/src/TypeConversionFunctions.h index 1e59dcc77..151185339 100644 --- a/libraries/ESP8266WiFiMesh/src/TypeConversionFunctions.h +++ b/libraries/ESP8266WiFiMesh/src/TypeConversionFunctions.h @@ -82,7 +82,7 @@ uint8_t *stringToMac(const String &macString, uint8_t *macArray); uint64_t macToUint64(const uint8_t *macArray); /** - * Takes a uint64_t value and stores the bits of the first 6 bytes in a uint8_t array. Assumes index 0 of the array should contain MSB. + * Takes a uint64_t value and stores the bits of the first 6 bytes (LSB) in a uint8_t array. Assumes index 0 of the array should contain MSB. * * @param macValue The uint64_t value to convert to a mac array. Value must fit within 6 bytes. * @param macArray A uint8_t array that will hold the mac address once the function returns. Should have a size of at least 6 bytes.