1
0
mirror of https://github.com/esp8266/Arduino.git synced 2025-07-24 19:42:27 +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

@ -1,7 +1,7 @@
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMesh.h>
#include <TypeConversionFunctions.h>
#include <assert.h>
#include <FloodingMesh.h>
/**
NOTE: Although we could define the strings below as normal String variables,
@ -14,84 +14,94 @@
https://github.com/esp8266/Arduino/issues/1143
https://arduino-esp8266.readthedocs.io/en/latest/PROGMEM.html
*/
const char exampleMeshName[] PROGMEM = "MeshNode_";
const char exampleWiFiPassword[] PROGMEM = "ChangeThisWiFiPassword_TODO";
const char exampleMeshName[] PROGMEM = "MeshNode_"; // The name of the mesh network. Used as prefix for the node SSID and to find other network nodes in the example networkFilter and broadcastFilter functions below.
const char exampleWiFiPassword[] PROGMEM = "ChangeThisWiFiPassword_TODO"; // The password has to be min 8 and max 64 characters long, otherwise an AP which uses it will not be found during scans.
unsigned int requestNumber = 0;
unsigned int responseNumber = 0;
// A custom encryption key is required when using encrypted ESP-NOW transmissions. There is always a default Kok set, but it can be replaced if desired.
// All ESP-NOW keys below must match in an encrypted connection pair for encrypted communication to be possible.
uint8_t espnowEncryptionKey[16] = {0x33, 0x44, 0x33, 0x44, 0x33, 0x44, 0x33, 0x44, // This is the key for encrypting transmissions.
0x33, 0x44, 0x33, 0x44, 0x33, 0x44, 0x32, 0x11
};
uint8_t espnowHashKey[16] = {0xEF, 0x44, 0x33, 0x0C, 0x33, 0x44, 0xFE, 0x44, // This is the secret key used for HMAC during encrypted connection requests.
0x33, 0x44, 0x33, 0xB0, 0x33, 0x44, 0x32, 0xAD
};
String manageRequest(const String &request, ESP8266WiFiMesh &meshInstance);
transmission_status_t manageResponse(const String &response, ESP8266WiFiMesh &meshInstance);
void networkFilter(int numberOfNetworks, ESP8266WiFiMesh &meshInstance);
bool meshMessageHandler(String &message, FloodingMesh &meshInstance);
/* Create the mesh node object */
ESP8266WiFiMesh meshNode = ESP8266WiFiMesh(manageRequest, manageResponse, networkFilter, FPSTR(exampleWiFiPassword), FPSTR(exampleMeshName), "", true);
FloodingMesh floodingMesh = FloodingMesh(meshMessageHandler, FPSTR(exampleWiFiPassword), espnowEncryptionKey, espnowHashKey, FPSTR(exampleMeshName), uint64ToString(ESP.getChipId()), true);
bool theOne = true;
String theOneMac = "";
bool useLED = false; // Change this to true if you wish the onboard LED to mark The One.
/**
Callback for when other nodes send you a request
Callback for when a message is received from the mesh network.
@param request The request string received from another node in the mesh
@param meshInstance The ESP8266WiFiMesh instance that called the function.
@returns The string to send back to the other node
@param message The message String received from the mesh.
Modifications to this String are passed on when the message is forwarded from this node to other nodes.
However, the forwarded message will still use the same messageID.
Thus it will not be sent to nodes that have already received this messageID.
If you want to send a new message to the whole network, use a new broadcast from within the loop() instead.
@param meshInstance The FloodingMesh instance that received the message.
@return True if this node should forward the received message to other nodes. False otherwise.
*/
String manageRequest(const String &request, ESP8266WiFiMesh &meshInstance) {
// We do not store strings in flash (via F()) in this function.
// The reason is that the other node will be waiting for our response,
// so keeping the strings in RAM will give a (small) improvement in response time.
// Of course, it is advised to adjust this approach based on RAM requirements.
bool meshMessageHandler(String &message, FloodingMesh &meshInstance) {
int32_t delimiterIndex = message.indexOf(meshInstance.broadcastMetadataDelimiter());
if (delimiterIndex == 0) {
Serial.print("Message received from STA " + meshInstance.getEspnowMeshBackend().getSenderMac() + ": ");
Serial.println(message.substring(1, 101));
/* Print out received message */
Serial.print("Request received: ");
Serial.println(request);
String potentialMac = message.substring(1, 13);
/* return a string to send back */
return ("Hello world response #" + String(responseNumber++) + " from " + meshInstance.getMeshName() + meshInstance.getNodeID() + ".");
}
if (potentialMac > theOneMac) {
if (theOne) {
if (useLED) {
digitalWrite(LED_BUILTIN, HIGH); // Turn LED off (LED is active low)
}
/**
Callback for when you get a response from other nodes
theOne = false;
}
@param response The response string received from another node in the mesh
@param meshInstance The ESP8266WiFiMesh instance that called the function.
@returns The status code resulting from the response, as an int
*/
transmission_status_t manageResponse(const String &response, ESP8266WiFiMesh &meshInstance) {
transmission_status_t statusCode = TS_TRANSMISSION_COMPLETE;
theOneMac = potentialMac;
/* Print out received message */
Serial.print(F("Request sent: "));
Serial.println(meshInstance.getMessage());
Serial.print(F("Response received: "));
Serial.println(response);
return true;
} else {
return false;
}
} else if (delimiterIndex > 0) {
if (meshInstance.getOriginMac() == theOneMac) {
uint32_t totalBroadcasts = strtoul(message.c_str(), nullptr, 0); // strtoul stops reading input when an invalid character is discovered.
// Our last request got a response, so time to create a new request.
meshInstance.setMessage(String(F("Hello world request #")) + String(++requestNumber) + String(F(" from "))
+ meshInstance.getMeshName() + meshInstance.getNodeID() + String(F(".")));
// Static variables are only initialized once.
static uint32_t firstBroadcast = totalBroadcasts;
// (void)meshInstance; // This is useful to remove a "unused parameter" compiler warning. Does nothing else.
return statusCode;
}
if (totalBroadcasts - firstBroadcast >= 100) { // Wait a little to avoid start-up glitches
static uint32_t missedBroadcasts = 1; // Starting at one to compensate for initial -1 below.
static uint32_t previousTotalBroadcasts = totalBroadcasts;
static uint32_t totalReceivedBroadcasts = 0;
totalReceivedBroadcasts++;
/**
Callback used to decide which networks to connect to once a WiFi scan has been completed.
missedBroadcasts += totalBroadcasts - previousTotalBroadcasts - 1; // We expect an increment by 1.
previousTotalBroadcasts = totalBroadcasts;
@param numberOfNetworks The number of networks found in the WiFi scan.
@param meshInstance The ESP8266WiFiMesh instance that called the function.
*/
void networkFilter(int numberOfNetworks, ESP8266WiFiMesh &meshInstance) {
for (int networkIndex = 0; networkIndex < numberOfNetworks; ++networkIndex) {
String currentSSID = WiFi.SSID(networkIndex);
int meshNameIndex = currentSSID.indexOf(meshInstance.getMeshName());
/* Connect to any _suitable_ APs which contain meshInstance.getMeshName() */
if (meshNameIndex >= 0) {
uint64_t targetNodeID = stringToUint64(currentSSID.substring(meshNameIndex + meshInstance.getMeshName().length()));
if (targetNodeID < stringToUint64(meshInstance.getNodeID())) {
ESP8266WiFiMesh::connectionQueue.push_back(NetworkInfo(networkIndex));
if (totalReceivedBroadcasts % 50 == 0) {
Serial.println("missed/total: " + String(missedBroadcasts) + '/' + String(totalReceivedBroadcasts));
}
if (totalReceivedBroadcasts % 500 == 0) {
Serial.println("Benchmark message: " + message.substring(0, 100));
}
}
}
} else {
// Only show first 100 characters because printing a large String takes a lot of time, which is a bad thing for a callback function.
// If you need to print the whole String it is better to store it and print it in the loop() later.
Serial.print("Message with origin " + meshInstance.getOriginMac() + " received: ");
Serial.println(message.substring(0, 100));
}
return true;
}
void setup() {
@ -102,7 +112,7 @@ void setup() {
Serial.begin(115200);
delay(50); // Wait for Serial.
//yield(); // Use this if you don't want to wait for Serial.
//yield(); // Use this if you don't want to wait for Serial, but not with the ESP-NOW backend (yield() causes crashes with ESP-NOW).
// The WiFi.disconnect() ensures that the WiFi is working correctly. If this is not done before receiving WiFi connections,
// those WiFi connections will take a long time to make or sometimes will not work at all.
@ -111,52 +121,52 @@ void setup() {
Serial.println();
Serial.println();
Serial.println(F("Note that this library can use static IP:s for the nodes to speed up connection times.\n"
"Use the setStaticIP method as shown in this example to enable this.\n"
"Ensure that nodes connecting to the same AP have distinct static IP:s.\n"
"Also, remember to change the default mesh network password!\n\n"));
Serial.println(F("If you have an onboard LED on your ESP8266 it is recommended that you change the useLED variable to true.\n"
"That way you will get instant confirmation of the mesh communication.\n"
"Also, remember to change the default mesh network password and ESP-NOW keys!\n"));
Serial.println(F("Setting up mesh node..."));
/* Initialise the mesh node */
meshNode.begin();
meshNode.activateAP(); // Each AP requires a separate server port.
meshNode.setStaticIP(IPAddress(192, 168, 4, 22)); // Activate static IP mode to speed up connection times.
floodingMesh.begin();
uint8_t apMacArray[6] {0};
theOneMac = macToString(WiFi.softAPmacAddress(apMacArray));
if (useLED) {
pinMode(LED_BUILTIN, OUTPUT); // Initialize the LED_BUILTIN pin as an output
digitalWrite(LED_BUILTIN, LOW); // Turn LED on (LED is active low)
}
floodingMeshDelay(5000); // Give some time for user to start the nodes
}
int32_t timeOfLastScan = -10000;
int32_t timeOfLastProclamation = -10000;
void loop() {
if (millis() - timeOfLastScan > 3000 // Give other nodes some time to connect between data transfers.
|| (WiFi.status() != WL_CONNECTED && millis() - timeOfLastScan > 2000)) { // Scan for networks with two second intervals when not already connected.
String request = String(F("Hello world request #")) + String(requestNumber) + String(F(" from ")) + meshNode.getMeshName() + meshNode.getNodeID() + String(F("."));
meshNode.attemptTransmission(request, false);
timeOfLastScan = millis();
static uint32_t benchmarkCount = 0;
static uint32_t loopStart = millis();
// One way to check how attemptTransmission worked out
if (ESP8266WiFiMesh::latestTransmissionSuccessful()) {
Serial.println(F("Transmission successful."));
// The floodingMeshDelay() method performs all the background operations for the FloodingMesh (via FloodingMesh::performMeshMaintainance()).
// It is recommended to place one of these methods in the beginning of the loop(), unless there is a need to put them elsewhere.
// Among other things, the method cleans up old ESP-NOW log entries (freeing up RAM) and forwards received mesh messages.
// Note that depending on the amount of messages to forward and their length, this method can take tens or even hundreds of milliseconds to complete.
// More intense transmission activity and less frequent calls to performMeshMaintainance will likely cause the method to take longer to complete, so plan accordingly.
floodingMeshDelay(1);
if (theOne) {
if (millis() - timeOfLastProclamation > 10000) {
uint32_t startTime = millis();
floodingMesh.broadcast(String(floodingMesh.broadcastMetadataDelimiter()) + theOneMac + " is The One.");
Serial.println("Proclamation broadcast done in " + String(millis() - startTime) + " ms.");
timeOfLastProclamation = millis();
floodingMeshDelay(20);
}
// Another way to check how attemptTransmission worked out
if (ESP8266WiFiMesh::latestTransmissionOutcomes.empty()) {
Serial.println(F("No mesh AP found."));
} else {
for (TransmissionResult &transmissionResult : ESP8266WiFiMesh::latestTransmissionOutcomes) {
if (transmissionResult.transmissionStatus == TS_TRANSMISSION_FAILED) {
Serial.println(String(F("Transmission failed to mesh AP ")) + transmissionResult.SSID);
} else if (transmissionResult.transmissionStatus == TS_CONNECTION_FAILED) {
Serial.println(String(F("Connection failed to mesh AP ")) + transmissionResult.SSID);
} else if (transmissionResult.transmissionStatus == TS_TRANSMISSION_COMPLETE) {
// No need to do anything, transmission was successful.
} else {
Serial.println(String(F("Invalid transmission status for ")) + transmissionResult.SSID + String(F("!")));
assert(F("Invalid transmission status returned from responseHandler!") && false);
}
}
if (millis() - loopStart > 23000) { // Start benchmarking the mesh once three proclamations have been made
uint32_t startTime = millis();
floodingMesh.broadcast(String(benchmarkCount++) + String(floodingMesh.broadcastMetadataDelimiter()) + ": Not a spoon in sight.");
Serial.println("Benchmark broadcast done in " + String(millis() - startTime) + " ms.");
floodingMeshDelay(20);
}
Serial.println();
} else {
/* Accept any incoming connections */
meshNode.acceptRequest();
}
}

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.