1
0
mirror of https://github.com/esp8266/Arduino.git synced 2025-08-05 13:16:13 +03:00

- Add working FloodingMesh. Unencrypted broadcasts should work well, but are untested in large mesh networks. Encrypted broadcast support is currently experimental.

- Add BroadcastTransmissionRedundancy and related functionality to reduce the transmission loss during broadcasts. Broadcast transmissions are now re-transmitted once per default. Broadcast throughput halved per default.

- Add getSenderAPMac method.

- Add FloodingMesh example in the HelloMesh.ino file.

- Improve JSON identifier names.

- Improve comments.

- Improve documentation.
This commit is contained in:
Anders
2019-11-03 14:00:05 +01:00
parent 176f2851e4
commit 6b763686de
9 changed files with 997 additions and 133 deletions

View File

@@ -753,14 +753,19 @@ void EspnowMeshBackend::espnowReceiveCallback(uint8_t *macaddr, uint8_t *dataArr
{
if(messageType == 'B')
{
auto key = std::make_pair(macAndType, messageID);
if(receivedEspnowTransmissions.find(key) != receivedEspnowTransmissions.end())
return; // Should not call BroadcastFilter more than once for an accepted message
String message = espnowGetMessageContent(dataArray, len);
setSenderMac(macaddr);
espnowGetTransmissionMac(dataArray, _senderAPMac);
setReceivedEncryptedMessage(usesEncryption(messageID));
bool acceptBroadcast = getBroadcastFilter()(message, *this);
if(acceptBroadcast)
{
// Does nothing if key already in receivedEspnowTransmissions
receivedEspnowTransmissions.insert(std::make_pair(std::make_pair(macAndType, messageID), MessageData(message, espnowGetTransmissionsRemaining(dataArray))));
receivedEspnowTransmissions.insert(std::make_pair(key, MessageData(message, espnowGetTransmissionsRemaining(dataArray))));
}
else
{
@@ -822,6 +827,7 @@ void EspnowMeshBackend::espnowReceiveCallback(uint8_t *macaddr, uint8_t *dataArr
//Serial.println("methodStart request stored " + String(millis() - methodStart));
setSenderMac(macaddr);
espnowGetTransmissionMac(dataArray, _senderAPMac);
setReceivedEncryptedMessage(usesEncryption(messageID));
String response = getRequestHandler()(totalMessage, *this);
//Serial.println("methodStart response acquired " + String(millis() - methodStart));
@@ -847,6 +853,7 @@ void EspnowMeshBackend::espnowReceiveCallback(uint8_t *macaddr, uint8_t *dataArr
}
setSenderMac(macaddr);
espnowGetTransmissionMac(dataArray, _senderAPMac);
setReceivedEncryptedMessage(usesEncryption(messageID));
getResponseHandler()(totalMessage, *this);
}
@@ -1135,39 +1142,46 @@ transmission_status_t EspnowMeshBackend::espnowSendToNodeUnsynchronized(const St
////// Transmit //////
_espnowSendConfirmed = false;
uint32_t transmissionStartTime = millis();
while(!_espnowSendConfirmed && millis() - transmissionStartTime < getEspnowTransmissionTimeout())
uint32_t retransmissions = 0;
if(messageType == 'B')
retransmissions = espnowInstance->getBroadcastTransmissionRedundancy();
for(uint32_t i = 0; i <= retransmissions; i++)
{
if(esp_now_send(_transmissionTargetBSSID, transmission, transmissionSize) == 0) // == 0 => Success
_espnowSendConfirmed = false;
uint32_t transmissionStartTime = millis();
while(!_espnowSendConfirmed && millis() - transmissionStartTime < getEspnowTransmissionTimeout())
{
uint32_t transmissionAttemptStart = millis();
while(!_espnowSendConfirmed
&& (millis() - transmissionAttemptStart < getEspnowRetransmissionInterval())
&& (millis() - transmissionStartTime < getEspnowTransmissionTimeout()))
{
delay(1); // Note that callbacks can be called during delay time, so it is possible to receive a transmission during this delay.
if(esp_now_send(_transmissionTargetBSSID, transmission, transmissionSize) == 0) // == 0 => Success
{
uint32_t transmissionAttemptStart = millis();
while(!_espnowSendConfirmed
&& (millis() - transmissionAttemptStart < getEspnowRetransmissionInterval())
&& (millis() - transmissionStartTime < getEspnowTransmissionTimeout()))
{
delay(1); // Note that callbacks can be called during delay time, so it is possible to receive a transmission during this delay.
}
}
if(_espnowSendConfirmed)
{
if(messageStart)
{
if(encryptedConnection && !usesConstantSessionKey(messageType) && encryptedConnection->getOwnSessionKey() == messageID)
{
encryptedConnection->setDesync(false);
encryptedConnection->incrementOwnSessionKey();
}
messageStart = false;
}
break;
}
}
if(_espnowSendConfirmed)
{
if(messageStart)
{
if(encryptedConnection && !usesConstantSessionKey(messageType) && encryptedConnection->getOwnSessionKey() == messageID)
{
encryptedConnection->setDesync(false);
encryptedConnection->incrementOwnSessionKey();
}
messageStart = false;
}
break;
}
}
if(!_espnowSendConfirmed)
{
_transmissionsFailed++;
@@ -1395,6 +1409,18 @@ uint8_t *EspnowMeshBackend::getSenderMac(uint8_t *macArray)
return macArray;
}
void EspnowMeshBackend::setSenderAPMac(uint8_t *macArray)
{
std::copy_n(macArray, 6, _senderAPMac);
}
String EspnowMeshBackend::getSenderAPMac() {return macToString(_senderAPMac);}
uint8_t *EspnowMeshBackend::getSenderAPMac(uint8_t *macArray)
{
std::copy_n(_senderAPMac, 6, macArray);
return macArray;
}
void EspnowMeshBackend::setReceivedEncryptedMessage(bool receivedEncryptedMessage) { _receivedEncryptedMessage = receivedEncryptedMessage; }
bool EspnowMeshBackend::receivedEncryptedMessage() {return _receivedEncryptedMessage;}
@@ -2241,6 +2267,9 @@ void EspnowMeshBackend::broadcast(const String &message)
espnowSendToNode(message, broadcastMac, 'B', this);
}
void EspnowMeshBackend::setBroadcastTransmissionRedundancy(uint8_t redundancy) { _broadcastTransmissionRedundancy = redundancy; }
uint8_t EspnowMeshBackend::getBroadcastTransmissionRedundancy() { return _broadcastTransmissionRedundancy; }
void EspnowMeshBackend::sendStoredEspnowMessages(const ExpiringTimeTracker *estimatedMaxDurationTracker)
{
sendPeerRequestConfirmations(estimatedMaxDurationTracker);
@@ -2532,7 +2561,7 @@ String EspnowMeshBackend::serializeUnencryptedConnection()
{
using namespace JsonTranslator;
// Returns: {"connectionState":{"uMessageID":"123"}}
// Returns: {"connectionState":{"unencMsgID":"123"}}
return jsonConnectionState + createJsonEndPair(jsonUnencryptedMessageID, String(_unencryptedMessageID));
}

View File

@@ -132,7 +132,7 @@ protected:
public:
/**
* WiFiMesh Constructor method. Creates a WiFi Mesh Node, ready to be initialised.
* ESP-NOW constructor method. Creates an ESP-NOW node, ready to be initialised.
*
* @param requestHandler The callback handler for dealing with received requests. Takes a string as an argument which
* is the request string received from another node and returns the string to send back.
@@ -141,6 +141,8 @@ public:
* @param networkFilter The callback handler for deciding which WiFi networks to connect to.
* @param broadcastFilter The callback handler for deciding which ESP-NOW broadcasts to accept.
* @param meshPassword The WiFi password for the mesh network.
* @param espnowEncryptionKey An uint8_t array containing the key used by this EspnowMeshBackend instance for creating encrypted ESP-NOW connections.
* @param espnowHashKey An uint8_t array containing the secret key used by this EspnowMeshBackend to generate HMACs for encrypted ESP-NOW connections.
* @param ssidPrefix The prefix (first part) of the node SSID.
* @param ssidSuffix The suffix (last part) of the node SSID.
* @param verboseMode Determines if we should print the events occurring in the library to Serial. Off by default. This setting is shared by all EspnowMeshBackend instances.
@@ -293,6 +295,15 @@ public:
*/
void broadcast(const String &message);
/**
* Set the number of redundant transmissions that will be made for every broadcast.
* A greater number increases the likelihood that the broadcast is received, but also means it takes longer time to send.
*
* @param redundancy The number of extra transmissions to make of each broadcast. Defaults to 1.
*/
void setBroadcastTransmissionRedundancy(uint8_t redundancy);
uint8_t getBroadcastTransmissionRedundancy();
/**
* Set the EspnowMeshBackend instance responsible for handling incoming requests. The requestHandler of the instance will be called upon receiving ESP-NOW requests.
*
@@ -518,6 +529,23 @@ public:
*/
uint8_t *getSenderMac(uint8_t *macArray);
/**
* Get the AP MAC address of the sender of the most recently received ESP-NOW request, response or broadcast to this EspnowMeshBackend instance.
* Returns a String.
*
* @return A String filled with a hexadecimal representation of the AP MAC, without delimiters.
*/
String getSenderAPMac();
/**
* Get the AP MAC address of the sender of the most recently received ESP-NOW request, response or broadcast to this EspnowMeshBackend instance.
* Returns a uint8_t array.
*
* @param macArray The array that should store the MAC address. Must be at least 6 bytes.
* @return macArray filled with the sender AP MAC.
*/
uint8_t *getSenderAPMac(uint8_t *macArray);
/**
* Get whether the ESP-NOW request, response or broadcast which was most recently received by this EspnowMeshBackend instance was encrypted or not.
*
@@ -714,6 +742,13 @@ protected:
*/
void setSenderMac(uint8_t *macArray);
/**
* Set the MAC address considered to be the AP MAC of the sender of the most recently received ESP-NOW request, response or broadcast.
*
* @param macArray An uint8_t array which contains the MAC address to store. The method will store the first 6 bytes of the array.
*/
void setSenderAPMac(uint8_t *macArray);
/**
* Set whether the most recently received ESP-NOW request, response or broadcast is presented as having been encrypted or not.
*
@@ -875,6 +910,8 @@ private:
broadcastFilterType _broadcastFilter;
uint8_t _broadcastTransmissionRedundancy = 1;
static String _ongoingPeerRequestNonce;
static uint8_t _ongoingPeerRequestMac[6];
static EspnowMeshBackend *_ongoingPeerRequester;
@@ -896,6 +933,7 @@ private:
static uint32_t _unencryptedMessageID;
uint8_t _senderMac[6] = {0};
uint8_t _senderAPMac[6] = {0};
bool _receivedEncryptedMessage = false;
static bool _espnowSendToNodeMutex;

View File

@@ -0,0 +1,503 @@
/*
* Copyright (C) 2019 Anders Löfgren
*
* License (MIT license):
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include "FloodingMesh.h"
#include "TypeConversionFunctions.h"
#include "JsonTranslator.h"
std::set<FloodingMesh *> FloodingMesh::availableFloodingMeshes = {};
char FloodingMesh::_broadcastMetadataDelimiter = 23;
void floodingMeshDelay(uint32_t durationMs)
{
uint32_t startingTime = millis();
while(millis() - startingTime < durationMs)
{
delay(1);
FloodingMesh::performMeshMaintainance();
}
}
FloodingMesh::FloodingMesh(messageHandlerType messageHandler, const String &meshPassword, const uint8_t espnowEncryptionKey[EspnowProtocolInterpreter::espnowEncryptionKeyLength],
const uint8_t espnowHashKey[EspnowProtocolInterpreter::espnowHashKeyLength], const String &ssidPrefix,
const String &ssidSuffix, bool verboseMode, uint8 meshWiFiChannel)
: _espnowBackend(
[this](const String &request, MeshBackendBase &meshInstance){ return _defaultRequestHandler(request, meshInstance); },
[this](const String &response, MeshBackendBase &meshInstance){ return _defaultResponseHandler(response, meshInstance); },
[this](int numberOfNetworks, MeshBackendBase &meshInstance){ return _defaultNetworkFilter(numberOfNetworks, meshInstance); },
[this](String &firstTransmission, EspnowMeshBackend &meshInstance){ return _defaultBroadcastFilter(firstTransmission, meshInstance); },
meshPassword, espnowEncryptionKey, espnowHashKey, ssidPrefix, ssidSuffix, verboseMode, meshWiFiChannel)
{
setMessageHandler(messageHandler);
restoreDefaultTransmissionOutcomesUpdateHook();
}
FloodingMesh::FloodingMesh(const String &serializedMeshState, messageHandlerType messageHandler, const String &meshPassword,
const uint8_t espnowEncryptionKey[EspnowProtocolInterpreter::espnowEncryptionKeyLength],
const uint8_t espnowHashKey[EspnowProtocolInterpreter::espnowHashKeyLength], const String &ssidPrefix,
const String &ssidSuffix, bool verboseMode, uint8 meshWiFiChannel)
: FloodingMesh(messageHandler, meshPassword, espnowEncryptionKey, espnowHashKey, ssidPrefix, ssidSuffix, verboseMode, meshWiFiChannel)
{
loadMeshState(serializedMeshState);
}
FloodingMesh::~FloodingMesh()
{
availableFloodingMeshes.erase(this);
}
void FloodingMesh::begin()
{
// Initialise the mesh node
getEspnowMeshBackend().begin();
// Makes it possible to find the node through scans, and also makes it possible to recover from an encrypted ESP-NOW connection where only the other node is encrypted.
// Note that only one AP can be active at a time in total, and this will always be the one which was last activated.
// Thus the AP is shared by all backends.
getEspnowMeshBackend().activateAP();
availableFloodingMeshes.insert(this); // Returns std::pair<iterator,bool>
}
void FloodingMesh::performMeshMaintainance()
{
for(FloodingMesh *meshInstance : availableFloodingMeshes)
{
meshInstance->performMeshInstanceMaintainance();
}
}
void FloodingMesh::performMeshInstanceMaintainance()
{
EspnowMeshBackend::performEspnowMaintainance();
for(std::list<std::pair<String, bool>>::iterator backlogIterator = _forwardingBacklog.begin(); backlogIterator != _forwardingBacklog.end(); )
{
std::pair<String, bool> &messageData = *backlogIterator;
if(messageData.second) // message encrypted
{
_macIgnoreList = messageData.first.substring(0, 12) + ','; // The message should contain the messageID first
encryptedBroadcastKernel(messageData.first);
_macIgnoreList = "";
}
else
{
broadcastKernel(messageData.first);
}
backlogIterator = _forwardingBacklog.erase(backlogIterator);
EspnowMeshBackend::performEspnowMaintainance(); // It is best to performEspnowMaintainance frequently to keep the Espnow backend responsive. Especially if each encryptedBroadcast takes a lot of time.
}
}
String FloodingMesh::serializeMeshState()
{
using namespace JsonTranslator;
// Returns: {"meshState":{"connectionState":{"unencMsgID":"123"},"meshMsgCount":"123"}}
String connectionState = getEspnowMeshBackend().serializeUnencryptedConnection();
return
"{\"meshState\":{"
+ connectionState.substring(1, connectionState.length() - 1) + ","
+ createJsonEndPair(jsonMeshMessageCount, String(_messageCount));
}
void FloodingMesh::loadMeshState(const String &serializedMeshState)
{
using namespace JsonTranslator;
if(!getMeshMessageCount(serializedMeshState, _messageCount))
getEspnowMeshBackend().warningPrint("WARNING! serializedMeshState did not contain MeshMessageCount. Using default instead.");
String connectionState = "";
if(!getConnectionState(serializedMeshState, connectionState) || !getEspnowMeshBackend().addUnencryptedConnection(connectionState))
{
getEspnowMeshBackend().warningPrint("WARNING! serializedMeshState did not contain unencryptedMessageID. Using default instead.");
}
}
String FloodingMesh::generateMessageID()
{
char messageCountArray[2] = { 0 };
sprintf(messageCountArray, "%04X", _messageCount++);
uint8_t apMac[6] {0};
return macToString(WiFi.softAPmacAddress(apMac)) + String(messageCountArray); // We use the AP MAC address as ID since it is what shows up during WiFi scans
}
void FloodingMesh::broadcast(const String &message)
{
assert(message.length() <= maxUnencryptedMessageSize());
String messageID = generateMessageID();
// Remove getEspnowMeshBackend().getMeshName() from the broadcastMetadata below to broadcast to all ESP-NOW nodes regardless of MeshName.
String targetMeshName = getEspnowMeshBackend().getMeshName();
broadcastKernel(targetMeshName + String(broadcastMetadataDelimiter()) + messageID + String(broadcastMetadataDelimiter()) + message);
}
void FloodingMesh::broadcastKernel(const String &message)
{
getEspnowMeshBackend().broadcast(message);
}
void FloodingMesh::setBroadcastReceptionRedundancy(uint8_t redundancy)
{
assert(redundancy < 255);
_broadcastReceptionRedundancy = redundancy;
}
uint8_t FloodingMesh::getBroadcastReceptionRedundancy() { return _broadcastReceptionRedundancy; }
void FloodingMesh::encryptedBroadcast(const String &message)
{
assert(message.length() <= maxEncryptedMessageSize());
String messageID = generateMessageID();
encryptedBroadcastKernel(messageID + String(broadcastMetadataDelimiter()) + message);
}
void FloodingMesh::encryptedBroadcastKernel(const String &message)
{
getEspnowMeshBackend().attemptAutoEncryptingTransmission(message);
}
void FloodingMesh::clearMessageLogs()
{
_messageIDs.clear();
std::queue<messageQueueElementType>().swap(_messageIdOrder);
}
void FloodingMesh::clearForwardingBacklog()
{
_forwardingBacklog.clear();
}
void FloodingMesh::setMessageHandler(messageHandlerType messageHandler) { _messageHandler = messageHandler; }
FloodingMesh::messageHandlerType FloodingMesh::getMessageHandler() { return _messageHandler; }
void FloodingMesh::setOriginMac(uint8_t *macArray)
{
std::copy_n(macArray, 6, _originMac);
}
String FloodingMesh::getOriginMac() { return macToString(_originMac); }
uint8_t *FloodingMesh::getOriginMac(uint8_t *macArray)
{
std::copy_n(_originMac, 6, macArray);
return macArray;
}
uint32_t FloodingMesh::maxUnencryptedMessageSize()
{
return getEspnowMeshBackend().getMaxMessageLength() - MESSAGE_ID_LENGTH - (getEspnowMeshBackend().getMeshName().length() + 1); // Need room for mesh name + delimiter
}
uint32_t FloodingMesh::maxEncryptedMessageSize()
{
// Need 1 extra delimiter character for maximum metadata efficiency (makes it possible to store exactly 18 MACs in metadata by adding an extra transmission)
return getEspnowMeshBackend().getMaxMessageLength() - MESSAGE_ID_LENGTH - 1;
}
void FloodingMesh::setMessageLogSize(uint16_t messageLogSize)
{
assert(messageLogSize >= 1);
_messageLogSize = messageLogSize;
}
uint16_t FloodingMesh::messageLogSize() { return _messageLogSize; }
void FloodingMesh::setBroadcastMetadataDelimiter(char broadcastMetadataDelimiter)
{
// Using HEX number characters as a delimiter is a bad idea regardless of broadcast type, since they are always in the broadcast metadata
assert(broadcastMetadataDelimiter < 48 || 57 < broadcastMetadataDelimiter);
assert(broadcastMetadataDelimiter < 65 || 70 < broadcastMetadataDelimiter);
_broadcastMetadataDelimiter = broadcastMetadataDelimiter;
}
char FloodingMesh::broadcastMetadataDelimiter() { return _broadcastMetadataDelimiter; }
EspnowMeshBackend &FloodingMesh::getEspnowMeshBackend()
{
return _espnowBackend;
}
bool FloodingMesh::insertPreliminaryMessageID(uint64_t messageID)
{
uint8_t apMacArray[6] = { 0 };
if(messageID >> 16 == macToUint64(WiFi.softAPmacAddress(apMacArray)))
return false; // The node should not receive its own messages.
auto insertionResult = _messageIDs.emplace(messageID, 0); // Returns std::pair<iterator,bool>
if(insertionResult.second) // Insertion succeeded.
updateMessageQueue(insertionResult.first);
else if(insertionResult.first->second < getBroadcastReceptionRedundancy()) // messageID exists but not with desired redundancy
insertionResult.first->second++;
else
return false; // messageID already existed in _messageIDs with desired redundancy
return true;
}
bool FloodingMesh::insertCompletedMessageID(uint64_t messageID)
{
uint8_t apMacArray[6] = { 0 };
if(messageID >> 16 == macToUint64(WiFi.softAPmacAddress(apMacArray)))
return false; // The node should not receive its own messages.
auto insertionResult = _messageIDs.emplace(messageID, MESSAGE_COMPLETE); // Returns std::pair<iterator,bool>
if(insertionResult.second) // Insertion succeeded.
updateMessageQueue(insertionResult.first);
else if(insertionResult.first->second < MESSAGE_COMPLETE) // messageID exists but is not complete
insertionResult.first->second = MESSAGE_COMPLETE;
else
return false; // messageID already existed in _messageIDs and is complete
return true;
}
void FloodingMesh::updateMessageQueue(messageQueueElementType messageIterator)
{
_messageIdOrder.emplace(messageIterator);
if(_messageIDs.size() > messageLogSize())
{
_messageIDs.erase(_messageIdOrder.front());
_messageIdOrder.pop();
assert(_messageIDs.size() == messageLogSize()); // If this is false we either have too many elements in messageIDs or we deleted too many elements.
assert(_messageIDs.size() == _messageIdOrder.size()); // The containers should always be in sync
}
}
void FloodingMesh::restoreDefaultRequestHandler()
{
getEspnowMeshBackend().setRequestHandler([this](const String &request, MeshBackendBase &meshInstance){ return _defaultRequestHandler(request, meshInstance); });
}
void FloodingMesh::restoreDefaultResponseHandler()
{
getEspnowMeshBackend().setResponseHandler([this](const String &response, MeshBackendBase &meshInstance){ return _defaultResponseHandler(response, meshInstance); });
}
void FloodingMesh::restoreDefaultNetworkFilter()
{
getEspnowMeshBackend().setNetworkFilter([this](int numberOfNetworks, MeshBackendBase &meshInstance){ return _defaultNetworkFilter(numberOfNetworks, meshInstance); });
}
void FloodingMesh::restoreDefaultBroadcastFilter()
{
getEspnowMeshBackend().setBroadcastFilter([this](String &firstTransmission, EspnowMeshBackend &meshInstance){ return _defaultBroadcastFilter(firstTransmission, meshInstance); });
}
void FloodingMesh::restoreDefaultTransmissionOutcomesUpdateHook()
{
/* Optional way of doing things. Lambda is supposedly better https://stackoverflow.com/a/36596295 .
using namespace std::placeholders;
getEspnowMeshBackend().setTransmissionOutcomesUpdateHook(std::bind(&FloodingMesh::_defaultTransmissionOutcomesUpdateHook, this, _1));
*/
getEspnowMeshBackend().setTransmissionOutcomesUpdateHook([this](MeshBackendBase &meshInstance){ return _defaultTransmissionOutcomesUpdateHook(meshInstance); });
}
/**
* Callback for when other nodes send you a request
*
* @param request The request string received from another node in the mesh
* @param meshInstance The MeshBackendBase instance that called the function.
* @return The string to send back to the other node. For ESP-NOW, return an empy string ("") if no response should be sent.
*/
String FloodingMesh::_defaultRequestHandler(const String &request, MeshBackendBase &meshInstance)
{
(void)meshInstance; // This is useful to remove a "unused parameter" compiler warning. Does nothing else.
String broadcastTarget = "";
String remainingRequest = "";
if(request.charAt(0) == broadcastMetadataDelimiter())
{
int32_t broadcastTargetEndIndex = request.indexOf(broadcastMetadataDelimiter(), 1);
if(broadcastTargetEndIndex == -1)
return ""; // broadcastMetadataDelimiter not found
broadcastTarget = request.substring(1, broadcastTargetEndIndex + 1); // Include delimiter
remainingRequest = request.substring(broadcastTargetEndIndex + 1);
}
else
{
remainingRequest = request;
}
int32_t messageIDEndIndex = remainingRequest.indexOf(broadcastMetadataDelimiter());
if(messageIDEndIndex == -1)
return ""; // broadcastMetadataDelimiter not found
uint64_t messageID = stringToUint64(remainingRequest.substring(0, messageIDEndIndex));
if(insertCompletedMessageID(messageID))
{
uint8_t originMacArray[6] = { 0 };
setOriginMac(uint64ToMac(messageID >> 16, originMacArray));
String message = remainingRequest.substring(messageIDEndIndex + 1);
if(getMessageHandler()(message, *this))
{
message = broadcastTarget + remainingRequest.substring(0, messageIDEndIndex + 1) + message;
assert(message.length() <= _espnowBackend.getMaxMessageLength());
_forwardingBacklog.emplace_back(message, getEspnowMeshBackend().receivedEncryptedMessage());
}
}
return "";
}
/**
* Callback for when you get a response from other nodes
*
* @param response The response string received from another node in the mesh
* @param meshInstance The MeshBackendBase instance that called the function.
* @return The status code resulting from the response, as an int
*/
transmission_status_t FloodingMesh::_defaultResponseHandler(const String &response, MeshBackendBase &meshInstance)
{
transmission_status_t statusCode = TS_TRANSMISSION_COMPLETE;
getEspnowMeshBackend().warningPrint("WARNING! Response to FloodingMesh broadcast received, but none is expected!");
(void)response; // This is useful to remove a "unused parameter" compiler warning. Does nothing else.
(void)meshInstance; // This is useful to remove a "unused parameter" compiler warning. Does nothing else.
return statusCode;
}
/**
* Callback used to decide which networks to connect to once a WiFi scan has been completed.
*
* @param numberOfNetworks The number of networks found in the WiFi scan.
* @param meshInstance The MeshBackendBase instance that called the function.
*/
void FloodingMesh::_defaultNetworkFilter(int numberOfNetworks, MeshBackendBase &meshInstance)
{
// Note that the network index of a given node may change whenever a new scan is done.
for (int networkIndex = 0; networkIndex < numberOfNetworks; ++networkIndex)
{
String currentSSID = WiFi.SSID(networkIndex);
int meshNameIndex = currentSSID.indexOf(meshInstance.getMeshName());
// Connect to any APs which contain meshInstance.getMeshName()
if(meshNameIndex >= 0)
{
if(_macIgnoreList.indexOf(macToString(WiFi.BSSID(networkIndex))) == -1) // If the BSSID is not in the ignore list
{
if(EspnowMeshBackend *espnowInstance = meshBackendCast<EspnowMeshBackend *>(&meshInstance))
{
espnowInstance->connectionQueue().push_back(networkIndex);
}
else
{
Serial.println(String(F("Invalid mesh backend!")));
}
}
}
}
}
/**
* Callback used to decide which broadcast messages to accept. Only called for the first transmission in each broadcast.
* If true is returned from this callback, the first broadcast transmission is saved until the entire broadcast message has been received.
* The complete broadcast message will then be sent to the requestHandler.
* If false is returned from this callback, the broadcast message is discarded.
* Note that the BroadcastFilter may be called multiple times for messages that are discarded in this way, but is only called once for accepted messages.
*
* @param firstTransmission The first transmission of the broadcast.
* @param meshInstance The EspnowMeshBackend instance that called the function.
*
* @return True if the broadcast should be accepted. False otherwise.
*/
bool FloodingMesh::_defaultBroadcastFilter(String &firstTransmission, EspnowMeshBackend &meshInstance)
{
// This broadcastFilter will accept a transmission if it contains the broadcastMetadataDelimiter
// and as metaData either no targetMeshName or a targetMeshName that matches the MeshName of meshInstance
// and insertPreliminaryMessageID(messageID) returns true.
// Broadcast firstTransmission String structure: targetMeshName+messageID+message.
int32_t metadataEndIndex = firstTransmission.indexOf(broadcastMetadataDelimiter());
if(metadataEndIndex == -1)
return false; // broadcastMetadataDelimiter not found
String targetMeshName = firstTransmission.substring(0, metadataEndIndex);
if(targetMeshName != "" && meshInstance.getMeshName() != targetMeshName)
{
return false; // Broadcast is for another mesh network
}
else
{
int32_t messageIDEndIndex = firstTransmission.indexOf(broadcastMetadataDelimiter(), metadataEndIndex + 1);
if(messageIDEndIndex == -1)
return false; // broadcastMetadataDelimiter not found
uint64_t messageID = stringToUint64(firstTransmission.substring(metadataEndIndex + 1, messageIDEndIndex));
if(insertPreliminaryMessageID(messageID))
{
// Add broadcast identifier to stored message and mark as accepted broadcast.
firstTransmission = String(broadcastMetadataDelimiter()) + firstTransmission;
return true;
}
else
{
return false; // Broadcast has already been received the maximum number of times
}
}
}
/**
* Once passed to the setTransmissionOutcomesUpdateHook method of the ESP-NOW backend,
* this function will be called after each update of the latestTransmissionOutcomes vector during attemptTransmission.
* (which happens after each individual transmission has finished)
*
* @param meshInstance The MeshBackendBase instance that called the function.
*
* @return True if attemptTransmission should continue with the next entry in the connectionQueue. False if attemptTransmission should stop.
*/
bool FloodingMesh::_defaultTransmissionOutcomesUpdateHook(MeshBackendBase &meshInstance)
{
(void)meshInstance; // This is useful to remove a "unused parameter" compiler warning. Does nothing else.
return true;
}

View File

@@ -0,0 +1,285 @@
/*
* Copyright (C) 2019 Anders Löfgren
*
* License (MIT license):
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#ifndef __FLOODINGMESH_H__
#define __FLOODINGMESH_H__
#include "EspnowMeshBackend.h"
#include <set>
#include <unordered_map>
#include <queue>
/**
* An alternative to standard delay(). Will continuously call performMeshMaintainance() during the waiting time, so that the FloodingMesh node remains responsive.
* Note that if there is a lot of FloodingMesh transmission activity to the node during the floodingMeshDelay, the desired duration may be overshot by several ms.
* Thus, if precise timing is required, use standard delay() instead.
*
* Should not be used inside callbacks since performMeshMaintainance() can alter the ESP-NOW state.
*
* @param durationMs The shortest allowed delay duration, in milliseconds.
*/
void floodingMeshDelay(uint32_t durationMs);
class FloodingMesh {
protected:
typedef std::function<bool(String &, FloodingMesh &)> messageHandlerType;
typedef std::unordered_map<uint64_t, uint8_t>::iterator messageQueueElementType;
public:
/**
* FloodingMesh constructor method. Creates a FloodingMesh node, ready to be initialised.
*
* @param messageHandler The callback handler responsible for dealing with messages received from the mesh.
* @param meshPassword The WiFi password for the mesh network.
* @param espnowEncryptionKey An uint8_t array containing the key used by the EspnowMeshBackend instance for creating encrypted ESP-NOW connections.
* @param espnowHashKey An uint8_t array containing the secret key used by the EspnowMeshBackend instance to generate HMACs for encrypted ESP-NOW connections.
* @param ssidPrefix The prefix (first part) of the node SSID.
* @param ssidSuffix The suffix (last part) of the node SSID.
* @param verboseMode Determines if we should print the events occurring in the library to Serial. Off by default. This setting is shared by all EspnowMeshBackend instances.
* @param meshWiFiChannel The WiFi channel used by the mesh network. Valid values are integers from 1 to 13. Defaults to 1.
* WARNING: The ESP8266 has only one WiFi channel, and the the station/client mode is always prioritized for channel selection.
* This can cause problems if several mesh instances exist on the same ESP8266 and use different WiFi channels.
* In such a case, whenever the station of one mesh instance connects to an AP, it will silently force the
* WiFi channel of any active AP on the ESP8266 to match that of the station. This will cause disconnects and possibly
* make it impossible for other stations to detect the APs whose WiFi channels have changed.
*
*/
FloodingMesh(messageHandlerType messageHandler, const String &meshPassword, const uint8_t espnowEncryptionKey[EspnowProtocolInterpreter::espnowEncryptionKeyLength],
const uint8_t espnowHashKey[EspnowProtocolInterpreter::espnowHashKeyLength], const String &ssidPrefix,
const String &ssidSuffix, bool verboseMode = false, uint8 meshWiFiChannel = 1);
/**
* This constructor should be used in combination with serializeMeshState() when the node has gone to sleep while other nodes stayed awake.
* Otherwise the message ID will be reset after sleep, which means that the nodes that stayed awake may ignore new broadcasts for a while.
*
* @param serializedMeshState A String with a serialized mesh node state that the node should use.
*/
FloodingMesh(const String &serializedMeshState, messageHandlerType messageHandler, const String &meshPassword,
const uint8_t espnowEncryptionKey[EspnowProtocolInterpreter::espnowEncryptionKeyLength],
const uint8_t espnowHashKey[EspnowProtocolInterpreter::espnowHashKeyLength], const String &ssidPrefix,
const String &ssidSuffix, bool verboseMode = false, uint8 meshWiFiChannel = 1);
virtual ~FloodingMesh();
/**
* The method responsible for initialising this FloodingMesh instance.
*
* Since there is only one WiFi radio on the ESP8266, only the FloodingMesh instance that was the last to begin() will be visible to surrounding nodes.
* All FloodingMesh instances can still broadcast messages though, even if their AP is not visible.
*/
void begin();
/**
* Performs maintainance for all available Flooding Mesh instances
*/
static void performMeshMaintainance();
/**
* Performs maintainance for this particular Flooding Mesh instance
*/
void performMeshInstanceMaintainance();
/**
* Serialize the current mesh node state. Useful to save a state before the node goes to sleep.
* Note that this saves the current state only, so if a broadcast is made after this, the stored state is invalid.
*
* @return A string with the serialized current mesh node state.
*/
String serializeMeshState();
/**
* Make an unencrypted broadcast to the entire mesh network.
*
* It is recommended that there is at most one new message transmitted in the mesh every 10, 20, 30 ms for messages of length maxUnencryptedMessageSize()*n,
* where n is (roughly, depending on mesh name length) 1/4, 3/5 and 1 respectively. If transmissions are more frequent than this, message loss will increase.
*
* @param message The message to broadcast. Maximum message length is given by maxUnencryptedMessageSize(). The longer the message, the longer the transmission time.
*/
void broadcast(const String &message);
/**
* Set the maximum number of redundant copies that will be received of every broadcast. (from different senders)
* A greater number increases the likelihood that at least one of the copies is received successfully, but will also use more RAM.
*
* @param redundancy The maximum number of extra copies that will be accepted. Defaults to 2. Valid values are 0 to 254.
*/
void setBroadcastReceptionRedundancy(uint8_t redundancy);
uint8_t getBroadcastReceptionRedundancy();
/**
* Make an encrypted broadcast to the entire mesh network.
*
* ########## WARNING! This an experimental feature. API may change at any time. Only use if you like it when things break. ##########
* Will be very slow compared to unencrypted broadcasts. Probably works OK in a small mesh with a maximum of one new message transmitted in the mesh every second.
* Because of the throughput difference, mixing encypted and unencrypted broadcasts is not recommended if there are frequent mesh broadcasts (multiple per second),
* since a lot of unencrypted broadcasts can build up while a single encrypted broadcast is sent.
*
* It is recommended that verboseMode is turned off if using this, to avoid slowdowns due to excessive Serial printing.
*
* @param message The message to broadcast. Maximum message length is given by maxEncryptedMessageSize(). The longer the message, the longer the transmission time.
*/
void encryptedBroadcast(const String &message);
void clearMessageLogs();
void clearForwardingBacklog();
/**
* Set the callback handler responsible for dealing with messages received from the mesh.
*
* @param messageHandler The message handler callback function to use.
*/
void setMessageHandler(messageHandlerType messageHandler);
messageHandlerType getMessageHandler();
/**
* Get the origin AP MAC address of the most recently received mesh message.
* Returns a String.
*
* @return A String filled with a hexadecimal representation of the MAC, without delimiters.
*/
String getOriginMac();
/**
* Get the origin AP MAC address of the most recently received mesh message.
* Returns a uint8_t array.
*
* @param macArray The array that should store the MAC address. Must be at least 6 bytes.
* @return macArray filled with the origin MAC.
*/
uint8_t *getOriginMac(uint8_t *macArray);
/**
* The number of received messageID:s that will be stored by the node. Used to remember which messages have been received.
* Setting this too low will cause the same message to be received many times.
* Setting this too high will cause the node to run out of RAM.
* In practice, setting this value to more than 1337 is probably a bad idea since the node will run out of RAM quickly and crash as a result.
*
* Defaults to 100.
*
* @param messageLogSize The size of the message log for this FloodingMesh instance. Valid values are 1 to 65535 (uint16_t_max).
* If a value close to the maximum is chosen, there is a high risk the node will ignore transmissions on messageID rollover if they are sent only by one node
* (especially if some transmissions are missed), since the messageID also uses uint16_t.
*/
void setMessageLogSize(uint16_t messageLogSize);
uint16_t messageLogSize();
/**
* Hint: Use String.length() to get the ASCII length of a String.
*
* @return The maximum length in bytes an unencrypted ASCII message is allowed to be when broadcasted by this node.
* Note that non-ASCII characters usually require at least two bytes each.
* Also note that for unencrypted messages the maximum size will depend on getEspnowMeshBackend().getMeshName().length()
*/
uint32_t maxUnencryptedMessageSize();
/**
* Hint: Use String.length() to get the ASCII length of a String.
*
* @return The maximum length in bytes an encrypted ASCII message is allowed to be when broadcasted by this node.
* Note that non-ASCII characters usually require at least two bytes each.
*/
uint32_t maxEncryptedMessageSize();
/**
* Set the delimiter character used for metadata by every FloodingMesh instance.
* Using characters found in the mesh name or in HEX numbers is unwise, as is using ','.
*
* @param broadcastMetadataDelimiter The metadata delimiter character to use.
* Defaults to 23 = End-of-Transmission-Block (ETB) control character in ASCII
*/
static void setBroadcastMetadataDelimiter(char broadcastMetadataDelimiter);
static char broadcastMetadataDelimiter();
/*
* Gives you access to the EspnowMeshBackend used by the mesh node.
* The backend handles all mesh communication, and modifying it allows you to change every aspect of the mesh behaviour.
* Random interactions with the backend have a high chance of breaking the mesh network,
* and so are discouraged for those who prefer it when things just work.
*/
EspnowMeshBackend &getEspnowMeshBackend();
void restoreDefaultRequestHandler();
void restoreDefaultResponseHandler();
void restoreDefaultNetworkFilter();
void restoreDefaultBroadcastFilter();
void restoreDefaultTransmissionOutcomesUpdateHook();
protected:
static std::set<FloodingMesh *> availableFloodingMeshes;
String generateMessageID();
void broadcastKernel(const String &message);
void encryptedBroadcastKernel(const String &message);
bool insertPreliminaryMessageID(uint64_t messageID);
bool insertCompletedMessageID(uint64_t messageID);
void updateMessageQueue(messageQueueElementType messageIterator);
void loadMeshState(const String &serializedMeshState);
/**
* Set the MAC address considered to be the origin AP MAC address of the most recently received mesh message.
*
* @param macArray An uint8_t array which contains the MAC address to store. The method will store the first 6 bytes of the array.
*/
void setOriginMac(uint8_t *macArray);
private:
static const uint8_t MESSAGE_ID_LENGTH = 17; // 16 characters and one delimiter
static const uint8_t MESSAGE_COMPLETE = 255;
EspnowMeshBackend _espnowBackend;
messageHandlerType _messageHandler;
uint16_t _messageCount = 0;
uint16_t _messageLogSize = 100;
uint8_t _broadcastReceptionRedundancy = 2;
static char _broadcastMetadataDelimiter; // Defaults to 23 = End-of-Transmission-Block (ETB) control character in ASCII
uint8_t _originMac[6] = {0};
std::unordered_map<uint64_t, uint8_t> _messageIDs = {};
std::queue<messageQueueElementType> _messageIdOrder = {};
std::list<std::pair<String, bool>> _forwardingBacklog = {};
String _macIgnoreList = "";
String _defaultRequestHandler(const String &request, MeshBackendBase &meshInstance);
transmission_status_t _defaultResponseHandler(const String &response, MeshBackendBase &meshInstance);
void _defaultNetworkFilter(int numberOfNetworks, MeshBackendBase &meshInstance);
bool _defaultBroadcastFilter(String &firstTransmission, EspnowMeshBackend &meshInstance);
bool _defaultTransmissionOutcomesUpdateHook(MeshBackendBase &meshInstance);
};
#endif

View File

@@ -40,8 +40,8 @@ namespace JsonTranslator
const String jsonNonce = "\"nonce\":";
const String jsonHmac = "\"hmac\":";
const String jsonDesync = "\"desync\":";
const String jsonUnencryptedMessageID = "\"uMessageID\":";
const String jsonMeshMessageCount = "\"mMessageCount\":";
const String jsonUnencryptedMessageID = "\"unencMsgID\":";
const String jsonMeshMessageCount = "\"meshMsgCount\":";
String createJsonPair(const String &valueIdentifier, const String &value);
String createJsonEndPair(const String &valueIdentifier, const String &value);

View File

@@ -31,7 +31,6 @@ MessageData::MessageData(String &message, uint8_t transmissionsRemaining, uint32
TimeTracker(creationTimeMs)
{
_transmissionsExpected = transmissionsRemaining + 1;
assert(message.length() <= EspnowMeshBackend::getMaxMessageBytesPerTransmission()); // Should catch some cases where transmission is not null terminated.
_totalMessage += message;
_transmissionsReceived++;
}

View File

@@ -38,7 +38,7 @@ class TcpIpMeshBackend : public MeshBackendBase {
public:
/**
* WiFiMesh Constructor method. Creates a WiFi Mesh Node, ready to be initialised.
* TCP/IP constructor method. Creates a TCP/IP node, ready to be initialised.
*
* @param requestHandler The callback handler for dealing with received requests. Takes a string as an argument which
* is the request string received from another node and returns the string to send back.

View File

@@ -82,7 +82,7 @@ uint8_t *stringToMac(const String &macString, uint8_t *macArray);
uint64_t macToUint64(const uint8_t *macArray);
/**
* Takes a uint64_t value and stores the bits of the first 6 bytes in a uint8_t array. Assumes index 0 of the array should contain MSB.
* Takes a uint64_t value and stores the bits of the first 6 bytes (LSB) in a uint8_t array. Assumes index 0 of the array should contain MSB.
*
* @param macValue The uint64_t value to convert to a mac array. Value must fit within 6 bytes.
* @param macArray A uint8_t array that will hold the mac address once the function returns. Should have a size of at least 6 bytes.