diff --git a/libraries/ESP8266WiFiMesh/keywords.txt b/libraries/ESP8266WiFiMesh/keywords.txt index 2ea9f96aa..3574a231d 100644 --- a/libraries/ESP8266WiFiMesh/keywords.txt +++ b/libraries/ESP8266WiFiMesh/keywords.txt @@ -12,9 +12,6 @@ ESP8266WiFiMesh KEYWORD3 # Datatypes (KEYWORD1) ####################################### -ESP8266WiFiMesh KEYWORD1 -NetworkInfo KEYWORD1 -TransmissionResult KEYWORD1 transmission_status_t KEYWORD1 ####################################### @@ -41,7 +38,7 @@ getSSID KEYWORD2 setMessage KEYWORD2 getMessage KEYWORD2 attemptTransmission KEYWORD2 -acceptRequest KEYWORD2 +acceptRequests KEYWORD2 setStaticIP KEYWORD2 getStaticIP KEYWORD2 disableStaticIP->KEYWORD2 diff --git a/libraries/ESP8266WiFiMesh/src/EncryptedConnectionData.cpp b/libraries/ESP8266WiFiMesh/src/EncryptedConnectionData.cpp index ae30afac9..95a5eaf4e 100644 --- a/libraries/ESP8266WiFiMesh/src/EncryptedConnectionData.cpp +++ b/libraries/ESP8266WiFiMesh/src/EncryptedConnectionData.cpp @@ -89,6 +89,11 @@ uint8_t *EncryptedConnectionData::getPeerApMac(uint8_t *resultArray) const return resultArray; } +void EncryptedConnectionData::setPeerApMac(const uint8_t *peerApMac) +{ + std::copy_n(peerApMac, 6, _peerApMac); +} + bool EncryptedConnectionData::connectedTo(const uint8_t *peerMac) const { if(macEqual(peerMac, _peerStaMac) || macEqual(peerMac, _peerApMac)) @@ -146,10 +151,10 @@ bool EncryptedConnectionData::desync() const { return _desync; } String EncryptedConnectionData::serialize() const { - // Returns: {"connectionState":{"duration":"123","password":"abc","ownSessionKey":"1A2","peerSessionKey":"3B4","peerStaMac":"F2","peerApMac":"E3"}} + // Returns: {"connectionState":{"duration":"123","password":"abc","ownSK":"1A2","peerSK":"3B4","peerStaMac":"F2","peerApMac":"E3"}} return - "{\"connectionState\":{" + JsonTranslator::jsonConnectionState + (temporary() ? JsonTranslator::jsonDuration + "\"" + String(temporary()->remainingDuration()) + "\"," : "") + JsonTranslator::jsonDesync + "\"" + String(desync()) + "\"," + JsonTranslator::jsonOwnSessionKey + "\"" + uint64ToString(getOwnSessionKey()) + "\"," diff --git a/libraries/ESP8266WiFiMesh/src/EncryptedConnectionData.h b/libraries/ESP8266WiFiMesh/src/EncryptedConnectionData.h index 312b5139a..56443cbe6 100644 --- a/libraries/ESP8266WiFiMesh/src/EncryptedConnectionData.h +++ b/libraries/ESP8266WiFiMesh/src/EncryptedConnectionData.h @@ -55,7 +55,9 @@ public: // @param resultArray At least size 6. uint8_t *getPeerStaMac(uint8_t *resultArray) const; + void setPeerStaMac(const uint8_t *peerStaMac) = delete; // A method for setPeerStaMac would sometimes require interacting with the ESP-NOW API to change encrypted connections, so it is not implemented. uint8_t *getPeerApMac(uint8_t *resultArray) const; + void setPeerApMac(const uint8_t *peerApMac); bool connectedTo(const uint8_t *peerMac) const; diff --git a/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.cpp b/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.cpp index 072bf7fb7..28c9dc1d4 100644 --- a/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.cpp +++ b/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.cpp @@ -38,6 +38,7 @@ static const uint64_t uint64MSB = 0x8000000000000000; const uint8_t EspnowMeshBackend::broadcastMac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; bool EspnowMeshBackend::_espnowTransmissionMutex = false; +bool EspnowMeshBackend::_espnowConnectionQueueMutex = false; EspnowMeshBackend *EspnowMeshBackend::_espnowRequestManager = nullptr; @@ -61,9 +62,11 @@ uint32_t EspnowMeshBackend::_encryptionRequestTimeoutMs = 300; bool EspnowMeshBackend::_espnowSendConfirmed = false; String EspnowMeshBackend::_ongoingPeerRequestNonce = ""; +uint8_t EspnowMeshBackend::_ongoingPeerRequestMac[6] = {0}; EspnowMeshBackend *EspnowMeshBackend::_ongoingPeerRequester = nullptr; encrypted_connection_status_t EspnowMeshBackend::_ongoingPeerRequestResult = ECS_MAX_CONNECTIONS_REACHED_SELF; uint32_t EspnowMeshBackend::_ongoingPeerRequestEncryptionStart = 0; +bool EspnowMeshBackend::_reciprocalPeerRequestConfirmation = false; uint8_t EspnowMeshBackend::_espnowEncryptionKok[espnowEncryptionKeyLength] = { 0 }; bool EspnowMeshBackend::_espnowEncryptionKokSet = false; @@ -136,8 +139,78 @@ void EspnowMeshBackend::begin() activateEspnow(); } +bool EspnowMeshBackend::activateEspnow() +{ + if (esp_now_init()==0) + { + if(_espnowEncryptionKokSet && esp_now_set_kok(_espnowEncryptionKok, espnowEncryptionKeyLength)) // esp_now_set_kok returns 0 on success. + warningPrint("Failed to set ESP-NOW KoK!"); + + if(getEspnowRequestManager() == nullptr) + { + setEspnowRequestManager(this); + } + + esp_now_register_recv_cb(espnowReceiveCallbackWrapper); + esp_now_register_send_cb([](uint8_t* mac, uint8_t sendStatus) { + if(_espnowSendConfirmed) + return; + else if(!sendStatus && macEqual(mac, _transmissionTargetBSSID)) // sendStatus == 0 when send was OK. + _espnowSendConfirmed = true; // We do not want to reset this to false. That only happens before transmissions. Otherwise subsequent failed send attempts may obscure an initial successful one. + }); + + // Role must be set before adding peers. Cannot be changed while having peers. + // With ESP_NOW_ROLE_CONTROLLER, we always transmit from the station interface, which gives predictability. + if(esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER)) // esp_now_set_self_role returns 0 on success. + warningPrint("Failed to set ESP-NOW role! Maybe ESP-NOW peers are already added?"); + + verboseModePrint("ESP-NOW activated."); + verboseModePrint("My ESP-NOW STA MAC: " + WiFi.macAddress() + "\n"); // Get the station MAC address. The softAP MAC is different. + + return true; + } + else + { + warningPrint("ESP-NOW init failed!"); + return false; + } +} + +bool EspnowMeshBackend::deactivateEspnow() +{ + // esp_now_deinit() clears all ESP-NOW API settings, including receive callback, send callback, Kok and peers. + // The node will however continue to give acks to received ESP-NOW transmissions as long as the receiving interface (AP or STA) is active, even though the transmissions will not be processed. + if(esp_now_deinit() == 0) + { + responsesToSend.clear(); + peerRequestConfirmationsToSend.clear(); + receivedEspnowTransmissions.clear(); + sentRequests.clear(); + receivedRequests.clear(); + encryptedConnections.clear(); + EncryptedConnectionLog::setNewRemovalsScheduled(false); + + return true; + } + else + { + return false; + } +} + std::vector & EspnowMeshBackend::connectionQueue() { + MutexTracker connectionQueueMutexTracker(_espnowConnectionQueueMutex); + if(!connectionQueueMutexTracker.mutexCaptured()) + { + assert(false && "ERROR! connectionQueue locked. Don't call connectionQueue() from callbacks other than NetworkFilter as this may corrupt program state!"); + } + + return _connectionQueue; +} + +const std::vector & EspnowMeshBackend::constConnectionQueue() +{ return _connectionQueue; } @@ -146,8 +219,15 @@ std::vector & EspnowMeshBackend::latestTransmissionOutcomes return _latestTransmissionOutcomes; } -void EspnowMeshBackend::performEspnowMaintainance() +bool EspnowMeshBackend::latestTransmissionSuccessful() { + return latestTransmissionSuccessfulBase(latestTransmissionOutcomes()); +} + +void EspnowMeshBackend::performEspnowMaintainance(uint32_t estimatedMaxDuration) +{ + ExpiringTimeTracker estimatedMaxDurationTracker = ExpiringTimeTracker(estimatedMaxDuration); + // Doing this during an ESP-NOW transmission could invalidate iterators MutexTracker mutexTracker(_espnowTransmissionMutex, handlePostponedRemovals); if(!mutexTracker.mutexCaptured()) @@ -165,7 +245,17 @@ void EspnowMeshBackend::performEspnowMaintainance() updateTemporaryEncryptedConnections(); } - sendEspnowResponses(); + if(estimatedMaxDuration > 0) + { + if(estimatedMaxDurationTracker.expired()) + return; + else + sendStoredEspnowMessages(&estimatedMaxDurationTracker); + } + else + { + sendStoredEspnowMessages(); + } } void EspnowMeshBackend::updateTemporaryEncryptedConnections(bool scheduledRemovalOnly) @@ -195,9 +285,109 @@ void EspnowMeshBackend::updateTemporaryEncryptedConnections(bool scheduledRemova EncryptedConnectionLog::setNewRemovalsScheduled(false); } +template +void EspnowMeshBackend::deleteExpiredLogEntries(std::map, T> &logEntries, uint32_t maxEntryLifetimeMs) +{ + for(typename std::map, T>::iterator entryIterator = logEntries.begin(); + entryIterator != logEntries.end(); ) + { + if(entryIterator->second.timeSinceCreation() > maxEntryLifetimeMs) + { + entryIterator = logEntries.erase(entryIterator); + } + else + ++entryIterator; + } +} + +void EspnowMeshBackend::deleteExpiredLogEntries(std::map, RequestData> &logEntries, uint32_t requestLifetimeMs, uint32_t broadcastLifetimeMs) +{ + for(typename std::map, RequestData>::iterator entryIterator = logEntries.begin(); + entryIterator != logEntries.end(); ) + { + bool broadcast = entryIterator->first.first == uint64BroadcastMac; + uint32_t timeSinceCreation = entryIterator->second.timeSinceCreation(); + + if((!broadcast && timeSinceCreation > requestLifetimeMs) + || (broadcast && timeSinceCreation > broadcastLifetimeMs)) + { + entryIterator = logEntries.erase(entryIterator); + } + else + ++entryIterator; + } +} + +template +void EspnowMeshBackend::deleteExpiredLogEntries(std::list &logEntries, uint32_t maxEntryLifetimeMs) +{ + for(typename std::list::iterator entryIterator = logEntries.begin(); + entryIterator != logEntries.end(); ) + { + if(entryIterator->timeSinceCreation() > maxEntryLifetimeMs) + { + entryIterator = logEntries.erase(entryIterator); + } + else + ++entryIterator; + } +} + +template <> +void EspnowMeshBackend::deleteExpiredLogEntries(std::list &logEntries, uint32_t maxEntryLifetimeMs) +{ + for(typename std::list::iterator entryIterator = logEntries.begin(); + entryIterator != logEntries.end(); ) + { + auto timeTrackerPointer = entryIterator->temporary(); + if(timeTrackerPointer && timeTrackerPointer->timeSinceCreation() > maxEntryLifetimeMs) + { + entryIterator = logEntries.erase(entryIterator); + } + else + ++entryIterator; + } +} + +template <> +void EspnowMeshBackend::deleteExpiredLogEntries(std::list &logEntries, uint32_t maxEntryLifetimeMs) +{ + for(typename std::list::iterator entryIterator = logEntries.begin(); + entryIterator != logEntries.end(); ) + { + auto timeTrackerPointer = entryIterator->temporary(); + if(timeTrackerPointer && timeTrackerPointer->timeSinceCreation() > maxEntryLifetimeMs) + { + entryIterator = logEntries.erase(entryIterator); + } + else + ++entryIterator; + } +} + +void EspnowMeshBackend::clearOldLogEntries() +{ + // Clearing all old log entries at the same time should help minimize heap fragmentation. + + // uint32_t startTime = millis(); + + _timeOfLastLogClear = millis(); + + deleteExpiredLogEntries(receivedEspnowTransmissions, logEntryLifetimeMs()); + deleteExpiredLogEntries(receivedRequests, logEntryLifetimeMs()); // Just needs to be long enough to not accept repeated transmissions by mistake. + deleteExpiredLogEntries(sentRequests, logEntryLifetimeMs(), broadcastResponseTimeoutMs()); + deleteExpiredLogEntries(responsesToSend, logEntryLifetimeMs()); + deleteExpiredLogEntries(peerRequestConfirmationsToSend, getEncryptionRequestTimeout()); +} + void EspnowMeshBackend::espnowReceiveCallbackWrapper(uint8_t *macaddr, uint8_t *dataArray, uint8_t len) { using namespace EspnowProtocolInterpreter; + + // Since this callback can be called during any delay(), we should always consider all mutexes captured. + // This provides a consistent mutex environment, which facilitates development and debugging. + // Otherwise we get issues such as _espnowTransmissionMutex will usually be free, but occasionally taken (when callback occurs in a delay() during attemptTransmission). + MutexTracker captureBanTracker(MutexTracker::captureBan()); if(len >= EspnowProtocolInterpreter::espnowProtocolBytesSize()) // If we do not receive at least the protocol bytes, the transmission is invalid. { @@ -320,7 +510,7 @@ void EspnowMeshBackend::handlePeerRequest(uint8_t *macaddr, uint8_t *dataArray, // Pairing process ends when encryptedConnectionVerificationHeader is received, maxConnectionsReachedHeader is sent or timeout is reached. // Pairing process stages for request receiver: // Receive: encryptionRequestHeader or temporaryEncryptionRequestHeader. - // Send: maxConnectionsReachedHeader / basicConnectionInfoHeader -> encryptedConnectionInfoHeader or maxConnectionsReachedHeader. + // Send: maxConnectionsReachedHeader / basicConnectionInfoHeader -> encryptedConnectionInfoHeader or softLimitEncryptedConnectionInfoHeader or maxConnectionsReachedHeader. // Receive: encryptedConnectionVerificationHeader. using namespace EspnowProtocolInterpreter; @@ -340,6 +530,9 @@ void EspnowMeshBackend::handlePeerRequest(uint8_t *macaddr, uint8_t *dataArray, { int32_t connectionRequestTypeEndIndex = message.indexOf(':', messageHeaderEndIndex + 1); String connectionRequestType = message.substring(messageHeaderEndIndex + 1, connectionRequestTypeEndIndex + 1); + connectionLogIterator encryptedConnection = connectionLogEndIterator(); + if(!getEncryptedConnectionIterator(macaddr, encryptedConnection)) + assert(false && "We must have an encrypted connection if we received an encryptedConnectionVerificationHeader which was encryptedCorrectly."); if(connectionRequestType == encryptionRequestHeader) { @@ -347,10 +540,6 @@ void EspnowMeshBackend::handlePeerRequest(uint8_t *macaddr, uint8_t *dataArray, } else if(connectionRequestType == temporaryEncryptionRequestHeader) { - connectionLogIterator encryptedConnection = connectionLogEndIterator(); - if(!getEncryptedConnectionIterator(macaddr, encryptedConnection)) - assert(false && "We must have an encrypted connection if we received an encryptedConnectionVerificationHeader which was encryptedCorrectly."); - if(encryptedConnection->temporary()) // Should not change duration for existing permanent connections. { uint32_t connectionDuration = 0; @@ -372,10 +561,35 @@ void EspnowMeshBackend::handlePeerRequest(uint8_t *macaddr, uint8_t *dataArray, if(EspnowMeshBackend *currentEspnowRequestManager = getEspnowRequestManager()) { String requestNonce = ""; - if(JsonTranslator::getNonce(message, requestNonce)) + + if(JsonTranslator::getNonce(message, requestNonce) && requestNonce.length() >= 12) // The destination MAC address requires 12 characters. { + uint8_t destinationMac[6] = {0}; + stringToMac(requestNonce, destinationMac); + + uint8_t apMac[6] {0}; + WiFi.softAPmacAddress(apMac); + + bool correctDestination = false; + if(macEqual(destinationMac, apMac)) + { + correctDestination = true; + } + else + { + uint8_t staMac[6] {0}; + WiFi.macAddress(staMac); + + if(macEqual(destinationMac, staMac)) + { + correctDestination = true; + } + } + uint8_t apMacArray[6] = { 0 }; - peerRequestConfirmationsToSend.emplace_back(receivedMessageID, encryptedCorrectly, currentEspnowRequestManager->getMeshPassword(), requestNonce, macaddr, espnowGetTransmissionMac(dataArray, apMacArray), currentEspnowRequestManager->getEspnowHashKey()); + if(correctDestination && JsonTranslator::verifyEncryptionRequestHmac(message, macaddr, espnowGetTransmissionMac(dataArray, apMacArray), currentEspnowRequestManager->getEspnowHashKey(), espnowHashKeyLength)) + peerRequestConfirmationsToSend.emplace_back(receivedMessageID, encryptedCorrectly, currentEspnowRequestManager->getMeshPassword(), currentEspnowRequestManager->encryptedConnectionsSoftLimit(), + requestNonce, macaddr, apMacArray, currentEspnowRequestManager->getEspnowHashKey()); } } } @@ -396,7 +610,7 @@ void EspnowMeshBackend::handlePeerRequestConfirmation(uint8_t *macaddr, uint8_t // Pairing process ends when _ongoingPeerRequestNonce == "" or timeout is reached. // Pairing process stages for request sender: // Send: encryptionRequestHeader or temporaryEncryptionRequestHeader. - // Receive: maxConnectionsReachedHeader / basicConnectionInfoHeader -> encryptedConnectionInfoHeader or maxConnectionsReachedHeader. + // Receive: maxConnectionsReachedHeader / basicConnectionInfoHeader -> encryptedConnectionInfoHeader or softLimitEncryptedConnectionInfoHeader or maxConnectionsReachedHeader. // Send: encryptedConnectionVerificationHeader. using namespace EspnowProtocolInterpreter; @@ -405,18 +619,20 @@ void EspnowMeshBackend::handlePeerRequestConfirmation(uint8_t *macaddr, uint8_t { String message = espnowGetMessageContent(dataArray, len); String requestNonce = ""; - uint8_t macArray[6] = { 0 }; + if(JsonTranslator::getNonce(message, requestNonce) && requestNonce == _ongoingPeerRequestNonce) { int32_t messageHeaderEndIndex = message.indexOf(':'); String messageHeader = message.substring(0, messageHeaderEndIndex + 1); String messageBody = message.substring(messageHeaderEndIndex + 1); + uint8_t apMacArray[6] = { 0 }; + espnowGetTransmissionMac(dataArray, apMacArray); if(messageHeader == basicConnectionInfoHeader) { - // _ongoingPeerRequestResult == ECS_CONNECTION_ESTABLISHED means we have already received a basicConnectionInfoHeader - if(_ongoingPeerRequestResult != ECS_CONNECTION_ESTABLISHED && - JsonTranslator::verifyHmac(message, _ongoingPeerRequester->getEspnowHashKey(), espnowHashKeyLength)) + // encryptedConnectionEstablished(_ongoingPeerRequestResult) means we have already received a basicConnectionInfoHeader + if(!encryptedConnectionEstablished(_ongoingPeerRequestResult) && + JsonTranslator::verifyEncryptionRequestHmac(message, macaddr, apMacArray, _ongoingPeerRequester->getEspnowHashKey(), espnowHashKeyLength)) { _ongoingPeerRequestEncryptionStart = millis(); @@ -425,8 +641,7 @@ void EspnowMeshBackend::handlePeerRequestConfirmation(uint8_t *macaddr, uint8_t if(!getEncryptedConnectionIterator(macaddr, existingEncryptedConnection)) { // Although the newly created session keys are normally never used (they are replaced with synchronized ones later), the session keys must still be randomized to prevent attacks until replaced. - _ongoingPeerRequestResult = _ongoingPeerRequester->addTemporaryEncryptedConnection(macaddr, espnowGetTransmissionMac(dataArray, macArray), - createSessionKey(), createSessionKey(), getEncryptionRequestTimeout()); + _ongoingPeerRequestResult = _ongoingPeerRequester->addTemporaryEncryptedConnection(macaddr, apMacArray, createSessionKey(), createSessionKey(), getEncryptionRequestTimeout()); } else { @@ -442,57 +657,209 @@ void EspnowMeshBackend::handlePeerRequestConfirmation(uint8_t *macaddr, uint8_t } } - if(_ongoingPeerRequestResult != ECS_CONNECTION_ESTABLISHED) + if(!encryptedConnectionEstablished(_ongoingPeerRequestResult)) { // Adding connection failed, abort ongoing peer request. _ongoingPeerRequestNonce = ""; } } } - else + else if(messageHeader == encryptedConnectionInfoHeader || messageHeader == softLimitEncryptedConnectionInfoHeader) { - if(messageHeader == encryptedConnectionInfoHeader) + String messagePassword = ""; + + if(JsonTranslator::getPassword(messageBody, messagePassword) && messagePassword == _ongoingPeerRequester->getMeshPassword()) { - String messagePassword = ""; + // The mesh password is only shared via encrypted messages, so now we know this message is valid since it was encrypted and contained the correct nonce. - if(JsonTranslator::getPassword(messageBody, messagePassword) && messagePassword == _ongoingPeerRequester->getMeshPassword()) + EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(macaddr); + uint64_t peerSessionKey = 0; + uint64_t ownSessionKey = 0; + if(encryptedConnection && JsonTranslator::getPeerSessionKey(messageBody, peerSessionKey) && JsonTranslator::getOwnSessionKey(messageBody, ownSessionKey)) { - // The mesh password is only shared via encrypted messages, so now we know this message is valid since it was encrypted and contained the correct nonce. - - EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(macaddr); - uint64_t peerSessionKey = 0; - uint64_t ownSessionKey = 0; - if(encryptedConnection && JsonTranslator::getPeerSessionKey(messageBody, peerSessionKey) && JsonTranslator::getOwnSessionKey(messageBody, ownSessionKey)) - { - encryptedConnection->setPeerSessionKey(peerSessionKey); - encryptedConnection->setOwnSessionKey(ownSessionKey); - _ongoingPeerRequestResult = ECS_CONNECTION_ESTABLISHED; - } - else - { - _ongoingPeerRequestResult = ECS_REQUEST_TRANSMISSION_FAILED; - } + encryptedConnection->setPeerSessionKey(peerSessionKey); + encryptedConnection->setOwnSessionKey(ownSessionKey); - _ongoingPeerRequestNonce = ""; + if(messageHeader == encryptedConnectionInfoHeader) + _ongoingPeerRequestResult = ECS_CONNECTION_ESTABLISHED; + else if(messageHeader == softLimitEncryptedConnectionInfoHeader) + _ongoingPeerRequestResult = ECS_SOFT_LIMIT_CONNECTION_ESTABLISHED; + else + assert(false && "Unknown _ongoingPeerRequestResult!"); } - } - else if(messageHeader == maxConnectionsReachedHeader) - { - if(JsonTranslator::verifyHmac(message, _ongoingPeerRequester->getEspnowHashKey(), espnowHashKeyLength)) + else { - _ongoingPeerRequestResult = ECS_MAX_CONNECTIONS_REACHED_PEER; - _ongoingPeerRequestNonce = ""; + _ongoingPeerRequestResult = ECS_REQUEST_TRANSMISSION_FAILED; } + + _ongoingPeerRequestNonce = ""; } - else + } + else if(messageHeader == maxConnectionsReachedHeader) + { + if(JsonTranslator::verifyEncryptionRequestHmac(message, macaddr, apMacArray, _ongoingPeerRequester->getEspnowHashKey(), espnowHashKeyLength)) { - assert(messageHeader == basicConnectionInfoHeader || messageHeader == encryptedConnectionInfoHeader || messageHeader == maxConnectionsReachedHeader); + _ongoingPeerRequestResult = ECS_MAX_CONNECTIONS_REACHED_PEER; + _ongoingPeerRequestNonce = ""; } } + else + { + assert(messageHeader == basicConnectionInfoHeader || messageHeader == encryptedConnectionInfoHeader || + messageHeader == softLimitEncryptedConnectionInfoHeader || messageHeader == maxConnectionsReachedHeader); + } } } } +void EspnowMeshBackend::espnowReceiveCallback(uint8_t *macaddr, uint8_t *dataArray, uint8_t len) +{ + using namespace EspnowProtocolInterpreter; + + ////// ////// + /* + if(messageStart) + { + storeTransmission + } + else + { + if(messageFound) + storeTransmission or (erase and return) + else + return + } + + if(transmissionsRemaining != 0) + return + + processMessage + */ + ////// ////// + + char messageType = espnowGetMessageType(dataArray); + uint8_t transmissionsRemaining = espnowGetTransmissionsRemaining(dataArray); + uint64_t uint64Mac = macToUint64(macaddr); + + // The MAC is 6 bytes so two bytes of uint64Mac are free. We must include the messageType there since it is possible that we will + // receive both a request and a response that shares the same messageID from the same uint64Mac, being distinguished only by the messageType. + // This would otherwise potentially cause the request and response to be mixed into one message when they are multi-part transmissions sent roughly at the same time. + macAndType_td macAndType = createMacAndTypeValue(uint64Mac, messageType); + uint64_t messageID = espnowGetMessageID(dataArray); + + //uint32_t methodStart = millis(); + + if(espnowIsMessageStart(dataArray)) + { + if(messageType == 'B') + { + String message = espnowGetMessageContent(dataArray, len); + setSenderMac(macaddr); + 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)))); + } + else + { + return; + } + } + else + { + // Does nothing if key already in receivedEspnowTransmissions + receivedEspnowTransmissions.insert(std::make_pair(std::make_pair(macAndType, messageID), MessageData(dataArray, len))); + } + } + else + { + std::map, MessageData>::iterator storedMessageIterator = receivedEspnowTransmissions.find(std::make_pair(macAndType, messageID)); + + if(storedMessageIterator != receivedEspnowTransmissions.end()) // If we have not stored the key already, we missed the first message part. + { + if(!storedMessageIterator->second.addToMessage(dataArray, len)) + { + // If we received the wrong message part, remove the whole message if we have missed a part. + // Otherwise just ignore the received part since it has already been stored. + + uint8_t transmissionsRemainingExpected = storedMessageIterator->second.getTransmissionsRemaining() - 1; + + if(transmissionsRemaining < transmissionsRemainingExpected) + { + receivedEspnowTransmissions.erase(storedMessageIterator); + return; + } + } + } + else + { + return; + } + } + + //Serial.println("methodStart storage done " + String(millis() - methodStart)); + + if(transmissionsRemaining != 0) + { + return; + } + + std::map, MessageData>::iterator storedMessageIterator = receivedEspnowTransmissions.find(std::make_pair(macAndType, messageID)); + assert(storedMessageIterator != receivedEspnowTransmissions.end()); + + // Copy totalMessage in case user callbacks (request/responseHandler) do something odd with receivedEspnowTransmissions list. + String totalMessage = storedMessageIterator->second.getTotalMessage(); // https://stackoverflow.com/questions/134731/returning-a-const-reference-to-an-object-instead-of-a-copy It is likely that most compilers will perform Named Value Return Value Optimisation in this case + + receivedEspnowTransmissions.erase(storedMessageIterator); // Erase the extra copy of the totalMessage, to save RAM. + + //Serial.println("methodStart erase done " + String(millis() - methodStart)); + + if(messageType == 'Q' || messageType == 'B') // Question (request) or Broadcast + { + storeReceivedRequest(uint64Mac, messageID, TimeTracker(millis())); + //Serial.println("methodStart request stored " + String(millis() - methodStart)); + + setSenderMac(macaddr); + setReceivedEncryptedMessage(usesEncryption(messageID)); + String response = getRequestHandler()(totalMessage, *this); + //Serial.println("methodStart response acquired " + String(millis() - methodStart)); + + if(response.length() > 0) + { + responsesToSend.push_back(ResponseData(response, macaddr, messageID)); + + //Serial.println("methodStart Q done " + String(millis() - methodStart)); + } + } + else if(messageType == 'A') // Answer (response) + { + deleteSentRequest(uint64Mac, messageID); // Request has been answered, so stop accepting new answers about it. + + if(EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(macaddr)) + { + if(encryptedConnection->getOwnSessionKey() == messageID) + { + encryptedConnection->setDesync(false); // We just received an answer to the latest request we sent to the node, so the node sending the answer must now be in sync. + encryptedConnection->incrementOwnSessionKey(); + } + } + + setSenderMac(macaddr); + setReceivedEncryptedMessage(usesEncryption(messageID)); + getResponseHandler()(totalMessage, *this); + } + else + { + assert(messageType == 'Q' || messageType == 'A' || messageType == 'B'); + } + + ESP.wdtFeed(); // Prevents WDT reset in case we receive a lot of transmissions without break. + + //Serial.println("methodStart wdtFeed done " + String(millis() - methodStart)); +} + void EspnowMeshBackend::setEspnowRequestManager(EspnowMeshBackend *espnowMeshInstance) { _espnowRequestManager = espnowMeshInstance; @@ -505,63 +872,9 @@ bool EspnowMeshBackend::isEspnowRequestManager() return (this == getEspnowRequestManager()); } -bool EspnowMeshBackend::activateEspnow() +bool EspnowMeshBackend::encryptedConnectionEstablished(encrypted_connection_status_t connectionStatus) { - if (esp_now_init()==0) - { - if(_espnowEncryptionKokSet && esp_now_set_kok(_espnowEncryptionKok, espnowEncryptionKeyLength)) // esp_now_set_kok returns 0 on success. - warningPrint("Failed to set ESP-NOW KoK!"); - - if(getEspnowRequestManager() == nullptr) - { - setEspnowRequestManager(this); - } - - esp_now_register_recv_cb(espnowReceiveCallbackWrapper); - esp_now_register_send_cb([](uint8_t* mac, uint8_t sendStatus) { - if(_espnowSendConfirmed) - return; - else if(!sendStatus && macEqual(mac, _transmissionTargetBSSID)) // sendStatus == 0 when send was OK. - _espnowSendConfirmed = true; // We do not want to reset this to false. That only happens before transmissions. Otherwise subsequent failed send attempts may obscure an initial successful one. - }); - - // Role must be set before adding peers. Cannot be changed while having peers. - // With ESP_NOW_ROLE_CONTROLLER, we always transmit from the station interface, which gives predictability. - if(esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER)) // esp_now_set_self_role returns 0 on success. - warningPrint("Failed to set ESP-NOW role! Maybe ESP-NOW peers are already added?"); - - verboseModePrint("ESP-NOW activated."); - verboseModePrint("My ESP-NOW STA MAC: " + WiFi.macAddress() + "\n"); // Get the station MAC address. The softAP MAC is different. - - return true; - } - else - { - warningPrint("ESP-NOW init failed!"); - return false; - } -} - -bool EspnowMeshBackend::deactivateEspnow() -{ - // esp_now_deinit() clears all ESP-NOW API settings, including receive callback, send callback, Kok and peers. - // The node will however continue to give acks to received ESP-NOW transmissions as long as the receiving interface (AP or STA) is active, even though the transmissions will not be processed. - if(esp_now_deinit() == 0) - { - responsesToSend.clear(); - peerRequestConfirmationsToSend.clear(); - receivedEspnowTransmissions.clear(); - sentRequests.clear(); - receivedRequests.clear(); - encryptedConnections.clear(); - EncryptedConnectionLog::setNewRemovalsScheduled(false); - - return true; - } - else - { - return false; - } + return connectionStatus > 0; } uint32_t EspnowMeshBackend::logEntryLifetimeMs() @@ -584,101 +897,6 @@ uint32_t EspnowMeshBackend::criticalHeapLevelBuffer() return _criticalHeapLevelBuffer; } -template -void EspnowMeshBackend::deleteExpiredLogEntries(std::map, T> &logEntries, uint32_t maxEntryLifetimeMs) -{ - for(typename std::map, T>::iterator entryIterator = logEntries.begin(); - entryIterator != logEntries.end(); ) - { - if(entryIterator->second.timeSinceCreation() > maxEntryLifetimeMs) - { - entryIterator = logEntries.erase(entryIterator); - } - else - ++entryIterator; - } -} - -void EspnowMeshBackend::deleteExpiredLogEntries(std::map, RequestData> &logEntries, uint32_t requestLifetimeMs, uint32_t broadcastLifetimeMs) -{ - for(typename std::map, RequestData>::iterator entryIterator = logEntries.begin(); - entryIterator != logEntries.end(); ) - { - bool broadcast = entryIterator->first.first == uint64BroadcastMac; - uint32_t timeSinceCreation = entryIterator->second.timeSinceCreation(); - - if((!broadcast && timeSinceCreation > requestLifetimeMs) - || (broadcast && timeSinceCreation > broadcastLifetimeMs)) - { - entryIterator = logEntries.erase(entryIterator); - } - else - ++entryIterator; - } -} - -template -void EspnowMeshBackend::deleteExpiredLogEntries(std::list &logEntries, uint32_t maxEntryLifetimeMs) -{ - for(typename std::list::iterator entryIterator = logEntries.begin(); - entryIterator != logEntries.end(); ) - { - if(entryIterator->timeSinceCreation() > maxEntryLifetimeMs) - { - entryIterator = logEntries.erase(entryIterator); - } - else - ++entryIterator; - } -} - -template <> -void EspnowMeshBackend::deleteExpiredLogEntries(std::list &logEntries, uint32_t maxEntryLifetimeMs) -{ - for(typename std::list::iterator entryIterator = logEntries.begin(); - entryIterator != logEntries.end(); ) - { - auto timeTrackerPointer = entryIterator->temporary(); - if(timeTrackerPointer && timeTrackerPointer->timeSinceCreation() > maxEntryLifetimeMs) - { - entryIterator = logEntries.erase(entryIterator); - } - else - ++entryIterator; - } -} - -template <> -void EspnowMeshBackend::deleteExpiredLogEntries(std::list &logEntries, uint32_t maxEntryLifetimeMs) -{ - for(typename std::list::iterator entryIterator = logEntries.begin(); - entryIterator != logEntries.end(); ) - { - auto timeTrackerPointer = entryIterator->temporary(); - if(timeTrackerPointer && timeTrackerPointer->timeSinceCreation() > maxEntryLifetimeMs) - { - entryIterator = logEntries.erase(entryIterator); - } - else - ++entryIterator; - } -} - -void EspnowMeshBackend::clearOldLogEntries() -{ - // Clearing all old log entries at the same time should help minimize heap fragmentation. - - // uint32_t startTime = millis(); - - _timeOfLastLogClear = millis(); - - deleteExpiredLogEntries(receivedEspnowTransmissions, logEntryLifetimeMs()); - deleteExpiredLogEntries(receivedRequests, logEntryLifetimeMs()); // Just needs to be long enough to not accept repeated transmissions by mistake. - deleteExpiredLogEntries(sentRequests, logEntryLifetimeMs(), broadcastResponseTimeoutMs()); - deleteExpiredLogEntries(responsesToSend, logEntryLifetimeMs()); - deleteExpiredLogEntries(peerRequestConfirmationsToSend, getEncryptionRequestTimeout()); -} - template T *EspnowMeshBackend::getMapValue(std::map &mapIn, uint64_t keyIn) { @@ -807,6 +1025,8 @@ transmission_status_t EspnowMeshBackend::espnowSendToNode(const String &message, uint8_t encryptedMac[6] {0}; encryptedConnection->getEncryptedPeerMac(encryptedMac); + assert(esp_now_is_peer_exist(encryptedMac) > 0 && "ERROR! Attempting to send content marked as encrypted via unencrypted connection!"); + if(encryptedConnection->desync()) { espnowSendToNodeUnsynchronized(synchronizationRequestHeader, encryptedMac, 'S', generateMessageID(encryptedConnection), espnowInstance); @@ -987,6 +1207,7 @@ transmission_status_t EspnowMeshBackend::sendResponse(const String &message, uin if(encryptedConnection) { encryptedConnection->getEncryptedPeerMac(encryptedMac); + assert(esp_now_is_peer_exist(encryptedMac) > 0 && "ERROR! Attempting to send content marked as encrypted via unencrypted connection!"); } return espnowSendToNodeUnsynchronized(message, encryptedConnection ? encryptedMac : targetBSSID, 'A', requestID, this); @@ -1004,154 +1225,6 @@ uint64_t EspnowMeshBackend::macAndTypeToUint64Mac(const macAndType_td &macAndTyp return static_cast(macAndTypeValue) >> 8; } -void EspnowMeshBackend::espnowReceiveCallback(uint8_t *macaddr, uint8_t *dataArray, uint8_t len) -{ - using namespace EspnowProtocolInterpreter; - - ////// ////// - /* - if(messageStart) - { - storeTransmission - } - else - { - if(messageFound) - storeTransmission or (erase and return) - else - return - } - - if(transmissionsRemaining != 0) - return - - processMessage - */ - ////// ////// - - char messageType = espnowGetMessageType(dataArray); - uint8_t transmissionsRemaining = espnowGetTransmissionsRemaining(dataArray); - uint64_t uint64Mac = macToUint64(macaddr); - - // The MAC is 6 bytes so two bytes of uint64Mac are free. We must include the messageType there since it is possible that we will - // receive both a request and a response that shares the same messageID from the same uint64Mac, being distinguished only by the messageType. - // This would otherwise potentially cause the request and response to be mixed into one message when they are multi-part transmissions sent roughly at the same time. - macAndType_td macAndType = createMacAndTypeValue(uint64Mac, messageType); - uint64_t messageID = espnowGetMessageID(dataArray); - - //uint32_t methodStart = millis(); - - if(espnowIsMessageStart(dataArray)) - { - if(messageType == 'B') - { - String message = espnowGetMessageContent(dataArray, len); - setSenderMac(macaddr); - 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)))); - } - else - { - return; - } - } - else - { - // Does nothing if key already in receivedEspnowTransmissions - receivedEspnowTransmissions.insert(std::make_pair(std::make_pair(macAndType, messageID), MessageData(dataArray, len))); - } - } - else - { - std::map, MessageData>::iterator storedMessageIterator = receivedEspnowTransmissions.find(std::make_pair(macAndType, messageID)); - - if(storedMessageIterator != receivedEspnowTransmissions.end()) // If we have not stored the key already, we missed the first message part. - { - if(!storedMessageIterator->second.addToMessage(dataArray, len)) - { - // If we received the wrong message part, remove the whole message if we have missed a part. - // Otherwise just ignore the received part since it has already been stored. - - uint8_t transmissionsRemainingExpected = storedMessageIterator->second.getTransmissionsRemaining() - 1; - - if(transmissionsRemaining < transmissionsRemainingExpected) - { - receivedEspnowTransmissions.erase(storedMessageIterator); - return; - } - } - } - else - { - return; - } - } - - //Serial.println("methodStart storage done " + String(millis() - methodStart)); - - if(transmissionsRemaining != 0) - { - return; - } - - std::map, MessageData>::iterator storedMessageIterator = receivedEspnowTransmissions.find(std::make_pair(macAndType, messageID)); - assert(storedMessageIterator != receivedEspnowTransmissions.end()); - - // Copy totalMessage in case user callbacks (request/responseHandler) do something odd with receivedEspnowTransmissions list. - String totalMessage = storedMessageIterator->second.getTotalMessage(); // https://stackoverflow.com/questions/134731/returning-a-const-reference-to-an-object-instead-of-a-copy It is likely that most compilers will perform Named Value Return Value Optimisation in this case - - receivedEspnowTransmissions.erase(storedMessageIterator); // Erase the extra copy of the totalMessage, to save RAM. - - //Serial.println("methodStart erase done " + String(millis() - methodStart)); - - if(messageType == 'Q' || messageType == 'B') // Question (request) or Broadcast - { - storeReceivedRequest(uint64Mac, messageID, TimeTracker(millis())); - //Serial.println("methodStart request stored " + String(millis() - methodStart)); - - setSenderMac(macaddr); - setReceivedEncryptedMessage(usesEncryption(messageID)); - String response = getRequestHandler()(totalMessage, *this); - //Serial.println("methodStart response acquired " + String(millis() - methodStart)); - - if(response.length() > 0) - { - responsesToSend.push_back(ResponseData(response, macaddr, messageID)); - - //Serial.println("methodStart Q done " + String(millis() - methodStart)); - } - } - else if(messageType == 'A') // Answer (response) - { - deleteSentRequest(uint64Mac, messageID); // Request has been answered, so stop accepting new answers about it. - - if(EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(macaddr)) - { - if(encryptedConnection->getOwnSessionKey() == messageID) - { - encryptedConnection->setDesync(false); // We just received an answer to the latest request we sent to the node, so the node sending the answer must now be in sync. - encryptedConnection->incrementOwnSessionKey(); - } - } - - setSenderMac(macaddr); - setReceivedEncryptedMessage(usesEncryption(messageID)); - getResponseHandler()(totalMessage, *this); - } - else - { - assert(messageType == 'Q' || messageType == 'A' || messageType == 'B'); - } - - ESP.wdtFeed(); // Prevents WDT reset in case we receive a lot of transmissions without break. - - //Serial.println("methodStart wdtFeed done " + String(millis() - methodStart)); -} - void EspnowMeshBackend::setEspnowEncryptionKey(const uint8_t espnowEncryptionKey[espnowEncryptionKeyLength]) { assert(espnowEncryptionKey != nullptr); @@ -1325,6 +1398,11 @@ uint8_t *EspnowMeshBackend::getSenderMac(uint8_t *macArray) void EspnowMeshBackend::setReceivedEncryptedMessage(bool receivedEncryptedMessage) { _receivedEncryptedMessage = receivedEncryptedMessage; } bool EspnowMeshBackend::receivedEncryptedMessage() {return _receivedEncryptedMessage;} +bool EspnowMeshBackend::addUnencryptedConnection(const String &serializedConnectionState) +{ + return JsonTranslator::getUnencryptedMessageID(serializedConnectionState, _unencryptedMessageID); +} + encrypted_connection_status_t EspnowMeshBackend::addEncryptedConnection(uint8_t *peerStaMac, uint8_t *peerApMac, uint64_t peerSessionKey, uint64_t ownSessionKey) { assert(encryptedConnections.size() <= maxEncryptedConnections); // If this is not the case, ESP-NOW is no longer in sync with the library @@ -1508,6 +1586,8 @@ encrypted_connection_status_t EspnowMeshBackend::requestEncryptedConnectionKerne _ongoingPeerRequestResult = ECS_REQUEST_TRANSMISSION_FAILED; _ongoingPeerRequestNonce = requestNonce; _ongoingPeerRequester = this; + _reciprocalPeerRequestConfirmation = false; + std::copy_n(peerMac, 6, _ongoingPeerRequestMac); String requestMessage = encryptionRequestBuilder(requestNonce, existingTimeTracker); verboseModePrint("Sending encrypted connection request to: " + macToString(peerMac)); @@ -1519,6 +1599,9 @@ encrypted_connection_status_t EspnowMeshBackend::requestEncryptedConnectionKerne // _ongoingPeerRequestNonce is set to "" when a peer confirmation response from the mac is received while(millis() - startTime < getEncryptionRequestTimeout() && _ongoingPeerRequestNonce != "") { + // For obvious reasons dividing by exactly 10 is a good choice. + ExpiringTimeTracker maxDurationTracker = ExpiringTimeTracker(getEncryptionRequestTimeout()/10); + sendPeerRequestConfirmations(&maxDurationTracker); // Must be called before delay() to ensure _ongoingPeerRequestNonce != "" is still true, so reciprocal peer request order is preserved. delay(1); } } @@ -1529,9 +1612,17 @@ encrypted_connection_status_t EspnowMeshBackend::requestEncryptedConnectionKerne _ongoingPeerRequestResult = ECS_REQUEST_TRANSMISSION_FAILED; _ongoingPeerRequestNonce = ""; } - else if(_ongoingPeerRequestResult == ECS_CONNECTION_ESTABLISHED) + else if(encryptedConnectionEstablished(_ongoingPeerRequestResult)) { - requestMessage = encryptionRequestBuilder(requestNonce, existingTimeTracker); // Give the builder a chance to update the message + if(_ongoingPeerRequestResult == ECS_CONNECTION_ESTABLISHED) + // Give the builder a chance to update the message + requestMessage = encryptionRequestBuilder(requestNonce, existingTimeTracker); + else if(_ongoingPeerRequestResult == ECS_SOFT_LIMIT_CONNECTION_ESTABLISHED) + // We will only get a soft limit connection. Adjust future actions based on this. + requestMessage = JsonTranslator::createEncryptionRequestHmacMessage(temporaryEncryptionRequestHeader, requestNonce, getEspnowHashKey(), + espnowHashKeyLength, getAutoEncryptionDuration()); + else + assert(false && "Unknown _ongoingPeerRequestResult during encrypted connection finalization!"); int32_t messageHeaderEndIndex = requestMessage.indexOf(':'); String messageHeader = requestMessage.substring(0, messageHeaderEndIndex + 1); @@ -1542,12 +1633,17 @@ encrypted_connection_status_t EspnowMeshBackend::requestEncryptedConnectionKerne && millis() - _ongoingPeerRequestEncryptionStart < getEncryptionRequestTimeout()) { EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(peerMac); - if(!encryptedConnection || encryptedConnection->removalScheduled()) + if(!encryptedConnection) { assert(encryptedConnection && "requestEncryptedConnectionKernel cannot find an encrypted connection!"); // requestEncryptedConnectionRemoval received. _ongoingPeerRequestResult = ECS_REQUEST_TRANSMISSION_FAILED; } + else if(encryptedConnection->removalScheduled() || (encryptedConnection->temporary() && encryptedConnection->temporary()->expired())) + { + // Could possibly be caused by a simultaneous temporary peer request from the peer. + _ongoingPeerRequestResult = ECS_REQUEST_TRANSMISSION_FAILED; + } else { // Finalize connection @@ -1557,7 +1653,7 @@ encrypted_connection_status_t EspnowMeshBackend::requestEncryptedConnectionKerne } else if(messageHeader == temporaryEncryptionRequestHeader) { - if(!existingEncryptedConnection || existingEncryptedConnection->temporary()) + if(encryptedConnection->temporary()) { // Should not change duration of existing permanent connections. uint32_t connectionDuration = 0; @@ -1579,11 +1675,11 @@ encrypted_connection_status_t EspnowMeshBackend::requestEncryptedConnectionKerne } } - if(_ongoingPeerRequestResult != ECS_CONNECTION_ESTABLISHED) + if(!encryptedConnectionEstablished(_ongoingPeerRequestResult)) { - if(!existingEncryptedConnection) + if(!existingEncryptedConnection && !_reciprocalPeerRequestConfirmation) { - // Remove any connection that was added during the request attempt. + // Remove any connection that was added during the request attempt and is no longer in use. removeEncryptedConnectionUnprotected(peerMac); } } @@ -1593,31 +1689,16 @@ encrypted_connection_status_t EspnowMeshBackend::requestEncryptedConnectionKerne return _ongoingPeerRequestResult; } -String EspnowMeshBackend::defaultEncryptionRequestBuilder(const String &requestHeader, const uint32_t durationMs, +String EspnowMeshBackend::defaultEncryptionRequestBuilder(const String &requestHeader, const uint32_t durationMs, const uint8_t *hashKey, const String &requestNonce, const ExpiringTimeTracker &existingTimeTracker) { - using namespace JsonTranslator; - using EspnowProtocolInterpreter::temporaryEncryptionRequestHeader; - (void)existingTimeTracker; // This removes a "unused parameter" compiler warning. Does nothing else. - - String requestMessage = ""; - if(requestHeader == temporaryEncryptionRequestHeader) - { - requestMessage += createEncryptionRequestIntro(requestHeader, durationMs); - } - else - { - requestMessage += createEncryptionRequestIntro(requestHeader); - } - - requestMessage += createEncryptionRequestEnding(requestNonce); - - return requestMessage; + return JsonTranslator::createEncryptionRequestHmacMessage(requestHeader, requestNonce, hashKey, espnowHashKeyLength, durationMs); } -String EspnowMeshBackend::flexibleEncryptionRequestBuilder(const uint32_t minDurationMs, const String &requestNonce, const ExpiringTimeTracker &existingTimeTracker) +String EspnowMeshBackend::flexibleEncryptionRequestBuilder(const uint32_t minDurationMs, const uint8_t *hashKey, + const String &requestNonce, const ExpiringTimeTracker &existingTimeTracker) { using namespace JsonTranslator; using EspnowProtocolInterpreter::temporaryEncryptionRequestHeader; @@ -1625,25 +1706,26 @@ String EspnowMeshBackend::flexibleEncryptionRequestBuilder(const uint32_t minDur uint32_t connectionDuration = minDurationMs >= existingTimeTracker.remainingDuration() ? minDurationMs : existingTimeTracker.remainingDuration(); - return createEncryptionRequestIntro(temporaryEncryptionRequestHeader, connectionDuration) + createEncryptionRequestEnding(requestNonce); + return createEncryptionRequestHmacMessage(temporaryEncryptionRequestHeader, requestNonce, hashKey, espnowHashKeyLength, connectionDuration); } encrypted_connection_status_t EspnowMeshBackend::requestEncryptedConnection(uint8_t *peerMac) { using namespace std::placeholders; - return requestEncryptedConnectionKernel(peerMac, std::bind(defaultEncryptionRequestBuilder, EspnowProtocolInterpreter::encryptionRequestHeader, 0, _1, _2)); + return requestEncryptedConnectionKernel(peerMac, std::bind(defaultEncryptionRequestBuilder, EspnowProtocolInterpreter::encryptionRequestHeader, 0, getEspnowHashKey(), _1, _2)); } encrypted_connection_status_t EspnowMeshBackend::requestTemporaryEncryptedConnection(uint8_t *peerMac, uint32_t durationMs) { using namespace std::placeholders; - return requestEncryptedConnectionKernel(peerMac, std::bind(defaultEncryptionRequestBuilder, EspnowProtocolInterpreter::temporaryEncryptionRequestHeader, durationMs, _1, _2)); + return requestEncryptedConnectionKernel(peerMac, std::bind(defaultEncryptionRequestBuilder, EspnowProtocolInterpreter::temporaryEncryptionRequestHeader, + durationMs, getEspnowHashKey(), _1, _2)); } encrypted_connection_status_t EspnowMeshBackend::requestFlexibleTemporaryEncryptedConnection(uint8_t *peerMac, uint32_t minDurationMs) { using namespace std::placeholders; - return requestEncryptedConnectionKernel(peerMac, std::bind(flexibleEncryptionRequestBuilder, minDurationMs, _1, _2)); + return requestEncryptedConnectionKernel(peerMac, std::bind(flexibleEncryptionRequestBuilder, minDurationMs, getEspnowHashKey(), _1, _2)); } bool EspnowMeshBackend::temporaryEncryptedConnectionToPermanent(uint8_t *peerMac) @@ -1831,6 +1913,14 @@ encrypted_connection_removal_outcome_t EspnowMeshBackend::requestEncryptedConnec void EspnowMeshBackend::setAcceptsUnencryptedRequests(bool acceptsUnencryptedRequests) { _acceptsUnencryptedRequests = acceptsUnencryptedRequests; } bool EspnowMeshBackend::acceptsUnencryptedRequests() { return _acceptsUnencryptedRequests; } +void EspnowMeshBackend::setEncryptedConnectionsSoftLimit(uint8_t softLimit) +{ + assert(softLimit <= 6); // Valid values are 0 to 6, but uint8_t is always at least 0. + _encryptedConnectionsSoftLimit = softLimit; +} + +uint8_t EspnowMeshBackend::encryptedConnectionsSoftLimit() { return _encryptedConnectionsSoftLimit; } + template typename std::vector::iterator EspnowMeshBackend::getEncryptedConnectionIterator(const uint8_t *peerMac, typename std::vector &connectionVector) { @@ -1993,12 +2083,23 @@ void EspnowMeshBackend::attemptTransmission(const String &message, bool scan, bo } prepareForTransmission(message, scan, scanAllWiFiChannels); - - for(EspnowNetworkInfo ¤tNetwork : connectionQueue()) - { - transmission_status_t transmissionResult = initiateTransmission(getMessage(), currentNetwork); - latestTransmissionOutcomes().push_back(TransmissionOutcome{.origin = currentNetwork, .transmissionStatus = transmissionResult}); + MutexTracker connectionQueueMutexTracker(_espnowConnectionQueueMutex); + if(!connectionQueueMutexTracker.mutexCaptured()) + { + assert(false && "ERROR! connectionQueue locked. Don't call attemptTransmission from callbacks as this may corrupt program state! Aborting."); + } + else + { + for(const EspnowNetworkInfo ¤tNetwork : constConnectionQueue()) + { + transmission_status_t transmissionResult = initiateTransmission(getMessage(), currentNetwork); + + latestTransmissionOutcomes().push_back(TransmissionOutcome{.origin = currentNetwork, .transmissionStatus = transmissionResult}); + + if(!getTransmissionOutcomesUpdateHook()(*this)) + break; + } } printTransmissionStatistics(); @@ -2016,7 +2117,7 @@ transmission_status_t EspnowMeshBackend::attemptTransmission(const String &messa return initiateTransmission(message, recipientInfo); } -encrypted_connection_status_t EspnowMeshBackend::initiateAutoEncryptingConnection(const EspnowNetworkInfo &recipientInfo, bool createPermanentConnection, uint8_t *targetBSSID, EncryptedConnectionLog **encryptedConnection) +encrypted_connection_status_t EspnowMeshBackend::initiateAutoEncryptingConnection(const EspnowNetworkInfo &recipientInfo, bool requestPermanentConnection, uint8_t *targetBSSID, EncryptedConnectionLog **existingEncryptedConnection) { assert(recipientInfo.BSSID() != nullptr); // We need at least the BSSID to connect recipientInfo.getBSSID(targetBSSID); @@ -2027,10 +2128,10 @@ encrypted_connection_status_t EspnowMeshBackend::initiateAutoEncryptingConnectio verboseModePrint(F("")); } - *encryptedConnection = getEncryptedConnection(targetBSSID); + *existingEncryptedConnection = getEncryptedConnection(targetBSSID); encrypted_connection_status_t connectionStatus = ECS_MAX_CONNECTIONS_REACHED_SELF; - if(createPermanentConnection) + if(requestPermanentConnection) connectionStatus = requestEncryptedConnection(targetBSSID); else connectionStatus = requestFlexibleTemporaryEncryptedConnection(targetBSSID, getAutoEncryptionDuration()); @@ -2042,64 +2143,77 @@ transmission_status_t EspnowMeshBackend::initiateAutoEncryptingTransmission(cons { transmission_status_t transmissionResult = TS_CONNECTION_FAILED; - if(connectionStatus == ECS_CONNECTION_ESTABLISHED) + if(encryptedConnectionEstablished(connectionStatus)) { + uint8_t encryptedMac[6] {0}; + assert(getEncryptedMac(targetBSSID, encryptedMac) && esp_now_is_peer_exist(encryptedMac) > 0 && "ERROR! Attempting to send content marked as encrypted via unencrypted connection!"); transmissionResult = initiateTransmissionKernel(message, targetBSSID); } return transmissionResult; } -void EspnowMeshBackend::finalizeAutoEncryptingConnection(const uint8_t *targetBSSID, const EncryptedConnectionLog *encryptedConnection, bool createPermanentConnection) +void EspnowMeshBackend::finalizeAutoEncryptingConnection(const uint8_t *targetBSSID, const EncryptedConnectionLog *existingEncryptedConnection, bool requestPermanentConnection) { - if(!encryptedConnection && !createPermanentConnection) + if(!existingEncryptedConnection && !requestPermanentConnection && !_reciprocalPeerRequestConfirmation) { - // Remove any connection that was added during the transmission attempt. + // Remove any connection that was added during the transmission attempt and is no longer in use. removeEncryptedConnectionUnprotected(targetBSSID); } } -void EspnowMeshBackend::attemptAutoEncryptingTransmission(const String &message, bool scan, bool scanAllWiFiChannels, bool createPermanentConnections) +void EspnowMeshBackend::attemptAutoEncryptingTransmission(const String &message, bool requestPermanentConnections, bool scan, bool scanAllWiFiChannels) { MutexTracker outerMutexTracker(_espnowTransmissionMutex, handlePostponedRemovals); if(!outerMutexTracker.mutexCaptured()) { - assert(false && "ERROR! Transmission in progress. Don't call attemptTransmission from callbacks as this may corrupt program state! Aborting."); + assert(false && "ERROR! Transmission in progress. Don't call attemptAutoEncryptingTransmission from callbacks as this may corrupt program state! Aborting."); return; } prepareForTransmission(message, scan, scanAllWiFiChannels); outerMutexTracker.releaseMutex(); + + MutexTracker connectionQueueMutexTracker(_espnowConnectionQueueMutex); + if(!connectionQueueMutexTracker.mutexCaptured()) + { + assert(false && "ERROR! connectionQueue locked. Don't call attemptAutoEncryptingTransmission from callbacks as this may corrupt program state! Aborting."); + } + else + { + for(const EspnowNetworkInfo ¤tNetwork : constConnectionQueue()) + { + uint8_t currentBSSID[6] {0}; + EncryptedConnectionLog *existingEncryptedConnection = nullptr; + encrypted_connection_status_t connectionStatus = initiateAutoEncryptingConnection(currentNetwork, requestPermanentConnections, currentBSSID, &existingEncryptedConnection); - for(EspnowNetworkInfo ¤tNetwork : connectionQueue()) - { - uint8_t currentBSSID[6] {0}; - EncryptedConnectionLog *encryptedConnection = nullptr; - encrypted_connection_status_t connectionStatus = initiateAutoEncryptingConnection(currentNetwork, createPermanentConnections, currentBSSID, &encryptedConnection); - - MutexTracker innerMutexTracker = MutexTracker(_espnowTransmissionMutex); - if(!innerMutexTracker.mutexCaptured()) - { - assert(false && "ERROR! Unable to recapture Mutex in attemptAutoEncryptingTransmission. Aborting."); - return; + MutexTracker innerMutexTracker = MutexTracker(_espnowTransmissionMutex); + if(!innerMutexTracker.mutexCaptured()) + { + assert(false && "ERROR! Unable to recapture Mutex in attemptAutoEncryptingTransmission. Aborting."); + return; + } + + transmission_status_t transmissionResult = initiateAutoEncryptingTransmission(getMessage(), currentBSSID, connectionStatus); + + latestTransmissionOutcomes().push_back(TransmissionOutcome{.origin = currentNetwork, .transmissionStatus = transmissionResult}); + + finalizeAutoEncryptingConnection(currentBSSID, existingEncryptedConnection, requestPermanentConnections); + + if(!getTransmissionOutcomesUpdateHook()(*this)) + break; } - - transmission_status_t transmissionResult = initiateAutoEncryptingTransmission(getMessage(), currentBSSID, connectionStatus); - - latestTransmissionOutcomes().push_back(TransmissionOutcome{.origin = currentNetwork, .transmissionStatus = transmissionResult}); - - finalizeAutoEncryptingConnection(currentBSSID, encryptedConnection, createPermanentConnections); } printTransmissionStatistics(); } -transmission_status_t EspnowMeshBackend::attemptAutoEncryptingTransmission(const String &message, const EspnowNetworkInfo &recipientInfo, bool createPermanentConnection) +transmission_status_t EspnowMeshBackend::attemptAutoEncryptingTransmission(const String &message, const EspnowNetworkInfo &recipientInfo, bool requestPermanentConnection) { uint8_t targetBSSID[6] {0}; - EncryptedConnectionLog *encryptedConnection = nullptr; - encrypted_connection_status_t connectionStatus = initiateAutoEncryptingConnection(recipientInfo, createPermanentConnection, targetBSSID, &encryptedConnection); + EncryptedConnectionLog *existingEncryptedConnection = nullptr; + encrypted_connection_status_t connectionStatus = initiateAutoEncryptingConnection(recipientInfo, requestPermanentConnection, targetBSSID, &existingEncryptedConnection); MutexTracker mutexTracker(_espnowTransmissionMutex, handlePostponedRemovals); if(!mutexTracker.mutexCaptured()) @@ -2110,7 +2224,7 @@ transmission_status_t EspnowMeshBackend::attemptAutoEncryptingTransmission(const transmission_status_t transmissionResult = initiateAutoEncryptingTransmission(message, targetBSSID, connectionStatus); - finalizeAutoEncryptingConnection(targetBSSID, encryptedConnection, createPermanentConnection); + finalizeAutoEncryptingConnection(targetBSSID, existingEncryptedConnection, requestPermanentConnection); return transmissionResult; } @@ -2127,20 +2241,36 @@ void EspnowMeshBackend::broadcast(const String &message) espnowSendToNode(message, broadcastMac, 'B', this); } -void EspnowMeshBackend::sendEspnowResponses() +void EspnowMeshBackend::sendStoredEspnowMessages(const ExpiringTimeTracker *estimatedMaxDurationTracker) { - //uint32_t startTime = millis(); + sendPeerRequestConfirmations(estimatedMaxDurationTracker); + if(estimatedMaxDurationTracker && estimatedMaxDurationTracker->expired()) + return; + + sendEspnowResponses(estimatedMaxDurationTracker); +} + +void EspnowMeshBackend::sendPeerRequestConfirmations(const ExpiringTimeTracker *estimatedMaxDurationTracker) +{ uint32_t bufferedCriticalHeapLevel = criticalHeapLevel() + criticalHeapLevelBuffer(); // We preferably want to start clearing the logs a bit before things get critical. - + // _ongoingPeerRequestNonce can change during every delay(), but we need to remember the initial value to know from where sendPeerRequestConfirmations was called. + String initialOngoingPeerRequestNonce = _ongoingPeerRequestNonce; + for(std::list::iterator confirmationsIterator = peerRequestConfirmationsToSend.begin(); confirmationsIterator != peerRequestConfirmationsToSend.end(); ) { using namespace EspnowProtocolInterpreter; + + // True if confirmationsIterator contains a peer request received from the same node we are currently sending a peer request to. + bool reciprocalPeerRequest = initialOngoingPeerRequestNonce != "" && confirmationsIterator->connectedTo(_ongoingPeerRequestMac); auto timeTrackerPointer = confirmationsIterator->temporary(); assert(timeTrackerPointer); // peerRequestConfirmations should always expire and so should always have a timeTracker - if(timeTrackerPointer->timeSinceCreation() > getEncryptionRequestTimeout()) + if(timeTrackerPointer->timeSinceCreation() > getEncryptionRequestTimeout() + || (reciprocalPeerRequest && confirmationsIterator->getPeerRequestNonce() <= initialOngoingPeerRequestNonce)) { + // The peer request has expired, + // or the peer request comes from the node we are currently making a peer request to ourselves and we are supposed to wait in this event to avoid simultaneous session key transfer. ++confirmationsIterator; continue; } @@ -2164,7 +2294,8 @@ void EspnowMeshBackend::sendEspnowResponses() staticVerboseModePrint("Responding to encrypted connection request from MAC " + macToString(defaultBSSID)); - if(!existingEncryptedConnection && encryptedConnections.size() >= maxEncryptedConnections) + if(!existingEncryptedConnection && + ((reciprocalPeerRequest && encryptedConnections.size() >= maxEncryptedConnections) || (!reciprocalPeerRequest && reservedEncryptedConnections() >= maxEncryptedConnections))) { espnowSendToNodeUnsynchronized(JsonTranslator::createEncryptionRequestHmacMessage(maxConnectionsReachedHeader, confirmationsIterator->getPeerRequestNonce(), hashKey, espnowHashKeyLength), @@ -2200,7 +2331,7 @@ void EspnowMeshBackend::sendEspnowResponses() { warningPrint("WARNING! Ignoring received encrypted connection request since no EspnowRequestManager is assigned."); } - + if(!existingEncryptedConnection) { // Send "node full" message @@ -2210,11 +2341,29 @@ void EspnowMeshBackend::sendEspnowResponses() } else { - delay(1); // Give some time for the peer to add an encrypted connection + if(reciprocalPeerRequest) + _reciprocalPeerRequestConfirmation = true; + + delay(5); // Give some time for the peer to add an encrypted connection + + assert(esp_now_is_peer_exist(defaultBSSID) > 0 && "ERROR! Attempting to send content marked as encrypted via unencrypted connection!"); + + String messageHeader = ""; + + if(existingEncryptedConnection->temporary() && // Should never change permanent connections + ((reciprocalPeerRequest && encryptedConnections.size() > confirmationsIterator->getEncryptedConnectionsSoftLimit()) + || (!reciprocalPeerRequest && reservedEncryptedConnections() > confirmationsIterator->getEncryptedConnectionsSoftLimit()))) + { + messageHeader = softLimitEncryptedConnectionInfoHeader; + } + else + { + messageHeader = encryptedConnectionInfoHeader; + } // Send password and keys. // Probably no need to know which connection type to use, that is stored in request node and will be sent over for finalization. - espnowSendToNodeUnsynchronized(JsonTranslator::createEncryptedConnectionInfo( + espnowSendToNodeUnsynchronized(JsonTranslator::createEncryptedConnectionInfo(messageHeader, confirmationsIterator->getPeerRequestNonce(), confirmationsIterator->getAuthenticationPassword(), existingEncryptedConnection->getOwnSessionKey(), existingEncryptedConnection->getPeerSessionKey()), defaultBSSID, 'C', generateMessageID(nullptr)); // Generates a new message ID to avoid sending encrypted sessionKeys over unencrypted connections. @@ -2235,7 +2384,15 @@ void EspnowMeshBackend::sendEspnowResponses() clearOldLogEntries(); return; // confirmationsIterator may be invalid now. Also, we should give the main loop a chance to respond to the situation. } + + if(estimatedMaxDurationTracker && estimatedMaxDurationTracker->expired()) + return; } +} + +void EspnowMeshBackend::sendEspnowResponses(const ExpiringTimeTracker *estimatedMaxDurationTracker) +{ + uint32_t bufferedCriticalHeapLevel = criticalHeapLevel() + criticalHeapLevelBuffer(); // We preferably want to start clearing the logs a bit before things get critical. for(std::list::iterator responseIterator = responsesToSend.begin(); responseIterator != responsesToSend.end(); ) { @@ -2268,6 +2425,9 @@ void EspnowMeshBackend::sendEspnowResponses() clearOldLogEntries(); return; // responseIterator may be invalid now. Also, we should give the main loop a chance to respond to the situation. } + + if(estimatedMaxDurationTracker && estimatedMaxDurationTracker->expired()) + return; } } @@ -2300,6 +2460,15 @@ uint8_t EspnowMeshBackend::numberOfEncryptedConnections() return encryptedConnections.size(); } +uint8_t EspnowMeshBackend::reservedEncryptedConnections() +{ + if(_ongoingPeerRequestNonce != "") + if(!getEncryptedConnection(_ongoingPeerRequestMac)) + return encryptedConnections.size() + 1; // Reserve one connection spot if we are currently making a peer request to a new node. + + return encryptedConnections.size(); +} + espnow_connection_type_t EspnowMeshBackend::getConnectionInfoHelper(const EncryptedConnectionLog *encryptedConnection, uint32_t *remainingDuration, uint8_t *peerMac) { if(!encryptedConnection) @@ -2359,6 +2528,15 @@ void EspnowMeshBackend::resetTransmissionFailRate() _transmissionsTotal = 0; } +String EspnowMeshBackend::serializeUnencryptedConnection() +{ + using namespace JsonTranslator; + + // Returns: {"connectionState":{"uMessageID":"123"}} + + return jsonConnectionState + createJsonEndPair(jsonUnencryptedMessageID, String(_unencryptedMessageID)); +} + String EspnowMeshBackend::serializeEncryptedConnection(const uint8_t *peerMac) { EncryptedConnectionLog *encryptedConnection = nullptr; diff --git a/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.h b/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.h index 1bfcc2524..ec935d9c6 100644 --- a/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.h +++ b/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.h @@ -24,22 +24,48 @@ // but this backend has a much higher data transfer speed than ESP-NOW once connected (100x faster or so). /** + * This ESP-NOW framework uses a few different message types to enable easier interpretation of transmissions. + * The message type is stored in the first transmission byte, see EspnowProtocolInterpreter.h for more detailed information on the protocol. + * Available message types are 'Q' for question (request), 'A' for answer (response), + * 'B' for broadcast, 'S' for synchronization request, 'P' for peer request and 'C' for peer request confirmation. + * + * 'B', 'Q' and 'A' are the message types that are assigned to data transmitted by the user. + * 'S', 'P' and 'C' are used only for internal framework transmissions. + * + * Messages with type 'B' are only used for broadcasts. They cannot be encrypted. + * + * Messages with type 'Q' are used for requests sent by the user. They can be encrypted. + * + * Messages with type 'A' are used for responses given by the user when 'B' or 'Q' messages have been received. They can be encrypted. + * + * Messages with type 'P' and 'C' are used exclusively for automatically pairing two ESP-NOW nodes to each other. + * This enables flexible easy-to-use encrypted ESP-NOW communication. 'P' and 'C' messages can be encrypted. + * The encryption pairing process works as follows (from top to bottom): + * * Encryption pairing process, schematic overview: * - * Connection | Peer sends: | Peer requester sends: | Connection - * encrypted: | | | encrypted: - * | | Peer request + Nonce | - * | StaMac + Nonce + HMAC | | - * | | Ack | - * X | SessionKeys + Nonce + Password | | X - * X | | Ack | X - * X | | SessionKey | X - * X | Ack | | X - * | | | - * - * + * Connection | Peer sends ('C'): | Peer requester sends ('P'): | Connection + * encrypted: | | | encrypted: + * | | Peer request + Nonce + HMAC | + * | StaMac + Nonce + HMAC | | + * | | Ack | + * X | SessionKeys + Nonce + Password | | X + * X | | Ack | X + * X | | SessionKey | X + * X | Ack | | X + * | | | + * + * * The ESP-NOW CCMP encryption should have replay attack protection built in, * but since there is no official documentation from Espressif about this a 128 bit random nonce is included in encrypted connection requests. + * + * Messages with type 'S' are used exclusively when we try to send an encrypted 'R' or 'P' transmission and the last such transmission we tried failed to receive an ack. + * Since we then do not know if the receiving node has incremented its corresponding session key or not, we first send an 'S' request to make sure the key is incremented. + * Once we get an ack for our 'S' request we send the new encrypted 'R' or 'P' transmission. 'S' messages are always encrypted. + * + * Messages of type 'A' and 'C' are response types, and thus use the same session key as the corresponding 'R' and 'P' message they are responding to. + * This means they can never cause a desynchronization to occur, and therefore they do not trigger 'S' messages. + * */ #ifndef __ESPNOWMESHBACKEND_H__ @@ -64,13 +90,15 @@ typedef enum ECT_PERMANENT_CONNECTION = 2 } espnow_connection_type_t; +// A value greater than 0 means that an encrypted connection has been established. typedef enum { ECS_MAX_CONNECTIONS_REACHED_SELF = -3, ECS_REQUEST_TRANSMISSION_FAILED = -2, ECS_MAX_CONNECTIONS_REACHED_PEER = -1, ECS_API_CALL_FAILED = 0, - ECS_CONNECTION_ESTABLISHED = 1 + ECS_CONNECTION_ESTABLISHED = 1, + ECS_SOFT_LIMIT_CONNECTION_ESTABLISHED = 2 // Only used if _encryptedConnectionsSoftLimit is less than 6. } encrypted_connection_status_t; typedef enum @@ -87,7 +115,7 @@ typedef enum * Note that if there is a lot of ESP-NOW transmission activity to the node during the espnowDelay, the desired duration may be overshot by several ms. * Thus, if precise timing is required, use standard delay() instead. * - * Should not be used inside responseHandler, requestHandler or networkFilter callbacks since performEspnowMaintainance() can alter the ESP-NOW state. + * Should not be used inside responseHandler, requestHandler, networkFilter or broadcastFilter callbacks since performEspnowMaintainance() can alter the ESP-NOW state. * * @param durationMs The shortest allowed delay duration, in milliseconds. */ @@ -133,13 +161,20 @@ public: /** * Returns a vector that contains the NetworkInfo for each WiFi network to connect to. - * This vector is unique for each mesh backend. + * This vector is unique for each mesh backend, but NetworkInfo elements can be directly transferred between the vectors as long as both SSID and BSSID are present. * The connectionQueue vector is cleared before each new scan and filled via the networkFilter callback function once the scan completes. * WiFi connections will start with connectionQueue[0] and then incrementally proceed to higher vector positions. * Note that old network indicies often are invalidated whenever a new WiFi network scan occurs. + * + * Since the connectionQueue() is iterated over during transmissions, always use constConnectionQueue() from callbacks other than NetworkFilter. */ - std::vector & connectionQueue(); + static std::vector & connectionQueue(); + /** + * Same as connectionQueue(), but can be called from all callbacks since the returned reference is const. + */ + static const std::vector & constConnectionQueue(); + /** * Returns a vector with the TransmissionOutcome for each AP to which a transmission was attempted during the latest attemptTransmission call. * This vector is unique for each mesh backend. @@ -147,7 +182,13 @@ public: * Connection attempts are indexed in the same order they were attempted. * Note that old network indicies often are invalidated whenever a new WiFi network scan occurs. */ - std::vector & latestTransmissionOutcomes() override; + static std::vector & latestTransmissionOutcomes(); + + /** + * @return True if latest transmission was successful (i.e. latestTransmissionOutcomes is not empty and all entries have transmissionStatus TS_TRANSMISSION_COMPLETE). False otherwise. + * The result is unique for each mesh backend. + */ + static bool latestTransmissionSuccessful(); /** * Initialises the node. @@ -161,9 +202,13 @@ public: * Note that depending on the amount of responses to send and their length, this method can take tens or even hundreds of milliseconds to complete. * More intense transmission activity and less frequent calls to performEspnowMaintainance will likely cause the method to take longer to complete, so plan accordingly. * - * Should not be used inside responseHandler, requestHandler or networkFilter callbacks since performEspnowMaintainance() can alter the ESP-NOW state. + * Should not be used inside responseHandler, requestHandler, networkFilter or broadcastFilter callbacks since performEspnowMaintainance() can alter the ESP-NOW state. + * + * @param estimatedMaxDuration The desired max duration for the method. If set to 0 there is no duration limit. + * Note that setting the estimatedMaxDuration too low may result in missed ESP-NOW transmissions because of too little time for maintainance. + * Also note that although the method will try to respect the max duration limit, there is no guarantee. Overshoots by tens of milliseconds are possible. */ - static void performEspnowMaintainance(); + static void performEspnowMaintainance(uint32_t estimatedMaxDuration = 0); /** * At critical heap level no more incoming requests are accepted. @@ -200,6 +245,8 @@ public: /** * Transmit message to a single recipient without changing the local transmission state. * Will not change connectionQueue, latestTransmissionOutcomes or stored message. + * + * @param recipientInfo The recipient information. */ transmission_status_t attemptTransmission(const String &message, const EspnowNetworkInfo &recipientInfo); @@ -208,29 +255,32 @@ public: * establishing a temporary encrypted connection with duration getAutoEncryptionDuration() first if neccessary. * If an encrypted connection cannot be established to a target node, no message will be sent to that node. * Note that if an encrypted connection to a target node is not present before this method is called, the response from said node will likely not be received - * since it will be encrypted and the auto encrypted connection to the node is immediately removed after transmission (unless the createPermanentConnections argument is set to true). + * since it will be encrypted and the auto encrypted connection to the node is immediately removed after transmission (unless the requestPermanentConnections argument is set to true). * Also note that if a temporary encrypted connection already exists to a target node, this method will slightly extend the connection duration * depending on the time it takes to verify the connection to the node. This can substantially increase the connection duration if many auto encrypting * transmissions occurs. * * @param message The message to send to other nodes. It will be stored in the class instance until replaced via attemptTransmission or setMessage. + * @param requestPermanentConnections If true, the method will request that encrypted connections used for this transmission become permanent so they are not removed once the transmission is complete. + * This means that encrypted responses to the transmission are received, as long as the encrypted connection is not removed by other means. + * The receiving node has no obligation to obey the request, although it normally will. + * If encryptedConnectionsSoftLimit() is set to less than 6 for the transmission receiver, + * it is possible that a short lived autoEncryptionConnection is created instead of a permanent encrypted connection. + * Note that a maximum of 6 encrypted ESP-NOW connections can be maintained at the same time by the node. + * Defaults to false. * @param scan Scan for new networks and call the networkFilter function with the scan results. When set to false, only the data already in connectionQueue will be used for the transmission. * @param scanAllWiFiChannels Scan all WiFi channels during a WiFi scan, instead of just the channel the MeshBackendBase instance is using. * Scanning all WiFi channels takes about 2100 ms, compared to just 60 ms if only channel 1 (standard) is scanned. * Note that if the ESP8266 has an active AP, that AP will switch WiFi channel to match that of any other AP the ESP8266 connects to. * This can make it impossible for other nodes to detect the AP if they are scanning the wrong WiFi channel. - * @param createPermanentConnections Ensures encrypted connections used for this transmission are permanent and not removed once the transmission is complete. - * This guarantees that encrypted responses to the transmission is received, as long as the encrypted connection is not removed by other means. - * Note that a maximum of 6 encrypted ESP-NOW connections can be maintained at the same time by the node. - * Defaults to false. */ - void attemptAutoEncryptingTransmission(const String &message, bool scan = true, bool scanAllWiFiChannels = false, bool createPermanentConnections = false); + void attemptAutoEncryptingTransmission(const String &message, bool requestPermanentConnections = false, bool scan = true, bool scanAllWiFiChannels = false); /** * Transmit message to a single recipient without changing the local transmission state (apart from encrypted connections). * Will not change connectionQueue, latestTransmissionOutcomes or stored message. */ - transmission_status_t attemptAutoEncryptingTransmission(const String &message, const EspnowNetworkInfo &recipientInfo, bool createPermanentConnection = false); + transmission_status_t attemptAutoEncryptingTransmission(const String &message, const EspnowNetworkInfo &recipientInfo, bool requestPermanentConnection = false); /** * Send a message simultaneously to all nearby nodes which have ESP-NOW activated. @@ -475,6 +525,17 @@ public: */ bool receivedEncryptedMessage(); + /** + * Should be used together with serializeUnencryptedConnection() if the node sends unencrypted transmissions + * and will go to sleep for less than logEntryLifetimeMs() while other nodes stay awake. + * Otherwise the message ID will be reset after sleep, which means that the nodes that stayed awake may ignore new unencrypted transmissions until logEntryLifetimeMs() ms has passed. + * + * @param serializedConnectionState A serialized state of an unencrypted ESP-NOW connection. + * + * @return True if connection was added. False otherwise (e.g. if there is faulty input). + */ + static bool addUnencryptedConnection(const String &serializedConnectionState); + // Updates connection with current stored encryption key. // At least one of the leftmost 32 bits in each of the session keys should be 1, since the key otherwise indicates the connection is unencrypted. encrypted_connection_status_t addEncryptedConnection(uint8_t *peerStaMac, uint8_t *peerApMac, uint64_t peerSessionKey, uint64_t ownSessionKey); @@ -514,18 +575,44 @@ public: */ void setAcceptsUnencryptedRequests(bool acceptsUnencryptedRequests); bool acceptsUnencryptedRequests(); + + /** + * Set a soft upper limit on the number of encrypted connections this node can have when receiving encrypted connection requests. + * The soft limit can be used to ensure there is normally a pool of free encrypted connection slots that can be used if required. + * Each EspnowMeshBackend instance can have a separate value. The value used is that of the current EspnowRequestManager. + * The hard upper limit is 6 encrypted connections, mandated by the ESP-NOW API. + * + * When a request for encrypted connection is received from a node to which there is no existing permanent encrypted connection, + * and the number of encrypted connections exceeds the soft limit, + * this request will automatically be converted to an autoEncryptionRequest. + * This means it will be a temporary connection with very short duration (with default framework settings). + * + * @param softLimit The new soft limit. Valid values are 0 to 6. Default is 6. + */ + void setEncryptedConnectionsSoftLimit(uint8_t softLimit); + uint8_t encryptedConnectionsSoftLimit(); /** - * @ returns The current number of encrypted ESP-NOW connections. + * @return The current number of encrypted ESP-NOW connections. */ static uint8_t numberOfEncryptedConnections(); // @return resultArray filled with the MAC to the encrypted interface of the node, if an encrypted connection exists. nulltpr otherwise. static uint8_t *getEncryptedMac(const uint8_t *peerMac, uint8_t *resultArray); + /** + * Should be used together with addUnencryptedConnection if the node sends unencrypted transmissions + * and will go to sleep for less than logEntryLifetimeMs() while other nodes stay awake. + * Otherwise the message ID will be reset after sleep, which means that the nodes that stayed awake may ignore new unencrypted transmissions until logEntryLifetimeMs() ms has passed. + * + * @return The serialized state of the unencrypted ESP-NOW connection. + */ + static String serializeUnencryptedConnection(); + // Create a string containing the current state of the encrypted connection for this node. The result can be used as input to addEncryptedConnection. // Note that transferring the serialized state over an unencrypted connection will compromise the security of the stored connection. - // @ returns A String containing the serialized encrypted connection, or an empty String if there is no matching encrypted connection. + // Also note that this saves the current state only, so if encrypted communication between the nodes happen after this, the stored state is invalid. + // @return A String containing the serialized encrypted connection, or an empty String if there is no matching encrypted connection. static String serializeEncryptedConnection(const uint8_t *peerMac); static String serializeEncryptedConnection(uint32_t connectionIndex); @@ -537,7 +624,7 @@ public: * @param remainingDuration An optional pointer to a uint32_t variable. * If supplied and the connection type is ECT_TEMPORARY_CONNECTION the variable will be set to the remaining duration of the connection. * Otherwise the variable value is not modified. - * @ returns The espnow_connection_type_t of the connection with peerMac. + * @return The espnow_connection_type_t of the connection with peerMac. */ static espnow_connection_type_t getConnectionInfo(uint8_t *peerMac, uint32_t *remainingDuration = nullptr); @@ -550,7 +637,7 @@ public: * Otherwise the variable value is not modified. * @param peerMac An optional pointer to an uint8_t array with at least size 6. It will be filled with the MAC of the encrypted peer interface if an encrypted connection exists. * Otherwise the array is not modified. - * @ returns The espnow_connection_type_t of the connection given by connectionIndex. + * @return The espnow_connection_type_t of the connection given by connectionIndex. */ static espnow_connection_type_t getConnectionInfo(uint32_t connectionIndex, uint32_t *remainingDuration = nullptr, uint8_t *peerMac = nullptr); @@ -576,6 +663,8 @@ protected: static const uint64_t uint64BroadcastMac = 0xFFFFFFFFFFFF; bool activateEspnow(); + + static bool encryptedConnectionEstablished(encrypted_connection_status_t connectionStatus); /* * Note that ESP-NOW is not perfect and in rare cases messages may be dropped. @@ -584,8 +673,25 @@ protected: * * Note that although responses will generally be sent in the order they were created, this is not guaranteed to be the case. * For example, response order will be mixed up if some responses fail to transmit while others transmit successfully. + * + * @param estimatedMaxDurationTracker A pointer to an ExpiringTimeTracker initialized with the desired max duration for the method. If set to nullptr there is no duration limit. + * Note that setting the estimatedMaxDuration too low may result in missed ESP-NOW transmissions because of too little time for maintainance. + * Also note that although the method will try to respect the max duration limit, there is no guarantee. Overshoots by tens of milliseconds are possible. */ - static void sendEspnowResponses(); + static void sendStoredEspnowMessages(const ExpiringTimeTracker *estimatedMaxDurationTracker = nullptr); + /* + * @param estimatedMaxDurationTracker A pointer to an ExpiringTimeTracker initialized with the desired max duration for the method. If set to nullptr there is no duration limit. + * Note that setting the estimatedMaxDuration too low may result in missed ESP-NOW transmissions because of too little time for maintainance. + * Also note that although the method will try to respect the max duration limit, there is no guarantee. Overshoots by tens of milliseconds are possible. + */ + static void sendPeerRequestConfirmations(const ExpiringTimeTracker *estimatedMaxDurationTracker = nullptr); + /* + * @param estimatedMaxDurationTracker A pointer to an ExpiringTimeTracker initialized with the desired max duration for the method. If set to nullptr there is no duration limit. + * Note that setting the estimatedMaxDuration too low may result in missed ESP-NOW transmissions because of too little time for maintainance. + * Also note that although the method will try to respect the max duration limit, there is no guarantee. Overshoots by tens of milliseconds are possible. + */ + static void sendEspnowResponses(const ExpiringTimeTracker *estimatedMaxDurationTracker = nullptr); + static void clearOldLogEntries(); static uint32_t getMaxBytesPerTransmission(); @@ -622,6 +728,11 @@ protected: */ static bool _espnowTransmissionMutex; + /** + * Will be true when the connectionQueue should not be modified. + */ + static bool _espnowConnectionQueueMutex; + /** * Check if there is an ongoing ESP-NOW transmission in the library. Used to avoid interrupting transmissions. * @@ -670,8 +781,8 @@ protected: private: typedef std::function encryptionRequestBuilderType; - static String defaultEncryptionRequestBuilder(const String &requestHeader, const uint32_t durationMs, const String &requestNonce, const ExpiringTimeTracker &existingTimeTracker); - static String flexibleEncryptionRequestBuilder(const uint32_t minDurationMs, const String &requestNonce, const ExpiringTimeTracker &existingTimeTracker); + static String defaultEncryptionRequestBuilder(const String &requestHeader, const uint32_t durationMs, const uint8_t *hashKey, const String &requestNonce, const ExpiringTimeTracker &existingTimeTracker); + static String flexibleEncryptionRequestBuilder(const uint32_t minDurationMs, const uint8_t *hashKey, const String &requestNonce, const ExpiringTimeTracker &existingTimeTracker); /** * We can't feed esp_now_register_recv_cb our EspnowMeshBackend instance's espnowReceiveCallback method directly, so this callback wrapper is a workaround. @@ -701,6 +812,8 @@ private: uint32_t _autoEncryptionDuration = 50; + uint8_t _encryptedConnectionsSoftLimit = 6; + static bool _staticVerboseMode; static EspnowMeshBackend *_espnowRequestManager; @@ -709,6 +822,14 @@ private: static std::map, RequestData> sentRequests; static std::map, TimeTracker> receivedRequests; + /** + * reservedEncryptedConnections never underestimates but sometimes temporarily overestimates. + * numberOfEncryptedConnections sometimes temporarily underestimates but never overestimates. + * + * @return The current number of encrypted ESP-NOW connections, but with an encrypted connection immediately reserved if required while making a peer request. + */ + static uint8_t reservedEncryptedConnections(); + static std::list responsesToSend; static std::list peerRequestConfirmationsToSend; @@ -755,9 +876,11 @@ private: broadcastFilterType _broadcastFilter; static String _ongoingPeerRequestNonce; + static uint8_t _ongoingPeerRequestMac[6]; static EspnowMeshBackend *_ongoingPeerRequester; static encrypted_connection_status_t _ongoingPeerRequestResult; static uint32_t _ongoingPeerRequestEncryptionStart; + static bool _reciprocalPeerRequestConfirmation; template static T *getMapValue(std::map &mapIn, uint64_t keyIn); @@ -830,9 +953,9 @@ private: transmission_status_t initiateTransmissionKernel(const String &message, const uint8_t *targetBSSID); void printTransmissionStatistics(); - encrypted_connection_status_t initiateAutoEncryptingConnection(const EspnowNetworkInfo &recipientInfo, bool createPermanentConnection, uint8_t *targetBSSID, EncryptedConnectionLog **encryptedConnection); + encrypted_connection_status_t initiateAutoEncryptingConnection(const EspnowNetworkInfo &recipientInfo, bool requestPermanentConnection, uint8_t *targetBSSID, EncryptedConnectionLog **existingEncryptedConnection); transmission_status_t initiateAutoEncryptingTransmission(const String &message, const uint8_t *targetBSSID, encrypted_connection_status_t connectionStatus); - void finalizeAutoEncryptingConnection(const uint8_t *targetBSSID, const EncryptedConnectionLog *encryptedConnection, bool createPermanentConnection); + void finalizeAutoEncryptingConnection(const uint8_t *targetBSSID, const EncryptedConnectionLog *existingEncryptedConnection, bool requestPermanentConnection); // Used for verboseMode printing in attemptTransmission, _AT suffix used to reduce namespace clutter uint32_t totalDurationWhenSuccessful_AT = 0; diff --git a/libraries/ESP8266WiFiMesh/src/EspnowNetworkInfo.cpp b/libraries/ESP8266WiFiMesh/src/EspnowNetworkInfo.cpp index a8940a5d2..d68f24156 100644 --- a/libraries/ESP8266WiFiMesh/src/EspnowNetworkInfo.cpp +++ b/libraries/ESP8266WiFiMesh/src/EspnowNetworkInfo.cpp @@ -23,9 +23,15 @@ */ #include "EspnowNetworkInfo.h" +#include EspnowNetworkInfo::EspnowNetworkInfo(int networkIndex) : NetworkInfoBase(networkIndex) { }; +EspnowNetworkInfo::EspnowNetworkInfo(const NetworkInfoBase &originalNetworkInfo) : NetworkInfoBase(originalNetworkInfo) +{ + assert(BSSID() != defaultBSSID); // We need at least BSSID to be able to connect. +}; + EspnowNetworkInfo::EspnowNetworkInfo(const uint8_t BSSID[6], const String &SSID, int32_t wifiChannel, uint8_t encryptionType, int32_t RSSI , bool isHidden) : NetworkInfoBase(SSID, wifiChannel, BSSID, encryptionType, RSSI, isHidden) { } diff --git a/libraries/ESP8266WiFiMesh/src/EspnowNetworkInfo.h b/libraries/ESP8266WiFiMesh/src/EspnowNetworkInfo.h index 947d1028e..fecb7c5b3 100644 --- a/libraries/ESP8266WiFiMesh/src/EspnowNetworkInfo.h +++ b/libraries/ESP8266WiFiMesh/src/EspnowNetworkInfo.h @@ -36,6 +36,8 @@ public: */ EspnowNetworkInfo(int networkIndex); + EspnowNetworkInfo(const NetworkInfoBase &originalNetworkInfo); + EspnowNetworkInfo(const uint8_t BSSID[6], const String &SSID = defaultSSID, int32_t wifiChannel = defaultWifiChannel, uint8_t encryptionType = defaultEncryptionType, int32_t RSSI = defaultRSSI, bool isHidden = defaultIsHidden); }; diff --git a/libraries/ESP8266WiFiMesh/src/EspnowProtocolInterpreter.h b/libraries/ESP8266WiFiMesh/src/EspnowProtocolInterpreter.h index 5ed6acdea..0a5720331 100644 --- a/libraries/ESP8266WiFiMesh/src/EspnowProtocolInterpreter.h +++ b/libraries/ESP8266WiFiMesh/src/EspnowProtocolInterpreter.h @@ -44,6 +44,7 @@ namespace EspnowProtocolInterpreter const String temporaryEncryptionRequestHeader = "AddTEC:"; // Add temporary encrypted connection const String basicConnectionInfoHeader = "BasicCI:"; // Basic connection info const String encryptedConnectionInfoHeader = "EncryptedCI:"; // Encrypted connection info + const String softLimitEncryptedConnectionInfoHeader = "SLEncryptedCI:"; // Soft limit encrypted connection info const String maxConnectionsReachedHeader = "ECS_MAX_CONNECTIONS_REACHED_PEER:"; const String encryptedConnectionVerificationHeader = "ECVerified:"; // Encrypted connection verified const String encryptedConnectionRemovalRequestHeader = "RemoveEC:"; // Remove encrypted connection diff --git a/libraries/ESP8266WiFiMesh/src/ExpiringTimeTracker.cpp b/libraries/ESP8266WiFiMesh/src/ExpiringTimeTracker.cpp index 6c17d2dc2..73b6d3214 100644 --- a/libraries/ESP8266WiFiMesh/src/ExpiringTimeTracker.cpp +++ b/libraries/ESP8266WiFiMesh/src/ExpiringTimeTracker.cpp @@ -55,5 +55,5 @@ uint32_t ExpiringTimeTracker::remainingDuration() const bool ExpiringTimeTracker::expired() const { - return timeSinceCreation() > duration(); -} \ No newline at end of file + return timeSinceCreation() >= duration(); +} diff --git a/libraries/ESP8266WiFiMesh/src/JsonTranslator.cpp b/libraries/ESP8266WiFiMesh/src/JsonTranslator.cpp index c92f5f157..1bbbb444f 100644 --- a/libraries/ESP8266WiFiMesh/src/JsonTranslator.cpp +++ b/libraries/ESP8266WiFiMesh/src/JsonTranslator.cpp @@ -73,7 +73,8 @@ namespace JsonTranslator return false; } - bool verifyHmac(const String &encryptionRequestHmacMessage, const uint8_t *hashKey, uint8_t hashKeyLength) + bool verifyEncryptionRequestHmac(const String &encryptionRequestHmacMessage, const uint8_t *requesterStaMac, const uint8_t *requesterApMac, + const uint8_t *hashKey, uint8_t hashKeyLength) { String hmac = ""; if(getHmac(encryptionRequestHmacMessage, hmac)) @@ -82,7 +83,7 @@ namespace JsonTranslator if(hmacStartIndex < 0) return false; - if(verifyHmac(encryptionRequestHmacMessage.substring(0, hmacStartIndex), hmac, hashKey, hashKeyLength)) + if(verifyHmac(macToString(requesterStaMac) + macToString(requesterApMac) + encryptionRequestHmacMessage.substring(0, hmacStartIndex), hmac, hashKey, hashKeyLength)) { return true; } @@ -91,13 +92,12 @@ namespace JsonTranslator return false; } - String createEncryptedConnectionInfo(const String &requestNonce, const String &authenticationPassword, uint64_t ownSessionKey, uint64_t peerSessionKey) + String createEncryptedConnectionInfo(const String &infoHeader, const String &requestNonce, const String &authenticationPassword, uint64_t ownSessionKey, uint64_t peerSessionKey) { - // Returns: Encrypted connection info:{"arguments":{"nonce":"1F2","password":"abc","ownSessionKey":"3B4","peerSessionKey":"1A2"}} - + // Returns: Encrypted connection info:{"arguments":{"nonce":"1F2","password":"abc","ownSK":"3B4","peerSK":"1A2"}} return - EspnowProtocolInterpreter::encryptedConnectionInfoHeader + "{\"arguments\":{" + infoHeader + "{\"arguments\":{" + createJsonPair(jsonNonce, requestNonce) + createJsonPair(jsonPassword, authenticationPassword) + createJsonPair(jsonOwnSessionKey, uint64ToString(peerSessionKey)) // Exchanges session keys since it should be valid for the receiver. @@ -116,15 +116,13 @@ namespace JsonTranslator return createJsonEndPair(jsonNonce, requestNonce); } - String createEncryptionRequestMessage(const String &requestHeader, const String &requestNonce, uint32_t duration) - { - return createEncryptionRequestIntro(requestHeader, duration) + createEncryptionRequestEnding(requestNonce); - } - String createEncryptionRequestHmacMessage(const String &requestHeader, const String &requestNonce, const uint8_t *hashKey, uint8_t hashKeyLength, uint32_t duration) { String mainMessage = createEncryptionRequestIntro(requestHeader, duration) + createJsonPair(jsonNonce, requestNonce); - String hmac = createHmac(mainMessage, hashKey, hashKeyLength); + uint8_t staMac[6] {0}; + uint8_t apMac[6] {0}; + String requesterStaApMac = macToString(WiFi.macAddress(staMac)) + macToString(WiFi.softAPmacAddress(apMac)); + String hmac = createHmac(requesterStaApMac + mainMessage, hashKey, hashKeyLength); return mainMessage + createJsonEndPair(jsonHmac, hmac); } @@ -148,6 +146,20 @@ namespace JsonTranslator return endIndex; } + + bool getConnectionState(const String &jsonString, String &result) + { + int32_t startIndex = jsonString.indexOf(jsonConnectionState); + if(startIndex < 0) + return false; + + int32_t endIndex = jsonString.indexOf("}"); + if(endIndex < 0) + return false; + + result = jsonString.substring(startIndex, endIndex + 1); + return true; + } bool getPassword(const String &jsonString, String &result) { @@ -266,4 +278,27 @@ namespace JsonTranslator result = bool(strtoul(jsonString.substring(startIndex).c_str(), nullptr, 0)); // strtoul stops reading input when an invalid character is discovered. return true; } + + bool getUnencryptedMessageID(const String &jsonString, uint32_t &result) + { + int32_t startIndex = getStartIndex(jsonString, jsonUnencryptedMessageID); + if(startIndex < 0) + return false; + + result = strtoul(jsonString.substring(startIndex).c_str(), nullptr, 0); // strtoul stops reading input when an invalid character is discovered. + return true; + } + + bool getMeshMessageCount(const String &jsonString, uint16_t &result) + { + int32_t startIndex = getStartIndex(jsonString, jsonMeshMessageCount); + if(startIndex < 0) + return false; + + uint32_t longResult = strtoul(jsonString.substring(startIndex).c_str(), nullptr, 0); // strtoul stops reading input when an invalid character is discovered. + assert(longResult <= 65535); // Must fit within uint16_t + + result = longResult; + return true; + } } diff --git a/libraries/ESP8266WiFiMesh/src/JsonTranslator.h b/libraries/ESP8266WiFiMesh/src/JsonTranslator.h index 7e75ab9f1..8ff320e4d 100644 --- a/libraries/ESP8266WiFiMesh/src/JsonTranslator.h +++ b/libraries/ESP8266WiFiMesh/src/JsonTranslator.h @@ -30,6 +30,7 @@ namespace JsonTranslator { + const String jsonConnectionState = "{\"connectionState\":{"; const String jsonPassword = "\"password\":"; const String jsonOwnSessionKey = "\"ownSK\":"; const String jsonPeerSessionKey = "\"peerSK\":"; @@ -39,6 +40,8 @@ namespace JsonTranslator const String jsonNonce = "\"nonce\":"; const String jsonHmac = "\"hmac\":"; const String jsonDesync = "\"desync\":"; + const String jsonUnencryptedMessageID = "\"uMessageID\":"; + const String jsonMeshMessageCount = "\"mMessageCount\":"; String createJsonPair(const String &valueIdentifier, const String &value); String createJsonEndPair(const String &valueIdentifier, const String &value); @@ -47,12 +50,11 @@ namespace JsonTranslator String createHmac(const String &message, const uint8_t *hashKey, uint8_t hashKeyLength); bool verifyHmac(const String &message, const String &messageHmac, const uint8_t *hashKey, uint8_t hashKeyLength); - bool verifyHmac(const String &encryptionRequestHmacMessage, const uint8_t *hashKey, uint8_t hashKeyLength); + bool verifyEncryptionRequestHmac(const String &encryptionRequestHmacMessage, const uint8_t *requesterStaMac, const uint8_t *requesterApMac, const uint8_t *hashKey, uint8_t hashKeyLength); - String createEncryptedConnectionInfo(const String &requestNonce, const String &authenticationPassword, uint64_t ownSessionKey, uint64_t peerSessionKey); + String createEncryptedConnectionInfo(const String &infoHeader, const String &requestNonce, const String &authenticationPassword, uint64_t ownSessionKey, uint64_t peerSessionKey); String createEncryptionRequestIntro(const String &requestHeader, uint32_t duration = 0); String createEncryptionRequestEnding(const String &requestNonce); - String createEncryptionRequestMessage(const String &requestHeader, const String &requestNonce, uint32_t duration = 0); String createEncryptionRequestHmacMessage(const String &requestHeader, const String &requestNonce, const uint8_t *hashKey, uint8_t hashKeyLength, uint32_t duration = 0); /** @@ -76,6 +78,7 @@ namespace JsonTranslator */ int32_t getEndIndex(const String &jsonString, int32_t searchStartIndex); + bool getConnectionState(const String &jsonString, String &result); /** * Stores the value of the password field within jsonString into the result variable. * No changes to the result variable are made if jsonString does not contain a password. @@ -104,6 +107,8 @@ namespace JsonTranslator bool getNonce(const String &jsonString, String &result); bool getHmac(const String &jsonString, String &result); bool getDesync(const String &jsonString, bool &result); + bool getUnencryptedMessageID(const String &jsonString, uint32_t &result); + bool getMeshMessageCount(const String &jsonString, uint16_t &result); } #endif diff --git a/libraries/ESP8266WiFiMesh/src/MeshBackendBase.cpp b/libraries/ESP8266WiFiMesh/src/MeshBackendBase.cpp index 69232e506..38f1054ef 100644 --- a/libraries/ESP8266WiFiMesh/src/MeshBackendBase.cpp +++ b/libraries/ESP8266WiFiMesh/src/MeshBackendBase.cpp @@ -119,7 +119,7 @@ void MeshBackendBase::setWiFiChannel(uint8 newWiFiChannel) } } -uint8 MeshBackendBase::getWiFiChannel() +uint8 MeshBackendBase::getWiFiChannel() const { return _meshWiFiChannel; } @@ -147,28 +147,28 @@ void MeshBackendBase::setSSID(const String &newSSIDPrefix, const String &newSSID } } -String MeshBackendBase::getSSID() {return _SSID;} +String MeshBackendBase::getSSID() const {return _SSID;} void MeshBackendBase::setSSIDPrefix(const String &newSSIDPrefix) { setSSID(newSSIDPrefix); } -String MeshBackendBase::getSSIDPrefix() {return _SSIDPrefix;} +String MeshBackendBase::getSSIDPrefix() const {return _SSIDPrefix;} void MeshBackendBase::setSSIDRoot(const String &newSSIDRoot) { setSSID("", newSSIDRoot); } -String MeshBackendBase::getSSIDRoot() {return _SSIDRoot;} +String MeshBackendBase::getSSIDRoot() const {return _SSIDRoot;} void MeshBackendBase::setSSIDSuffix(const String &newSSIDSuffix) { setSSID("", "", newSSIDSuffix); } -String MeshBackendBase::getSSIDSuffix() {return _SSIDSuffix;} +String MeshBackendBase::getSSIDSuffix() const {return _SSIDSuffix;} void MeshBackendBase::setMeshName(const String &newMeshName) { @@ -195,10 +195,10 @@ void MeshBackendBase::setMeshPassword(const String &newMeshPassword) restartAP(); } -String MeshBackendBase::getMeshPassword() {return _meshPassword;} +String MeshBackendBase::getMeshPassword() const {return _meshPassword;} void MeshBackendBase::setMessage(const String &newMessage) {_message = newMessage;} -String MeshBackendBase::getMessage() {return _message;} +String MeshBackendBase::getMessage() const {return _message;} void MeshBackendBase::setRequestHandler(MeshBackendBase::requestHandlerType requestHandler) {_requestHandler = requestHandler;} MeshBackendBase::requestHandlerType MeshBackendBase::getRequestHandler() {return _requestHandler;} @@ -209,12 +209,15 @@ MeshBackendBase::responseHandlerType MeshBackendBase::getResponseHandler() {retu void MeshBackendBase::setNetworkFilter(MeshBackendBase::networkFilterType networkFilter) {_networkFilter = networkFilter;} MeshBackendBase::networkFilterType MeshBackendBase::getNetworkFilter() {return _networkFilter;} +void MeshBackendBase::setTransmissionOutcomesUpdateHook(MeshBackendBase::transmissionOutcomesUpdateHookType transmissionOutcomesUpdateHook) {_transmissionOutcomesUpdateHook = transmissionOutcomesUpdateHook;} +MeshBackendBase::transmissionOutcomesUpdateHookType MeshBackendBase::getTransmissionOutcomesUpdateHook() {return _transmissionOutcomesUpdateHook;} + void MeshBackendBase::setScanHidden(bool scanHidden) { _scanHidden = scanHidden; } -bool MeshBackendBase::getScanHidden() {return _scanHidden;} +bool MeshBackendBase::getScanHidden() const {return _scanHidden;} void MeshBackendBase::setAPHidden(bool apHidden) { @@ -228,14 +231,14 @@ void MeshBackendBase::setAPHidden(bool apHidden) } } -bool MeshBackendBase::getAPHidden() {return _apHidden;} +bool MeshBackendBase::getAPHidden() const {return _apHidden;} -bool MeshBackendBase::latestTransmissionSuccessful() +bool MeshBackendBase::latestTransmissionSuccessfulBase(const std::vector &latestTransmissionOutcomes) { - if(latestTransmissionOutcomes().empty()) + if(latestTransmissionOutcomes.empty()) return false; else - for(TransmissionOutcome &transmissionOutcome : latestTransmissionOutcomes()) + for(const TransmissionOutcome &transmissionOutcome : latestTransmissionOutcomes) if(transmissionOutcome.transmissionStatus() != TS_TRANSMISSION_COMPLETE) return false; diff --git a/libraries/ESP8266WiFiMesh/src/MeshBackendBase.h b/libraries/ESP8266WiFiMesh/src/MeshBackendBase.h index 778dbabdd..4820eab01 100644 --- a/libraries/ESP8266WiFiMesh/src/MeshBackendBase.h +++ b/libraries/ESP8266WiFiMesh/src/MeshBackendBase.h @@ -38,6 +38,7 @@ protected: typedef std::function requestHandlerType; typedef std::function responseHandlerType; typedef std::function networkFilterType; + typedef std::function transmissionOutcomesUpdateHookType; public: @@ -45,20 +46,6 @@ public: virtual ~MeshBackendBase(); - /** - * Returns a vector with the TransmissionOutcome for each AP to which a transmission was attempted during the latest attemptTransmission call. - * This vector is unique for each mesh backend. - * The latestTransmissionOutcomes vector is cleared before each new transmission attempt. - * Connection attempts are indexed in the same order they were attempted. - * Note that old network indicies often are invalidated whenever a new WiFi network scan occurs. - */ - virtual std::vector & latestTransmissionOutcomes() = 0; - - /** - * @return True if latest transmission was successful (i.e. latestTransmissionOutcomes is not empty and all entries have transmissionStatus TS_TRANSMISSION_COMPLETE). False otherwise. - */ - bool latestTransmissionSuccessful(); - /** * Initialises the node. */ @@ -105,7 +92,7 @@ public: * */ void setWiFiChannel(uint8 newWiFiChannel); - uint8 getWiFiChannel(); + uint8 getWiFiChannel() const; /** * Change the SSID used by this MeshBackendBase instance. @@ -119,7 +106,7 @@ public: */ void setSSID(const String &newSSIDPrefix = ESP8266_MESH_EMPTY_STRING, const String &newSSIDRoot = ESP8266_MESH_EMPTY_STRING, const String &newSSIDSuffix = ESP8266_MESH_EMPTY_STRING); - String getSSID(); + String getSSID() const; /** * Change the first part of the SSID used by this MeshBackendBase instance. @@ -129,7 +116,7 @@ public: * @param newSSIDPrefix The new first part of the SSID. */ void setSSIDPrefix(const String &newSSIDPrefix); - String getSSIDPrefix(); + String getSSIDPrefix() const; /** * Change the middle part of the SSID used by this MeshBackendBase instance. @@ -139,7 +126,7 @@ public: * @param newSSIDPrefix The new middle part of the SSID. */ void setSSIDRoot(const String &newSSIDRoot); - String getSSIDRoot(); + String getSSIDRoot() const; /** * Change the last part of the SSID used by this MeshBackendBase instance. @@ -149,7 +136,7 @@ public: * @param newSSIDSuffix The new last part of the SSID. */ void setSSIDSuffix(const String &newSSIDSuffix); - String getSSIDSuffix(); + String getSSIDSuffix() const; /** * Change the mesh name used by this MeshBackendBase instance. @@ -183,7 +170,7 @@ public: * @param newMeshPassword The password to use. */ void setMeshPassword(const String &newMeshPassword); - String getMeshPassword(); + String getMeshPassword() const; /** * Set the message that will be sent to other nodes when calling attemptTransmission. @@ -191,7 +178,7 @@ public: * @param newMessage The message to send. */ void setMessage(const String &newMessage); - String getMessage(); + String getMessage() const; virtual void attemptTransmission(const String &message, bool scan = true, bool scanAllWiFiChannels = false) = 0; @@ -204,6 +191,16 @@ public: void setNetworkFilter(networkFilterType networkFilter); networkFilterType getNetworkFilter(); + /** + * Set a function that should be called after each update of the latestTransmissionOutcomes vector during attemptTransmission. (which happens after each individual transmission has finished) + * The function should return a bool. If this return value is true, attemptTransmission will continue with the next entry in the connectionQueue. If it is false, attemptTransmission will stop. + * The default transmissionOutcomesUpdateHook always returns true. + * + * Example use cases is modifying getMessage() between transmissions, or aborting attemptTransmission before all nodes in the connectionQueue have been contacted. + */ + void setTransmissionOutcomesUpdateHook(transmissionOutcomesUpdateHookType transmissionOutcomesUpdateHook); + transmissionOutcomesUpdateHookType getTransmissionOutcomesUpdateHook(); + /** * Set whether scan results from this MeshBackendBase instance will include WiFi networks with hidden SSIDs. * This is false by default. @@ -213,7 +210,7 @@ public: * @param scanHidden If true, WiFi networks with hidden SSIDs will be included in scan results. */ void setScanHidden(bool scanHidden); - bool getScanHidden(); + bool getScanHidden() const; /** * Set whether the AP controlled by this MeshBackendBase instance will have a WiFi network with hidden SSID. @@ -224,7 +221,7 @@ public: * @param apHidden If true, the WiFi network created will have a hidden SSID. */ void setAPHidden(bool apHidden); - bool getAPHidden(); + bool getAPHidden() const; /** * Set whether the normal events occurring in the library will be printed to Serial or not. Off by default. @@ -264,6 +261,13 @@ public: protected: + /** + * @param latestTransmissionOutcomes The transmission outcomes vector to check. + * + * @return True if latest transmission was successful (i.e. latestTransmissionOutcomes is not empty and all entries have transmissionStatus TS_TRANSMISSION_COMPLETE). False otherwise. + */ + static bool latestTransmissionSuccessfulBase(const std::vector &latestTransmissionOutcomes); + virtual void scanForNetworks(bool scanAllWiFiChannels); virtual void printAPInfo(const NetworkInfoBase &apNetworkInfo); @@ -303,6 +307,7 @@ private: requestHandlerType _requestHandler; responseHandlerType _responseHandler; networkFilterType _networkFilter; + transmissionOutcomesUpdateHookType _transmissionOutcomesUpdateHook = [](MeshBackendBase &){return true;}; static bool _printWarnings; }; diff --git a/libraries/ESP8266WiFiMesh/src/MutexTracker.cpp b/libraries/ESP8266WiFiMesh/src/MutexTracker.cpp index 97e84a75e..c864ea76a 100644 --- a/libraries/ESP8266WiFiMesh/src/MutexTracker.cpp +++ b/libraries/ESP8266WiFiMesh/src/MutexTracker.cpp @@ -24,6 +24,13 @@ #include "MutexTracker.h" +bool MutexTracker::_captureBan = false; + +bool &MutexTracker::captureBan() +{ + return _captureBan; +} + MutexTracker::MutexTracker(bool &mutexToCapture) { attemptMutexCapture(mutexToCapture); @@ -59,7 +66,7 @@ void MutexTracker::releaseMutex() bool MutexTracker::attemptMutexCapture(bool &mutexToCapture) { - if(!mutexToCapture) + if(!captureBan() && !mutexToCapture) { _capturedMutex = &mutexToCapture; *_capturedMutex = true; diff --git a/libraries/ESP8266WiFiMesh/src/MutexTracker.h b/libraries/ESP8266WiFiMesh/src/MutexTracker.h index a3922dc63..6e7026771 100644 --- a/libraries/ESP8266WiFiMesh/src/MutexTracker.h +++ b/libraries/ESP8266WiFiMesh/src/MutexTracker.h @@ -34,6 +34,13 @@ class MutexTracker { public: + /* + * If captureBan is true, trying to capture a mutex will always fail. + * Set to false by default. + * captureBan can be managed by MutexTracker like any other mutex. + */ + static bool &captureBan(); + /** * Attempts to capture the mutex. Use the mutexCaptured() method to check success. */ @@ -57,6 +64,8 @@ class MutexTracker private: + static bool _captureBan; + bool *_capturedMutex = nullptr; std::function _destructorHook = [](){ }; diff --git a/libraries/ESP8266WiFiMesh/src/NetworkInfoBase.cpp b/libraries/ESP8266WiFiMesh/src/NetworkInfoBase.cpp index 9d3667d33..46620612d 100644 --- a/libraries/ESP8266WiFiMesh/src/NetworkInfoBase.cpp +++ b/libraries/ESP8266WiFiMesh/src/NetworkInfoBase.cpp @@ -24,6 +24,7 @@ #include "NetworkInfoBase.h" +uint8_t * const NetworkInfoBase::defaultBSSID = nullptr; const String NetworkInfoBase::defaultSSID = ""; const int32_t NetworkInfoBase::defaultWifiChannel = NETWORK_INFO_DEFAULT_INT; const uint8_t NetworkInfoBase::defaultEncryptionType = 0; diff --git a/libraries/ESP8266WiFiMesh/src/NetworkInfoBase.h b/libraries/ESP8266WiFiMesh/src/NetworkInfoBase.h index 8650322ee..6fa7077b2 100644 --- a/libraries/ESP8266WiFiMesh/src/NetworkInfoBase.h +++ b/libraries/ESP8266WiFiMesh/src/NetworkInfoBase.h @@ -69,6 +69,7 @@ public: void setIsHidden(bool isHidden); bool isHidden() const; + static uint8_t * const defaultBSSID; static const String defaultSSID; static const int32_t defaultWifiChannel; static const uint8_t defaultEncryptionType; @@ -90,7 +91,7 @@ protected: private: uint8_t _bssidArray[6] {0}; - uint8_t *_BSSID = nullptr; + uint8_t *_BSSID = defaultBSSID; String _SSID = defaultSSID; int32_t _wifiChannel = defaultWifiChannel; uint8_t _encryptionType = defaultEncryptionType; // see enum wl_enc_type for values diff --git a/libraries/ESP8266WiFiMesh/src/PeerRequestLog.cpp b/libraries/ESP8266WiFiMesh/src/PeerRequestLog.cpp index dd9d33dbd..b606de540 100644 --- a/libraries/ESP8266WiFiMesh/src/PeerRequestLog.cpp +++ b/libraries/ESP8266WiFiMesh/src/PeerRequestLog.cpp @@ -27,14 +27,18 @@ using EspnowProtocolInterpreter::espnowHashKeyLength; -PeerRequestLog::PeerRequestLog(uint64_t requestID, bool requestEncrypted, const String &authenticationPassword, const String &peerRequestNonce, const uint8_t peerStaMac[6], const uint8_t peerApMac[6], const uint8_t hashKey[espnowHashKeyLength]) +PeerRequestLog::PeerRequestLog(uint64_t requestID, bool requestEncrypted, const String &authenticationPassword, uint8_t encryptedConnectionsSoftLimit, + const String &peerRequestNonce, const uint8_t peerStaMac[6], const uint8_t peerApMac[6], const uint8_t hashKey[espnowHashKeyLength]) : EncryptedConnectionData(peerStaMac, peerApMac, 0, 0, EspnowMeshBackend::getEncryptionRequestTimeout(), hashKey), - _requestID(requestID), _requestEncrypted(requestEncrypted), _authenticationPassword(authenticationPassword), _peerRequestNonce(peerRequestNonce) + _requestID(requestID), _requestEncrypted(requestEncrypted), _authenticationPassword(authenticationPassword), + _encryptedConnectionsSoftLimit(encryptedConnectionsSoftLimit), _peerRequestNonce(peerRequestNonce) { } -PeerRequestLog::PeerRequestLog(uint64_t requestID, bool requestEncrypted, const String &authenticationPassword, const String &peerRequestNonce, const uint8_t peerStaMac[6], const uint8_t peerApMac[6], uint64_t peerSessionKey, uint64_t ownSessionKey, const uint8_t hashKey[espnowHashKeyLength]) +PeerRequestLog::PeerRequestLog(uint64_t requestID, bool requestEncrypted, const String &authenticationPassword, uint8_t encryptedConnectionsSoftLimit, const String &peerRequestNonce, + const uint8_t peerStaMac[6], const uint8_t peerApMac[6], uint64_t peerSessionKey, uint64_t ownSessionKey, const uint8_t hashKey[espnowHashKeyLength]) : EncryptedConnectionData(peerStaMac, peerApMac, peerSessionKey, ownSessionKey, EspnowMeshBackend::getEncryptionRequestTimeout(), hashKey), - _requestID(requestID), _requestEncrypted(requestEncrypted), _authenticationPassword(authenticationPassword), _peerRequestNonce(peerRequestNonce) + _requestID(requestID), _requestEncrypted(requestEncrypted), _authenticationPassword(authenticationPassword), + _encryptedConnectionsSoftLimit(encryptedConnectionsSoftLimit), _peerRequestNonce(peerRequestNonce) { } void PeerRequestLog::setRequestID(uint64_t requestID) { _requestID = requestID; } @@ -46,5 +50,8 @@ bool PeerRequestLog::requestEncrypted() { return _requestEncrypted; } void PeerRequestLog::setAuthenticationPassword(const String &password) { _authenticationPassword = password; } String PeerRequestLog::getAuthenticationPassword() { return _authenticationPassword; } +void PeerRequestLog::setEncryptedConnectionsSoftLimit(uint8_t softLimit) { _encryptedConnectionsSoftLimit = softLimit; } +uint8_t PeerRequestLog::getEncryptedConnectionsSoftLimit() { return _encryptedConnectionsSoftLimit; } + void PeerRequestLog::setPeerRequestNonce(const String &nonce) { _peerRequestNonce = nonce; } String PeerRequestLog::getPeerRequestNonce() { return _peerRequestNonce; } diff --git a/libraries/ESP8266WiFiMesh/src/PeerRequestLog.h b/libraries/ESP8266WiFiMesh/src/PeerRequestLog.h index 7f058f171..2eb4c72bf 100644 --- a/libraries/ESP8266WiFiMesh/src/PeerRequestLog.h +++ b/libraries/ESP8266WiFiMesh/src/PeerRequestLog.h @@ -32,10 +32,11 @@ class PeerRequestLog : public EncryptedConnectionData { public: - PeerRequestLog(uint64_t requestID, bool requestEncrypted, const String &authenticationPassword, const String &peerRequestNonce, const uint8_t peerStaMac[6], - const uint8_t peerApMac[6], const uint8_t hashKey[EspnowProtocolInterpreter::espnowHashKeyLength]); - PeerRequestLog(uint64_t requestID, bool requestEncrypted, const String &authenticationPassword, const String &peerRequestNonce, const uint8_t peerStaMac[6], - const uint8_t peerApMac[6], uint64_t peerSessionKey, uint64_t ownSessionKey, const uint8_t hashKey[EspnowProtocolInterpreter::espnowHashKeyLength]); + PeerRequestLog(uint64_t requestID, bool requestEncrypted, const String &authenticationPassword, uint8_t encryptedConnectionsSoftLimit, const String &peerRequestNonce, + const uint8_t peerStaMac[6], const uint8_t peerApMac[6], const uint8_t hashKey[EspnowProtocolInterpreter::espnowHashKeyLength]); + PeerRequestLog(uint64_t requestID, bool requestEncrypted, const String &authenticationPassword, uint8_t encryptedConnectionsSoftLimit, const String &peerRequestNonce, + const uint8_t peerStaMac[6], const uint8_t peerApMac[6], uint64_t peerSessionKey, uint64_t ownSessionKey, + const uint8_t hashKey[EspnowProtocolInterpreter::espnowHashKeyLength]); void setRequestID(uint64_t requestID); uint64_t getRequestID(); @@ -45,6 +46,9 @@ public: void setAuthenticationPassword(const String &password); String getAuthenticationPassword(); + + void setEncryptedConnectionsSoftLimit(uint8_t softLimit); + uint8_t getEncryptedConnectionsSoftLimit(); void setPeerRequestNonce(const String &nonce); String getPeerRequestNonce(); @@ -54,6 +58,7 @@ private: uint64_t _requestID; bool _requestEncrypted; String _authenticationPassword = ""; + uint8_t _encryptedConnectionsSoftLimit; String _peerRequestNonce = ""; }; diff --git a/libraries/ESP8266WiFiMesh/src/TcpIpMeshBackend.cpp b/libraries/ESP8266WiFiMesh/src/TcpIpMeshBackend.cpp index 8dcab87ab..6932fcb3e 100644 --- a/libraries/ESP8266WiFiMesh/src/TcpIpMeshBackend.cpp +++ b/libraries/ESP8266WiFiMesh/src/TcpIpMeshBackend.cpp @@ -30,6 +30,7 @@ const IPAddress TcpIpMeshBackend::emptyIP = IPAddress(); bool TcpIpMeshBackend::_tcpIpTransmissionMutex = false; +bool TcpIpMeshBackend::_tcpIpConnectionQueueMutex = false; String TcpIpMeshBackend::lastSSID = ""; bool TcpIpMeshBackend::staticIPActivated = false; @@ -58,6 +59,17 @@ TcpIpMeshBackend::TcpIpMeshBackend(requestHandlerType requestHandler, responseHa std::vector & TcpIpMeshBackend::connectionQueue() { + MutexTracker connectionQueueMutexTracker(_tcpIpConnectionQueueMutex); + if(!connectionQueueMutexTracker.mutexCaptured()) + { + assert(false && "ERROR! connectionQueue locked. Don't call connectionQueue() from callbacks other than NetworkFilter as this may corrupt program state!"); + } + + return _connectionQueue; +} + +const std::vector & TcpIpMeshBackend::constConnectionQueue() +{ return _connectionQueue; } @@ -66,6 +78,11 @@ std::vector & TcpIpMeshBackend::latestTransmissionOutcomes( return _latestTransmissionOutcomes; } +bool TcpIpMeshBackend::latestTransmissionSuccessful() +{ + return latestTransmissionSuccessfulBase(latestTransmissionOutcomes()); +} + void TcpIpMeshBackend::begin() { if(!TcpIpMeshBackend::getAPController()) // If there is no active AP controller @@ -433,7 +450,7 @@ void TcpIpMeshBackend::attemptTransmission(const String &message, bool scan, boo if(WiFi.status() == WL_CONNECTED) { transmission_status_t transmissionResult = attemptDataTransfer(); - latestTransmissionOutcomes().push_back(TransmissionOutcome(connectionQueue().back(), transmissionResult)); + latestTransmissionOutcomes().push_back(TransmissionOutcome(constConnectionQueue().back(), transmissionResult)); } else { @@ -443,11 +460,22 @@ void TcpIpMeshBackend::attemptTransmission(const String &message, bool scan, boo scanForNetworks(scanAllWiFiChannels); } - for(TcpIpNetworkInfo ¤tNetwork : connectionQueue()) + MutexTracker connectionQueueMutexTracker(_tcpIpConnectionQueueMutex); + if(!connectionQueueMutexTracker.mutexCaptured()) { - transmission_status_t transmissionResult = initiateTransmission(currentNetwork); - - latestTransmissionOutcomes().push_back(TransmissionOutcome{.origin = currentNetwork, .transmissionStatus = transmissionResult}); + assert(false && "ERROR! connectionQueue locked. Don't call attemptTransmission from callbacks as this may corrupt program state! Aborting."); + } + else + { + for(const TcpIpNetworkInfo ¤tNetwork : constConnectionQueue()) + { + transmission_status_t transmissionResult = initiateTransmission(currentNetwork); + + latestTransmissionOutcomes().push_back(TransmissionOutcome{.origin = currentNetwork, .transmissionStatus = transmissionResult}); + + if(!getTransmissionOutcomesUpdateHook()(*this)) + break; + } } } @@ -492,12 +520,12 @@ transmission_status_t TcpIpMeshBackend::attemptTransmission(const String &messag return transmissionResult; } -void TcpIpMeshBackend::acceptRequest() +void TcpIpMeshBackend::acceptRequests() { MutexTracker mutexTracker(_tcpIpTransmissionMutex); if(!mutexTracker.mutexCaptured()) { - assert(false && "ERROR! TCP/IP transmission in progress. Don't call acceptRequest from TCP/IP callbacks as this may corrupt program state! Aborting."); + assert(false && "ERROR! TCP/IP transmission in progress. Don't call acceptRequests from callbacks as this may corrupt program state! Aborting."); return; } diff --git a/libraries/ESP8266WiFiMesh/src/TcpIpMeshBackend.h b/libraries/ESP8266WiFiMesh/src/TcpIpMeshBackend.h index e83cbac35..b0929c7a6 100644 --- a/libraries/ESP8266WiFiMesh/src/TcpIpMeshBackend.h +++ b/libraries/ESP8266WiFiMesh/src/TcpIpMeshBackend.h @@ -67,12 +67,19 @@ public: /** * Returns a vector that contains the NetworkInfo for each WiFi network to connect to. - * This vector is unique for each mesh backend. + * This vector is unique for each mesh backend, but NetworkInfo elements can be directly transferred between the vectors as long as both SSID and BSSID are present. * The connectionQueue vector is cleared before each new scan and filled via the networkFilter callback function once the scan completes. * WiFi connections will start with connectionQueue[0] and then incrementally proceed to higher vector positions. * Note that old network indicies often are invalidated whenever a new WiFi network scan occurs. + * + * Since the connectionQueue() is iterated over during transmissions, always use constConnectionQueue() from callbacks other than NetworkFilter. */ - std::vector & connectionQueue(); + static std::vector & connectionQueue(); + + /** + * Same as connectionQueue(), but can be called from all callbacks since the returned reference is const. + */ + static const std::vector & constConnectionQueue(); /** * Returns a vector with the TransmissionOutcome for each AP to which a transmission was attempted during the latest attemptTransmission call. @@ -81,7 +88,13 @@ public: * Connection attempts are indexed in the same order they were attempted. * Note that old network indicies often are invalidated whenever a new WiFi network scan occurs. */ - std::vector & latestTransmissionOutcomes() override; + static std::vector & latestTransmissionOutcomes(); + + /** + * @return True if latest transmission was successful (i.e. latestTransmissionOutcomes is not empty and all entries have transmissionStatus TS_TRANSMISSION_COMPLETE). False otherwise. + * The result is unique for each mesh backend. + */ + static bool latestTransmissionSuccessful(); /** * Initialises the node. @@ -116,7 +129,7 @@ public: /** * If any clients are connected, accept their requests and call the requestHandler function for each one. */ - void acceptRequest(); + void acceptRequests(); /** * Get the TCP/IP message that is currently scheduled for transmission. @@ -185,7 +198,7 @@ public: /** * Set the timeout to use for transmissions when this TcpIpMeshBackend instance acts as an AP (i.e. when receiving connections from other stations). - * This will affect the timeout of the acceptRequest method. + * This will affect the timeout of the acceptRequests method. * The timeout is 4 500 ms by default. * Will also change the setting for the active AP (without an AP restart) * if this TcpIpMeshBackend instance is the current AP controller. @@ -217,6 +230,11 @@ protected: */ static bool _tcpIpTransmissionMutex; + /** + * Will be true when the connectionQueue should not be modified. + */ + static bool _tcpIpConnectionQueueMutex; + /** * Check if there is an ongoing TCP/IP transmission in the library. Used to avoid interrupting transmissions. * diff --git a/libraries/ESP8266WiFiMesh/src/TcpIpNetworkInfo.cpp b/libraries/ESP8266WiFiMesh/src/TcpIpNetworkInfo.cpp index 73a6e232e..ad3b6fe16 100644 --- a/libraries/ESP8266WiFiMesh/src/TcpIpNetworkInfo.cpp +++ b/libraries/ESP8266WiFiMesh/src/TcpIpNetworkInfo.cpp @@ -23,9 +23,16 @@ */ #include "TcpIpNetworkInfo.h" +#include TcpIpNetworkInfo::TcpIpNetworkInfo(int networkIndex) : NetworkInfoBase(networkIndex) { }; + +TcpIpNetworkInfo::TcpIpNetworkInfo(const NetworkInfoBase &originalNetworkInfo) : NetworkInfoBase(originalNetworkInfo) +{ + assert(SSID() != defaultSSID); // We need at least SSID to be able to connect. +}; + TcpIpNetworkInfo::TcpIpNetworkInfo(const String &SSID, int32_t wifiChannel, const uint8_t BSSID[6], uint8_t encryptionType, int32_t RSSI , bool isHidden) : NetworkInfoBase(SSID, wifiChannel, BSSID, encryptionType, RSSI, isHidden) { } diff --git a/libraries/ESP8266WiFiMesh/src/TcpIpNetworkInfo.h b/libraries/ESP8266WiFiMesh/src/TcpIpNetworkInfo.h index eed1a9981..e59bb5690 100644 --- a/libraries/ESP8266WiFiMesh/src/TcpIpNetworkInfo.h +++ b/libraries/ESP8266WiFiMesh/src/TcpIpNetworkInfo.h @@ -36,10 +36,13 @@ public: */ TcpIpNetworkInfo(int networkIndex); + + TcpIpNetworkInfo(const NetworkInfoBase &originalNetworkInfo); + /** * Without giving wifiChannel and BSSID, connection time is longer. */ - TcpIpNetworkInfo(const String &SSID, int32_t wifiChannel = defaultWifiChannel, const uint8_t BSSID[6] = nullptr, uint8_t encryptionType = defaultEncryptionType, + TcpIpNetworkInfo(const String &SSID, int32_t wifiChannel = defaultWifiChannel, const uint8_t BSSID[6] = defaultBSSID, uint8_t encryptionType = defaultEncryptionType, int32_t RSSI = defaultRSSI, bool isHidden = defaultIsHidden); };