1
0
mirror of https://github.com/esp8266/Arduino.git synced 2025-04-21 10:26:06 +03:00
esp8266/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.cpp
Anders 5834c54717 - Add broadcast functionality.
- Add createPermanentConnections argument to attemptAutoEncryptingTransmission method.

- Reduce risk of misinterpreting acks by adding check for ack sender MAC.

- Reduce _encryptionRequestTimeoutMs from 500 ms to 300 ms since this should give enough (100 %) margin to the level where problems start appearing (150 ms timeout) and also save a lot of time in case of request failure.

- Improve comments.
2019-08-10 20:35:48 +02:00

2360 lines
90 KiB
C++

/*
EspnowMeshBackend
Copyright (C) 2019 Anders Löfgren
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <ESP8266WiFi.h>
extern "C" {
#include <espnow.h>
}
#include "EspnowMeshBackend.h"
#include "TypeConversionFunctions.h"
#include "UtilityFunctions.h"
#include "MutexTracker.h"
#include "JsonTranslator.h"
#include "Crypto.h"
using EspnowProtocolInterpreter::espnowEncryptionKeyLength;
using EspnowProtocolInterpreter::espnowHashKeyLength;
static const uint8_t maxEncryptedConnections = 6; // This is limited by the ESP-NOW API. Max 6 in AP or AP+STA mode. Max 10 in STA mode. See "ESP-NOW User Guide" for more info.
static const uint64_t uint64MSB = 0x8000000000000000;
const uint8_t EspnowMeshBackend::broadcastMac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
bool EspnowMeshBackend::_espnowTransmissionMutex = false;
EspnowMeshBackend *EspnowMeshBackend::_espnowRequestManager = nullptr;
std::map<std::pair<EspnowMeshBackend::macAndType_td, EspnowMeshBackend::messageID_td>, MessageData> EspnowMeshBackend::receivedEspnowTransmissions = {};
std::map<std::pair<EspnowMeshBackend::peerMac_td, EspnowMeshBackend::messageID_td>, RequestData> EspnowMeshBackend::sentRequests = {};
std::map<std::pair<EspnowMeshBackend::peerMac_td, EspnowMeshBackend::messageID_td>, TimeTracker> EspnowMeshBackend::receivedRequests = {};
std::list<ResponseData> EspnowMeshBackend::responsesToSend = {};
std::list<PeerRequestLog> EspnowMeshBackend::peerRequestConfirmationsToSend = {};
std::vector<EncryptedConnectionLog> EspnowMeshBackend::encryptedConnections = {};
uint32_t EspnowMeshBackend::_espnowTransmissionTimeoutMs = 40;
uint32_t EspnowMeshBackend::_espnowRetransmissionIntervalMs = 15;
uint32_t EspnowMeshBackend::_encryptionRequestTimeoutMs = 300;
bool EspnowMeshBackend::_espnowSendConfirmed = false;
String EspnowMeshBackend::_ongoingPeerRequestNonce = "";
EspnowMeshBackend *EspnowMeshBackend::_ongoingPeerRequester = nullptr;
encrypted_connection_status_t EspnowMeshBackend::_ongoingPeerRequestResult = ECS_MAX_CONNECTIONS_REACHED_SELF;
uint32_t EspnowMeshBackend::_ongoingPeerRequestEncryptionStart = 0;
uint8_t EspnowMeshBackend::_espnowEncryptionKok[espnowEncryptionKeyLength] = { 0 };
bool EspnowMeshBackend::_espnowEncryptionKokSet = false;
uint32_t EspnowMeshBackend::_unencryptedMessageID = 0;
// _logEntryLifetimeMs is based on someone storing 40 responses of 750 bytes each = 30 000 bytes (roughly full memory),
// which takes 2000 ms + some margin to send. Also, we want to avoid old entries taking up memory if they cannot be sent,
// so storage duration should not be too long.
uint32_t EspnowMeshBackend::_logEntryLifetimeMs = 2500;
uint32_t EspnowMeshBackend::_broadcastResponseTimeoutMs = 1000; // This is shorter than _logEntryLifetimeMs to preserve RAM since broadcasts are not deleted from sentRequests until they expire.
uint32_t EspnowMeshBackend::_timeOfLastLogClear = 0;
uint32_t EspnowMeshBackend::_criticalHeapLevel = 6000; // In bytes
uint32_t EspnowMeshBackend::_criticalHeapLevelBuffer = 6000; // In bytes
uint8_t EspnowMeshBackend::_maxTransmissionsPerMessage = 3;
bool EspnowMeshBackend::_espnowSendToNodeMutex = false;
uint8_t EspnowMeshBackend::_transmissionTargetBSSID[6] = {0};
double EspnowMeshBackend::_transmissionsTotal = 0;
double EspnowMeshBackend::_transmissionsFailed = 0;
bool EspnowMeshBackend::_staticVerboseMode = false;
void espnowDelay(uint32_t durationMs)
{
uint32_t startingTime = millis();
while(millis() - startingTime < durationMs)
{
delay(1);
EspnowMeshBackend::performEspnowMaintainance();
}
}
EspnowMeshBackend::EspnowMeshBackend(requestHandlerType requestHandler, responseHandlerType responseHandler, networkFilterType networkFilter,
broadcastFilterType broadcastFilter, const String &meshPassword, const uint8_t espnowEncryptionKey[espnowEncryptionKeyLength],
const uint8_t espnowHashKey[espnowHashKeyLength], const String &ssidPrefix, const String &ssidSuffix, bool verboseMode,
uint8 meshWiFiChannel)
: MeshBackendBase(requestHandler, responseHandler, networkFilter, MB_ESP_NOW)
{
// Reserve the maximum possible usage early on to prevent heap fragmentation later.
encryptedConnections.reserve(maxEncryptedConnections);
setBroadcastFilter(broadcastFilter);
setSSID(ssidPrefix, "", ssidSuffix);
setMeshPassword(meshPassword);
setEspnowEncryptionKey(espnowEncryptionKey);
setEspnowHashKey(espnowHashKey);
setVerboseModeState(verboseMode);
setWiFiChannel(meshWiFiChannel);
}
EspnowMeshBackend::~EspnowMeshBackend()
{
if(isEspnowRequestManager())
{
setEspnowRequestManager(nullptr);
}
deleteSentRequestsByOwner(this);
}
void EspnowMeshBackend::begin()
{
if(!getAPController()) // If there is no active AP controller
WiFi.mode(WIFI_STA); // WIFI_AP_STA mode automatically sets up an AP, so we can't use that as default.
activateEspnow();
}
void EspnowMeshBackend::performEspnowMaintainance()
{
// Doing this during an ESP-NOW transmission could invalidate iterators
MutexTracker mutexTracker(_espnowTransmissionMutex, handlePostponedRemovals);
if(!mutexTracker.mutexCaptured())
{
assert(false && "ERROR! Transmission in progress. Don't call performEspnowMaintainance from callbacks as this may corrupt program state! Aborting.");
return;
}
if(millis() - _timeOfLastLogClear >= 500) // Clearing too frequently will cause a lot of unnecessary container iterations.
{
clearOldLogEntries();
}
if(EncryptedConnectionLog::getSoonestExpiringConnectionTracker() && EncryptedConnectionLog::getSoonestExpiringConnectionTracker()->expired())
{
updateTemporaryEncryptedConnections();
}
sendEspnowResponses();
}
void EspnowMeshBackend::updateTemporaryEncryptedConnections(bool scheduledRemovalOnly)
{
EncryptedConnectionLog::clearSoonestExpiringConnectionTracker();
for(auto connectionIterator = encryptedConnections.begin(); connectionIterator != encryptedConnections.end(); )
{
if(auto timeTrackerPointer = connectionIterator->temporary())
{
if(timeTrackerPointer->expired() && (!scheduledRemovalOnly || connectionIterator->removalScheduled()))
{
uint8_t macArray[6] = { 0 };
removeEncryptedConnectionUnprotected(connectionIterator->getEncryptedPeerMac(macArray), &connectionIterator);
continue;
}
else
{
EncryptedConnectionLog::updateSoonestExpiringConnectionTracker(timeTrackerPointer->remainingDuration());
}
}
assert(!connectionIterator->removalScheduled()); // timeTracker should always exist and be expired if removal is scheduled.
++connectionIterator;
}
EncryptedConnectionLog::setNewRemovalsScheduled(false);
}
void EspnowMeshBackend::espnowReceiveCallbackWrapper(uint8_t *macaddr, uint8_t *dataArray, uint8_t len)
{
using namespace EspnowProtocolInterpreter;
if(len >= EspnowProtocolInterpreter::espnowProtocolBytesSize()) // If we do not receive at least the protocol bytes, the transmission is invalid.
{
//uint32_t callbackStart = millis();
// If there is a espnowRequestManager, get it
EspnowMeshBackend *currentEspnowRequestManager = getEspnowRequestManager();
char messageType = espnowGetMessageType(dataArray);
uint64_t receivedMessageID = espnowGetMessageID(dataArray);
if(currentEspnowRequestManager && !currentEspnowRequestManager->acceptsUnencryptedRequests()
&& !usesConstantSessionKey(messageType) && !verifyPeerSessionKey(receivedMessageID, macaddr, messageType))
{
return;
}
uint64_t uint64StationMac = macToUint64(macaddr);
bool transmissionEncrypted = usesEncryption(receivedMessageID);
// Useful when debugging the protocol
//Serial.print("Received from Mac: " + macToString(macaddr) + " ID: " + uint64ToString(receivedMessageID));
//Serial.println(transmissionEncrypted ? " Encrypted" : " Unencrypted");
if(messageType == 'Q' || messageType == 'B') // Question (request) or Broadcast
{
if(ESP.getFreeHeap() <= criticalHeapLevel())
{
warningPrint("WARNING! Free heap below critical level. Suspending ESP-NOW request processing until the situation improves.");
return;
}
if(currentEspnowRequestManager)
{
if(!requestReceived(uint64StationMac, receivedMessageID)) // If the request has not already been received
{
if(transmissionEncrypted)
{
EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(macaddr);
if(!encryptedConnection || (!synchronizePeerSessionKey(receivedMessageID, *encryptedConnection) &&
!verifyPeerSessionKey(receivedMessageID, *encryptedConnection, uint64StationMac, messageType)))
{
// We received an encrypted transmission
// and we have no encrypted connection to the transmitting node (in which case we want to avoid sending the secret session key back in an unencrypted response)
// or the transmission has the wrong session key
// and it doesn't have a session key that matches any multi-part transmission we are currently receiving (in which case the transmission is invalid).
return;
}
}
//Serial.println("espnowReceiveCallbackWrapper before internal callback " + String(millis() - callbackStart));
currentEspnowRequestManager->espnowReceiveCallback(macaddr, dataArray, len);
}
}
}
else if(messageType == 'A') // Answer (response)
{
EspnowMeshBackend *requestSender = nullptr;
uint64_t requestMac = 0;
if(transmissionEncrypted)
{
// An encrypted transmission can only be sent to the station interface, since it otherwise won't arrive (because of ESP_NOW_ROLE_CONTROLLER).
requestMac = uint64StationMac;
requestSender = getOwnerOfSentRequest(requestMac, receivedMessageID);
}
else
{
// An unencrypted transmission was probably sent to the AP interface as a result of a scan.
requestMac = espnowGetTransmissionMac(dataArray);
requestSender = getOwnerOfSentRequest(requestMac, receivedMessageID);
// But if not, also check if it was sent to the station interface.
if(!requestSender)
{
requestMac = uint64StationMac;
requestSender = getOwnerOfSentRequest(requestMac, receivedMessageID);
}
// Or if it was sent as a broadcast. (A broadcast can never be encrypted)
if(!requestSender)
{
requestSender = getOwnerOfSentRequest(uint64BroadcastMac, receivedMessageID);
}
}
// If this node sent the request and it has not already been answered.
if(requestSender)
{
uint8_t macArray[6] = { 0 };
requestSender->espnowReceiveCallback(uint64ToMac(requestMac, macArray), dataArray, len);
}
}
else if(messageType == 'S') // Synchronization request
{
synchronizePeerSessionKey(receivedMessageID, macaddr);
}
else if(messageType == 'P') // Peer request
{
handlePeerRequest(macaddr, dataArray, len, uint64StationMac, receivedMessageID);
}
else if(messageType == 'C') // peer request Confirmation
{
handlePeerRequestConfirmation(macaddr, dataArray, len);
}
else
{
assert(messageType == 'Q' || messageType == 'A' || messageType == 'B' || messageType == 'S' || messageType == 'P' || messageType == 'C');
}
//Serial.println("espnowReceiveCallbackWrapper duration " + String(millis() - callbackStart));
}
}
void EspnowMeshBackend::handlePeerRequest(uint8_t *macaddr, uint8_t *dataArray, uint8_t len, uint64_t uint64StationMac, uint64_t receivedMessageID)
{
// 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.
// Receive: encryptedConnectionVerificationHeader.
using namespace EspnowProtocolInterpreter;
if(!requestReceived(uint64StationMac, receivedMessageID))
{
storeReceivedRequest(uint64StationMac, receivedMessageID, TimeTracker(millis()));
bool encryptedCorrectly = synchronizePeerSessionKey(receivedMessageID, macaddr);
String message = espnowGetMessageContent(dataArray, len);
int32_t messageHeaderEndIndex = message.indexOf(':');
String messageHeader = message.substring(0, messageHeaderEndIndex + 1);
if(messageHeader == encryptedConnectionVerificationHeader)
{
if(encryptedCorrectly)
{
int32_t connectionRequestTypeEndIndex = message.indexOf(':', messageHeaderEndIndex + 1);
String connectionRequestType = message.substring(messageHeaderEndIndex + 1, connectionRequestTypeEndIndex + 1);
if(connectionRequestType == encryptionRequestHeader)
{
temporaryEncryptedConnectionToPermanent(macaddr);
}
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;
if(JsonTranslator::getDuration(message, connectionDuration))
{
encryptedConnection->setRemainingDuration(connectionDuration);
}
}
}
else
{
assert(false && "Unknown P-type verification message received!");
}
}
}
else if(messageHeader == encryptionRequestHeader || messageHeader == temporaryEncryptionRequestHeader)
{
// If there is a espnowRequestManager, get it
if(EspnowMeshBackend *currentEspnowRequestManager = getEspnowRequestManager())
{
String requestNonce = "";
if(JsonTranslator::getNonce(message, requestNonce))
{
uint8_t apMacArray[6] = { 0 };
peerRequestConfirmationsToSend.emplace_back(receivedMessageID, encryptedCorrectly, currentEspnowRequestManager->getMeshPassword(), requestNonce, macaddr, espnowGetTransmissionMac(dataArray, apMacArray), currentEspnowRequestManager->getEspnowHashKey());
}
}
}
else if(messageHeader == encryptedConnectionRemovalRequestHeader)
{
if(encryptedCorrectly)
removeEncryptedConnection(macaddr);
}
else
{
assert(false && "Unknown P-type message received!");
}
}
}
void EspnowMeshBackend::handlePeerRequestConfirmation(uint8_t *macaddr, uint8_t *dataArray, uint8_t len)
{
// 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.
// Send: encryptedConnectionVerificationHeader.
using namespace EspnowProtocolInterpreter;
if(_ongoingPeerRequestNonce != "")
{
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);
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))
{
_ongoingPeerRequestEncryptionStart = millis();
connectionLogIterator existingEncryptedConnection = connectionLogEndIterator();
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());
}
else
{
// Encrypted connection already exists
_ongoingPeerRequestResult = ECS_CONNECTION_ESTABLISHED;
if(auto timeTrackerPointer = existingEncryptedConnection->temporary())
{
if(timeTrackerPointer->remainingDuration() < getEncryptionRequestTimeout()) // Should only extend duration for existing connections.
{
existingEncryptedConnection->setRemainingDuration(getEncryptionRequestTimeout());
}
}
}
if(_ongoingPeerRequestResult != ECS_CONNECTION_ESTABLISHED)
{
// Adding connection failed, abort ongoing peer request.
_ongoingPeerRequestNonce = "";
}
}
}
else
{
if(messageHeader == encryptedConnectionInfoHeader)
{
String messagePassword = "";
if(JsonTranslator::getPassword(messageBody, messagePassword) && messagePassword == _ongoingPeerRequester->getMeshPassword())
{
// 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;
}
_ongoingPeerRequestNonce = "";
}
}
else if(messageHeader == maxConnectionsReachedHeader)
{
if(JsonTranslator::verifyHmac(message, _ongoingPeerRequester->getEspnowHashKey(), espnowHashKeyLength))
{
_ongoingPeerRequestResult = ECS_MAX_CONNECTIONS_REACHED_PEER;
_ongoingPeerRequestNonce = "";
}
}
else
{
assert(messageHeader == basicConnectionInfoHeader || messageHeader == encryptedConnectionInfoHeader || messageHeader == maxConnectionsReachedHeader);
}
}
}
}
}
void EspnowMeshBackend::setEspnowRequestManager(EspnowMeshBackend *espnowMeshInstance)
{
_espnowRequestManager = espnowMeshInstance;
}
EspnowMeshBackend *EspnowMeshBackend::getEspnowRequestManager() {return _espnowRequestManager;}
bool EspnowMeshBackend::isEspnowRequestManager()
{
return (this == getEspnowRequestManager());
}
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;
}
}
uint32_t EspnowMeshBackend::logEntryLifetimeMs()
{
return _logEntryLifetimeMs;
}
uint32_t EspnowMeshBackend::broadcastResponseTimeoutMs()
{
return _broadcastResponseTimeoutMs;
}
void EspnowMeshBackend::setCriticalHeapLevelBuffer(uint32_t bufferInBytes)
{
_criticalHeapLevelBuffer = bufferInBytes;
}
uint32_t EspnowMeshBackend::criticalHeapLevelBuffer()
{
return _criticalHeapLevelBuffer;
}
template <typename T, typename U>
void EspnowMeshBackend::deleteExpiredLogEntries(std::map<std::pair<U, uint64_t>, T> &logEntries, uint32_t maxEntryLifetimeMs)
{
for(typename std::map<std::pair<U, uint64_t>, T>::iterator entryIterator = logEntries.begin();
entryIterator != logEntries.end(); )
{
if(entryIterator->second.timeSinceCreation() > maxEntryLifetimeMs)
{
entryIterator = logEntries.erase(entryIterator);
}
else
++entryIterator;
}
}
void EspnowMeshBackend::deleteExpiredLogEntries(std::map<std::pair<peerMac_td, messageID_td>, RequestData> &logEntries, uint32_t requestLifetimeMs, uint32_t broadcastLifetimeMs)
{
for(typename std::map<std::pair<peerMac_td, messageID_td>, 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 <typename T>
void EspnowMeshBackend::deleteExpiredLogEntries(std::list<T> &logEntries, uint32_t maxEntryLifetimeMs)
{
for(typename std::list<T>::iterator entryIterator = logEntries.begin();
entryIterator != logEntries.end(); )
{
if(entryIterator->timeSinceCreation() > maxEntryLifetimeMs)
{
entryIterator = logEntries.erase(entryIterator);
}
else
++entryIterator;
}
}
template <>
void EspnowMeshBackend::deleteExpiredLogEntries(std::list<EncryptedConnectionLog> &logEntries, uint32_t maxEntryLifetimeMs)
{
for(typename std::list<EncryptedConnectionLog>::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<PeerRequestLog> &logEntries, uint32_t maxEntryLifetimeMs)
{
for(typename std::list<PeerRequestLog>::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 <typename T>
T *EspnowMeshBackend::getMapValue(std::map<uint64_t, T> &mapIn, uint64_t keyIn)
{
typename std::map<uint64_t, T>::iterator mapIterator = mapIn.find(keyIn);
if(mapIterator != mapIn.end())
{
return &mapIterator->second;
}
return nullptr;
}
void EspnowMeshBackend::storeSentRequest(const uint64_t targetBSSID, const uint64_t messageID, const RequestData &requestData)
{
sentRequests.insert(std::make_pair(std::make_pair(targetBSSID, messageID), requestData));
}
void EspnowMeshBackend::storeReceivedRequest(const uint64_t senderBSSID, const uint64_t messageID, const TimeTracker &timeTracker)
{
receivedRequests.insert(std::make_pair(std::make_pair(senderBSSID, messageID), timeTracker));
}
EspnowMeshBackend *EspnowMeshBackend::getOwnerOfSentRequest(uint64_t requestMac, uint64_t requestID)
{
std::map<std::pair<peerMac_td, messageID_td>, RequestData>::iterator sentRequest = sentRequests.find(std::make_pair(requestMac, requestID));
if(sentRequest != sentRequests.end())
{
return &sentRequest->second.getMeshInstance();
}
return nullptr;
}
size_t EspnowMeshBackend::deleteSentRequest(uint64_t requestMac, uint64_t requestID)
{
return sentRequests.erase(std::make_pair(requestMac, requestID));
}
size_t EspnowMeshBackend::deleteSentRequestsByOwner(EspnowMeshBackend *instancePointer)
{
size_t numberDeleted = 0;
for(std::map<std::pair<peerMac_td, messageID_td>, RequestData>::iterator requestIterator = sentRequests.begin();
requestIterator != sentRequests.end(); )
{
if(&requestIterator->second.getMeshInstance() == instancePointer) // If instance at instancePointer made the request
{
requestIterator = sentRequests.erase(requestIterator);
numberDeleted++;
}
else
++requestIterator;
}
return numberDeleted;
}
bool EspnowMeshBackend::requestReceived(uint64_t requestMac, uint64_t requestID)
{
return receivedRequests.count(std::make_pair(requestMac, requestID));
}
uint32_t EspnowMeshBackend::criticalHeapLevel()
{
return _criticalHeapLevel;
}
uint64_t EspnowMeshBackend::generateMessageID(EncryptedConnectionLog *encryptedConnection)
{
if(encryptedConnection)
{
return encryptedConnection->getOwnSessionKey();
}
return _unencryptedMessageID++;
}
uint64_t EspnowMeshBackend::createSessionKey()
{
uint64_t newSessionKey = randomUint64();
return EspnowProtocolInterpreter::usesEncryption(newSessionKey) ? newSessionKey : (newSessionKey | ((uint64_t)RANDOM_REG32) << 32 | uint64MSB);
}
void EspnowMeshBackend::setEspnowTransmissionTimeout(uint32_t timeoutMs)
{
_espnowTransmissionTimeoutMs = timeoutMs;
}
uint32_t EspnowMeshBackend::getEspnowTransmissionTimeout() {return _espnowTransmissionTimeoutMs;}
void EspnowMeshBackend::setEspnowRetransmissionInterval(uint32_t intervalMs)
{
_espnowRetransmissionIntervalMs = intervalMs;
}
uint32_t EspnowMeshBackend::getEspnowRetransmissionInterval() {return _espnowRetransmissionIntervalMs;}
void EspnowMeshBackend::setEncryptionRequestTimeout(uint32_t timeoutMs)
{
_encryptionRequestTimeoutMs = timeoutMs;
}
uint32_t EspnowMeshBackend::getEncryptionRequestTimeout() {return _encryptionRequestTimeoutMs;}
void EspnowMeshBackend::setAutoEncryptionDuration(uint32_t duration)
{
_autoEncryptionDuration = duration;
}
uint32_t EspnowMeshBackend::getAutoEncryptionDuration() {return _autoEncryptionDuration;}
void EspnowMeshBackend::setBroadcastFilter(broadcastFilterType broadcastFilter) {_broadcastFilter = broadcastFilter;}
EspnowMeshBackend::broadcastFilterType EspnowMeshBackend::getBroadcastFilter() {return _broadcastFilter;}
bool EspnowMeshBackend::usesConstantSessionKey(char messageType)
{
return messageType == 'A' || messageType == 'C';
}
transmission_status_t EspnowMeshBackend::espnowSendToNode(const String &message, const uint8_t *targetBSSID, char messageType, EspnowMeshBackend *espnowInstance)
{
using EspnowProtocolInterpreter::synchronizationRequestHeader;
EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(targetBSSID);
if(encryptedConnection)
{
uint8_t encryptedMac[6] {0};
encryptedConnection->getEncryptedPeerMac(encryptedMac);
if(encryptedConnection->desync())
{
espnowSendToNodeUnsynchronized(synchronizationRequestHeader, encryptedMac, 'S', generateMessageID(encryptedConnection), espnowInstance);
if(encryptedConnection->desync())
{
return TS_TRANSMISSION_FAILED;
}
}
return espnowSendToNodeUnsynchronized(message, encryptedMac, messageType, generateMessageID(encryptedConnection), espnowInstance);
}
else
{
return espnowSendToNodeUnsynchronized(message, targetBSSID, messageType, generateMessageID(encryptedConnection), espnowInstance);
}
}
transmission_status_t EspnowMeshBackend::espnowSendToNodeUnsynchronized(const String message, const uint8_t *targetBSSID, char messageType, uint64_t messageID, EspnowMeshBackend *espnowInstance)
{
using namespace EspnowProtocolInterpreter;
MutexTracker mutexTracker(_espnowSendToNodeMutex);
if(!mutexTracker.mutexCaptured())
{
assert(false && "ERROR! espnowSendToNode already in progress. Don't call espnowSendToNode from callbacks as this will make it impossible to know which transmissions succeed! Aborting.");
return TS_TRANSMISSION_FAILED;
}
// We copy the message String and bssid array from the arguments in this method to make sure they are
// not modified by a callback during the delay(1) calls further down.
// This also makes it possible to get the current _transmissionTargetBSSID outside of the method.
std::copy_n(targetBSSID, 6, _transmissionTargetBSSID);
EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(_transmissionTargetBSSID);
int32_t transmissionsRequired = ceil((double)message.length() / getMaxMessageBytesPerTransmission());
int32_t transmissionsRemaining = transmissionsRequired > 1 ? transmissionsRequired - 1 : 0;
_transmissionsTotal++;
// Though it is possible to handle messages requiring more than 3 transmissions with the current design, transmission fail rates would increase dramatically.
// Messages composed of up to 128 transmissions can be handled without modification, but RAM limitations on the ESP8266 would make this hard in practice.
// We thus prefer to keep the code simple and performant instead.
// Very large messages can always be split by the user as required.
assert(transmissionsRequired <= getMaxTransmissionsPerMessage());
assert(messageType == 'Q' || messageType == 'A' || messageType == 'B' || messageType == 'S' || messageType == 'P' || messageType == 'C');
if(messageType == 'P' || messageType == 'C')
{
assert(transmissionsRequired == 1); // These messages are assumed to be contained in one message by the receive callbacks.
}
uint8_t transmissionSize = 0;
bool messageStart = true;
uint8_t sizeOfProtocolBytes = espnowProtocolBytesSize();
do
{
////// Manage logs //////
if(transmissionsRemaining == 0 && (messageType == 'Q' || messageType == 'B'))
{
assert(espnowInstance); // espnowInstance required when transmitting 'Q' and 'B' type messages.
// If we are sending the last transmission of a request we should store the sent request in the log no matter if we receive an ack for the final transmission or not.
// That way we will always be ready to receive the response to the request when there is a chance the request message was transmitted successfully,
// even if the final ack for the request message was lost.
storeSentRequest(macToUint64(_transmissionTargetBSSID), messageID, RequestData(*espnowInstance));
}
////// Create transmission array //////
if(transmissionsRemaining > 0)
transmissionSize = getMaxBytesPerTransmission();
else if(message.length() == 0)
transmissionSize = sizeOfProtocolBytes;
else
transmissionSize = sizeOfProtocolBytes + (message.length() % getMaxMessageBytesPerTransmission() == 0 ?
getMaxMessageBytesPerTransmission() : message.length() % getMaxMessageBytesPerTransmission());
uint8_t transmission[transmissionSize];
////// Fill protocol bytes //////
transmission[espnowMessageTypeIndex] = messageType;
if(messageStart)
{
transmission[espnowTransmissionsRemainingIndex] = (char)(transmissionsRemaining | 0x80);
}
else
{
transmission[espnowTransmissionsRemainingIndex] = (char)transmissionsRemaining;
}
// Fills indicies in range [espnowTransmissionMacIndex, espnowTransmissionMacIndex + 5] (6 bytes) with the MAC address of the WiFi AP interface.
// We always transmit from the station interface (due to using ESP_NOW_ROLE_CONTROLLER), so this makes it possible to always know both interface MAC addresses of a node that sends a transmission.
WiFi.softAPmacAddress(transmission + espnowTransmissionMacIndex);
espnowSetMessageID(transmission, messageID);
////// Fill message bytes //////
int32_t transmissionStartIndex = (transmissionsRequired - transmissionsRemaining - 1) * getMaxMessageBytesPerTransmission();
std::copy_n(message.substring(transmissionStartIndex, transmissionStartIndex + transmissionSize - sizeOfProtocolBytes).c_str(),
transmissionSize - sizeOfProtocolBytes, transmission + sizeOfProtocolBytes);
////// Transmit //////
_espnowSendConfirmed = false;
uint32_t transmissionStartTime = millis();
while(!_espnowSendConfirmed && millis() - transmissionStartTime < getEspnowTransmissionTimeout())
{
if(esp_now_send(_transmissionTargetBSSID, transmission, transmissionSize) == 0) // == 0 => Success
{
uint32_t transmissionAttemptStart = millis();
while(!_espnowSendConfirmed
&& (millis() - transmissionAttemptStart < getEspnowRetransmissionInterval())
&& (millis() - transmissionStartTime < getEspnowTransmissionTimeout()))
{
delay(1); // Note that callbacks can be called during delay time, so it is possible to receive a transmission during this delay.
}
}
if(_espnowSendConfirmed)
{
if(messageStart)
{
if(encryptedConnection && !usesConstantSessionKey(messageType) && encryptedConnection->getOwnSessionKey() == messageID)
{
encryptedConnection->setDesync(false);
encryptedConnection->incrementOwnSessionKey();
}
messageStart = false;
}
break;
}
}
if(!_espnowSendConfirmed)
{
_transmissionsFailed++;
staticVerboseModePrint("espnowSendToNode failed!");
staticVerboseModePrint("Transmission #: " + String(transmissionsRequired - transmissionsRemaining) + "/" + String(transmissionsRequired));
staticVerboseModePrint("Transmission fail rate (up) " + String(getTransmissionFailRate()));
if(messageStart && encryptedConnection && !usesConstantSessionKey(messageType) && encryptedConnection->getOwnSessionKey() == messageID)
encryptedConnection->setDesync(true);
return TS_TRANSMISSION_FAILED;
}
transmissionsRemaining--; // This is used when transfering multi-transmission messages.
} while(transmissionsRemaining >= 0);
// Useful when debugging the protocol
//staticVerboseModePrint("Sent to Mac: " + macToString(_transmissionTargetBSSID) + " ID: " + uint64ToString(messageID));
return TS_TRANSMISSION_COMPLETE;
}
transmission_status_t EspnowMeshBackend::sendRequest(const String &message, const uint8_t *targetBSSID)
{
transmission_status_t transmissionStatus = espnowSendToNode(message, targetBSSID, 'Q', this);
return transmissionStatus;
}
transmission_status_t EspnowMeshBackend::sendResponse(const String &message, uint64_t requestID, const uint8_t *targetBSSID)
{
EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(targetBSSID);
uint8_t encryptedMac[6] {0};
if(encryptedConnection)
{
encryptedConnection->getEncryptedPeerMac(encryptedMac);
}
return espnowSendToNodeUnsynchronized(message, encryptedConnection ? encryptedMac : targetBSSID, 'A', requestID, this);
}
bool EspnowMeshBackend::transmissionInProgress(){return _espnowTransmissionMutex;}
EspnowMeshBackend::macAndType_td EspnowMeshBackend::createMacAndTypeValue(uint64_t uint64Mac, char messageType)
{
return static_cast<macAndType_td>(uint64Mac << 8 | (uint64_t)messageType);
}
uint64_t EspnowMeshBackend::macAndTypeToUint64Mac(const macAndType_td &macAndTypeValue)
{
return static_cast<uint64_t>(macAndTypeValue) >> 8;
}
void EspnowMeshBackend::espnowReceiveCallback(uint8_t *macaddr, uint8_t *dataArray, uint8_t len)
{
using namespace EspnowProtocolInterpreter;
////// <Method overview> //////
/*
if(messageStart)
{
storeTransmission
}
else
{
if(messageFound)
storeTransmission or (erase and return)
else
return
}
if(transmissionsRemaining != 0)
return
processMessage
*/
////// </Method overview> //////
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<std::pair<macAndType_td, messageID_td>, 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<std::pair<macAndType_td, messageID_td>, 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);
for(int i = 0; i < espnowEncryptionKeyLength; i++)
{
_espnowEncryptionKey[i] = espnowEncryptionKey[i];
}
}
const uint8_t *EspnowMeshBackend::getEspnowEncryptionKey()
{
return _espnowEncryptionKey;
}
uint8_t *EspnowMeshBackend::getEspnowEncryptionKey(uint8_t resultArray[espnowEncryptionKeyLength])
{
std::copy_n(_espnowEncryptionKey, espnowEncryptionKeyLength, resultArray);
return resultArray;
}
bool EspnowMeshBackend::setEspnowEncryptionKok(uint8_t espnowEncryptionKok[espnowEncryptionKeyLength])
{
if(espnowEncryptionKok == nullptr || esp_now_set_kok(espnowEncryptionKok, espnowEncryptionKeyLength)) // esp_now_set_kok failed if not == 0
return false;
for(int i = 0; i < espnowEncryptionKeyLength; i++)
{
_espnowEncryptionKok[i] = espnowEncryptionKok[i];
}
_espnowEncryptionKokSet = true;
return true;
}
const uint8_t *EspnowMeshBackend::getEspnowEncryptionKok()
{
if(_espnowEncryptionKokSet)
return _espnowEncryptionKok;
else
return nullptr;
}
void EspnowMeshBackend::setEspnowHashKey(const uint8_t espnowHashKey[espnowHashKeyLength])
{
assert(espnowHashKey != nullptr);
for(int i = 0; i < espnowHashKeyLength; i++)
{
_espnowHashKey[i] = espnowHashKey[i];
}
}
const uint8_t *EspnowMeshBackend::getEspnowHashKey()
{
return _espnowHashKey;
}
bool EspnowMeshBackend::verifyPeerSessionKey(uint64_t sessionKey, const uint8_t *peerMac, char messageType)
{
if(EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(peerMac))
{
return verifyPeerSessionKey(sessionKey, *encryptedConnection, macToUint64(peerMac), messageType);
}
return false;
}
bool EspnowMeshBackend::verifyPeerSessionKey(uint64_t sessionKey, EncryptedConnectionLog &encryptedConnection, uint64_t uint64PeerMac, char messageType)
{
if(EspnowProtocolInterpreter::usesEncryption(sessionKey))
{
if(sessionKey == encryptedConnection.getPeerSessionKey()
|| receivedEspnowTransmissions.find(std::make_pair(createMacAndTypeValue(uint64PeerMac, messageType), sessionKey))
!= receivedEspnowTransmissions.end())
{
// If sessionKey is correct or sessionKey is one part of a multi-part transmission.
return true;
}
}
return false;
}
bool EspnowMeshBackend::synchronizePeerSessionKey(uint64_t sessionKey, const uint8_t *peerMac)
{
if(EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(peerMac))
{
return synchronizePeerSessionKey(sessionKey, *encryptedConnection);
}
return false;
}
bool EspnowMeshBackend::synchronizePeerSessionKey(uint64_t sessionKey, EncryptedConnectionLog &encryptedConnection)
{
if(EspnowProtocolInterpreter::usesEncryption(sessionKey))
{
if(sessionKey == encryptedConnection.getPeerSessionKey())
{
uint8_t hashKey[espnowHashKeyLength] {0};
encryptedConnection.setPeerSessionKey(EncryptedConnectionLog::incrementSessionKey(sessionKey, encryptedConnection.getHashKey(hashKey), espnowHashKeyLength));
return true;
}
}
return false;
}
std::list<ResponseData>::const_iterator EspnowMeshBackend::getScheduledResponse(uint32_t responseIndex)
{
assert(responseIndex < numberOfScheduledResponses());
bool startFromBeginning = responseIndex < numberOfScheduledResponses()/2;
auto responseIterator = startFromBeginning ? responsesToSend.cbegin() : responsesToSend.cend();
uint32_t stepsToTarget = startFromBeginning ? responseIndex : numberOfScheduledResponses() - responseIndex; // cend is one element beyond the last
while(stepsToTarget > 0)
{
startFromBeginning ? ++responseIterator : --responseIterator;
stepsToTarget--;
}
return responseIterator;
}
String EspnowMeshBackend::getScheduledResponseMessage(uint32_t responseIndex)
{
return getScheduledResponse(responseIndex)->getMessage();
}
const uint8_t *EspnowMeshBackend::getScheduledResponseRecipient(uint32_t responseIndex)
{
return getScheduledResponse(responseIndex)->getRecipientMac();
}
void EspnowMeshBackend::clearAllScheduledResponses()
{
responsesToSend.clear();
}
uint32_t EspnowMeshBackend::numberOfScheduledResponses() {return responsesToSend.size();}
void EspnowMeshBackend::deleteScheduledResponsesByRecipient(const uint8_t *recipientMac, bool encryptedOnly)
{
for(auto responseIterator = responsesToSend.begin(); responseIterator != responsesToSend.end(); )
{
if(macEqual(responseIterator->getRecipientMac(), recipientMac) && (!encryptedOnly || EspnowProtocolInterpreter::usesEncryption(responseIterator->getRequestID())))
{
responseIterator = responsesToSend.erase(responseIterator);
}
else
++responseIterator;
}
}
void EspnowMeshBackend::setSenderMac(uint8_t *macArray)
{
std::copy_n(macArray, 6, _senderMac);
}
String EspnowMeshBackend::getSenderMac() {return macToString(_senderMac);}
uint8_t *EspnowMeshBackend::getSenderMac(uint8_t *macArray)
{
std::copy_n(_senderMac, 6, macArray);
return macArray;
}
void EspnowMeshBackend::setReceivedEncryptedMessage(bool receivedEncryptedMessage) { _receivedEncryptedMessage = receivedEncryptedMessage; }
bool EspnowMeshBackend::receivedEncryptedMessage() {return _receivedEncryptedMessage;}
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
uint8_t encryptionKeyArray[espnowEncryptionKeyLength] = { 0 };
if(EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(peerStaMac))
{
// Encrypted connection with MAC already exists, so no need to replace it, just updating is enough.
temporaryEncryptedConnectionToPermanent(peerStaMac);
encryptedConnection->setPeerSessionKey(peerSessionKey);
encryptedConnection->setOwnSessionKey(ownSessionKey);
esp_now_set_peer_key(peerStaMac, getEspnowEncryptionKey(encryptionKeyArray), espnowEncryptionKeyLength);
encryptedConnection->setHashKey(getEspnowHashKey());
return ECS_CONNECTION_ESTABLISHED;
}
if(encryptedConnections.size() == maxEncryptedConnections)
{
// No capacity for more encrypted connections.
return ECS_MAX_CONNECTIONS_REACHED_SELF;
}
// returns 0 on success: int esp_now_add_peer(u8 *mac_addr, u8 role, u8 channel, u8 *key, u8 key_len)
// Only MAC, encryption key and key length (16) actually matter. The rest is not used by ESP-NOW.
else if(0 == esp_now_add_peer(peerStaMac, ESP_NOW_ROLE_CONTROLLER, getWiFiChannel(), getEspnowEncryptionKey(encryptionKeyArray), espnowEncryptionKeyLength))
{
encryptedConnections.emplace_back(peerStaMac, peerApMac, peerSessionKey, ownSessionKey, getEspnowHashKey());
return ECS_CONNECTION_ESTABLISHED;
}
else
{
return ECS_API_CALL_FAILED;
}
}
encrypted_connection_status_t EspnowMeshBackend::addEncryptedConnection(const String &serializedConnectionState, bool ignoreDuration)
{
uint32_t duration = 0;
bool desync = false;
uint64_t ownSessionKey = 0;
uint64_t peerSessionKey = 0;
uint8_t peerStaMac[6] = { 0 };
uint8_t peerApMac[6] = { 0 };
if(JsonTranslator::getDesync(serializedConnectionState, desync)
&& JsonTranslator::getOwnSessionKey(serializedConnectionState, ownSessionKey) && JsonTranslator::getPeerSessionKey(serializedConnectionState, peerSessionKey)
&& JsonTranslator::getPeerStaMac(serializedConnectionState, peerStaMac) && JsonTranslator::getPeerApMac(serializedConnectionState, peerApMac))
{
encrypted_connection_status_t result = ECS_API_CALL_FAILED;
if(!ignoreDuration && JsonTranslator::getDuration(serializedConnectionState, duration))
{
result = addTemporaryEncryptedConnection(peerStaMac, peerApMac, peerSessionKey, ownSessionKey, duration);
}
else
{
result = addEncryptedConnection(peerStaMac, peerApMac, peerSessionKey, ownSessionKey);
}
if(result == ECS_CONNECTION_ESTABLISHED)
{
EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(peerStaMac);
encryptedConnection->setDesync(desync);
}
return result;
}
else
{
return ECS_REQUEST_TRANSMISSION_FAILED;
}
}
encrypted_connection_status_t EspnowMeshBackend::addTemporaryEncryptedConnection(uint8_t *peerStaMac, uint8_t *peerApMac, uint64_t peerSessionKey, uint64_t ownSessionKey, uint32_t duration)
{
assert(encryptedConnections.size() <= maxEncryptedConnections); // If this is not the case, ESP-NOW is no longer in sync with the library
uint8_t encryptionKeyArray[espnowEncryptionKeyLength] = { 0 };
connectionLogIterator encryptedConnection = connectionLogEndIterator();
if(getEncryptedConnectionIterator(peerStaMac, encryptedConnection))
{
// There is already an encrypted connection to this mac, so no need to replace it, just updating is enough.
encryptedConnection->setPeerSessionKey(peerSessionKey);
encryptedConnection->setOwnSessionKey(ownSessionKey);
esp_now_set_peer_key(peerStaMac, getEspnowEncryptionKey(encryptionKeyArray), espnowEncryptionKeyLength);
encryptedConnection->setHashKey(getEspnowHashKey());
if(encryptedConnection->temporary())
{
encryptedConnection->setRemainingDuration(duration);
}
return ECS_CONNECTION_ESTABLISHED;
}
encrypted_connection_status_t result = addEncryptedConnection(peerStaMac, peerApMac, peerSessionKey, ownSessionKey);
if(result == ECS_CONNECTION_ESTABLISHED)
{
if(!getEncryptedConnectionIterator(peerStaMac, encryptedConnection))
assert(false && "No connection found despite being added in addTemporaryEncryptedConnection.");
encryptedConnection->setRemainingDuration(duration);
}
return result;
}
encrypted_connection_status_t EspnowMeshBackend::addTemporaryEncryptedConnection(const String &serializedConnectionState, uint32_t duration)
{
bool desync = false;
uint64_t ownSessionKey = 0;
uint64_t peerSessionKey = 0;
uint8_t peerStaMac[6] = { 0 };
uint8_t peerApMac[6] = { 0 };
if(JsonTranslator::getDesync(serializedConnectionState, desync)
&& JsonTranslator::getOwnSessionKey(serializedConnectionState, ownSessionKey) && JsonTranslator::getPeerSessionKey(serializedConnectionState, peerSessionKey)
&& JsonTranslator::getPeerStaMac(serializedConnectionState, peerStaMac) && JsonTranslator::getPeerApMac(serializedConnectionState, peerApMac))
{
encrypted_connection_status_t result = addTemporaryEncryptedConnection(peerStaMac, peerApMac, peerSessionKey, ownSessionKey, duration);
if(result == ECS_CONNECTION_ESTABLISHED)
{
EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(peerStaMac);
encryptedConnection->setDesync(desync);
}
return result;
}
else
{
return ECS_REQUEST_TRANSMISSION_FAILED;
}
}
void EspnowMeshBackend::handlePostponedRemovals()
{
MutexTracker mutexTracker(_espnowTransmissionMutex);
if(!mutexTracker.mutexCaptured())
{
assert(false && "ERROR! Transmission in progress. Don't call handlePostponedRemovals from callbacks as this may corrupt program state! Aborting.");
return;
}
if(EncryptedConnectionLog::newRemovalsScheduled())
{
updateTemporaryEncryptedConnections(true);
}
}
encrypted_connection_status_t EspnowMeshBackend::requestEncryptedConnectionKernel(uint8_t *peerMac, const encryptionRequestBuilderType &encryptionRequestBuilder)
{
using namespace EspnowProtocolInterpreter;
assert(encryptedConnections.size() <= maxEncryptedConnections); // If this is not the case, ESP-NOW is no longer in sync with the library
MutexTracker mutexTracker(_espnowTransmissionMutex, handlePostponedRemovals);
if(!mutexTracker.mutexCaptured())
{
assert(false && "ERROR! Transmission in progress. Don't call requestEncryptedConnection from callbacks as this may corrupt program state! Aborting.");
return ECS_REQUEST_TRANSMISSION_FAILED;
}
EncryptedConnectionLog *existingEncryptedConnection = getEncryptedConnection(peerMac);
ExpiringTimeTracker existingTimeTracker = existingEncryptedConnection && existingEncryptedConnection->temporary() ?
*existingEncryptedConnection->temporary() : ExpiringTimeTracker(0);
if(!existingEncryptedConnection && encryptedConnections.size() >= maxEncryptedConnections)
{
assert(encryptedConnections.size() == maxEncryptedConnections);
// No capacity for more encrypted connections.
return ECS_MAX_CONNECTIONS_REACHED_SELF;
}
String requestNonce = macToString(peerMac) + uint64ToString(randomUint64()) + uint64ToString(randomUint64());
_ongoingPeerRequestResult = ECS_REQUEST_TRANSMISSION_FAILED;
_ongoingPeerRequestNonce = requestNonce;
_ongoingPeerRequester = this;
String requestMessage = encryptionRequestBuilder(requestNonce, existingTimeTracker);
verboseModePrint("Sending encrypted connection request to: " + macToString(peerMac));
if(espnowSendToNode(requestMessage, peerMac, 'P') == TS_TRANSMISSION_COMPLETE)
{
uint32_t startTime = millis();
// _ongoingPeerRequestNonce is set to "" when a peer confirmation response from the mac is received
while(millis() - startTime < getEncryptionRequestTimeout() && _ongoingPeerRequestNonce != "")
{
delay(1);
}
}
if(_ongoingPeerRequestNonce != "")
{
// If nonce != "" we only received the basic connection info, so the pairing process is incomplete
_ongoingPeerRequestResult = ECS_REQUEST_TRANSMISSION_FAILED;
_ongoingPeerRequestNonce = "";
}
else if(_ongoingPeerRequestResult == ECS_CONNECTION_ESTABLISHED)
{
requestMessage = encryptionRequestBuilder(requestNonce, existingTimeTracker); // Give the builder a chance to update the message
int32_t messageHeaderEndIndex = requestMessage.indexOf(':');
String messageHeader = requestMessage.substring(0, messageHeaderEndIndex + 1);
String messageBody = requestMessage.substring(messageHeaderEndIndex + 1);
// If we do not get an ack within getEncryptionRequestTimeout() the peer has probably had the time to delete the temporary encrypted connection.
if(espnowSendToNode(encryptedConnectionVerificationHeader + requestMessage, peerMac, 'P') == TS_TRANSMISSION_COMPLETE
&& millis() - _ongoingPeerRequestEncryptionStart < getEncryptionRequestTimeout())
{
EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(peerMac);
if(!encryptedConnection || encryptedConnection->removalScheduled())
{
assert(encryptedConnection && "requestEncryptedConnectionKernel cannot find an encrypted connection!");
// requestEncryptedConnectionRemoval received.
_ongoingPeerRequestResult = ECS_REQUEST_TRANSMISSION_FAILED;
}
else
{
// Finalize connection
if(messageHeader == encryptionRequestHeader)
{
temporaryEncryptedConnectionToPermanent(peerMac);
}
else if(messageHeader == temporaryEncryptionRequestHeader)
{
if(!existingEncryptedConnection || existingEncryptedConnection->temporary())
{
// Should not change duration of existing permanent connections.
uint32_t connectionDuration = 0;
bool durationFound = JsonTranslator::getDuration(messageBody, connectionDuration);
assert(durationFound);
encryptedConnection->setRemainingDuration(connectionDuration);
}
}
else
{
assert(false && "Unknown messageHeader during encrypted connection finalization!");
_ongoingPeerRequestResult = ECS_API_CALL_FAILED;
}
}
}
else
{
_ongoingPeerRequestResult = ECS_REQUEST_TRANSMISSION_FAILED;
}
}
if(_ongoingPeerRequestResult != ECS_CONNECTION_ESTABLISHED)
{
if(!existingEncryptedConnection)
{
// Remove any connection that was added during the request attempt.
removeEncryptedConnectionUnprotected(peerMac);
}
}
_ongoingPeerRequester = nullptr;
return _ongoingPeerRequestResult;
}
String EspnowMeshBackend::defaultEncryptionRequestBuilder(const String &requestHeader, const uint32_t durationMs,
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;
}
String EspnowMeshBackend::flexibleEncryptionRequestBuilder(const uint32_t minDurationMs, const String &requestNonce, const ExpiringTimeTracker &existingTimeTracker)
{
using namespace JsonTranslator;
using EspnowProtocolInterpreter::temporaryEncryptionRequestHeader;
uint32_t connectionDuration = minDurationMs >= existingTimeTracker.remainingDuration() ?
minDurationMs : existingTimeTracker.remainingDuration();
return createEncryptionRequestIntro(temporaryEncryptionRequestHeader, connectionDuration) + createEncryptionRequestEnding(requestNonce);
}
encrypted_connection_status_t EspnowMeshBackend::requestEncryptedConnection(uint8_t *peerMac)
{
using namespace std::placeholders;
return requestEncryptedConnectionKernel(peerMac, std::bind(defaultEncryptionRequestBuilder, EspnowProtocolInterpreter::encryptionRequestHeader, 0, _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));
}
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));
}
bool EspnowMeshBackend::temporaryEncryptedConnectionToPermanent(uint8_t *peerMac)
{
if(EncryptedConnectionLog *temporaryConnection = getTemporaryEncryptedConnection(peerMac))
{
temporaryConnection->removeDuration();
return true;
}
else
{
return false;
}
}
encrypted_connection_removal_outcome_t EspnowMeshBackend::removeEncryptedConnection(uint8_t *peerMac)
{
auto connectionIterator = getEncryptedConnectionIterator(peerMac, encryptedConnections);
if(connectionIterator != encryptedConnections.end())
{
MutexTracker mutexTracker(_espnowTransmissionMutex);
if(!mutexTracker.mutexCaptured())
{
// We should not remove an encrypted connection while there is a transmission in progress, since that may cause encrypted data to be sent unencrypted.
// Thus when a transmission is in progress we just schedule the encrypted connection for removal, so it will be removed during the next updateTemporaryEncryptedConnections() call.
connectionIterator->scheduleForRemoval();
return ECRO_REMOVAL_SCHEDULED;
}
else
{
return removeEncryptedConnectionUnprotected(peerMac);
}
}
else
{
// peerMac is already removed
return ECRO_REMOVAL_SUCCEEDED;
}
}
encrypted_connection_removal_outcome_t EspnowMeshBackend::removeEncryptedConnectionUnprotected(uint8_t *peerMac, std::vector<EncryptedConnectionLog>::iterator *resultingIterator)
{
connectionLogIterator connectionIterator = getEncryptedConnectionIterator(peerMac, encryptedConnections);
return removeEncryptedConnectionUnprotected(connectionIterator, resultingIterator);
}
encrypted_connection_removal_outcome_t EspnowMeshBackend::removeEncryptedConnectionUnprotected(connectionLogIterator &connectionIterator, std::vector<EncryptedConnectionLog>::iterator *resultingIterator)
{
assert(encryptedConnections.size() <= maxEncryptedConnections); // If this is not the case, ESP-NOW is no longer in sync with the library
if(connectionIterator != connectionLogEndIterator())
{
uint8_t encryptedMac[6] {0};
connectionIterator->getEncryptedPeerMac(encryptedMac);
staticVerboseModePrint("Removing connection " + macToString(encryptedMac) + "... ", false);
bool removalSucceeded = esp_now_del_peer(encryptedMac) == 0;
if(removalSucceeded)
{
if(resultingIterator != nullptr)
{
*resultingIterator = encryptedConnections.erase(connectionIterator);
}
else
{
encryptedConnections.erase(connectionIterator);
}
staticVerboseModePrint("Removal succeeded");
// Not deleting encrypted responses here would cause them to be sent unencrypted,
// exposing the peer session key which can be misused later if the encrypted connection is re-established.
deleteScheduledResponsesByRecipient(encryptedMac, true);
// Not deleting these entries here may cause issues if the encrypted connection is quickly re-added
// and happens to get the same session keys as before (e.g. requestReceived() could then give false positives).
deleteEntriesByMac(receivedEspnowTransmissions, encryptedMac, true);
deleteEntriesByMac(sentRequests, encryptedMac, true);
deleteEntriesByMac(receivedRequests, encryptedMac, true);
return ECRO_REMOVAL_SUCCEEDED;
}
else
{
staticVerboseModePrint("Removal failed");
return ECRO_REMOVAL_FAILED;
}
}
else
{
// connection is already removed
return ECRO_REMOVAL_SUCCEEDED;
}
}
template <typename T>
void EspnowMeshBackend::deleteEntriesByMac(std::map<std::pair<macAndType_td, uint64_t>, T> &logEntries, const uint8_t *peerMac, bool encryptedOnly)
{
bool macFound = false;
for(typename std::map<std::pair<macAndType_td, uint64_t>, T>::iterator entryIterator = logEntries.begin();
entryIterator != logEntries.end(); )
{
if(macAndTypeToUint64Mac(entryIterator->first.first) == macToUint64(peerMac))
{
macFound = true;
if(!encryptedOnly || EspnowProtocolInterpreter::usesEncryption(entryIterator->first.second))
{
entryIterator = logEntries.erase(entryIterator);
continue;
}
}
else if(macFound)
{
// Since the map is sorted by MAC, we know here that no more matching MAC will be found.
return;
}
++entryIterator;
}
}
template <typename T>
void EspnowMeshBackend::deleteEntriesByMac(std::map<std::pair<uint64_t, uint64_t>, T> &logEntries, const uint8_t *peerMac, bool encryptedOnly)
{
bool macFound = false;
for(typename std::map<std::pair<uint64_t, uint64_t>, T>::iterator entryIterator = logEntries.begin();
entryIterator != logEntries.end(); )
{
if(entryIterator->first.first == macToUint64(peerMac))
{
macFound = true;
if(!encryptedOnly || EspnowProtocolInterpreter::usesEncryption(entryIterator->first.second))
{
entryIterator = logEntries.erase(entryIterator);
continue;
}
}
else if(macFound)
{
// Since the map is sorted by MAC, we know here that no more matching MAC will be found.
return;
}
++entryIterator;
}
}
encrypted_connection_removal_outcome_t EspnowMeshBackend::requestEncryptedConnectionRemoval(uint8_t *peerMac)
{
using EspnowProtocolInterpreter::encryptedConnectionRemovalRequestHeader;
assert(encryptedConnections.size() <= maxEncryptedConnections); // If this is not the case, ESP-NOW is no longer in sync with the library
MutexTracker mutexTracker(_espnowTransmissionMutex, handlePostponedRemovals);
if(!mutexTracker.mutexCaptured())
{
assert(false && "ERROR! Transmission in progress. Don't call requestEncryptedConnectionRemoval from callbacks as this may corrupt program state! Aborting.");
return ECRO_REMOVAL_REQUEST_FAILED;
}
if(EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(peerMac))
{
if(espnowSendToNode(encryptedConnectionRemovalRequestHeader, peerMac, 'P') == TS_TRANSMISSION_COMPLETE)
{
return removeEncryptedConnectionUnprotected(peerMac);
}
else
{
if(encryptedConnection->removalScheduled())
return ECRO_REMOVAL_SUCCEEDED; // Removal will be completed by mutex destructorHook.
else
return ECRO_REMOVAL_REQUEST_FAILED;
}
}
else
{
// peerMac is already removed
return ECRO_REMOVAL_SUCCEEDED;
}
}
void EspnowMeshBackend::setAcceptsUnencryptedRequests(bool acceptsUnencryptedRequests) { _acceptsUnencryptedRequests = acceptsUnencryptedRequests; }
bool EspnowMeshBackend::acceptsUnencryptedRequests() { return _acceptsUnencryptedRequests; }
template <typename T>
typename std::vector<T>::iterator EspnowMeshBackend::getEncryptedConnectionIterator(const uint8_t *peerMac, typename std::vector<T> &connectionVector)
{
typename std::vector<T>::iterator connectionIterator = connectionVector.begin();
while(connectionIterator != connectionVector.end())
{
if(connectionIterator->connectedTo(peerMac))
break;
else
++connectionIterator;
}
return connectionIterator;
}
EspnowMeshBackend::connectionLogIterator EspnowMeshBackend::connectionLogEndIterator()
{
return encryptedConnections.end();
}
bool EspnowMeshBackend::getEncryptedConnectionIterator(const uint8_t *peerMac, connectionLogIterator &iterator)
{
connectionLogIterator result = getEncryptedConnectionIterator(peerMac, encryptedConnections);
if(result != connectionLogEndIterator())
{
iterator = result;
return true;
}
else
{
return false;
}
}
bool EspnowMeshBackend::getTemporaryEncryptedConnectionIterator(const uint8_t *peerMac, connectionLogIterator &iterator)
{
connectionLogIterator result = connectionLogEndIterator();
if(getEncryptedConnectionIterator(peerMac, result) && result->temporary())
{
iterator = result;
return true;
}
else
{
return false;
}
}
EncryptedConnectionLog *EspnowMeshBackend::getEncryptedConnection(const uint8_t *peerMac)
{
auto connectionIterator = getEncryptedConnectionIterator(peerMac, encryptedConnections);
if(connectionIterator != encryptedConnections.end())
{
return &(*connectionIterator);
}
else
{
return nullptr;
}
}
EncryptedConnectionLog *EspnowMeshBackend::getTemporaryEncryptedConnection(const uint8_t *peerMac)
{
connectionLogIterator connectionIterator = connectionLogEndIterator();
if(getTemporaryEncryptedConnectionIterator(peerMac, connectionIterator))
{
return &(*connectionIterator);
}
else
{
return nullptr;
}
}
uint8_t *EspnowMeshBackend::getEncryptedMac(const uint8_t *peerMac, uint8_t *resultArray)
{
if(EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(peerMac))
{
return encryptedConnection->getEncryptedPeerMac(resultArray);
}
else
{
return nullptr;
}
}
void EspnowMeshBackend::attemptTransmission(const String &message, bool scan, bool scanAllWiFiChannels)
{
MutexTracker mutexTracker(_espnowTransmissionMutex, handlePostponedRemovals);
if(!mutexTracker.mutexCaptured())
{
assert(false && "ERROR! Transmission in progress. Don't call attemptTransmission from callbacks as this may corrupt program state! Aborting.");
return;
}
setMessage(message);
latestTransmissionOutcomes.clear();
if(scan)
{
scanForNetworks(scanAllWiFiChannels);
}
for(NetworkInfo &currentNetwork : connectionQueue)
{
String currentSSID = "";
int currentWiFiChannel = NETWORK_INFO_DEFAULT_INT;
uint8_t *currentBSSID = NULL;
// If a BSSID has been assigned, it is prioritized over an assigned networkIndex since the networkIndex is more likely to change.
if(currentNetwork.BSSID != NULL)
{
currentSSID = currentNetwork.SSID;
currentWiFiChannel = currentNetwork.wifiChannel;
currentBSSID = currentNetwork.BSSID;
}
else // Use only networkIndex
{
currentSSID = WiFi.SSID(currentNetwork.networkIndex);
currentWiFiChannel = WiFi.channel(currentNetwork.networkIndex);
currentBSSID = WiFi.BSSID(currentNetwork.networkIndex);
}
if(verboseMode()) // Avoid string generation if not required
{
printAPInfo(currentNetwork.networkIndex, currentSSID, currentWiFiChannel);
verboseModePrint(F(""));
}
uint32_t transmissionStartTime = millis();
transmission_status_t transmissionResult = sendRequest(getMessage(), currentBSSID);
uint32_t transmissionDuration = millis() - transmissionStartTime;
if(verboseMode() && transmissionResult == TS_TRANSMISSION_COMPLETE) // Avoid calculations if not required
{
totalDurationWhenSuccessful_AT += transmissionDuration;
successfulTransmissions_AT++;
if(transmissionDuration > maxTransmissionDuration_AT)
{
maxTransmissionDuration_AT = transmissionDuration;
}
}
latestTransmissionOutcomes.push_back(TransmissionResult{.origin = currentNetwork, .transmissionStatus = transmissionResult});
}
if(verboseMode() && successfulTransmissions_AT > 0) // Avoid calculations if not required
{
verboseModePrint("Average duration of successful transmissions: " + String(totalDurationWhenSuccessful_AT/successfulTransmissions_AT) + " ms.");
verboseModePrint("Maximum duration of successful transmissions: " + String(maxTransmissionDuration_AT) + " ms.");
}
else
{
verboseModePrint("No successful transmission.");
}
}
void EspnowMeshBackend::attemptAutoEncryptingTransmission(const String &message, bool scan, bool scanAllWiFiChannels, bool createPermanentConnections)
{
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.");
return;
}
setMessage(message);
latestTransmissionOutcomes.clear();
if(scan)
{
scanForNetworks(scanAllWiFiChannels);
}
outerMutexTracker.releaseMutex();
for(NetworkInfo &currentNetwork : connectionQueue)
{
MutexTracker innerMutexTracker = MutexTracker(_espnowTransmissionMutex);
if(!innerMutexTracker.mutexCaptured())
{
assert(false && "ERROR! Unable to recapture Mutex in attemptAutoEncryptingTransmission. Aborting.");
return;
}
String currentSSID = "";
int currentWiFiChannel = NETWORK_INFO_DEFAULT_INT;
uint8_t *currentBSSID = NULL;
// If a BSSID has been assigned, it is prioritized over an assigned networkIndex since the networkIndex is more likely to change.
if(currentNetwork.BSSID != NULL)
{
currentSSID = currentNetwork.SSID;
currentWiFiChannel = currentNetwork.wifiChannel;
currentBSSID = currentNetwork.BSSID;
}
else // Use only networkIndex
{
currentSSID = WiFi.SSID(currentNetwork.networkIndex);
currentWiFiChannel = WiFi.channel(currentNetwork.networkIndex);
currentBSSID = WiFi.BSSID(currentNetwork.networkIndex);
}
if(verboseMode()) // Avoid string generation if not required
{
printAPInfo(currentNetwork.networkIndex, currentSSID, currentWiFiChannel);
verboseModePrint(F(""));
}
EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(currentBSSID);
encrypted_connection_status_t connectionStatus = ECS_MAX_CONNECTIONS_REACHED_SELF;
innerMutexTracker.releaseMutex();
if(createPermanentConnections)
connectionStatus = requestEncryptedConnection(currentBSSID);
else
connectionStatus = requestFlexibleTemporaryEncryptedConnection(currentBSSID, getAutoEncryptionDuration());
innerMutexTracker = MutexTracker(_espnowTransmissionMutex);
if(!innerMutexTracker.mutexCaptured())
{
assert(false && "ERROR! Unable to recapture Mutex in attemptAutoEncryptingTransmission. Aborting.");
return;
}
if(connectionStatus == ECS_CONNECTION_ESTABLISHED)
{
uint32_t transmissionStartTime = millis();
transmission_status_t transmissionResult = sendRequest(getMessage(), currentBSSID);
uint32_t transmissionDuration = millis() - transmissionStartTime;
if(verboseMode() && transmissionResult == TS_TRANSMISSION_COMPLETE) // Avoid calculations if not required
{
totalDurationWhenSuccessful_AT += transmissionDuration;
successfulTransmissions_AT++;
if(transmissionDuration > maxTransmissionDuration_AT)
{
maxTransmissionDuration_AT = transmissionDuration;
}
}
latestTransmissionOutcomes.push_back(TransmissionResult{.origin = currentNetwork, .transmissionStatus = transmissionResult});
}
else
{
latestTransmissionOutcomes.push_back(TransmissionResult{.origin = currentNetwork, .transmissionStatus = TS_CONNECTION_FAILED});
}
if(!encryptedConnection && !createPermanentConnections)
{
// Remove any connection that was added during the transmission attempt.
removeEncryptedConnectionUnprotected(currentBSSID);
}
}
if(verboseMode() && successfulTransmissions_AT > 0) // Avoid calculations if not required
{
verboseModePrint("Average duration of successful transmissions: " + String(totalDurationWhenSuccessful_AT/successfulTransmissions_AT) + " ms.");
verboseModePrint("Maximum duration of successful transmissions: " + String(maxTransmissionDuration_AT) + " ms.");
}
else
{
verboseModePrint("No successful transmission.");
}
}
void EspnowMeshBackend::broadcast(const String &message)
{
MutexTracker mutexTracker(_espnowTransmissionMutex, handlePostponedRemovals);
if(!mutexTracker.mutexCaptured())
{
assert(false && "ERROR! Transmission in progress. Don't call broadcast from callbacks as this may corrupt program state! Aborting.");
return;
}
espnowSendToNode(message, broadcastMac, 'B', this);
}
void EspnowMeshBackend::sendEspnowResponses()
{
//uint32_t startTime = millis();
uint32_t bufferedCriticalHeapLevel = criticalHeapLevel() + criticalHeapLevelBuffer(); // We preferably want to start clearing the logs a bit before things get critical.
for(std::list<PeerRequestLog>::iterator confirmationsIterator = peerRequestConfirmationsToSend.begin(); confirmationsIterator != peerRequestConfirmationsToSend.end(); )
{
using namespace EspnowProtocolInterpreter;
auto timeTrackerPointer = confirmationsIterator->temporary();
assert(timeTrackerPointer); // peerRequestConfirmations should always expire and so should always have a timeTracker
if(timeTrackerPointer->timeSinceCreation() > getEncryptionRequestTimeout())
{
++confirmationsIterator;
continue;
}
uint8_t defaultBSSID[6] {0};
confirmationsIterator->getEncryptedPeerMac(defaultBSSID);
uint8_t unencryptedBSSID[6] {0};
confirmationsIterator->getUnencryptedPeerMac(unencryptedBSSID);
uint8_t hashKey[espnowHashKeyLength] {0};
confirmationsIterator->getHashKey(hashKey);
EncryptedConnectionLog *existingEncryptedConnection = getEncryptedConnection(defaultBSSID);
// If we receive a non-encrypted request for encrypted connection from a node that already exists as an encrypted peer for us we cannot send a response to the encrypted MAC
// since that transmission will then be encrypted and impossible for the request sender to read. Of course, removing the existing encrypted connection would also work,
// but make it very simple for a third party to disrupt an encrypted connection by just sending random requests for encrypted connection.
bool sendToDefaultBSSID = confirmationsIterator->requestEncrypted() || !existingEncryptedConnection;
// Note that callbacks can be called during delay time, so it is possible to receive a transmission during espnowSendToNode
// (which may add an element to the peerRequestConfirmationsToSend list).
staticVerboseModePrint("Responding to encrypted connection request from MAC " + macToString(defaultBSSID));
if(!existingEncryptedConnection && encryptedConnections.size() >= maxEncryptedConnections)
{
espnowSendToNodeUnsynchronized(JsonTranslator::createEncryptionRequestHmacMessage(maxConnectionsReachedHeader,
confirmationsIterator->getPeerRequestNonce(), hashKey, espnowHashKeyLength),
defaultBSSID, 'C', generateMessageID(nullptr)); // Generates a new message ID to avoid sending encrypted sessionKeys over unencrypted connections.
confirmationsIterator = peerRequestConfirmationsToSend.erase(confirmationsIterator);
}
else if(espnowSendToNodeUnsynchronized(JsonTranslator::createEncryptionRequestHmacMessage(basicConnectionInfoHeader,
confirmationsIterator->getPeerRequestNonce(), hashKey, espnowHashKeyLength),
sendToDefaultBSSID ? defaultBSSID : unencryptedBSSID, 'C', generateMessageID(nullptr)) // Generates a new message ID to avoid sending encrypted sessionKeys over unencrypted connections.
== TS_TRANSMISSION_COMPLETE)
{
// Try to add encrypted connection. If connection added send confirmation with encryptedConnection->getOwnSessionKey() as session key and C type message (won't increment key). Then proceed with next request (no need to wait for answer).
if(existingEncryptedConnection)
{
if(auto timeTrackerPointer = existingEncryptedConnection->temporary())
{
if(getEncryptionRequestTimeout() > timeTrackerPointer->remainingDuration())
{
existingEncryptedConnection->setRemainingDuration(getEncryptionRequestTimeout());
}
}
}
else if(EspnowMeshBackend *currentEspnowRequestManager = getEspnowRequestManager())
{
uint8_t staMacArray[6] = { 0 };
uint8_t apMacArray[6] = { 0 };
currentEspnowRequestManager->addTemporaryEncryptedConnection(confirmationsIterator->getPeerStaMac(staMacArray), confirmationsIterator->getPeerApMac(apMacArray),
createSessionKey(), createSessionKey(), getEncryptionRequestTimeout());
existingEncryptedConnection = getEncryptedConnection(defaultBSSID);
}
else
{
warningPrint("WARNING! Ignoring received encrypted connection request since no EspnowRequestManager is assigned.");
}
if(!existingEncryptedConnection)
{
// Send "node full" message
espnowSendToNodeUnsynchronized(JsonTranslator::createEncryptionRequestHmacMessage(maxConnectionsReachedHeader,
confirmationsIterator->getPeerRequestNonce(), hashKey, espnowHashKeyLength),
defaultBSSID, 'C', generateMessageID(nullptr)); // Generates a new message ID to avoid sending encrypted sessionKeys over unencrypted connections.
}
else
{
delay(1); // Give some time for the peer to add an encrypted connection
// 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(
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.
}
confirmationsIterator = peerRequestConfirmationsToSend.erase(confirmationsIterator);
}
else
{
++confirmationsIterator;
}
if(ESP.getFreeHeap() <= bufferedCriticalHeapLevel)
{
// Heap is getting very low, which probably means we are receiving a lot of transmissions while trying to transmit responses.
// Clear all old data to try to avoid running out of memory.
warningPrint("WARNING! Free heap below chosen minimum. Performing emergency log clearing.");
clearOldLogEntries();
return; // confirmationsIterator may be invalid now. Also, we should give the main loop a chance to respond to the situation.
}
}
for(std::list<ResponseData>::iterator responseIterator = responsesToSend.begin(); responseIterator != responsesToSend.end(); )
{
if(responseIterator->timeSinceCreation() > logEntryLifetimeMs())
{
// If the response is older than logEntryLifetimeMs(), the corresponding request log entry has been deleted at the request sender,
// so the request sender will not accept our response any more.
// This probably happens because we have a high transmission activity and more requests coming in than we can handle.
++responseIterator;
continue;
}
// Note that callbacks can be called during delay time, so it is possible to receive a transmission during espnowSendToNode
// (which may add an element to the responsesToSend list).
if(espnowSendToNodeUnsynchronized(responseIterator->getMessage(), responseIterator->getRecipientMac(), 'A', responseIterator->getRequestID())
== TS_TRANSMISSION_COMPLETE)
{
responseIterator = responsesToSend.erase(responseIterator);
}
else
{
++responseIterator;
}
if(ESP.getFreeHeap() <= bufferedCriticalHeapLevel)
{
// Heap is getting very low, which probably means we are receiving a lot of transmissions while trying to transmit responses.
// Clear all old data to try to avoid running out of memory.
warningPrint("WARNING! Free heap below chosen minimum. Performing emergency log clearing.");
clearOldLogEntries();
return; // responseIterator may be invalid now. Also, we should give the main loop a chance to respond to the situation.
}
}
}
uint32_t EspnowMeshBackend::getMaxBytesPerTransmission()
{
return _maxBytesPerTransmission;
}
uint32_t EspnowMeshBackend::getMaxMessageBytesPerTransmission()
{
return getMaxBytesPerTransmission() - EspnowProtocolInterpreter::espnowProtocolBytesSize();
}
void EspnowMeshBackend::setMaxTransmissionsPerMessage(uint8_t maxTransmissionsPerMessage)
{
assert(1 <= maxTransmissionsPerMessage && maxTransmissionsPerMessage <= 128);
_maxTransmissionsPerMessage = maxTransmissionsPerMessage;
}
uint8_t EspnowMeshBackend::getMaxTransmissionsPerMessage() {return _maxTransmissionsPerMessage;}
uint32_t EspnowMeshBackend::getMaxMessageLength()
{
return getMaxTransmissionsPerMessage() * getMaxMessageBytesPerTransmission();
}
uint8_t EspnowMeshBackend::numberOfEncryptedConnections()
{
return encryptedConnections.size();
}
espnow_connection_type_t EspnowMeshBackend::getConnectionInfoHelper(const EncryptedConnectionLog *encryptedConnection, uint32_t *remainingDuration, uint8_t *peerMac)
{
if(!encryptedConnection)
{
return ECT_NO_CONNECTION;
}
else
{
if(peerMac)
encryptedConnection->getEncryptedPeerMac(peerMac);
if(const ExpiringTimeTracker *timeTracker = encryptedConnection->temporary())
{
if(remainingDuration)
*remainingDuration = timeTracker->remainingDuration();
return ECT_TEMPORARY_CONNECTION;
}
else
{
return ECT_PERMANENT_CONNECTION;
}
}
}
espnow_connection_type_t EspnowMeshBackend::getConnectionInfo(uint8_t *peerMac, uint32_t *remainingDuration)
{
EncryptedConnectionLog *encryptedConnection = nullptr;
if(peerMac)
encryptedConnection = getEncryptedConnection(peerMac);
return getConnectionInfoHelper(encryptedConnection, remainingDuration);
}
espnow_connection_type_t EspnowMeshBackend::getConnectionInfo(uint32_t connectionIndex, uint32_t *remainingDuration, uint8_t *peerMac)
{
EncryptedConnectionLog *encryptedConnection = nullptr;
if(connectionIndex < numberOfEncryptedConnections())
encryptedConnection = &encryptedConnections[connectionIndex];
return getConnectionInfoHelper(encryptedConnection, remainingDuration, peerMac);
}
double EspnowMeshBackend::getTransmissionFailRate()
{
if(_transmissionsTotal == 0)
return 0;
else
return _transmissionsFailed/_transmissionsTotal;
}
void EspnowMeshBackend::resetTransmissionFailRate()
{
_transmissionsFailed = 0;
_transmissionsTotal = 0;
}
String EspnowMeshBackend::serializeEncryptedConnection(const uint8_t *peerMac)
{
EncryptedConnectionLog *encryptedConnection = nullptr;
if(peerMac)
encryptedConnection = getEncryptedConnection(peerMac);
if(encryptedConnection)
return encryptedConnection->serialize();
else
return "";
}
String EspnowMeshBackend::serializeEncryptedConnection(uint32_t connectionIndex)
{
if(connectionIndex < numberOfEncryptedConnections())
return encryptedConnections[connectionIndex].serialize();
else
return "";
}