/* * 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" #include "Serializer.h" namespace { namespace TypeCast = MeshTypeConversionFunctions; constexpr uint8_t MESSAGE_ID_LENGTH = 17; // 16 characters and one delimiter constexpr uint8_t MESSAGE_COMPLETE = 255; char _metadataDelimiter = 23; // Defaults to 23 = End-of-Transmission-Block (ETB) control character in ASCII } std::set FloodingMesh::availableFloodingMeshes = {}; void floodingMeshDelay(const uint32_t durationMs) { ExpiringTimeTracker timeout(durationMs); do { // We want to delay before performMeshMaintenance() so background tasks can be managed first. // Initial while combined with YieldAndDelayMs polledTimeout::YieldPolicy is not suitable since the delay then occurs before evaluating the condition (meaning durationMs = 1 never executes the loop interior). delay(1); FloodingMesh::performMeshMaintenance(); } while(!timeout); } FloodingMesh::FloodingMesh(messageHandlerType messageHandler, const String &meshPassword, const uint8_t espnowEncryptedConnectionKey[EspnowProtocolInterpreter::encryptedConnectionKeyLength], const uint8_t espnowHashKey[EspnowProtocolInterpreter::hashKeyLength], const String &ssidPrefix, const String &ssidSuffix, const bool verboseMode, const 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, espnowEncryptedConnectionKey, espnowHashKey, ssidPrefix, ssidSuffix, verboseMode, meshWiFiChannel) { setMessageHandler(messageHandler); restoreDefaultTransmissionOutcomesUpdateHook(); restoreDefaultResponseTransmittedHook(); } FloodingMesh::FloodingMesh(messageHandlerType messageHandler, const String &meshPassword, const String &espnowEncryptedConnectionKeySeed, const String &espnowHashKeySeed, const String &ssidPrefix, const String &ssidSuffix, const bool verboseMode, const uint8 meshWiFiChannel) : FloodingMesh(messageHandler, meshPassword, (const uint8_t[EspnowProtocolInterpreter::encryptedConnectionKeyLength]){0}, (const uint8_t[EspnowProtocolInterpreter::hashKeyLength]){0}, ssidPrefix, ssidSuffix, verboseMode, meshWiFiChannel) { getEspnowMeshBackend().setEspnowEncryptedConnectionKey(espnowEncryptedConnectionKeySeed); getEspnowMeshBackend().setEspnowHashKey(espnowHashKeySeed); } FloodingMesh::FloodingMesh(const String &serializedMeshState, messageHandlerType messageHandler, const String &meshPassword, const uint8_t espnowEncryptedConnectionKey[EspnowProtocolInterpreter::encryptedConnectionKeyLength], const uint8_t espnowHashKey[EspnowProtocolInterpreter::hashKeyLength], const String &ssidPrefix, const String &ssidSuffix, const bool verboseMode, const uint8 meshWiFiChannel) : FloodingMesh(messageHandler, meshPassword, espnowEncryptedConnectionKey, espnowHashKey, ssidPrefix, ssidSuffix, verboseMode, meshWiFiChannel) { loadMeshState(serializedMeshState); } FloodingMesh::FloodingMesh(const String &serializedMeshState, messageHandlerType messageHandler, const String &meshPassword, const String &espnowEncryptedConnectionKeySeed, const String &espnowHashKeySeed, const String &ssidPrefix, const String &ssidSuffix, const bool verboseMode, const uint8 meshWiFiChannel) : FloodingMesh(messageHandler, meshPassword, espnowEncryptedConnectionKeySeed, espnowHashKeySeed, ssidPrefix, ssidSuffix, verboseMode, meshWiFiChannel) { loadMeshState(serializedMeshState); } FloodingMesh::~FloodingMesh() { availableFloodingMeshes.erase(this); } void FloodingMesh::begin() { // Initialise the mesh node getEspnowMeshBackend().begin(); // Used for encrypted broadcasts getEspnowMeshBackend().setEncryptedConnectionsSoftLimit(3); availableFloodingMeshes.insert(this); // Returns std::pair } void FloodingMesh::activateAP() { getEspnowMeshBackend().activateAP(); } void FloodingMesh::deactivateAP() { MeshBackendBase::deactivateAP(); } void FloodingMesh::performMeshMaintenance() { for(FloodingMesh *meshInstance : availableFloodingMeshes) { meshInstance->performMeshInstanceMaintenance(); } } void FloodingMesh::performMeshInstanceMaintenance() { EspnowMeshBackend::performEspnowMaintenance(); for(std::list>::iterator backlogIterator = getForwardingBacklog().begin(); backlogIterator != getForwardingBacklog().end(); ) { std::pair &messageData = *backlogIterator; if(messageData.second) // message encrypted { getMacIgnoreList() = messageData.first.substring(0, 12) + ','; // The message should contain the messageID first encryptedBroadcastKernel(messageData.first); getMacIgnoreList() = emptyString; } else { broadcastKernel(messageData.first); } backlogIterator = getForwardingBacklog().erase(backlogIterator); EspnowMeshBackend::performEspnowMaintenance(); // It is best to performEspnowMaintenance frequently to keep the Espnow backend responsive. Especially if each encryptedBroadcast takes a lot of time. } } String FloodingMesh::serializeMeshState() const { String connectionState = getEspnowMeshBackendConst().serializeUnencryptedConnection(); uint32_t unsyncMsgID = 0; JsonTranslator::getUnsynchronizedMessageID(connectionState, unsyncMsgID); return Serializer::serializeMeshState(String(unsyncMsgID), String(_messageCount)); } void FloodingMesh::loadMeshState(const String &serializedMeshState) { using namespace JsonTranslator; if(!getMeshMessageCount(serializedMeshState, _messageCount)) getEspnowMeshBackend().warningPrint(String(F("WARNING! serializedMeshState did not contain MeshMessageCount. Using default instead."))); String connectionState; if(!getConnectionState(serializedMeshState, connectionState) || !getEspnowMeshBackend().addUnencryptedConnection(connectionState)) { getEspnowMeshBackend().warningPrint(String(F("WARNING! serializedMeshState did not contain unsynchronizedMessageID. Using default instead."))); } } String FloodingMesh::generateMessageID() { char messageCountArray[5] = { 0 }; snprintf(messageCountArray, 5, "%04X", _messageCount++); uint8_t apMac[6] {0}; return TypeCast::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() <= maxUnencryptedMessageLength()); String messageID = generateMessageID(); // Remove getEspnowMeshBackend().getMeshName() from the metadata below to broadcast to all ESP-NOW nodes regardless of MeshName. String targetMeshName = getEspnowMeshBackend().getMeshName(); broadcastKernel(targetMeshName + String(metadataDelimiter()) + messageID + String(metadataDelimiter()) + message); } void FloodingMesh::broadcastKernel(const String &message) { getEspnowMeshBackend().broadcast(message); } void FloodingMesh::setBroadcastReceptionRedundancy(const uint8_t redundancy) { assert(redundancy < 255); _broadcastReceptionRedundancy = redundancy; } uint8_t FloodingMesh::getBroadcastReceptionRedundancy() const { return _broadcastReceptionRedundancy; } void FloodingMesh::encryptedBroadcast(const String &message) { assert(message.length() <= maxEncryptedMessageLength()); String messageID = generateMessageID(); encryptedBroadcastKernel(messageID + String(metadataDelimiter()) + message); } void FloodingMesh::encryptedBroadcastKernel(const String &message) { getEspnowMeshBackend().attemptAutoEncryptingTransmission(message, true); } void FloodingMesh::clearMessageLogs() { _messageIDs.clear(); std::queue().swap(_messageIdOrder); } void FloodingMesh::clearForwardingBacklog() { getForwardingBacklog().clear(); } void FloodingMesh::setMessageHandler(const messageHandlerType messageHandler) { _messageHandler = messageHandler; } FloodingMesh::messageHandlerType FloodingMesh::getMessageHandler() const { return _messageHandler; } void FloodingMesh::setOriginMac(const uint8_t *macArray) { std::copy_n(macArray, 6, _originMac); } String FloodingMesh::getOriginMac() const { return TypeCast::macToString(_originMac); } uint8_t *FloodingMesh::getOriginMac(uint8_t *macArray) const { std::copy_n(_originMac, 6, macArray); return macArray; } std::list> & FloodingMesh::getForwardingBacklog() { return _forwardingBacklog; } String & FloodingMesh::getMacIgnoreList() { return _macIgnoreList; } uint32_t FloodingMesh::maxUnencryptedMessageLength() const { return getEspnowMeshBackendConst().getMaxMessageLength() - MESSAGE_ID_LENGTH - (getEspnowMeshBackendConst().getMeshName().length() + 1); // Need room for mesh name + delimiter } uint32_t FloodingMesh::maxEncryptedMessageLength() const { // 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 getEspnowMeshBackendConst().getMaxMessageLength() - MESSAGE_ID_LENGTH - 1; } void FloodingMesh::setMessageLogSize(const uint16_t messageLogSize) { assert(messageLogSize >= 1); _messageLogSize = messageLogSize; } uint16_t FloodingMesh::messageLogSize() const { return _messageLogSize; } void FloodingMesh::setMetadataDelimiter(const char metadataDelimiter) { // Using HEX number characters as a delimiter is a bad idea regardless of broadcast type, since they are always in the broadcast metadata. // We therefore check for those characters below. assert(metadataDelimiter < '0' || '9' < metadataDelimiter); assert(metadataDelimiter < 'A' || 'F' < metadataDelimiter); assert(metadataDelimiter < 'a' || 'f' < metadataDelimiter); // Reserved for encryptedBroadcast for now assert(metadataDelimiter != ','); _metadataDelimiter = metadataDelimiter; } char FloodingMesh::metadataDelimiter() { return _metadataDelimiter; } EspnowMeshBackend &FloodingMesh::getEspnowMeshBackend() { return _espnowBackend; } const EspnowMeshBackend &FloodingMesh::getEspnowMeshBackendConst() const { return _espnowBackend; } bool FloodingMesh::insertPreliminaryMessageID(const uint64_t messageID) { uint8_t apMacArray[6] = { 0 }; if(messageID >> 16 == TypeCast::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(const uint64_t messageID) { uint8_t apMacArray[6] = { 0 }; if(messageID >> 16 == TypeCast::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(const 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); }); } void FloodingMesh::restoreDefaultResponseTransmittedHook() { getEspnowMeshBackend().setResponseTransmittedHook([this](bool transmissionSuccessful, const String &response, const uint8_t *recipientMac, uint32_t responseIndex, EspnowMeshBackend &meshInstance) { return _defaultResponseTransmittedHook(transmissionSuccessful, response, recipientMac, responseIndex, 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 = request; if(request.charAt(0) == metadataDelimiter()) { int32_t broadcastTargetEndIndex = request.indexOf(metadataDelimiter(), 1); if(broadcastTargetEndIndex == -1) return emptyString; // metadataDelimiter not found broadcastTarget = request.substring(1, broadcastTargetEndIndex + 1); // Include delimiter remainingRequest.remove(0, broadcastTargetEndIndex + 1); } int32_t messageIDEndIndex = remainingRequest.indexOf(metadataDelimiter()); if(messageIDEndIndex == -1) return emptyString; // metadataDelimiter not found uint64_t messageID = TypeCast::stringToUint64(remainingRequest.substring(0, messageIDEndIndex)); if(insertCompletedMessageID(messageID)) { uint8_t originMacArray[6] = { 0 }; setOriginMac(TypeCast::uint64ToMac(messageID >> 16, originMacArray)); // messageID consists of MAC + 16 bit counter String message = remainingRequest; message.remove(0, messageIDEndIndex + 1); // This approach avoids the null value removal of substring() if(getMessageHandler()(message, *this)) { message = broadcastTarget + remainingRequest.substring(0, messageIDEndIndex + 1) + message; assert(message.length() <= _espnowBackend.getMaxMessageLength()); getForwardingBacklog().emplace_back(message, getEspnowMeshBackend().receivedEncryptedTransmission()); } } return emptyString; } /** * 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 */ TransmissionStatusType FloodingMesh::_defaultResponseHandler(const String &response, MeshBackendBase &meshInstance) { TransmissionStatusType statusCode = TransmissionStatusType::TRANSMISSION_COMPLETE; getEspnowMeshBackend().warningPrint(String(F("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(const 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(getMacIgnoreList().indexOf(TypeCast::macToString(WiFi.BSSID(networkIndex))) == -1) // If the BSSID is not in the ignore list { if(EspnowMeshBackend *espnowInstance = TypeCast::meshBackendCast(&meshInstance)) { espnowInstance->connectionQueue().emplace_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 metadataDelimiter // 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(metadataDelimiter()); if(metadataEndIndex == -1) return false; // metadataDelimiter not found String targetMeshName = firstTransmission.substring(0, metadataEndIndex); if(!targetMeshName.isEmpty() && meshInstance.getMeshName() != targetMeshName) { return false; // Broadcast is for another mesh network } int32_t messageIDEndIndex = firstTransmission.indexOf(metadataDelimiter(), metadataEndIndex + 1); if(messageIDEndIndex == -1) return false; // metadataDelimiter not found uint64_t messageID = TypeCast::stringToUint64(firstTransmission.substring(metadataEndIndex + 1, messageIDEndIndex)); if(insertPreliminaryMessageID(messageID)) { // Add broadcast identifier to stored message and mark as accepted broadcast. firstTransmission = String(metadataDelimiter()) + firstTransmission; return true; } 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; } /** * Once passed to the setResponseTransmittedHook method of the ESP-NOW backend, * this function will be called after each attempted ESP-NOW response transmission. * In case of a successful response transmission, this happens just before the response is removed from the waiting list. * Only the hook of the EspnowMeshBackend instance that is getEspnowRequestManager() will be called. * * @param transmissionSuccessful True if the response was transmitted successfully. False otherwise. * @param response The sent response. * @param recipientMac The MAC address the response was sent to. * @param responseIndex The index of the response in the waiting list. * @param meshInstance The EspnowMeshBackend instance that called the function. * * @return True if the response transmission process should continue with the next response in the waiting list. * False if the response transmission process should stop after processing of the just sent response is complete. */ bool FloodingMesh::_defaultResponseTransmittedHook(bool transmissionSuccessful, const String &response, const uint8_t *recipientMac, const uint32_t responseIndex, EspnowMeshBackend &meshInstance) { (void)transmissionSuccessful; // This is useful to remove a "unused parameter" compiler warning. Does nothing else. (void)response; (void)recipientMac; (void)responseIndex; (void)meshInstance; return true; }