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:
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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;
|
||||
|
503
libraries/ESP8266WiFiMesh/src/FloodingMesh.cpp
Normal file
503
libraries/ESP8266WiFiMesh/src/FloodingMesh.cpp
Normal 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;
|
||||
}
|
285
libraries/ESP8266WiFiMesh/src/FloodingMesh.h
Normal file
285
libraries/ESP8266WiFiMesh/src/FloodingMesh.h
Normal 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
|
@ -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);
|
||||
|
@ -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++;
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
Reference in New Issue
Block a user