From d20177ae3553e90caaf90678b38b45e760f1ece1 Mon Sep 17 00:00:00 2001 From: Anders Date: Wed, 10 Jul 2019 02:29:01 +0200 Subject: [PATCH] - Add new ESP-NOW mesh backend. - Add HelloEspnow.ino example to demonstrate the ESP-NOW mesh backend features. - Deprecate the ESP8266WiFiMesh class in favour of the new ESP-NOW and TCP/IP backends. - Update the TCP/IP mesh backend to use the new lwIP version preprocessor flag and remove obsolete preprocessor flags. --- libraries/ESP8266WiFiMesh/README.md | 16 +- .../examples/HelloEspnow/HelloEspnow.ino | 312 +++ libraries/ESP8266WiFiMesh/library.properties | 4 +- libraries/ESP8266WiFiMesh/src/Crypto.cpp | 1002 +++++++ libraries/ESP8266WiFiMesh/src/Crypto.h | 254 ++ .../ESP8266WiFiMesh/src/ESP8266WiFiMesh.cpp | 89 +- .../ESP8266WiFiMesh/src/ESP8266WiFiMesh.h | 32 +- .../src/EncryptedConnectionData.cpp | 181 ++ .../src/EncryptedConnectionData.h | 96 + .../src/EncryptedConnectionLog.cpp | 92 + .../src/EncryptedConnectionLog.h | 68 + .../ESP8266WiFiMesh/src/EspnowMeshBackend.cpp | 2295 +++++++++++++++++ .../ESP8266WiFiMesh/src/EspnowMeshBackend.h | 764 ++++++ .../src/EspnowProtocolInterpreter.cpp | 105 + .../src/EspnowProtocolInterpreter.h | 74 + .../src/ExpiringTimeTracker.cpp | 59 + .../ESP8266WiFiMesh/src/ExpiringTimeTracker.h | 48 + .../ESP8266WiFiMesh/src/JsonTranslator.cpp | 269 ++ .../ESP8266WiFiMesh/src/JsonTranslator.h | 109 + .../ESP8266WiFiMesh/src/MeshBackendBase.cpp | 276 ++ .../ESP8266WiFiMesh/src/MeshBackendBase.h | 302 +++ libraries/ESP8266WiFiMesh/src/MessageData.cpp | 69 + libraries/ESP8266WiFiMesh/src/MessageData.h | 54 + .../ESP8266WiFiMesh/src/MutexTracker.cpp | 72 + libraries/ESP8266WiFiMesh/src/MutexTracker.h | 71 + .../ESP8266WiFiMesh/src/PeerRequestLog.cpp | 50 + .../ESP8266WiFiMesh/src/PeerRequestLog.h | 60 + libraries/ESP8266WiFiMesh/src/RequestData.cpp | 32 + libraries/ESP8266WiFiMesh/src/RequestData.h | 47 + .../ESP8266WiFiMesh/src/ResponseData.cpp | 79 + libraries/ESP8266WiFiMesh/src/ResponseData.h | 59 + .../ESP8266WiFiMesh/src/TcpIpMeshBackend.cpp | 469 ++++ .../ESP8266WiFiMesh/src/TcpIpMeshBackend.h | 214 ++ libraries/ESP8266WiFiMesh/src/TimeTracker.cpp | 39 + libraries/ESP8266WiFiMesh/src/TimeTracker.h | 45 + .../src/TypeConversionFunctions.cpp | 87 + .../src/TypeConversionFunctions.h | 65 + .../ESP8266WiFiMesh/src/UtilityFunctions.cpp | 45 + .../ESP8266WiFiMesh/src/UtilityFunctions.h | 35 + .../ESP8266WiFiMesh/src/UtilityMethods.cpp | 80 +- 40 files changed, 8027 insertions(+), 92 deletions(-) create mode 100644 libraries/ESP8266WiFiMesh/examples/HelloEspnow/HelloEspnow.ino create mode 100644 libraries/ESP8266WiFiMesh/src/Crypto.cpp create mode 100644 libraries/ESP8266WiFiMesh/src/Crypto.h create mode 100644 libraries/ESP8266WiFiMesh/src/EncryptedConnectionData.cpp create mode 100644 libraries/ESP8266WiFiMesh/src/EncryptedConnectionData.h create mode 100644 libraries/ESP8266WiFiMesh/src/EncryptedConnectionLog.cpp create mode 100644 libraries/ESP8266WiFiMesh/src/EncryptedConnectionLog.h create mode 100644 libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.cpp create mode 100644 libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.h create mode 100644 libraries/ESP8266WiFiMesh/src/EspnowProtocolInterpreter.cpp create mode 100644 libraries/ESP8266WiFiMesh/src/EspnowProtocolInterpreter.h create mode 100644 libraries/ESP8266WiFiMesh/src/ExpiringTimeTracker.cpp create mode 100644 libraries/ESP8266WiFiMesh/src/ExpiringTimeTracker.h create mode 100644 libraries/ESP8266WiFiMesh/src/JsonTranslator.cpp create mode 100644 libraries/ESP8266WiFiMesh/src/JsonTranslator.h create mode 100644 libraries/ESP8266WiFiMesh/src/MeshBackendBase.cpp create mode 100644 libraries/ESP8266WiFiMesh/src/MeshBackendBase.h create mode 100644 libraries/ESP8266WiFiMesh/src/MessageData.cpp create mode 100644 libraries/ESP8266WiFiMesh/src/MessageData.h create mode 100644 libraries/ESP8266WiFiMesh/src/MutexTracker.cpp create mode 100644 libraries/ESP8266WiFiMesh/src/MutexTracker.h create mode 100644 libraries/ESP8266WiFiMesh/src/PeerRequestLog.cpp create mode 100644 libraries/ESP8266WiFiMesh/src/PeerRequestLog.h create mode 100644 libraries/ESP8266WiFiMesh/src/RequestData.cpp create mode 100644 libraries/ESP8266WiFiMesh/src/RequestData.h create mode 100644 libraries/ESP8266WiFiMesh/src/ResponseData.cpp create mode 100644 libraries/ESP8266WiFiMesh/src/ResponseData.h create mode 100644 libraries/ESP8266WiFiMesh/src/TcpIpMeshBackend.cpp create mode 100644 libraries/ESP8266WiFiMesh/src/TcpIpMeshBackend.h create mode 100644 libraries/ESP8266WiFiMesh/src/TimeTracker.cpp create mode 100644 libraries/ESP8266WiFiMesh/src/TimeTracker.h create mode 100644 libraries/ESP8266WiFiMesh/src/UtilityFunctions.cpp create mode 100644 libraries/ESP8266WiFiMesh/src/UtilityFunctions.h diff --git a/libraries/ESP8266WiFiMesh/README.md b/libraries/ESP8266WiFiMesh/README.md index 8d955220c..dfd6f8fdd 100644 --- a/libraries/ESP8266WiFiMesh/README.md +++ b/libraries/ESP8266WiFiMesh/README.md @@ -3,9 +3,9 @@ ESP8266 WiFi Mesh A library for turning your ESP8266 into a mesh network node. -The library has been tested and works with Arduino core for ESP8266 version 2.3.0 (with default lwIP) and 2.4.2 or higher (with lwIP 1.4 and lwIP2). +The library has been tested and works with Arduino core for ESP8266 version 2.6.0 (with lwIP2). It may work with earlier and later core releases, but this has not been tested during development. -**Note:** This mesh library has been rewritten for core release 2.4.2. The old method signatures have been retained for compatibility purposes, but will be removed in core release 2.5.0. If you are still using these old method signatures please consider migrating to the new API shown in the `ESP8266WiFiMesh.h` source file. +**Note:** This mesh library has been rewritten for core release 2.6.0. The old method signatures have been retained for compatibility purposes, but will be removed in core release 3.0.0. If you are still using these old method signatures please consider migrating to the new API shown in the `EspnowMeshBackend.h` or `TcpIpMeshBackend.h` source files. Usage ----- @@ -48,14 +48,18 @@ ESP8266WiFiMesh(requestHandlerType requestHandler, responseHandlerType responseH ### Note -* This library can use static IP:s for the nodes to speed up connection times. To enable this, use the `setStaticIP` method after calling the `begin` method, as in the included example. Ensure that nodes connecting to the same AP have distinct static IP:s. Node IP:s need to be at the same subnet as the server gateway (192.168.4 for this library by default). It may also be worth noting that station gateway IP must match the IP for the server on the nodes, though this is the default setting for the library. +* This library can use static IP:s for the nodes to speed up connection times. To enable this, use the `setStaticIP` method after calling the `begin` method, as in the included example. When using static IP, the following is good to keep in mind: - At the moment static IP is a global setting, meaning that all ESP8266WiFiMesh instances on the same ESP8266 share the same static IP settings. + Ensure that nodes connecting to the same AP have distinct static IP:s. + + Node IP:s need to be at the same subnet as the server gateway (192.168.4 for this library by default). + + Station gateway IP must match the IP for the server on the nodes. This is the default setting for the library. + + Static IP is a global setting (for now), meaning that all ESP8266WiFiMesh instances on the same ESP8266 share the same static IP settings. * When Arduino core for ESP8266 version 2.4.2 or higher is used, there are optimizations available for WiFi scans and static IP use to reduce the time it takes for nodes to connect to each other. These optimizations are enabled by default. To take advantage of the static IP optimizations you also need to use lwIP2. The lwIP version can be changed in the Tools menu of Arduino IDE. - If you are using a core version prior to 2.4.2 it is possible to disable the WiFi scan and static IP optimizations by commenting out the `ENABLE_STATIC_IP_OPTIMIZATION` and `ENABLE_WIFI_SCAN_OPTIMIZATION` defines in ESP8266WiFiMesh.h. Press Ctrl+K in the Arduino IDE while an example from the mesh library is opened, to open the library folder (or click "Show Sketch Folder" in the Sketch menu). ESP8266WiFiMesh.h can then be found at ESP8266WiFiMesh/src. Edit the file with any text editor. - * The WiFi scan optimization mentioned above works by making WiFi scans only search through the same WiFi channel as the ESP8266WiFiMesh instance is using. If you would like to scan all WiFi channels instead, set the `scanAllWiFiChannels` argument of the `attemptTransmission` method to `true`. Note that scanning all WiFi channels will slow down scans considerably and make it more likely that existing WiFi connections will break during scans. Also note that if the ESP8266 has an active AP, that AP will switch WiFi channel to match that of any other AP the ESP8266 connects to (compare next bullet point). This can make it impossible for other nodes to detect the AP if they are scanning the wrong WiFi channel. To remedy this, force the AP back on the original channel by using the `restartAP` method of the current AP controller once the ESP8266 has disconnected from the other AP. This would typically be done like so: ``` diff --git a/libraries/ESP8266WiFiMesh/examples/HelloEspnow/HelloEspnow.ino b/libraries/ESP8266WiFiMesh/examples/HelloEspnow/HelloEspnow.ino new file mode 100644 index 000000000..bcbad36f2 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/examples/HelloEspnow/HelloEspnow.ino @@ -0,0 +1,312 @@ +#include +#include +#include +#include + +/** + NOTE: Although we could define the strings below as normal String variables, + here we are using PROGMEM combined with the FPSTR() macro (and also just the F() macro further down in the file). + The reason is that this approach will place the strings in flash memory which will help save RAM during program execution. + Reading strings from flash will be slower than reading them from RAM, + but this will be a negligible difference when printing them to Serial. + + More on F(), FPSTR() and PROGMEM: + https://github.com/esp8266/Arduino/issues/1143 + https://arduino-esp8266.readthedocs.io/en/latest/PROGMEM.html +*/ +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 function 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. + +// 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 espnowEncryptionKok[16] = {0x22, 0x44, 0x33, 0x44, 0x33, 0x44, 0x33, 0x44, // This is the key for encrypting the encryption key. + 0x33, 0x44, 0x33, 0x44, 0x33, 0x44, 0x32, 0x33 + }; +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 + }; + +unsigned int requestNumber = 0; +unsigned int responseNumber = 0; + +String manageRequest(const String &request, MeshBackendBase &meshInstance); +transmission_status_t manageResponse(const String &response, MeshBackendBase &meshInstance); +void networkFilter(int numberOfNetworks, MeshBackendBase &meshInstance); + +/* Create the mesh node object */ +EspnowMeshBackend espnowNode = EspnowMeshBackend(manageRequest, manageResponse, networkFilter, FPSTR(exampleWiFiPassword), espnowEncryptionKey, espnowHashKey, FPSTR(exampleMeshName), uint64ToString(ESP.getChipId()), true); + +/** + 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. + @returns The string to send back to the other node. For ESP-NOW, return an empy string ("") if no response should be sent. +*/ +String manageRequest(const String &request, MeshBackendBase &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. + + // To get the actual class of the polymorphic meshInstance, do as follows (meshBackendCast replaces dynamic_cast since RTTI is disabled) + if (EspnowMeshBackend *espnowInstance = meshBackendCast(&meshInstance)) { + String messageEncrypted = espnowInstance->receivedEncryptedMessage() ? ", Encrypted" : ", Unencrypted"; + Serial.print("ESP-NOW (" + espnowInstance->getSenderMac() + messageEncrypted + "): "); + } else if (TcpIpMeshBackend *tcpIpInstance = meshBackendCast(&meshInstance)) { + (void)tcpIpInstance; // This is useful to remove a "unused parameter" compiler warning. Does nothing else. + Serial.print("TCP/IP: "); + } else { + Serial.print("UNKNOWN!: "); + } + + /* Print out received message */ + // 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("Request received: "); + Serial.println(request.substring(0, 100)); + + /* return a string to send back */ + return ("Hello world response #" + String(responseNumber++) + " from " + meshInstance.getMeshName() + meshInstance.getNodeID() + " with AP MAC " + WiFi.softAPmacAddress() + "."); +} + +/** + 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. + @returns The status code resulting from the response, as an int +*/ +transmission_status_t manageResponse(const String &response, MeshBackendBase &meshInstance) { + transmission_status_t statusCode = TS_TRANSMISSION_COMPLETE; + + // To get the actual class of the polymorphic meshInstance, do as follows (meshBackendCast replaces dynamic_cast since RTTI is disabled) + if (EspnowMeshBackend *espnowInstance = meshBackendCast(&meshInstance)) { + String messageEncrypted = espnowInstance->receivedEncryptedMessage() ? ", Encrypted" : ", Unencrypted"; + Serial.print("ESP-NOW (" + espnowInstance->getSenderMac() + messageEncrypted + "): "); + } else if (TcpIpMeshBackend *tcpIpInstance = meshBackendCast(&meshInstance)) { + Serial.print("TCP/IP: "); + + // Getting the sent message like this will work as long as ONLY(!) TCP/IP is used. + // With TCP/IP the response will follow immediately after the request, so the stored message will not have changed. + // With ESP-NOW there is no guarantee when or if a response will show up, it can happen before or after the stored message is changed. + // So for ESP-NOW, adding unique identifiers in the response and request is required to associate a response with a request. + Serial.print(F("Request sent: ")); + Serial.println(tcpIpInstance->getMessage().substring(0, 100)); + } else { + Serial.print("UNKNOWN!: "); + } + + /* Print out received message */ + // 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(F("Response received: ")); + Serial.println(response.substring(0, 100)); + + 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 networkFilter(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 _suitable_ APs which contain meshInstance.getMeshName() */ + if (meshNameIndex >= 0) { + uint64_t targetNodeID = stringToUint64(currentSSID.substring(meshNameIndex + meshInstance.getMeshName().length())); + + if (targetNodeID < stringToUint64(meshInstance.getNodeID())) { + MeshBackendBase::connectionQueue.push_back(NetworkInfo(networkIndex)); + } + } + } +} + +void setup() { + // Prevents the flash memory from being worn out, see: https://github.com/esp8266/Arduino/issues/1054 . + // This will however delay node WiFi start-up by about 700 ms. The delay is 900 ms if we otherwise would have stored the WiFi network we want to connect to. + WiFi.persistent(false); + + Serial.begin(115200); + delay(50); // 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. + WiFi.disconnect(); + + Serial.println(); + Serial.println(); + + Serial.println(F("Note that this library can use static IP:s for the nodes with the TCP/IP backend to speed up connection times.\n" + "Use the setStaticIP method 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 and ESP-NOW keys!\n\n")); + + Serial.println(F("Setting up mesh node...")); + + /* Initialise the mesh node */ + espnowNode.begin(); + + // Note: This changes the Kok for all EspnowMeshBackend instances on this ESP8266. + // Encrypted connections added before the Kok change will retain their old Kok. + // Both Kok and encryption key must match in an encrypted connection pair for encrypted communication to be possible. + // Otherwise the transmissions will never reach the recipient, even though acks are received by the sender. + EspnowMeshBackend::setEspnowEncryptionKok(espnowEncryptionKok); + espnowNode.setEspnowEncryptionKey(espnowEncryptionKey); + + // Makes it possible to find the node through scans, and also makes it possible to recover from an encrypted 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. + espnowNode.activateAP(); + + // Storing our message in the EspnowMeshBackend instance is not required, but can be useful for organizing code, especially when using many EspnowMeshBackend instances. + // Note that calling espnowNode.attemptTransmission will replace the stored message with whatever message is transmitted. + espnowNode.setMessage(String(F("Hello world request #")) + String(requestNumber) + String(F(" from ")) + espnowNode.getMeshName() + espnowNode.getNodeID() + String(F("."))); +} + +int32_t timeOfLastScan = -10000; +void loop() { + // The performEspnowMaintainance() method performs all the background operations for the EspnowMeshBackend. + // It is recommended to place it in the beginning of the loop(), unless there is a need to put it elsewhere. + // Among other things, the method cleans up old Espnow log entries (freeing up RAM) and sends the responses you provide to Espnow requests. + // Note that depending on the amount of responses to send and their length, this method can take tens or even hundreds of milliseconds to complete. + // More intense transmission activity and less frequent calls to performEspnowMaintainance will likely cause the method to take longer to complete, so plan accordingly. + + //Should not be used inside responseHandler, requestHandler or networkFilter callbacks since performEspnowMaintainance() can alter the ESP-NOW state. + EspnowMeshBackend::performEspnowMaintainance(); + + if (millis() - timeOfLastScan > 10000) { // Give other nodes some time to connect between data transfers. + uint32_t startTime = millis(); + + Serial.println("\nPerforming unencrypted ESP-NOW transmissions."); + espnowNode.attemptTransmission(espnowNode.getMessage()); + Serial.println("Scan and " + String(MeshBackendBase::latestTransmissionOutcomes.size()) + " transmissions done in " + String(millis() - startTime) + " ms."); + + // Wait for response. espnowDelay continuously calls performEspnowMaintainance() so we will respond to ESP-NOW request while waiting. + // Should not be used inside responseHandler, requestHandler or networkFilter callbacks since performEspnowMaintainance() can alter the ESP-NOW state. + espnowDelay(100); + + timeOfLastScan = millis(); + + // One way to check how attemptTransmission worked out + if (MeshBackendBase::latestTransmissionSuccessful()) { + Serial.println(F("Transmission successful.")); + } + + // Another way to check how attemptTransmission worked out + if (MeshBackendBase::latestTransmissionOutcomes.empty()) { + Serial.println(F("No mesh AP found.")); + } else { + for (TransmissionResult &transmissionResult : MeshBackendBase::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); + } + } + + Serial.println("\nPerforming encrypted ESP-NOW transmissions."); + + // We can create encrypted connections to individual nodes so that all ESP-NOW communication with the node will be encrypted. + if (espnowNode.requestEncryptedConnection(MeshBackendBase::connectionQueue[0].BSSID) == ECS_CONNECTION_ESTABLISHED) { + // The WiFi scan will detect the AP MAC, but this will automatically be converted to the encrypted STA MAC by the framework. + String peerMac = macToString(MeshBackendBase::connectionQueue[0].BSSID); + + Serial.println("Encrypted ESP-NOW connection with " + peerMac + " established!"); + + // Making a transmission now will cause messages to MeshBackendBase::connectionQueue[0].BSSID to be encrypted. + String espnowMessage = "This message is encrypted only when received by node " + peerMac; + Serial.println("\nTransmitting: " + espnowMessage); + espnowNode.attemptTransmission(espnowMessage, false); + espnowDelay(100); // Wait for response. + + // A connection can be serialized and stored for later use. + // Note that this saves the current state only, so if encrypted communication between the nodes happen after this, the stored state is invalid. + String serializedEncryptedConnection = EspnowMeshBackend::serializeEncryptedConnection(MeshBackendBase::connectionQueue[0].BSSID); + + Serial.println(); + // We can remove an encrypted connection like so. + espnowNode.removeEncryptedConnection(MeshBackendBase::connectionQueue[0].BSSID); + + // Note that the peer will still be encrypted, so although we can send unencrypted messages to the peer, we cannot read the encrypted responses it sends back. + espnowMessage = "This message is no longer encrypted when received by node " + peerMac; + Serial.println("\nTransmitting: " + espnowMessage); + espnowNode.attemptTransmission(espnowMessage, false); + espnowDelay(100); // Wait for response. + Serial.println("Cannot read the encrypted response..."); + + // Let's re-add our stored connection so we can communicate properly with MeshBackendBase::connectionQueue[0].BSSID again! + espnowNode.addEncryptedConnection(serializedEncryptedConnection); + + espnowMessage = "This message is once again encrypted when received by node " + peerMac; + Serial.println("\nTransmitting: " + espnowMessage); + espnowNode.attemptTransmission(espnowMessage, false); + espnowDelay(100); // Wait for response. + + Serial.println(); + // If we want to remove the encrypted connection on both nodes, we can do it like this. + encrypted_connection_removal_outcome_t removalOutcome = espnowNode.requestEncryptedConnectionRemoval(MeshBackendBase::connectionQueue[0].BSSID); + if (removalOutcome == ECRO_REMOVAL_SUCCEEDED) { + Serial.println(peerMac + " is no longer encrypted!\n"); + + // Of course, we can also just create a temporary encrypted connection that will remove itself once its duration has passed. + if (espnowNode.requestTemporaryEncryptedConnection(MeshBackendBase::connectionQueue[0].BSSID, 1000) == ECS_CONNECTION_ESTABLISHED) { + espnowDelay(42); + uint32_t remainingDuration = 0; + EspnowMeshBackend::getConnectionInfo(MeshBackendBase::connectionQueue[0].BSSID, &remainingDuration); + + espnowMessage = "Messages this node sends to " + peerMac + " will be encrypted for " + String(remainingDuration) + " ms more."; + Serial.println("\nTransmitting: " + espnowMessage); + espnowNode.attemptTransmission(espnowMessage, false); + + EspnowMeshBackend::getConnectionInfo(MeshBackendBase::connectionQueue[0].BSSID, &remainingDuration); + espnowDelay(remainingDuration + 100); + + espnowMessage = "Due to encrypted connection expiration, this message is no longer encrypted when received by node " + peerMac; + Serial.println("\nTransmitting: " + espnowMessage); + espnowNode.attemptTransmission(espnowMessage, false); + espnowDelay(100); // Wait for response. + } + + // Or if we prefer we can just let the library automatically create brief encrypted connections which are long enough to transmit an encrypted message. + // Note that encrypted responses will not be received, unless there already was an encrypted connection established with the peer before attemptAutoEncryptingTransmission was called. + espnowMessage = "This message is always encrypted, regardless of receiver."; + Serial.println("\nTransmitting: " + espnowMessage); + espnowNode.attemptAutoEncryptingTransmission(espnowMessage); + espnowDelay(100); // Wait for response. + } else { + Serial.println("Ooops! Encrypted connection removal failed. Status: " + String(removalOutcome)); + } + + // Finally, should you ever want to stop other parties from sending unencrypted messages to the node + // setAcceptsUnencryptedRequests(false); + // can be used for this. It applies to both encrypted connection requests and regular transmissions. + + Serial.println("\n##############################################################################################"); + } + + // Our last request was sent to all nodes found, so time to create a new request. + espnowNode.setMessage(String(F("Hello world request #")) + String(++requestNumber) + String(F(" from ")) + + espnowNode.getMeshName() + espnowNode.getNodeID() + String(F("."))); + } + + Serial.println(); + } +} diff --git a/libraries/ESP8266WiFiMesh/library.properties b/libraries/ESP8266WiFiMesh/library.properties index ddfea96a5..2ff47b0ea 100644 --- a/libraries/ESP8266WiFiMesh/library.properties +++ b/libraries/ESP8266WiFiMesh/library.properties @@ -1,6 +1,6 @@ name=ESP8266WiFiMesh -version=2.1 -author=Julian Fell +version=2.2 +author=Julian Fell, Anders Löfgren maintainer=Anders Löfgren sentence=Mesh network library paragraph=The library sets up a Mesh Node which acts as a router, creating a Mesh Network with other nodes. diff --git a/libraries/ESP8266WiFiMesh/src/Crypto.cpp b/libraries/ESP8266WiFiMesh/src/Crypto.cpp new file mode 100644 index 000000000..bd8f51dea --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/Crypto.cpp @@ -0,0 +1,1002 @@ +/** + * An extremely minimal crypto library for Arduino devices. + * + * The SHA256 and AES implementations are derived from axTLS + * (http://axtls.sourceforge.net/), Copyright (c) 2008, Cameron Rich. + * + * Ported and refactored by Chris Ellis 2016. + * pkcs7 padding routines added by Mike Killewald Nov 26, 2017 (adopted from https://github.com/spaniakos/AES). + * + License + ======= + Balsa SCGI + Copyright (c) 2012, Chris Ellis + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Crypto.h" + +/** + * Byte order helpers + */ + + +//#if BYTE_ORDER == BIG_ENDIAN +/* +inline static uint16_t crypto_htons(uint16_t x) +{ + return x; +} + +inline static uint16_t crypto_ntohs(uint16_t x) +{ + return x; +} + +inline static uint32_t crypto_htonl(uint32_t x) +{ + return x; +} + +inline static uint32_t crypto_ntohl(uint32_t x) +{ + return x; +} +*/ +//#else + +inline static uint16_t crypto_htons(uint16_t x) +{ + return ( + ((x & 0xff) << 8) | + ((x & 0xff00) >> 8) + ); +} + +inline static uint16_t crypto_ntohs(uint16_t x) +{ + return ( + ((x & 0xff) << 8) | + ((x & 0xff00) >> 8) + ); +} + +inline static uint32_t crypto_htonl(uint32_t x) +{ + return ( + ((x & 0xff) << 24) | + ((x & 0xff00) << 8) | + ((x & 0xff0000UL) >> 8) | + ((x & 0xff000000UL) >> 24) + ); +} + +inline static uint32_t crypto_ntohl(uint32_t x) +{ + return ( + ((x & 0xff) << 24) | + ((x & 0xff00) << 8) | + ((x & 0xff0000UL) >> 8) | + ((x & 0xff000000UL) >> 24) + ); +} + +//#endif + +#define GET_UINT32(n,b,i) \ +{ \ + (n) = ((uint32_t) (b)[(i) ] << 24) \ + | ((uint32_t) (b)[(i) + 1] << 16) \ + | ((uint32_t) (b)[(i) + 2] << 8) \ + | ((uint32_t) (b)[(i) + 3] ); \ +} + +#define PUT_UINT32(n,b,i) \ +{ \ + (b)[(i) ] = (byte) ((n) >> 24); \ + (b)[(i) + 1] = (byte) ((n) >> 16); \ + (b)[(i) + 2] = (byte) ((n) >> 8); \ + (b)[(i) + 3] = (byte) ((n) ); \ +} + +static const byte sha256_padding[64] = +{ + 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +}; + +/** + * Initialize the SHA256 hash + */ +SHA256::SHA256() +{ + total[0] = 0; + total[1] = 0; + state[0] = 0x6A09E667; + state[1] = 0xBB67AE85; + state[2] = 0x3C6EF372; + state[3] = 0xA54FF53A; + state[4] = 0x510E527F; + state[5] = 0x9B05688C; + state[6] = 0x1F83D9AB; + state[7] = 0x5BE0CD19; +} + +void SHA256::SHA256_Process(const byte digest[64]) +{ + uint32_t temp1, temp2, W[64]; + uint32_t A, B, C, D, E, F, G, H; + + GET_UINT32(W[0], digest, 0); + GET_UINT32(W[1], digest, 4); + GET_UINT32(W[2], digest, 8); + GET_UINT32(W[3], digest, 12); + GET_UINT32(W[4], digest, 16); + GET_UINT32(W[5], digest, 20); + GET_UINT32(W[6], digest, 24); + GET_UINT32(W[7], digest, 28); + GET_UINT32(W[8], digest, 32); + GET_UINT32(W[9], digest, 36); + GET_UINT32(W[10], digest, 40); + GET_UINT32(W[11], digest, 44); + GET_UINT32(W[12], digest, 48); + GET_UINT32(W[13], digest, 52); + GET_UINT32(W[14], digest, 56); + GET_UINT32(W[15], digest, 60); + +#define SHR(x,n) ((x & 0xFFFFFFFF) >> n) +#define ROTR(x,n) (SHR(x,n) | (x << (32 - n))) + +#define S0(x) (ROTR(x, 7) ^ ROTR(x,18) ^ SHR(x, 3)) +#define S1(x) (ROTR(x,17) ^ ROTR(x,19) ^ SHR(x,10)) + +#define S2(x) (ROTR(x, 2) ^ ROTR(x,13) ^ ROTR(x,22)) +#define S3(x) (ROTR(x, 6) ^ ROTR(x,11) ^ ROTR(x,25)) + +#define F0(x,y,z) ((x & y) | (z & (x | y))) +#define F1(x,y,z) (z ^ (x & (y ^ z))) + +#define R(t) \ +( \ + W[t] = S1(W[t - 2]) + W[t - 7] + \ + S0(W[t - 15]) + W[t - 16] \ +) + +#define P(a,b,c,d,e,f,g,h,x,K) \ +{ \ + temp1 = h + S3(e) + F1(e,f,g) + K + x; \ + temp2 = S2(a) + F0(a,b,c); \ + d += temp1; h = temp1 + temp2; \ +} + + A = state[0]; + B = state[1]; + C = state[2]; + D = state[3]; + E = state[4]; + F = state[5]; + G = state[6]; + H = state[7]; + + P(A, B, C, D, E, F, G, H, W[ 0], 0x428A2F98); + P(H, A, B, C, D, E, F, G, W[ 1], 0x71374491); + P(G, H, A, B, C, D, E, F, W[ 2], 0xB5C0FBCF); + P(F, G, H, A, B, C, D, E, W[ 3], 0xE9B5DBA5); + P(E, F, G, H, A, B, C, D, W[ 4], 0x3956C25B); + P(D, E, F, G, H, A, B, C, W[ 5], 0x59F111F1); + P(C, D, E, F, G, H, A, B, W[ 6], 0x923F82A4); + P(B, C, D, E, F, G, H, A, W[ 7], 0xAB1C5ED5); + P(A, B, C, D, E, F, G, H, W[ 8], 0xD807AA98); + P(H, A, B, C, D, E, F, G, W[ 9], 0x12835B01); + P(G, H, A, B, C, D, E, F, W[10], 0x243185BE); + P(F, G, H, A, B, C, D, E, W[11], 0x550C7DC3); + P(E, F, G, H, A, B, C, D, W[12], 0x72BE5D74); + P(D, E, F, G, H, A, B, C, W[13], 0x80DEB1FE); + P(C, D, E, F, G, H, A, B, W[14], 0x9BDC06A7); + P(B, C, D, E, F, G, H, A, W[15], 0xC19BF174); + P(A, B, C, D, E, F, G, H, R(16), 0xE49B69C1); + P(H, A, B, C, D, E, F, G, R(17), 0xEFBE4786); + P(G, H, A, B, C, D, E, F, R(18), 0x0FC19DC6); + P(F, G, H, A, B, C, D, E, R(19), 0x240CA1CC); + P(E, F, G, H, A, B, C, D, R(20), 0x2DE92C6F); + P(D, E, F, G, H, A, B, C, R(21), 0x4A7484AA); + P(C, D, E, F, G, H, A, B, R(22), 0x5CB0A9DC); + P(B, C, D, E, F, G, H, A, R(23), 0x76F988DA); + P(A, B, C, D, E, F, G, H, R(24), 0x983E5152); + P(H, A, B, C, D, E, F, G, R(25), 0xA831C66D); + P(G, H, A, B, C, D, E, F, R(26), 0xB00327C8); + P(F, G, H, A, B, C, D, E, R(27), 0xBF597FC7); + P(E, F, G, H, A, B, C, D, R(28), 0xC6E00BF3); + P(D, E, F, G, H, A, B, C, R(29), 0xD5A79147); + P(C, D, E, F, G, H, A, B, R(30), 0x06CA6351); + P(B, C, D, E, F, G, H, A, R(31), 0x14292967); + P(A, B, C, D, E, F, G, H, R(32), 0x27B70A85); + P(H, A, B, C, D, E, F, G, R(33), 0x2E1B2138); + P(G, H, A, B, C, D, E, F, R(34), 0x4D2C6DFC); + P(F, G, H, A, B, C, D, E, R(35), 0x53380D13); + P(E, F, G, H, A, B, C, D, R(36), 0x650A7354); + P(D, E, F, G, H, A, B, C, R(37), 0x766A0ABB); + P(C, D, E, F, G, H, A, B, R(38), 0x81C2C92E); + P(B, C, D, E, F, G, H, A, R(39), 0x92722C85); + P(A, B, C, D, E, F, G, H, R(40), 0xA2BFE8A1); + P(H, A, B, C, D, E, F, G, R(41), 0xA81A664B); + P(G, H, A, B, C, D, E, F, R(42), 0xC24B8B70); + P(F, G, H, A, B, C, D, E, R(43), 0xC76C51A3); + P(E, F, G, H, A, B, C, D, R(44), 0xD192E819); + P(D, E, F, G, H, A, B, C, R(45), 0xD6990624); + P(C, D, E, F, G, H, A, B, R(46), 0xF40E3585); + P(B, C, D, E, F, G, H, A, R(47), 0x106AA070); + P(A, B, C, D, E, F, G, H, R(48), 0x19A4C116); + P(H, A, B, C, D, E, F, G, R(49), 0x1E376C08); + P(G, H, A, B, C, D, E, F, R(50), 0x2748774C); + P(F, G, H, A, B, C, D, E, R(51), 0x34B0BCB5); + P(E, F, G, H, A, B, C, D, R(52), 0x391C0CB3); + P(D, E, F, G, H, A, B, C, R(53), 0x4ED8AA4A); + P(C, D, E, F, G, H, A, B, R(54), 0x5B9CCA4F); + P(B, C, D, E, F, G, H, A, R(55), 0x682E6FF3); + P(A, B, C, D, E, F, G, H, R(56), 0x748F82EE); + P(H, A, B, C, D, E, F, G, R(57), 0x78A5636F); + P(G, H, A, B, C, D, E, F, R(58), 0x84C87814); + P(F, G, H, A, B, C, D, E, R(59), 0x8CC70208); + P(E, F, G, H, A, B, C, D, R(60), 0x90BEFFFA); + P(D, E, F, G, H, A, B, C, R(61), 0xA4506CEB); + P(C, D, E, F, G, H, A, B, R(62), 0xBEF9A3F7); + P(B, C, D, E, F, G, H, A, R(63), 0xC67178F2); + + state[0] += A; + state[1] += B; + state[2] += C; + state[3] += D; + state[4] += E; + state[5] += F; + state[6] += G; + state[7] += H; +#if defined ESP8266 + ESP.wdtFeed(); +#endif +} + +/** + * Accepts an array of octets as the next portion of the message. + */ +void SHA256::doUpdate(const byte * msg, uint32_t len) +{ + uint32_t left = total[0] & 0x3F; + uint32_t fill = 64 - left; + + total[0] += len; + total[0] &= 0xFFFFFFFF; + + if (total[0] < len) + total[1]++; + + if (left && len >= fill) + { + memcpy((void *) (buffer + left), (void *) msg, fill); + SHA256::SHA256_Process(buffer); + len -= fill; + msg += fill; + left = 0; + } + + while (len >= 64) + { + SHA256::SHA256_Process(msg); + len -= 64; + msg += 64; + } + + if (len) + { + memcpy((void *) (buffer + left), (void *) msg, len); + } +} + +/** + * Return the 256-bit message digest into the user's array + */ +void SHA256::doFinal(byte *digest) +{ + uint32_t last, padn; + uint32_t high, low; + byte msglen[8]; + + high = (total[0] >> 29) + | (total[1] << 3); + low = (total[0] << 3); + + PUT_UINT32(high, msglen, 0); + PUT_UINT32(low, msglen, 4); + + last = total[0] & 0x3F; + padn = (last < 56) ? (56 - last) : (120 - last); + + SHA256::doUpdate(sha256_padding, padn); + SHA256::doUpdate(msglen, 8); + + PUT_UINT32(state[0], digest, 0); + PUT_UINT32(state[1], digest, 4); + PUT_UINT32(state[2], digest, 8); + PUT_UINT32(state[3], digest, 12); + PUT_UINT32(state[4], digest, 16); + PUT_UINT32(state[5], digest, 20); + PUT_UINT32(state[6], digest, 24); + PUT_UINT32(state[7], digest, 28); +#if defined ESP8266 + ESP.wdtFeed(); +#endif +} + +bool SHA256::matches(const byte *expected) +{ + byte theDigest[SHA256_SIZE]; + doFinal(theDigest); + for (byte i = 0; i < SHA256_SIZE; i++) + { + if (expected[i] != theDigest[i]) + return false; + } +#if defined ESP8266 + ESP.wdtFeed(); +#endif + return true; +} + +/******************************************************************************/ + +#define rot1(x) (((x) << 24) | ((x) >> 8)) +#define rot2(x) (((x) << 16) | ((x) >> 16)) +#define rot3(x) (((x) << 8) | ((x) >> 24)) + +/* + * This cute trick does 4 'mul by two' at once. Stolen from + * Dr B. R. Gladman but I'm sure the u-(u>>7) is + * a standard graphics trick + * The key to this is that we need to xor with 0x1b if the top bit is set. + * a 1xxx xxxx 0xxx 0xxx First we mask the 7bit, + * b 1000 0000 0000 0000 then we shift right by 7 putting the 7bit in 0bit, + * c 0000 0001 0000 0000 we then subtract (c) from (b) + * d 0111 1111 0000 0000 and now we and with our mask + * e 0001 1011 0000 0000 + */ +#define mt 0x80808080 +#define ml 0x7f7f7f7f +#define mh 0xfefefefe +#define mm 0x1b1b1b1b +#define mul2(x,t) ((t)=((x)&mt), \ + ((((x)+(x))&mh)^(((t)-((t)>>7))&mm))) + +#define inv_mix_col(x,f2,f4,f8,f9) (\ + (f2)=mul2(x,f2), \ + (f4)=mul2(f2,f4), \ + (f8)=mul2(f4,f8), \ + (f9)=(x)^(f8), \ + (f8)=((f2)^(f4)^(f8)), \ + (f2)^=(f9), \ + (f4)^=(f9), \ + (f8)^=rot3(f2), \ + (f8)^=rot2(f4), \ + (f8)^rot1(f9)) + +/* + * AES S-box + */ +static const uint8_t aes_sbox[256] = +{ + 0x63,0x7C,0x77,0x7B,0xF2,0x6B,0x6F,0xC5, + 0x30,0x01,0x67,0x2B,0xFE,0xD7,0xAB,0x76, + 0xCA,0x82,0xC9,0x7D,0xFA,0x59,0x47,0xF0, + 0xAD,0xD4,0xA2,0xAF,0x9C,0xA4,0x72,0xC0, + 0xB7,0xFD,0x93,0x26,0x36,0x3F,0xF7,0xCC, + 0x34,0xA5,0xE5,0xF1,0x71,0xD8,0x31,0x15, + 0x04,0xC7,0x23,0xC3,0x18,0x96,0x05,0x9A, + 0x07,0x12,0x80,0xE2,0xEB,0x27,0xB2,0x75, + 0x09,0x83,0x2C,0x1A,0x1B,0x6E,0x5A,0xA0, + 0x52,0x3B,0xD6,0xB3,0x29,0xE3,0x2F,0x84, + 0x53,0xD1,0x00,0xED,0x20,0xFC,0xB1,0x5B, + 0x6A,0xCB,0xBE,0x39,0x4A,0x4C,0x58,0xCF, + 0xD0,0xEF,0xAA,0xFB,0x43,0x4D,0x33,0x85, + 0x45,0xF9,0x02,0x7F,0x50,0x3C,0x9F,0xA8, + 0x51,0xA3,0x40,0x8F,0x92,0x9D,0x38,0xF5, + 0xBC,0xB6,0xDA,0x21,0x10,0xFF,0xF3,0xD2, + 0xCD,0x0C,0x13,0xEC,0x5F,0x97,0x44,0x17, + 0xC4,0xA7,0x7E,0x3D,0x64,0x5D,0x19,0x73, + 0x60,0x81,0x4F,0xDC,0x22,0x2A,0x90,0x88, + 0x46,0xEE,0xB8,0x14,0xDE,0x5E,0x0B,0xDB, + 0xE0,0x32,0x3A,0x0A,0x49,0x06,0x24,0x5C, + 0xC2,0xD3,0xAC,0x62,0x91,0x95,0xE4,0x79, + 0xE7,0xC8,0x37,0x6D,0x8D,0xD5,0x4E,0xA9, + 0x6C,0x56,0xF4,0xEA,0x65,0x7A,0xAE,0x08, + 0xBA,0x78,0x25,0x2E,0x1C,0xA6,0xB4,0xC6, + 0xE8,0xDD,0x74,0x1F,0x4B,0xBD,0x8B,0x8A, + 0x70,0x3E,0xB5,0x66,0x48,0x03,0xF6,0x0E, + 0x61,0x35,0x57,0xB9,0x86,0xC1,0x1D,0x9E, + 0xE1,0xF8,0x98,0x11,0x69,0xD9,0x8E,0x94, + 0x9B,0x1E,0x87,0xE9,0xCE,0x55,0x28,0xDF, + 0x8C,0xA1,0x89,0x0D,0xBF,0xE6,0x42,0x68, + 0x41,0x99,0x2D,0x0F,0xB0,0x54,0xBB,0x16, +}; + +/* + * AES is-box + */ +static const uint8_t aes_isbox[256] = +{ + 0x52,0x09,0x6a,0xd5,0x30,0x36,0xa5,0x38, + 0xbf,0x40,0xa3,0x9e,0x81,0xf3,0xd7,0xfb, + 0x7c,0xe3,0x39,0x82,0x9b,0x2f,0xff,0x87, + 0x34,0x8e,0x43,0x44,0xc4,0xde,0xe9,0xcb, + 0x54,0x7b,0x94,0x32,0xa6,0xc2,0x23,0x3d, + 0xee,0x4c,0x95,0x0b,0x42,0xfa,0xc3,0x4e, + 0x08,0x2e,0xa1,0x66,0x28,0xd9,0x24,0xb2, + 0x76,0x5b,0xa2,0x49,0x6d,0x8b,0xd1,0x25, + 0x72,0xf8,0xf6,0x64,0x86,0x68,0x98,0x16, + 0xd4,0xa4,0x5c,0xcc,0x5d,0x65,0xb6,0x92, + 0x6c,0x70,0x48,0x50,0xfd,0xed,0xb9,0xda, + 0x5e,0x15,0x46,0x57,0xa7,0x8d,0x9d,0x84, + 0x90,0xd8,0xab,0x00,0x8c,0xbc,0xd3,0x0a, + 0xf7,0xe4,0x58,0x05,0xb8,0xb3,0x45,0x06, + 0xd0,0x2c,0x1e,0x8f,0xca,0x3f,0x0f,0x02, + 0xc1,0xaf,0xbd,0x03,0x01,0x13,0x8a,0x6b, + 0x3a,0x91,0x11,0x41,0x4f,0x67,0xdc,0xea, + 0x97,0xf2,0xcf,0xce,0xf0,0xb4,0xe6,0x73, + 0x96,0xac,0x74,0x22,0xe7,0xad,0x35,0x85, + 0xe2,0xf9,0x37,0xe8,0x1c,0x75,0xdf,0x6e, + 0x47,0xf1,0x1a,0x71,0x1d,0x29,0xc5,0x89, + 0x6f,0xb7,0x62,0x0e,0xaa,0x18,0xbe,0x1b, + 0xfc,0x56,0x3e,0x4b,0xc6,0xd2,0x79,0x20, + 0x9a,0xdb,0xc0,0xfe,0x78,0xcd,0x5a,0xf4, + 0x1f,0xdd,0xa8,0x33,0x88,0x07,0xc7,0x31, + 0xb1,0x12,0x10,0x59,0x27,0x80,0xec,0x5f, + 0x60,0x51,0x7f,0xa9,0x19,0xb5,0x4a,0x0d, + 0x2d,0xe5,0x7a,0x9f,0x93,0xc9,0x9c,0xef, + 0xa0,0xe0,0x3b,0x4d,0xae,0x2a,0xf5,0xb0, + 0xc8,0xeb,0xbb,0x3c,0x83,0x53,0x99,0x61, + 0x17,0x2b,0x04,0x7e,0xba,0x77,0xd6,0x26, + 0xe1,0x69,0x14,0x63,0x55,0x21,0x0c,0x7d +}; + +static const unsigned char Rcon[30]= +{ + 0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80, + 0x1b,0x36,0x6c,0xd8,0xab,0x4d,0x9a,0x2f, + 0x5e,0xbc,0x63,0xc6,0x97,0x35,0x6a,0xd4, + 0xb3,0x7d,0xfa,0xef,0xc5,0x91, +}; + +/* Perform doubling in Galois Field GF(2^8) using the irreducible polynomial + x^8+x^4+x^3+x+1 */ +static unsigned char AES_xtime(uint32_t x) +{ + return (x&0x80) ? (x<<1)^0x1b : x<<1; +} + + +/** + * Encrypt a single block (16 bytes) of data + */ +void AES::encrypt(uint32_t *data) +{ + /* To make this code smaller, generate the sbox entries on the fly. + * This will have a really heavy effect upon performance. + */ + uint32_t tmp[4]; + uint32_t tmp1, old_a0, a0, a1, a2, a3, row; + int curr_rnd; + int rounds = _rounds; + const uint32_t *k = _ks; + + /* Pre-round key addition */ + for (row = 0; row < 4; row++) + data[row] ^= *(k++); + + /* Encrypt one block. */ + for (curr_rnd = 0; curr_rnd < rounds; curr_rnd++) + { + /* Perform ByteSub and ShiftRow operations together */ + for (row = 0; row < 4; row++) + { + a0 = (uint32_t)aes_sbox[(data[row%4]>>24)&0xFF]; + a1 = (uint32_t)aes_sbox[(data[(row+1)%4]>>16)&0xFF]; + a2 = (uint32_t)aes_sbox[(data[(row+2)%4]>>8)&0xFF]; + a3 = (uint32_t)aes_sbox[(data[(row+3)%4])&0xFF]; + + /* Perform MixColumn iff not last round */ + if (curr_rnd < (rounds - 1)) + { + tmp1 = a0 ^ a1 ^ a2 ^ a3; + old_a0 = a0; + a0 ^= tmp1 ^ AES_xtime(a0 ^ a1); + a1 ^= tmp1 ^ AES_xtime(a1 ^ a2); + a2 ^= tmp1 ^ AES_xtime(a2 ^ a3); + a3 ^= tmp1 ^ AES_xtime(a3 ^ old_a0); + } + + tmp[row] = ((a0 << 24) | (a1 << 16) | (a2 << 8) | a3); + } + + /* KeyAddition - note that it is vital that this loop is separate from + the MixColumn operation, which must be atomic...*/ + for (row = 0; row < 4; row++) + data[row] = tmp[row] ^ *(k++); + } +#if defined ESP8266 + ESP.wdtFeed(); +#endif +} + +/** + * Decrypt a single block (16 bytes) of data + */ +void AES::decrypt(uint32_t *data) +{ + uint32_t tmp[4]; + uint32_t xt0,xt1,xt2,xt3,xt4,xt5,xt6; + uint32_t a0, a1, a2, a3, row; + int curr_rnd; + int rounds = _rounds; + const uint32_t *k = _ks + ((rounds+1)*4); + + /* pre-round key addition */ + for (row=4; row > 0;row--) + data[row-1] ^= *(--k); + + /* Decrypt one block */ + for (curr_rnd = 0; curr_rnd < rounds; curr_rnd++) + { + /* Perform ByteSub and ShiftRow operations together */ + for (row = 4; row > 0; row--) + { + a0 = aes_isbox[(data[(row+3)%4]>>24)&0xFF]; + a1 = aes_isbox[(data[(row+2)%4]>>16)&0xFF]; + a2 = aes_isbox[(data[(row+1)%4]>>8)&0xFF]; + a3 = aes_isbox[(data[row%4])&0xFF]; + + /* Perform MixColumn iff not last round */ + if (curr_rnd<(rounds-1)) + { + /* The MDS cofefficients (0x09, 0x0B, 0x0D, 0x0E) + are quite large compared to encryption; this + operation slows decryption down noticeably. */ + xt0 = AES_xtime(a0^a1); + xt1 = AES_xtime(a1^a2); + xt2 = AES_xtime(a2^a3); + xt3 = AES_xtime(a3^a0); + xt4 = AES_xtime(xt0^xt1); + xt5 = AES_xtime(xt1^xt2); + xt6 = AES_xtime(xt4^xt5); + + xt0 ^= a1^a2^a3^xt4^xt6; + xt1 ^= a0^a2^a3^xt5^xt6; + xt2 ^= a0^a1^a3^xt4^xt6; + xt3 ^= a0^a1^a2^xt5^xt6; + tmp[row-1] = ((xt0<<24)|(xt1<<16)|(xt2<<8)|xt3); + } + else + tmp[row-1] = ((a0<<24)|(a1<<16)|(a2<<8)|a3); + } + + for (row = 4; row > 0; row--) + data[row-1] = tmp[row-1] ^ *(--k); + } +#if defined ESP8266 + ESP.wdtFeed(); +#endif +} + +AES::AES(const uint8_t *key, const uint8_t *iv, AES_MODE mode, CIPHER_MODE cipherMode) +{ + _cipherMode = cipherMode; + + int i, ii; + uint32_t *W, tmp, tmp2; + const unsigned char *ip; + int words; + + _arr_pad[0] = 0x01; + _arr_pad[1] = 0x02; + _arr_pad[2] = 0x03; + _arr_pad[3] = 0x04; + _arr_pad[4] = 0x05; + _arr_pad[5] = 0x06; + _arr_pad[6] = 0x07; + _arr_pad[7] = 0x08; + _arr_pad[8] = 0x09; + _arr_pad[9] = 0x0a; + _arr_pad[10] = 0x0b; + _arr_pad[11] = 0x0c; + _arr_pad[12] = 0x0d; + _arr_pad[13] = 0x0e; + _arr_pad[14] = 0x0f; + + switch (mode) + { + case AES_MODE_128: + i = 10; + words = 4; + break; + + case AES_MODE_256: + i = 14; + words = 8; + break; + + default: /* fail silently */ + return; + } + + _rounds = i; + _key_size = words; + W = _ks; + for (i = 0; i < words; i+=2) + { + W[i+0]= ((uint32_t)key[ 0]<<24)| + ((uint32_t)key[ 1]<<16)| + ((uint32_t)key[ 2]<< 8)| + ((uint32_t)key[ 3] ); + W[i+1]= ((uint32_t)key[ 4]<<24)| + ((uint32_t)key[ 5]<<16)| + ((uint32_t)key[ 6]<< 8)| + ((uint32_t)key[ 7] ); + key += 8; + } + + ip = Rcon; + ii = 4 * (_rounds+1); + for (i = words; i> 8)&0xff]<<16; + tmp2|=(uint32_t)aes_sbox[(tmp>>16)&0xff]<<24; + tmp2|=(uint32_t)aes_sbox[(tmp>>24) ]; + tmp=tmp2^(((unsigned int)*ip)<<24); + ip++; + } + + if ((words == 8) && ((i % words) == 4)) + { + tmp2 =(uint32_t)aes_sbox[(tmp )&0xff] ; + tmp2|=(uint32_t)aes_sbox[(tmp>> 8)&0xff]<< 8; + tmp2|=(uint32_t)aes_sbox[(tmp>>16)&0xff]<<16; + tmp2|=(uint32_t)aes_sbox[(tmp>>24) ]<<24; + tmp=tmp2; + } + + W[i]=W[i-words]^tmp; + } + + /* copy the iv across */ + memcpy(_iv, iv, 16); + + /* Do we need to convert the key */ + if (_cipherMode == CIPHER_DECRYPT) + { + convertKey(); + } +#if defined ESP8266 + ESP.wdtFeed(); +#endif +} + +int AES::getSize() +{ + return _size; +} + +void AES::setSize(int size) +{ + _size = size; +} + +int AES::calcSizeAndPad(int in_size) +{ + in_size++; // +1 for null terminater on input string + int buf = round(in_size / AES_BLOCKSIZE) * AES_BLOCKSIZE; + _size = (buf <= in_size) ? buf + AES_BLOCKSIZE : buf; + _pad_size = _size - in_size; + return _size; +} + +void AES::padPlaintext(const uint8_t* in, uint8_t* out) +{ + memcpy(out, in, _size); + for (int i = _size - _pad_size; i < _size; i++) + { + out[i] = _arr_pad[_pad_size - 1]; + } +} + +bool AES::checkPad(uint8_t* in, int lsize) +{ + if (in[lsize-1] <= 0x0f) + { + int lpad = (int)in[lsize-1]; + for (int i = lsize - 1; i >= lsize-lpad; i--) + { + if (_arr_pad[lpad - 1] != in[i]) + { + return false; + } + } + } + else + { + return true; + } + return true; +} + +void AES::processNoPad(const uint8_t *in, uint8_t *out, int length) +{ + if (_cipherMode == CIPHER_ENCRYPT) + { + encryptCBC(in, out, length); + } + else + { + decryptCBC(in, out, length); + } +} + +void AES::process(const uint8_t *in, uint8_t *out, int length) +{ + if (_cipherMode == CIPHER_ENCRYPT) + { + calcSizeAndPad(length); + uint8_t in_pad[getSize()]; + padPlaintext(in, in_pad); + encryptCBC(in_pad, out, getSize()); + } + else + { + decryptCBC(in, out, length); + } +} + +void AES::encryptCBC(const uint8_t *in, uint8_t *out, int length) +{ + int i; + uint32_t tin[4], tout[4], iv[4]; + + memcpy(iv, _iv, AES_IV_SIZE); + for (i = 0; i < 4; i++) + tout[i] = crypto_ntohl(iv[i]); + + for (length -= AES_BLOCKSIZE; length >= 0; length -= AES_BLOCKSIZE) + { + uint32_t msg_32[4]; + uint32_t out_32[4]; + memcpy(msg_32, in, AES_BLOCKSIZE); + in += AES_BLOCKSIZE; + + for (i = 0; i < 4; i++) + tin[i] = crypto_ntohl(msg_32[i])^tout[i]; + + AES::encrypt(tin); + + for (i = 0; i < 4; i++) + { + tout[i] = tin[i]; + out_32[i] = crypto_htonl(tout[i]); + } + + memcpy(out, out_32, AES_BLOCKSIZE); + out += AES_BLOCKSIZE; + } + + for (i = 0; i < 4; i++) + iv[i] = crypto_htonl(tout[i]); + memcpy(_iv, iv, AES_IV_SIZE); +#if defined ESP8266 + ESP.wdtFeed(); +#endif +} + +void AES::decryptCBC(const uint8_t *in, uint8_t *out, int length) +{ + int i; + uint32_t tin[4], bufxor[4], tout[4], data[4], iv[4]; + + memcpy(iv, _iv, AES_IV_SIZE); + for (i = 0; i < 4; i++) + bufxor[i] = crypto_ntohl(iv[i]); + + for (length -= 16; length >= 0; length -= 16) + { + uint32_t msg_32[4]; + uint32_t out_32[4]; + memcpy(msg_32, in, AES_BLOCKSIZE); + in += AES_BLOCKSIZE; + + for (i = 0; i < 4; i++) + { + tin[i] = crypto_ntohl(msg_32[i]); + data[i] = tin[i]; + } + + AES::decrypt(data); + + for (i = 0; i < 4; i++) + { + tout[i] = data[i] ^ bufxor[i]; + bufxor[i] = tin[i]; + out_32[i] = crypto_htonl(tout[i]); + } + + memcpy(out, out_32, AES_BLOCKSIZE); + out += AES_BLOCKSIZE; + } + + for (i = 0; i < 4; i++) + iv[i] = crypto_htonl(bufxor[i]); + memcpy(_iv, iv, AES_IV_SIZE); +#if defined ESP8266 + ESP.wdtFeed(); +#endif +} + +void AES::convertKey() +{ + int i; + uint32_t *k,w,t1,t2,t3,t4; + + k = _ks; + k += 4; + + for (i= _rounds*4; i > 4; i--) + { + w= *k; + w = inv_mix_col(w,t1,t2,t3,t4); + *k++ =w; + } +} + +#if defined ESP8266 || defined ESP32 +/** + * ESP8266 and ESP32 specific hardware true random number generator. + * + * Acording to the ESP32 documentation, you should not call the tRNG + * faster than 5MHz + * + */ + +void RNG::fill(uint8_t *dst, unsigned int length) +{ + // ESP8266 and ESP32 only + for (uint32_t i = 0; i < length; i++) + { + dst[i] = get(); + } +#if defined ESP8266 + ESP.wdtFeed(); +#endif +} + +byte RNG::get() +{ +#if defined ESP32 + // ESP32 only + uint32_t* randReg = (uint32_t*) 0x3FF75144; + return (byte) *randReg; +#elif defined ESP8266 + // ESP8266 only + uint32_t* randReg = (uint32_t*) 0x3FF20E44L; + return (byte) *randReg; +#else + // NOT SUPPORTED + return 0; +#endif +} + +uint32_t RNG::getLong() +{ +#if defined ESP32 + // ESP32 only + uint32_t* randReg = (uint32_t*) 0x3FF75144; + return (byte) *randReg; +#elif defined ESP8266 + // ESP8266 only + uint32_t* randReg = (uint32_t*) 0x3FF20E44L; + return *randReg; +#else + // NOT SUPPORTED + return 0; +#endif +} +#endif + + +/** + * SHA256 HMAC + */ + +SHA256HMAC::SHA256HMAC(const byte *key, unsigned int keyLen) +{ + // sort out the key + byte theKey[SHA256HMAC_BLOCKSIZE]; + memset(theKey, 0, SHA256HMAC_BLOCKSIZE); + if (keyLen > SHA256HMAC_BLOCKSIZE) + { + // take a hash of the key + SHA256 keyHahser; + keyHahser.doUpdate(key, keyLen); + keyHahser.doFinal(theKey); + } + else + { + // we already set the buffer to 0s, so just copy keyLen + // bytes from key + memcpy(theKey, key, keyLen); + } + // explicitly zero pads + memset(_innerKey, 0, SHA256HMAC_BLOCKSIZE); + memset(_outerKey, 0, SHA256HMAC_BLOCKSIZE); + // compute the keys + blockXor(theKey, _innerKey, HMAC_IPAD, SHA256HMAC_BLOCKSIZE); + blockXor(theKey, _outerKey, HMAC_OPAD, SHA256HMAC_BLOCKSIZE); + // start the intermediate hash + _hash.doUpdate(_innerKey, SHA256HMAC_BLOCKSIZE); +} + +void SHA256HMAC::doUpdate(const byte *msg, unsigned int len) +{ + _hash.doUpdate(msg, len); +} + +void SHA256HMAC::doFinal(byte *digest) +{ + // compute the intermediate hash + byte interHash[SHA256_SIZE]; + _hash.doFinal(interHash); + // compute the final hash + SHA256 finalHash; + finalHash.doUpdate(_outerKey, SHA256HMAC_BLOCKSIZE); + finalHash.doUpdate(interHash, SHA256_SIZE); + finalHash.doFinal(digest); +} + +bool SHA256HMAC::matches(const byte *expected) +{ + byte theDigest[SHA256_SIZE]; + doFinal(theDigest); + for (byte i = 0; i < SHA256_SIZE; i++) + { + if (expected[i] != theDigest[i]) + return false; + } + return true; +} + +void SHA256HMAC::blockXor(const byte *in, byte *out, byte val, byte len) +{ + for (byte i = 0; i < len; i++) + { + out[i] = in[i] ^ val; + } +} diff --git a/libraries/ESP8266WiFiMesh/src/Crypto.h b/libraries/ESP8266WiFiMesh/src/Crypto.h new file mode 100644 index 000000000..9df82251b --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/Crypto.h @@ -0,0 +1,254 @@ +/** + * An extremely minimal crypto library for Arduino devices. + * + * The SHA256 and AES implementations are derived from axTLS + * (http://axtls.sourceforge.net/), Copyright (c) 2008, Cameron Rich. + * + * Ported and refactored by Chris Ellis 2016. + * pkcs7 padding routines added by Mike Killewald Nov 26, 2017 (adopted from https://github.com/spaniakos/AES). + * + License + ======= + Balsa SCGI + Copyright (c) 2012, Chris Ellis + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef CRYPTO_h +#define CRYPTO_h + +#include + +#if defined ESP8266 +#include +#endif + +#define SHA256_SIZE 32 +#define SHA256HMAC_SIZE 32 +#define SHA256HMAC_BLOCKSIZE 64 +#define AES_MAXROUNDS 14 +#define AES_BLOCKSIZE 16 +#define AES_IV_SIZE 16 +#define AES_IV_LENGTH 16 +#define AES_128_KEY_LENGTH 16 +#define AES_256_KEY_LENGTH 16 + +/** + * Compute a SHA256 hash + */ +class SHA256 +{ + public: + SHA256(); + /** + * Update the hash with new data + */ + void doUpdate(const byte *msg, uint32_t len); + void doUpdate(const char *msg, unsigned int len) { doUpdate((byte*) msg, len); } + void doUpdate(const char *msg) { doUpdate((byte*) msg, strlen(msg)); } + /** + * Compute the final hash and store it in [digest], digest must be + * at least 32 bytes + */ + void doFinal(byte *digest); + /** + * Compute the final hash and check it matches this given expected hash + */ + bool matches(const byte *expected); + private: + void SHA256_Process(const byte digest[64]); + uint32_t total[2]; + uint32_t state[8]; + uint8_t buffer[64]; +}; + +#define HMAC_OPAD 0x5C +#define HMAC_IPAD 0x36 + +/** + * Compute a HMAC using SHA256 + */ +class SHA256HMAC +{ + public: + /** + * Compute a SHA256 HMAC with the given [key] key of [length] bytes + * for authenticity + */ + SHA256HMAC(const byte *key, unsigned int keyLen); + /** + * Update the hash with new data + */ + void doUpdate(const byte *msg, unsigned int len); + void doUpdate(const char *msg, unsigned int len) { doUpdate((byte*) msg, len); } + void doUpdate(const char *msg) { doUpdate((byte*) msg, strlen(msg)); } + /** + * Compute the final hash and store it in [digest], digest must be + * at least 32 bytes + */ + void doFinal(byte *digest); + /** + * Compute the final hash and check it matches this given expected hash + */ + bool matches(const byte *expected); + private: + void blockXor(const byte *in, byte *out, byte val, byte len); + SHA256 _hash; + byte _innerKey[SHA256HMAC_BLOCKSIZE]; + byte _outerKey[SHA256HMAC_BLOCKSIZE]; +}; + +/** + * AES 128 and 256, based on code from axTLS + */ +class AES +{ + public: + typedef enum + { + AES_MODE_128, + AES_MODE_256 + } AES_MODE; + typedef enum + { + CIPHER_ENCRYPT = 0x01, + CIPHER_DECRYPT = 0x02 + } CIPHER_MODE; + + /** + * Create this cipher instance in either encrypt or decrypt mode + * + * Use the given [key] which must be 16 bytes long for AES 128 and + * 32 bytes for AES 256 + * + * Use the given [iv] initialistion vection which must be 16 bytes long + * + * Use the either AES 128 or AES 256 as specified by [mode] + * + * Either encrypt or decrypt as specified by [cipherMode] + */ + AES(const uint8_t *key, const uint8_t *iv, AES_MODE mode, CIPHER_MODE cipherMode); + + /** + * Either encrypt or decrypt [in] and store into [out] for [length] bytes, applying no padding + * + * Note: the length must be a multiple of 16 bytes + */ + void processNoPad(const uint8_t *in, uint8_t *out, int length); + + /** + * Either encrypt or decrypt [in] and store into [out] for [length] bytes, applying padding as needed + * + * Note: the length must be a multiple of 16 bytes + */ + void process(const uint8_t *in, uint8_t *out, int length); + + /** Getter method for size + * + * This function returns the size + * @return an integer, that is the size of the of the padded plaintext, + * thus, the size of the ciphertext. + */ + int getSize(); + + /** Setter method for size + * + * This function sets the size of the plaintext+pad + * + */ + void setSize(int size); + + /** Calculates the size of the plaintext and the padding. + * + * Calculates the size of the plaintext with the size of the + * padding needed. Moreover it stores them in their class variables. + * + * @param in_size the size of the byte array ex sizeof(plaintext) + * @return an int the size of the plaintext plus the padding + */ + int calcSizeAndPad(int in_size); + + /** Pads the plaintext + * + * This function pads the plaintext and returns an char array with the + * plaintext and the padding in order for the plaintext to be compatible with + * 16bit size blocks required by AES + * + * @param in the string of the plaintext in a byte array + * @param out The string of the out array. + * @return no return, The padded plaintext is stored in the out pointer. + */ + void padPlaintext(const uint8_t* in, uint8_t* out); + + /** Check the if the padding is correct. + * + * This functions checks the padding of the plaintext. + * + * @param in the string of the plaintext in a byte array + * @param size the size of the string + * @return true if correct / false if not + */ + bool checkPad(uint8_t* in, int lsize); + + private: + void encryptCBC(const uint8_t *in, uint8_t *out, int length); + void decryptCBC(const uint8_t *in, uint8_t *out, int length); + void convertKey(); + void encrypt(uint32_t *data); + void decrypt(uint32_t *data); + uint16_t _rounds; + uint16_t _key_size; + uint32_t _ks[(AES_MAXROUNDS+1)*8]; + uint8_t _iv[AES_IV_SIZE]; + int _pad_size; // size of padding to add to plaintext + int _size; // size of plaintext plus padding to be ciphered + uint8_t _arr_pad[15]; + + CIPHER_MODE _cipherMode; +}; + +#if defined ESP8266 || defined ESP32 +/** + * ESP8266 and ESP32 specific true random number generator + */ +class RNG +{ + public: + /** + * Fill the [dst] array with [length] random bytes + */ + static void fill(uint8_t *dst, unsigned int length); + /** + * Get a random byte + */ + static byte get(); + /** + * Get a 32bit random number + */ + static uint32_t getLong(); + private: +}; +#endif + + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/ESP8266WiFiMesh.cpp b/libraries/ESP8266WiFiMesh/src/ESP8266WiFiMesh.cpp index fead562e6..12a41dd8e 100644 --- a/libraries/ESP8266WiFiMesh/src/ESP8266WiFiMesh.cpp +++ b/libraries/ESP8266WiFiMesh/src/ESP8266WiFiMesh.cpp @@ -18,6 +18,28 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ + + + + + + +/******************************************************************************************** +* NOTE! +* +* This class is deprecated and will be removed in core version 3.0.0. +* If you are still using this class, please consider migrating to the new API shown in +* the EspnowMeshBackend.h or TcpIpMeshBackend.h source files. +* +* TODO: delete this file. +********************************************************************************************/ + + + + + + + #include #include #include @@ -29,7 +51,6 @@ #define SERVER_IP_ADDR "192.168.4.1" const IPAddress ESP8266WiFiMesh::emptyIP = IPAddress(); -const uint32_t ESP8266WiFiMesh::lwipVersion203Signature[3] {2,0,3}; String ESP8266WiFiMesh::lastSSID = ""; bool ESP8266WiFiMesh::staticIPActivated = false; @@ -51,10 +72,8 @@ ESP8266WiFiMesh::~ESP8266WiFiMesh() ESP8266WiFiMesh::ESP8266WiFiMesh(ESP8266WiFiMesh::requestHandlerType requestHandler, ESP8266WiFiMesh::responseHandlerType responseHandler, ESP8266WiFiMesh::networkFilterType networkFilter, const String &meshPassword, const String &meshName, const String &nodeID, bool verboseMode, uint8 meshWiFiChannel, uint16_t serverPort) - : _server(serverPort), _lwipVersion{0, 0, 0} -{ - storeLwipVersion(); - + : _server(serverPort) +{ updateNetworkNames(meshName, (nodeID != "" ? nodeID : uint64ToString(ESP.getChipId()))); _requestHandler = requestHandler; _responseHandler = responseHandler; @@ -99,15 +118,10 @@ void ESP8266WiFiMesh::begin() if(!ESP8266WiFiMesh::getAPController()) // If there is no active AP controller WiFi.mode(WIFI_STA); // WIFI_AP_STA mode automatically sets up an AP, so we can't use that as default. - #ifdef ENABLE_STATIC_IP_OPTIMIZATION - if(atLeastLwipVersion(lwipVersion203Signature)) - { - verboseModePrint(F("lwIP version is at least 2.0.3. Static ip optimizations enabled.\n")); - } - else - { - verboseModePrint(F("lwIP version is less than 2.0.3. Static ip optimizations DISABLED.\n")); - } + #if LWIP_VERSION_MAJOR >= 2 + verboseModePrint(F("lwIP version is at least 2. Static ip optimizations enabled.\n")); + #else + verboseModePrint(F("lwIP version is less than 2. Static ip optimizations DISABLED.\n")); #endif } } @@ -455,25 +469,18 @@ transmission_status_t ESP8266WiFiMesh::connectToNode(const String &targetSSID, i { if(staticIPActivated && lastSSID != "" && lastSSID != targetSSID) // So we only do this once per connection, in case there is a performance impact. { - #ifdef ENABLE_STATIC_IP_OPTIMIZATION - if(atLeastLwipVersion(lwipVersion203Signature)) - { - // Can be used with Arduino core for ESP8266 version 2.4.2 or higher with lwIP2 enabled to keep static IP on even during network switches. - WiFiMode_t storedWiFiMode = WiFi.getMode(); - WiFi.mode(WIFI_OFF); - WiFi.mode(storedWiFiMode); - yield(); - } - else - { - // Disable static IP so that we can connect to other servers via DHCP (DHCP is slower but required for connecting to more than one server, it seems (possible bug?)). - disableStaticIP(); - verboseModePrint(F("\nConnecting to a different network. Static IP deactivated to make this possible.")); - } + #if LWIP_VERSION_MAJOR >= 2 + // Can be used with Arduino core for ESP8266 version 2.4.2 or higher with lwIP2 enabled to keep static IP on even during network switches. + WiFiMode_t storedWiFiMode = WiFi.getMode(); + WiFi.mode(WIFI_OFF); + WiFi.mode(storedWiFiMode); + yield(); + #else // Disable static IP so that we can connect to other servers via DHCP (DHCP is slower but required for connecting to more than one server, it seems (possible bug?)). disableStaticIP(); verboseModePrint(F("\nConnecting to a different network. Static IP deactivated to make this possible.")); + #endif } lastSSID = targetSSID; @@ -537,10 +544,9 @@ void ESP8266WiFiMesh::attemptTransmission(const String &message, bool concluding /* Scan for APs */ connectionQueue.clear(); - // If scanAllWiFiChannels is true or Arduino core for ESP8266 version < 2.4.2 scanning will cause the WiFi radio to cycle through all WiFi channels. + // If scanAllWiFiChannels is true scanning will cause the WiFi radio to cycle through all WiFi channels. // This means existing WiFi connections are likely to break or work poorly if done frequently. int n = 0; - #ifdef ENABLE_WIFI_SCAN_OPTIMIZATION if(scanAllWiFiChannels) { n = WiFi.scanNetworks(false, _scanHidden); @@ -550,9 +556,6 @@ void ESP8266WiFiMesh::attemptTransmission(const String &message, bool concluding // Scan function argument overview: scanNetworks(bool async = false, bool show_hidden = false, uint8 channel = 0, uint8* ssid = NULL) n = WiFi.scanNetworks(false, _scanHidden, _meshWiFiChannel); } - #else - n = WiFi.scanNetworks(false, _scanHidden); - #endif _networkFilter(n, *this); // Update the connectionQueue. } @@ -650,18 +653,18 @@ void ESP8266WiFiMesh::acceptRequest() if (!waitForClientTransmission(_client, _apModeTimeoutMs) || !_client.available()) { continue; } - + /* Read in request and pass it to the supplied requestHandler */ String request = _client.readStringUntil('\r'); yield(); _client.flush(); - + String response = _requestHandler(request, *this); /* Send the response back to the client */ if (_client.connected()) { - verboseModePrint("Responding"); // Not storing strings in flash (via F()) to avoid performance impacts when using the string. + verboseModePrint("Responding"); // Not storing strings in flash (via F()) to avoid performance impacts when using the string. _client.print(response + "\r"); _client.flush(); yield(); @@ -669,3 +672,15 @@ void ESP8266WiFiMesh::acceptRequest() } } } + + +void ESP8266WiFiMesh::verboseModePrint(const String &stringToPrint, bool newline) +{ + if(_verboseMode) + { + if(newline) + Serial.println(stringToPrint); + else + Serial.print(stringToPrint); + } +} diff --git a/libraries/ESP8266WiFiMesh/src/ESP8266WiFiMesh.h b/libraries/ESP8266WiFiMesh/src/ESP8266WiFiMesh.h index ceca8f0ff..7c5e90838 100644 --- a/libraries/ESP8266WiFiMesh/src/ESP8266WiFiMesh.h +++ b/libraries/ESP8266WiFiMesh/src/ESP8266WiFiMesh.h @@ -18,6 +18,28 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ + + + + + + +/******************************************************************************************** +* NOTE! +* +* This class is deprecated and will be removed in core version 3.0.0. +* If you are still using this class, please consider migrating to the new API shown in +* the EspnowMeshBackend.h or TcpIpMeshBackend.h source files. +* +* TODO: delete this file. +********************************************************************************************/ + + + + + + + #ifndef __WIFIMESH_H__ #define __WIFIMESH_H__ @@ -28,9 +50,6 @@ #include "NetworkInfo.h" #include "TransmissionResult.h" -#define ENABLE_STATIC_IP_OPTIMIZATION // Requires Arduino core for ESP8266 version 2.4.2 or higher and lwIP2 (lwIP can be changed in "Tools" menu of Arduino IDE). -#define ENABLE_WIFI_SCAN_OPTIMIZATION // Requires Arduino core for ESP8266 version 2.4.2 or higher. Scan time should go from about 2100 ms to around 60 ms if channel 1 (standard) is used. - const String WIFI_MESH_EMPTY_STRING = ""; class ESP8266WiFiMesh { @@ -44,8 +63,6 @@ private: uint8 _meshWiFiChannel; bool _verboseMode; WiFiServer _server; - uint32_t _lwipVersion[3]; - static const uint32_t lwipVersion203Signature[3]; String _message = WIFI_MESH_EMPTY_STRING; bool _scanHidden = false; bool _apHidden = false; @@ -56,6 +73,7 @@ private: static String lastSSID; static bool staticIPActivated; + bool useStaticIP; static IPAddress staticIP; static IPAddress gateway; static IPAddress subnetMask; @@ -78,8 +96,6 @@ private: bool waitForClientTransmission(WiFiClient &currClient, uint32_t maxWait); transmission_status_t attemptDataTransfer(); transmission_status_t attemptDataTransferKernel(); - void storeLwipVersion(); - bool atLeastLwipVersion(const uint32_t minLwipVersion[3]); @@ -133,7 +149,7 @@ public: //////////////////////////// TODO: REMOVE IN 2.5.0//////////////////////////// ~ESP8266WiFiMesh(); - + /** * WiFiMesh Constructor method. Creates a WiFi Mesh Node, ready to be initialised. * diff --git a/libraries/ESP8266WiFiMesh/src/EncryptedConnectionData.cpp b/libraries/ESP8266WiFiMesh/src/EncryptedConnectionData.cpp new file mode 100644 index 000000000..ae30afac9 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/EncryptedConnectionData.cpp @@ -0,0 +1,181 @@ +/* + * 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 "EncryptedConnectionData.h" +#include "UtilityFunctions.h" +#include "TypeConversionFunctions.h" +#include "JsonTranslator.h" + +using EspnowProtocolInterpreter::espnowHashKeyLength; + +EncryptedConnectionData::EncryptedConnectionData(const uint8_t peerStaMac[6], const uint8_t peerApMac[6], uint64_t peerSessionKey, uint64_t ownSessionKey, const uint8_t hashKey[espnowHashKeyLength]) + : _peerSessionKey(peerSessionKey), _ownSessionKey(ownSessionKey) +{ + std::copy_n(peerStaMac, 6, _peerStaMac); + std::copy_n(peerApMac, 6, _peerApMac); + std::copy_n(hashKey, espnowHashKeyLength, _hashKey); +} + +EncryptedConnectionData::EncryptedConnectionData(const uint8_t peerStaMac[6], const uint8_t peerApMac[6], uint64_t peerSessionKey, uint64_t ownSessionKey, uint32_t duration, const uint8_t hashKey[espnowHashKeyLength]) + : EncryptedConnectionData(peerStaMac, peerApMac, peerSessionKey, ownSessionKey, hashKey) +{ + setRemainingDuration(duration); +} + +EncryptedConnectionData::EncryptedConnectionData(const EncryptedConnectionData &other) + : _peerSessionKey(other.getPeerSessionKey()), _ownSessionKey(other.getOwnSessionKey()), _desync(other.desync()), + _timeTracker(other.temporary() ? new ExpiringTimeTracker(*other.temporary()) : nullptr) +{ + other.getPeerStaMac(_peerStaMac); + other.getPeerApMac(_peerApMac); + other.getHashKey(_hashKey); +} + +EncryptedConnectionData & EncryptedConnectionData::operator=(const EncryptedConnectionData &other) +{ + if(this != &other) + { + other.getPeerStaMac(_peerStaMac); + other.getPeerApMac(_peerApMac); + _peerSessionKey = other.getPeerSessionKey(); + _ownSessionKey = other.getOwnSessionKey(); + other.getHashKey(_hashKey); + _desync = other.desync(); + _timeTracker = std::unique_ptr(other.temporary() ? new ExpiringTimeTracker(*other.temporary()) : nullptr); + } + return *this; +} + +uint8_t *EncryptedConnectionData::getEncryptedPeerMac(uint8_t *resultArray) const +{ + return getPeerStaMac(resultArray); +} + +uint8_t *EncryptedConnectionData::getUnencryptedPeerMac(uint8_t *resultArray) const +{ + return getPeerApMac(resultArray); +} + +uint8_t *EncryptedConnectionData::getPeerStaMac(uint8_t *resultArray) const +{ + std::copy_n(_peerStaMac, 6, resultArray); + return resultArray; +} + +uint8_t *EncryptedConnectionData::getPeerApMac(uint8_t *resultArray) const +{ + std::copy_n(_peerApMac, 6, resultArray); + return resultArray; +} + +bool EncryptedConnectionData::connectedTo(const uint8_t *peerMac) const +{ + if(macEqual(peerMac, _peerStaMac) || macEqual(peerMac, _peerApMac)) + { + return true; + } + else + { + return false; + } +} + +void EncryptedConnectionData::setHashKey(const uint8_t hashKey[espnowHashKeyLength]) +{ + assert(hashKey != nullptr); + + std::copy_n(hashKey, espnowHashKeyLength, _hashKey); +} + +uint8_t *EncryptedConnectionData::getHashKey(uint8_t *resultArray) const +{ + std::copy_n(_hashKey, espnowHashKeyLength, resultArray); + return resultArray; +} + +void EncryptedConnectionData::setPeerSessionKey(uint64_t sessionKey) { _peerSessionKey = sessionKey; } +uint64_t EncryptedConnectionData::getPeerSessionKey() const { return _peerSessionKey; } + +void EncryptedConnectionData::setOwnSessionKey(uint64_t sessionKey) { _ownSessionKey = sessionKey; } +uint64_t EncryptedConnectionData::getOwnSessionKey() const { return _ownSessionKey; } + +uint64_t EncryptedConnectionData::incrementSessionKey(uint64_t sessionKey, const uint8_t *hashKey, uint8_t hashKeyLength) +{ + String hmac = JsonTranslator::createHmac(uint64ToString(sessionKey), hashKey, hashKeyLength); + + /* HMAC truncation should be OK since hmac sha256 is a PRF and we are truncating to the leftmost (MSB) bits. + PRF: https://crypto.stackexchange.com/questions/26410/whats-the-gcm-sha-256-of-a-tls-protocol/26434#26434 + Truncate to leftmost bits: https://tools.ietf.org/html/rfc2104#section-5 */ + uint64_t newLeftmostBits = strtoul(hmac.substring(0, 8).c_str(), nullptr, HEX); // strtoul stops reading input when an invalid character is discovered. + if(newLeftmostBits == 0) + newLeftmostBits = RANDOM_REG32 | (1 << 31); // We never want newLeftmostBits == 0 since that would indicate an unencrypted transmission. + + uint64_t newRightmostBits = (uint32_t)(sessionKey + 1); + + return (newLeftmostBits << 32) | newRightmostBits; +} + +void EncryptedConnectionData::incrementOwnSessionKey() +{ + setOwnSessionKey(incrementSessionKey(getOwnSessionKey(), _hashKey, EspnowProtocolInterpreter::espnowHashKeyLength)); +} + +void EncryptedConnectionData::setDesync(bool desync) { _desync = desync; } +bool EncryptedConnectionData::desync() const { return _desync; } + +String EncryptedConnectionData::serialize() const +{ + // Returns: {"connectionState":{"duration":"123","password":"abc","ownSessionKey":"1A2","peerSessionKey":"3B4","peerStaMac":"F2","peerApMac":"E3"}} + + return + "{\"connectionState\":{" + + (temporary() ? JsonTranslator::jsonDuration + "\"" + String(temporary()->remainingDuration()) + "\"," : "") + + JsonTranslator::jsonDesync + "\"" + String(desync()) + "\"," + + JsonTranslator::jsonOwnSessionKey + "\"" + uint64ToString(getOwnSessionKey()) + "\"," + + JsonTranslator::jsonPeerSessionKey + "\"" + uint64ToString(getPeerSessionKey()) + "\"," + + JsonTranslator::jsonPeerStaMac + "\"" + macToString(_peerStaMac) + "\"," + + JsonTranslator::jsonPeerApMac + "\"" + macToString(_peerApMac) + "\"}}"; +} + +const ExpiringTimeTracker *EncryptedConnectionData::temporary() const +{ + return _timeTracker.get(); +} + +void EncryptedConnectionData::setRemainingDuration(uint32_t remainingDuration) +{ + if(!_timeTracker) + { + _timeTracker = std::unique_ptr(new ExpiringTimeTracker(remainingDuration)); // TODO: Change to std::make_unique(remainingDuration); once compiler fully supports C++14 + } + else + { + _timeTracker->setRemainingDuration(remainingDuration); + } +} + +void EncryptedConnectionData::removeDuration() +{ + _timeTracker = nullptr; +} diff --git a/libraries/ESP8266WiFiMesh/src/EncryptedConnectionData.h b/libraries/ESP8266WiFiMesh/src/EncryptedConnectionData.h new file mode 100644 index 000000000..6b008b99a --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/EncryptedConnectionData.h @@ -0,0 +1,96 @@ +/* + * 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 __ESPNOWENCRYPTEDCONNECTIONDATA_H__ +#define __ESPNOWENCRYPTEDCONNECTIONDATA_H__ + +#include "ExpiringTimeTracker.h" +#include "EspnowProtocolInterpreter.h" +#include +#include + +class EncryptedConnectionData { + +public: + + virtual ~EncryptedConnectionData() = default; + + EncryptedConnectionData(const uint8_t peerStaMac[6], const uint8_t peerApMac[6], uint64_t peerSessionKey, uint64_t ownSessionKey, + const uint8_t hashKey[EspnowProtocolInterpreter::espnowHashKeyLength]); + EncryptedConnectionData(const uint8_t peerStaMac[6], const uint8_t peerApMac[6], uint64_t peerSessionKey, uint64_t ownSessionKey, + uint32_t duration, const uint8_t hashKey[EspnowProtocolInterpreter::espnowHashKeyLength]); + + EncryptedConnectionData(const EncryptedConnectionData &other); + + EncryptedConnectionData & operator=(const EncryptedConnectionData &other); + + /** + * @param resultArray An uint8_t array with at least size 6. + * + * @returns The interface MAC used for communicating with the peer. + */ + uint8_t *getEncryptedPeerMac(uint8_t *resultArray) const; + uint8_t *getUnencryptedPeerMac(uint8_t *resultArray) const; + + // @param resultArray At least size 6. + uint8_t *getPeerStaMac(uint8_t *resultArray) const; + uint8_t *getPeerApMac(uint8_t *resultArray) const; + + bool connectedTo(const uint8_t *peerMac) const; + + void setHashKey(const uint8_t hashKey[EspnowProtocolInterpreter::espnowHashKeyLength]); + // @param resultArray At least size espnowHashKeyLength. + uint8_t *getHashKey(uint8_t *resultArray) const; + + void setPeerSessionKey(uint64_t sessionKey); + uint64_t getPeerSessionKey() const; + void setOwnSessionKey(uint64_t sessionKey); + uint64_t getOwnSessionKey() const; + + static uint64_t incrementSessionKey(uint64_t sessionKey, const uint8_t *hashKey, uint8_t hashKeyLength); + void incrementOwnSessionKey(); + + void setDesync(bool desync); + bool desync() const; + + // Note that the espnowEncryptionKey, espnowEncryptionKok and espnowHashKey are not serialized. + // These will be set to the values of the EspnowMeshBackend instance that is adding the serialized encrypted connection. + String serialize() const; + + const ExpiringTimeTracker *temporary() const; + virtual void setRemainingDuration(uint32_t remainingDuration); + virtual void removeDuration(); + +private: + + uint8_t _peerStaMac[6] {0}; + uint8_t _peerApMac[6] {0}; + uint64_t _peerSessionKey; + uint64_t _ownSessionKey; + uint8_t _hashKey[EspnowProtocolInterpreter::espnowHashKeyLength] {0}; + bool _desync = false; + std::unique_ptr _timeTracker = nullptr; +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/EncryptedConnectionLog.cpp b/libraries/ESP8266WiFiMesh/src/EncryptedConnectionLog.cpp new file mode 100644 index 000000000..45bcd1dd0 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/EncryptedConnectionLog.cpp @@ -0,0 +1,92 @@ +/* + * 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 "EncryptedConnectionLog.h" + +using EspnowProtocolInterpreter::espnowHashKeyLength; + +EncryptedConnectionLog::EncryptedConnectionLog(const uint8_t peerStaMac[6], const uint8_t peerApMac[6], uint64_t peerSessionKey, uint64_t ownSessionKey, const uint8_t hashKey[espnowHashKeyLength]) + : EncryptedConnectionData(peerStaMac, peerApMac, peerSessionKey, ownSessionKey, hashKey) +{ } + +EncryptedConnectionLog::EncryptedConnectionLog(const uint8_t peerStaMac[6], const uint8_t peerApMac[6], uint64_t peerSessionKey, uint64_t ownSessionKey, uint32_t duration, const uint8_t hashKey[espnowHashKeyLength]) + : EncryptedConnectionData(peerStaMac, peerApMac, peerSessionKey, ownSessionKey, duration, hashKey) +{ } + +std::unique_ptr EncryptedConnectionLog::_soonestExpiringConnectionTracker = nullptr; + +bool EncryptedConnectionLog::_newRemovalsScheduled = false; + +void EncryptedConnectionLog::setRemainingDuration(uint32_t remainingDuration) +{ + EncryptedConnectionData::setRemainingDuration(remainingDuration); + + setScheduledForRemoval(false); + + updateSoonestExpiringConnectionTracker(remainingDuration); +} + +void EncryptedConnectionLog::removeDuration() +{ + EncryptedConnectionData::removeDuration(); + setScheduledForRemoval(false); +} + +void EncryptedConnectionLog::scheduleForRemoval() +{ + // When we give the connection 0 remaining duration it will be removed during the next performEspnowMaintainance() call. + // Duration must be changed before setting the scheduledForRemoval flag to true, since the flag is otherwise cleared. + setRemainingDuration(0); + setScheduledForRemoval(true); +} + +void EncryptedConnectionLog::setScheduledForRemoval(bool scheduledForRemoval) +{ + _scheduledForRemoval = scheduledForRemoval; + + if(scheduledForRemoval) + setNewRemovalsScheduled(true); +} +bool EncryptedConnectionLog::removalScheduled() const { return _scheduledForRemoval; } + +void EncryptedConnectionLog::setNewRemovalsScheduled(bool newRemovalsScheduled) { _newRemovalsScheduled = newRemovalsScheduled; } +bool EncryptedConnectionLog::newRemovalsScheduled( ){ return _newRemovalsScheduled; } + +const ExpiringTimeTracker *EncryptedConnectionLog::getSoonestExpiringConnectionTracker() +{ + return _soonestExpiringConnectionTracker.get(); +} + +void EncryptedConnectionLog::updateSoonestExpiringConnectionTracker(uint32_t remainingDuration) +{ + if(!getSoonestExpiringConnectionTracker() || remainingDuration < getSoonestExpiringConnectionTracker()->remainingDuration()) + { + _soonestExpiringConnectionTracker = std::unique_ptr(new ExpiringTimeTracker(remainingDuration)); // TODO: Change to std::make_unique(remainingDuration); once compiler fully supports C++14 + } +} + +void EncryptedConnectionLog::clearSoonestExpiringConnectionTracker() +{ + _soonestExpiringConnectionTracker = nullptr; +} diff --git a/libraries/ESP8266WiFiMesh/src/EncryptedConnectionLog.h b/libraries/ESP8266WiFiMesh/src/EncryptedConnectionLog.h new file mode 100644 index 000000000..2c1a99154 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/EncryptedConnectionLog.h @@ -0,0 +1,68 @@ +/* + * 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 __ESPNOWENCRYPTEDCONNECTIONLOG_H__ +#define __ESPNOWENCRYPTEDCONNECTIONLOG_H__ + +#include "EncryptedConnectionData.h" +#include "EspnowProtocolInterpreter.h" + +class EncryptedConnectionLog : public EncryptedConnectionData { + +public: + + EncryptedConnectionLog(const uint8_t peerStaMac[6], const uint8_t peerApMac[6], uint64_t peerSessionKey, uint64_t ownSessionKey, + const uint8_t hashKey[EspnowProtocolInterpreter::espnowHashKeyLength]); + EncryptedConnectionLog(const uint8_t peerStaMac[6], const uint8_t peerApMac[6], uint64_t peerSessionKey, uint64_t ownSessionKey, + uint32_t duration, const uint8_t hashKey[EspnowProtocolInterpreter::espnowHashKeyLength]); + + // Only guaranteed to expire at the latest when the soonestExpiringConnection does. Can expire before the soonestExpiringConnection since it is not updated on connection removal. + // Needs to be a copy to avoid invalidation during operations on temporaryEncryptedConnections. + static std::unique_ptr _soonestExpiringConnectionTracker; + + // Only indicates if at least one removal was scheduled since the flag was last cleared, not if the removal is still scheduled to happen. + // Canceling a removal will not update the flag. + static bool _newRemovalsScheduled; + + // Can be used to set a duration both for temporary and permanent encrypted connections (transforming the latter into a temporary connection in the process). + void setRemainingDuration(uint32_t remainingDuration) override; + void removeDuration() override; + + void scheduleForRemoval(); + bool removalScheduled() const; + + static void setNewRemovalsScheduled(bool newRemovalsScheduled); + static bool newRemovalsScheduled(); + + static const ExpiringTimeTracker *getSoonestExpiringConnectionTracker(); + static void updateSoonestExpiringConnectionTracker(uint32_t remainingDuration); + static void clearSoonestExpiringConnectionTracker(); + +private: + + bool _scheduledForRemoval = false; + void setScheduledForRemoval(bool scheduledForRemoval); +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.cpp b/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.cpp new file mode 100644 index 000000000..7bcca0d57 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.cpp @@ -0,0 +1,2295 @@ +/* + EspnowMeshBackend + + Copyright (C) 2019 Anders Löfgren + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include +extern "C" { + #include +} + +#include "EspnowMeshBackend.h" +#include "TypeConversionFunctions.h" +#include "UtilityFunctions.h" +#include "MutexTracker.h" +#include "JsonTranslator.h" +#include "Crypto.h" + +using EspnowProtocolInterpreter::espnowEncryptionKeyLength; +using EspnowProtocolInterpreter::espnowHashKeyLength; + +static const uint8_t maxEncryptedConnections = 6; // This is limited by the ESP-NOW API. Max 6 in AP or AP+STA mode. Max 10 in STA mode. See "ESP-NOW User Guide" for more info. + +static const uint64_t uint64MSB = 0x8000000000000000; + +bool EspnowMeshBackend::_espnowTransmissionMutex = false; + +EspnowMeshBackend *EspnowMeshBackend::_espnowRequestManager = nullptr; + +std::map, MessageData> EspnowMeshBackend::receivedEspnowTransmissions = {}; +std::map, RequestData> EspnowMeshBackend::sentRequests = {}; +std::map, TimeTracker> EspnowMeshBackend::receivedRequests = {}; + +std::list EspnowMeshBackend::responsesToSend = {}; +std::list EspnowMeshBackend::peerRequestConfirmationsToSend = {}; + +std::vector EspnowMeshBackend::encryptedConnections = {}; + +uint32_t EspnowMeshBackend::_espnowTransmissionTimeoutMs = 40; +uint32_t EspnowMeshBackend::_espnowRetransmissionIntervalMs = 15; + +uint32_t EspnowMeshBackend::_encryptionRequestTimeoutMs = 500; + +bool EspnowMeshBackend::_espnowSendConfirmed = false; + +String EspnowMeshBackend::_ongoingPeerRequestNonce = ""; +EspnowMeshBackend *EspnowMeshBackend::_ongoingPeerRequester = nullptr; +encrypted_connection_status_t EspnowMeshBackend::_ongoingPeerRequestResult = ECS_MAX_CONNECTIONS_REACHED_SELF; +uint32_t EspnowMeshBackend::_ongoingPeerRequestEncryptionStart = 0; + +uint8_t EspnowMeshBackend::_espnowEncryptionKok[espnowEncryptionKeyLength] = { 0 }; +bool EspnowMeshBackend::_espnowEncryptionKokSet = false; + +uint32_t EspnowMeshBackend::_unencryptedMessageID = 0; + +// _logEntryLifetimeMs is based on someone storing 40 responses of 750 bytes each = 30 000 bytes (roughly full memory), +// which takes 2000 ms + some margin to send. Also, we want to avoid old entries taking up memory if they cannot be sent, +// so storage duration should not be too long. +uint32_t EspnowMeshBackend::_logEntryLifetimeMs = 2500; +uint32_t EspnowMeshBackend::_responseTimeoutMs = 5000; +uint32_t EspnowMeshBackend::_timeOfLastLogClear = 0; +uint32_t EspnowMeshBackend::_criticalHeapLevel = 6000; // In bytes +uint32_t EspnowMeshBackend::_criticalHeapLevelBuffer = 6000; // In bytes + +uint8_t EspnowMeshBackend::_maxTransmissionsPerMessage = 3; + +bool EspnowMeshBackend::_espnowSendToNodeMutex = false; +uint8_t EspnowMeshBackend::_transmissionTargetBSSID[6] = {0}; + +double EspnowMeshBackend::_transmissionsTotal = 0; +double EspnowMeshBackend::_transmissionsFailed = 0; + +bool EspnowMeshBackend::_staticVerboseMode = false; + +void espnowDelay(uint32_t durationMs) +{ + uint32_t startingTime = millis(); + + while(millis() - startingTime < durationMs) + { + delay(1); + EspnowMeshBackend::performEspnowMaintainance(); + } +} + +EspnowMeshBackend::EspnowMeshBackend(requestHandlerType requestHandler, responseHandlerType responseHandler, + networkFilterType networkFilter, const String &meshPassword, const uint8_t espnowEncryptionKey[espnowEncryptionKeyLength], + const uint8_t espnowHashKey[espnowHashKeyLength], const String &ssidPrefix, const String &ssidSuffix, bool verboseMode, + uint8 meshWiFiChannel) + : MeshBackendBase(requestHandler, responseHandler, networkFilter, MB_ESP_NOW) +{ + // Reserve the maximum possible usage early on to prevent heap fragmentation later. + encryptedConnections.reserve(maxEncryptedConnections); + + setSSID(ssidPrefix, "", ssidSuffix); + setMeshPassword(meshPassword); + setEspnowEncryptionKey(espnowEncryptionKey); + setEspnowHashKey(espnowHashKey); + setVerboseModeState(verboseMode); + setWiFiChannel(meshWiFiChannel); +} + +EspnowMeshBackend::~EspnowMeshBackend() +{ + if(isEspnowRequestManager()) + { + setEspnowRequestManager(nullptr); + } + + deleteSentRequestsByOwner(this); +} + +void EspnowMeshBackend::begin() +{ + if(!getAPController()) // If there is no active AP controller + WiFi.mode(WIFI_STA); // WIFI_AP_STA mode automatically sets up an AP, so we can't use that as default. + + activateEspnow(); +} + +void EspnowMeshBackend::performEspnowMaintainance() +{ + // Doing this during an ESP-NOW transmission could invalidate iterators + MutexTracker mutexTracker(_espnowTransmissionMutex, handlePostponedRemovals); + if(!mutexTracker.mutexCaptured()) + { + assert(false && "ERROR! Transmission in progress. Don't call performEspnowMaintainance from callbacks as this may corrupt program state! Aborting."); + return; + } + + if(millis() - _timeOfLastLogClear >= 500) // Clearing too frequently will cause a lot of unnecessary container iterations. + { + clearOldLogEntries(); + } + if(EncryptedConnectionLog::getSoonestExpiringConnectionTracker() && EncryptedConnectionLog::getSoonestExpiringConnectionTracker()->expired()) + { + updateTemporaryEncryptedConnections(); + } + + sendEspnowResponses(); +} + +void EspnowMeshBackend::updateTemporaryEncryptedConnections(bool scheduledRemovalOnly) +{ + EncryptedConnectionLog::clearSoonestExpiringConnectionTracker(); + + for(auto connectionIterator = encryptedConnections.begin(); connectionIterator != encryptedConnections.end(); ) + { + if(auto timeTrackerPointer = connectionIterator->temporary()) + { + if(timeTrackerPointer->expired() && (!scheduledRemovalOnly || connectionIterator->removalScheduled())) + { + uint8_t macArray[6] = { 0 }; + removeEncryptedConnectionUnprotected(connectionIterator->getEncryptedPeerMac(macArray), &connectionIterator); + continue; + } + else + { + EncryptedConnectionLog::updateSoonestExpiringConnectionTracker(timeTrackerPointer->remainingDuration()); + } + } + assert(!connectionIterator->removalScheduled()); // timeTracker should always exist and be expired if removal is scheduled. + + ++connectionIterator; + } + + EncryptedConnectionLog::setNewRemovalsScheduled(false); +} + +void EspnowMeshBackend::espnowReceiveCallbackWrapper(uint8_t *macaddr, uint8_t *dataArray, uint8_t len) +{ + using namespace EspnowProtocolInterpreter; + + if(len >= EspnowProtocolInterpreter::espnowProtocolBytesSize()) // If we do not receive at least the protocol bytes, the transmission is invalid. + { + //uint32_t callbackStart = millis(); + + // If there is a espnowRequestManager, get it + EspnowMeshBackend *currentEspnowRequestManager = getEspnowRequestManager(); + + char messageType = espnowGetMessageType(dataArray); + uint64_t receivedMessageID = espnowGetMessageID(dataArray); + + if(currentEspnowRequestManager && !currentEspnowRequestManager->acceptsUnencryptedRequests() + && !usesConstantSessionKey(messageType) && !verifyPeerSessionKey(receivedMessageID, macaddr, messageType)) + { + return; + } + + uint64_t uint64StationMac = macToUint64(macaddr); + bool transmissionEncrypted = usesEncryption(receivedMessageID); + + // Useful when debugging the protocol + //Serial.print("Received from Mac: " + macToString(macaddr) + " ID: " + uint64ToString(receivedMessageID)); + //Serial.println(transmissionEncrypted ? " Encrypted" : " Unencrypted"); + + if(messageType == 'Q') // Question (request) + { + if(ESP.getFreeHeap() <= criticalHeapLevel()) + { + warningPrint("WARNING! Free heap below critical level. Suspending ESP-NOW request processing until the situation improves."); + return; + } + + if(currentEspnowRequestManager) + { + if(!requestReceived(uint64StationMac, receivedMessageID)) // If the request has not already been received + { + if(transmissionEncrypted) + { + EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(macaddr); + + if(!encryptedConnection || (!synchronizePeerSessionKey(receivedMessageID, *encryptedConnection) && + !verifyPeerSessionKey(receivedMessageID, *encryptedConnection, uint64StationMac, messageType))) + { + // We received an encrypted transmission + // and we have no encrypted connection to the transmitting node (in which case we want to avoid sending the secret session key back in an unencrypted response) + // or the transmission has the wrong session key + // and it doesn't have a session key that matches any multi-part transmission we are currently receiving (in which case the transmission is invalid). + return; + } + } + + //Serial.println("espnowReceiveCallbackWrapper before internal callback " + String(millis() - callbackStart)); + + currentEspnowRequestManager->espnowReceiveCallback(macaddr, dataArray, len); + } + } + } + else if(messageType == 'A') // Answer (response) + { + EspnowMeshBackend *requestSender = nullptr; + uint64_t requestMac = 0; + + if(transmissionEncrypted) + { + // An encrypted transmission can only be sent to the station interface, since it otherwise won't arrive (because of ESP_NOW_ROLE_CONTROLLER). + requestMac = uint64StationMac; + requestSender = getOwnerOfSentRequest(requestMac, receivedMessageID); + } + else + { + // An unencrypted transmission was probably sent to the AP interface as a result of a scan. + requestMac = espnowGetTransmissionMac(dataArray); + requestSender = getOwnerOfSentRequest(requestMac, receivedMessageID); + + // But if not, also check if it was sent to the station interface. + if(!requestSender) + { + requestMac = uint64StationMac; + requestSender = getOwnerOfSentRequest(requestMac, receivedMessageID); + } + } + + // If this node sent the request and it has not already been answered. + if(requestSender) + { + uint8_t macArray[6] = { 0 }; + + requestSender->espnowReceiveCallback(uint64ToMac(requestMac, macArray), dataArray, len); + } + } + else if(messageType == 'S') // Synchronization request + { + synchronizePeerSessionKey(receivedMessageID, macaddr); + } + else if(messageType == 'P') // Peer request + { + handlePeerRequest(macaddr, dataArray, len, uint64StationMac, receivedMessageID); + } + else if(messageType == 'C') // peer request Confirmation + { + handlePeerRequestConfirmation(macaddr, dataArray, len); + } + else + { + assert(messageType == 'Q' || messageType == 'A' || messageType == 'S' || messageType == 'P' || messageType == 'C'); + } + + //Serial.println("espnowReceiveCallbackWrapper duration " + String(millis() - callbackStart)); + } +} + +void EspnowMeshBackend::handlePeerRequest(uint8_t *macaddr, uint8_t *dataArray, uint8_t len, uint64_t uint64StationMac, uint64_t receivedMessageID) +{ + // Pairing process ends when encryptedConnectionVerificationHeader is received, maxConnectionsReachedHeader is sent or timeout is reached. + // Pairing process stages for request receiver: + // Receive: encryptionRequestHeader or temporaryEncryptionRequestHeader. + // Send: maxConnectionsReachedHeader / basicConnectionInfoHeader -> encryptedConnectionInfoHeader or maxConnectionsReachedHeader. + // Receive: encryptedConnectionVerificationHeader. + + using namespace EspnowProtocolInterpreter; + + if(!requestReceived(uint64StationMac, receivedMessageID)) + { + storeReceivedRequest(uint64StationMac, receivedMessageID, TimeTracker(millis())); + + bool encryptedCorrectly = synchronizePeerSessionKey(receivedMessageID, macaddr); + String message = espnowGetMessageContent(dataArray, len); + int32_t messageHeaderEndIndex = message.indexOf(':'); + String messageHeader = message.substring(0, messageHeaderEndIndex + 1); + + if(messageHeader == encryptedConnectionVerificationHeader) + { + if(encryptedCorrectly) + { + int32_t connectionRequestTypeEndIndex = message.indexOf(':', messageHeaderEndIndex + 1); + String connectionRequestType = message.substring(messageHeaderEndIndex + 1, connectionRequestTypeEndIndex + 1); + + if(connectionRequestType == encryptionRequestHeader) + { + temporaryEncryptedConnectionToPermanent(macaddr); + } + else if(connectionRequestType == temporaryEncryptionRequestHeader) + { + connectionLogIterator encryptedConnection = connectionLogEndIterator(); + if(!getEncryptedConnectionIterator(macaddr, encryptedConnection)) + assert(false && "We must have an encrypted connection if we received an encryptedConnectionVerificationHeader which was encryptedCorrectly."); + + if(encryptedConnection->temporary()) // Should not change duration for existing permanent connections. + { + uint32_t connectionDuration = 0; + if(JsonTranslator::getDuration(message, connectionDuration)) + { + encryptedConnection->setRemainingDuration(connectionDuration); + } + } + } + else + { + assert(false && "Unknown P-type verification message received!"); + } + } + } + else if(messageHeader == encryptionRequestHeader || messageHeader == temporaryEncryptionRequestHeader) + { + // If there is a espnowRequestManager, get it + if(EspnowMeshBackend *currentEspnowRequestManager = getEspnowRequestManager()) + { + String requestNonce = ""; + if(JsonTranslator::getNonce(message, requestNonce)) + { + uint8_t apMacArray[6] = { 0 }; + peerRequestConfirmationsToSend.emplace_back(receivedMessageID, encryptedCorrectly, currentEspnowRequestManager->getMeshPassword(), requestNonce, macaddr, espnowGetTransmissionMac(dataArray, apMacArray), currentEspnowRequestManager->getEspnowHashKey()); + } + } + } + else if(messageHeader == encryptedConnectionRemovalRequestHeader) + { + if(encryptedCorrectly) + removeEncryptedConnection(macaddr); + } + else + { + assert(false && "Unknown P-type message received!"); + } + } +} + +void EspnowMeshBackend::handlePeerRequestConfirmation(uint8_t *macaddr, uint8_t *dataArray, uint8_t len) +{ + // Pairing process ends when _ongoingPeerRequestNonce == "" or timeout is reached. + // Pairing process stages for request sender: + // Send: encryptionRequestHeader or temporaryEncryptionRequestHeader. + // Receive: maxConnectionsReachedHeader / basicConnectionInfoHeader -> encryptedConnectionInfoHeader or maxConnectionsReachedHeader. + // Send: encryptedConnectionVerificationHeader. + + using namespace EspnowProtocolInterpreter; + + if(_ongoingPeerRequestNonce != "") + { + String message = espnowGetMessageContent(dataArray, len); + String requestNonce = ""; + uint8_t macArray[6] = { 0 }; + if(JsonTranslator::getNonce(message, requestNonce) && requestNonce == _ongoingPeerRequestNonce) + { + int32_t messageHeaderEndIndex = message.indexOf(':'); + String messageHeader = message.substring(0, messageHeaderEndIndex + 1); + String messageBody = message.substring(messageHeaderEndIndex + 1); + + if(messageHeader == basicConnectionInfoHeader) + { + // _ongoingPeerRequestResult == ECS_CONNECTION_ESTABLISHED means we have already received a basicConnectionInfoHeader + if(_ongoingPeerRequestResult != ECS_CONNECTION_ESTABLISHED && + JsonTranslator::verifyHmac(message, _ongoingPeerRequester->getEspnowHashKey(), espnowHashKeyLength)) + { + _ongoingPeerRequestEncryptionStart = millis(); + + connectionLogIterator existingEncryptedConnection = connectionLogEndIterator(); + + if(!getEncryptedConnectionIterator(macaddr, existingEncryptedConnection)) + { + // Although the newly created session keys are normally never used (they are replaced with synchronized ones later), the session keys must still be randomized to prevent attacks until replaced. + _ongoingPeerRequestResult = _ongoingPeerRequester->addTemporaryEncryptedConnection(macaddr, espnowGetTransmissionMac(dataArray, macArray), + createSessionKey(), createSessionKey(), getEncryptionRequestTimeout()); + } + else + { + // Encrypted connection already exists + _ongoingPeerRequestResult = ECS_CONNECTION_ESTABLISHED; + + if(auto timeTrackerPointer = existingEncryptedConnection->temporary()) + { + if(timeTrackerPointer->remainingDuration() < getEncryptionRequestTimeout()) // Should only extend duration for existing connections. + { + existingEncryptedConnection->setRemainingDuration(getEncryptionRequestTimeout()); + } + } + } + + if(_ongoingPeerRequestResult != ECS_CONNECTION_ESTABLISHED) + { + // Adding connection failed, abort ongoing peer request. + _ongoingPeerRequestNonce = ""; + } + } + } + else + { + if(messageHeader == encryptedConnectionInfoHeader) + { + String messagePassword = ""; + + if(JsonTranslator::getPassword(messageBody, messagePassword) && messagePassword == _ongoingPeerRequester->getMeshPassword()) + { + // The mesh password is only shared via encrypted messages, so now we know this message is valid since it was encrypted and contained the correct nonce. + + EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(macaddr); + uint64_t peerSessionKey = 0; + uint64_t ownSessionKey = 0; + if(encryptedConnection && JsonTranslator::getPeerSessionKey(messageBody, peerSessionKey) && JsonTranslator::getOwnSessionKey(messageBody, ownSessionKey)) + { + encryptedConnection->setPeerSessionKey(peerSessionKey); + encryptedConnection->setOwnSessionKey(ownSessionKey); + _ongoingPeerRequestResult = ECS_CONNECTION_ESTABLISHED; + } + else + { + _ongoingPeerRequestResult = ECS_REQUEST_TRANSMISSION_FAILED; + } + + _ongoingPeerRequestNonce = ""; + } + } + else if(messageHeader == maxConnectionsReachedHeader) + { + if(JsonTranslator::verifyHmac(message, _ongoingPeerRequester->getEspnowHashKey(), espnowHashKeyLength)) + { + _ongoingPeerRequestResult = ECS_MAX_CONNECTIONS_REACHED_PEER; + _ongoingPeerRequestNonce = ""; + } + } + else + { + assert(messageHeader == basicConnectionInfoHeader || messageHeader == encryptedConnectionInfoHeader || messageHeader == maxConnectionsReachedHeader); + } + } + } + } +} + +void EspnowMeshBackend::setEspnowRequestManager(EspnowMeshBackend *espnowMeshInstance) +{ + _espnowRequestManager = espnowMeshInstance; +} + +EspnowMeshBackend *EspnowMeshBackend::getEspnowRequestManager() {return _espnowRequestManager;} + +bool EspnowMeshBackend::isEspnowRequestManager() +{ + return (this == getEspnowRequestManager()); +} + +bool EspnowMeshBackend::activateEspnow() +{ + if (esp_now_init()==0) + { + if(_espnowEncryptionKokSet && esp_now_set_kok(_espnowEncryptionKok, espnowEncryptionKeyLength)) // esp_now_set_kok returns 0 on success. + warningPrint("Failed to set ESP-NOW KoK!"); + + if(getEspnowRequestManager() == nullptr) + { + setEspnowRequestManager(this); + } + + esp_now_register_recv_cb(espnowReceiveCallbackWrapper); + esp_now_register_send_cb([](uint8_t* mac, uint8_t sendStatus) { + (void)mac; // This is useful to remove a "unused parameter" compiler warning. Does nothing else. + if(!sendStatus) // sendStatus == 0 when send was OK. + _espnowSendConfirmed = true; // We do not want to reset this to false. That only happens before transmissions. Otherwise subsequent failed send attempts may obscure an initial successful one. + }); + + // Role must be set before adding peers. Cannot be changed while having peers. + // With ESP_NOW_ROLE_CONTROLLER, we always transmit from the station interface, which gives predictability. + if(esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER)) // esp_now_set_self_role returns 0 on success. + warningPrint("Failed to set ESP-NOW role! Maybe ESP-NOW peers are already added?"); + + verboseModePrint("ESP-NOW activated."); + verboseModePrint("My ESP-NOW STA MAC: " + WiFi.macAddress() + "\n"); // Get the station MAC address. The softAP MAC is different. + + return true; + } + else + { + warningPrint("ESP-NOW init failed!"); + return false; + } +} + +bool EspnowMeshBackend::deactivateEspnow() +{ + // esp_now_deinit() clears all ESP-NOW API settings, including receive callback, send callback, Kok and peers. + // The node will however continue to give acks to received ESP-NOW transmissions as long as the receiving interface (AP or STA) is active, even though the transmissions will not be processed. + if(esp_now_deinit() == 0) + { + responsesToSend.clear(); + peerRequestConfirmationsToSend.clear(); + receivedEspnowTransmissions.clear(); + sentRequests.clear(); + receivedRequests.clear(); + encryptedConnections.clear(); + EncryptedConnectionLog::setNewRemovalsScheduled(false); + + return true; + } + else + { + return false; + } +} + +uint32_t EspnowMeshBackend::logEntryLifetimeMs() +{ + return _logEntryLifetimeMs; +} + +uint32_t EspnowMeshBackend::responseTimeoutMs() +{ + return _responseTimeoutMs; +} + +void EspnowMeshBackend::setCriticalHeapLevelBuffer(uint32_t bufferInBytes) +{ + _criticalHeapLevelBuffer = bufferInBytes; +} + +uint32_t EspnowMeshBackend::criticalHeapLevelBuffer() +{ + return _criticalHeapLevelBuffer; +} + +template +void EspnowMeshBackend::deleteExpiredLogEntries(std::map, T> &logEntries, uint32_t maxEntryLifetimeMs) +{ + for(typename std::map, T>::iterator entryIterator = logEntries.begin(); + entryIterator != logEntries.end(); ) + { + if(entryIterator->second.timeSinceCreation() > maxEntryLifetimeMs) + { + entryIterator = logEntries.erase(entryIterator); + } + else + ++entryIterator; + } +} + +template +void EspnowMeshBackend::deleteExpiredLogEntries(std::list &logEntries, uint32_t maxEntryLifetimeMs) +{ + for(typename std::list::iterator entryIterator = logEntries.begin(); + entryIterator != logEntries.end(); ) + { + if(entryIterator->timeSinceCreation() > maxEntryLifetimeMs) + { + entryIterator = logEntries.erase(entryIterator); + } + else + ++entryIterator; + } +} + +template <> +void EspnowMeshBackend::deleteExpiredLogEntries(std::list &logEntries, uint32_t maxEntryLifetimeMs) +{ + for(typename std::list::iterator entryIterator = logEntries.begin(); + entryIterator != logEntries.end(); ) + { + auto timeTrackerPointer = entryIterator->temporary(); + if(timeTrackerPointer && timeTrackerPointer->timeSinceCreation() > maxEntryLifetimeMs) + { + entryIterator = logEntries.erase(entryIterator); + } + else + ++entryIterator; + } +} + +template <> +void EspnowMeshBackend::deleteExpiredLogEntries(std::list &logEntries, uint32_t maxEntryLifetimeMs) +{ + for(typename std::list::iterator entryIterator = logEntries.begin(); + entryIterator != logEntries.end(); ) + { + auto timeTrackerPointer = entryIterator->temporary(); + if(timeTrackerPointer && timeTrackerPointer->timeSinceCreation() > maxEntryLifetimeMs) + { + entryIterator = logEntries.erase(entryIterator); + } + else + ++entryIterator; + } +} + +void EspnowMeshBackend::clearOldLogEntries() +{ + // Clearing all old log entries at the same time should help minimize heap fragmentation. + + // uint32_t startTime = millis(); + + _timeOfLastLogClear = millis(); + + deleteExpiredLogEntries(receivedEspnowTransmissions, logEntryLifetimeMs()); + deleteExpiredLogEntries(receivedRequests, logEntryLifetimeMs()); // Just needs to be long enough to not accept repeated transmissions by mistake. + deleteExpiredLogEntries(sentRequests, logEntryLifetimeMs()); + deleteExpiredLogEntries(responsesToSend, logEntryLifetimeMs()); + deleteExpiredLogEntries(peerRequestConfirmationsToSend, getEncryptionRequestTimeout()); +} + +template +T *EspnowMeshBackend::getMapValue(std::map &mapIn, uint64_t keyIn) +{ + typename std::map::iterator mapIterator = mapIn.find(keyIn); + + if(mapIterator != mapIn.end()) + { + return &mapIterator->second; + } + + return nullptr; +} + +void EspnowMeshBackend::storeSentRequest(const uint64_t targetBSSID, const uint64_t messageID, const RequestData &requestData) +{ + sentRequests.insert(std::make_pair(std::make_pair(targetBSSID, messageID), requestData)); +} + +void EspnowMeshBackend::storeReceivedRequest(const uint64_t senderBSSID, const uint64_t messageID, const TimeTracker &timeTracker) +{ + receivedRequests.insert(std::make_pair(std::make_pair(senderBSSID, messageID), timeTracker)); +} + +EspnowMeshBackend *EspnowMeshBackend::getOwnerOfSentRequest(uint64_t requestMac, uint64_t requestID) +{ + std::map, RequestData>::iterator sentRequest = sentRequests.find(std::make_pair(requestMac, requestID)); + + if(sentRequest != sentRequests.end()) + { + return &sentRequest->second.getMeshInstance(); + } + + return nullptr; +} + +size_t EspnowMeshBackend::deleteSentRequest(uint64_t requestMac, uint64_t requestID) +{ + return sentRequests.erase(std::make_pair(requestMac, requestID)); +} + +size_t EspnowMeshBackend::deleteSentRequestsByOwner(EspnowMeshBackend *instancePointer) +{ + size_t numberDeleted = 0; + + for(std::map, RequestData>::iterator requestIterator = sentRequests.begin(); + requestIterator != sentRequests.end(); ) + { + if(&requestIterator->second.getMeshInstance() == instancePointer) // If instance at instancePointer made the request + { + requestIterator = sentRequests.erase(requestIterator); + numberDeleted++; + } + else + ++requestIterator; + } + + return numberDeleted; +} + +bool EspnowMeshBackend::requestReceived(uint64_t requestMac, uint64_t requestID) +{ + return receivedRequests.count(std::make_pair(requestMac, requestID)); +} + +uint32_t EspnowMeshBackend::criticalHeapLevel() +{ + return _criticalHeapLevel; +} + +uint64_t EspnowMeshBackend::generateMessageID(EncryptedConnectionLog *encryptedConnection) +{ + if(encryptedConnection) + { + return encryptedConnection->getOwnSessionKey(); + } + + return _unencryptedMessageID++; +} + +uint64_t EspnowMeshBackend::createSessionKey() +{ + uint64_t newSessionKey = randomUint64(); + return EspnowProtocolInterpreter::usesEncryption(newSessionKey) ? newSessionKey : (newSessionKey | ((uint64_t)RANDOM_REG32) << 32 | uint64MSB); +} + +void EspnowMeshBackend::setEspnowTransmissionTimeout(uint32_t timeoutMs) +{ + _espnowTransmissionTimeoutMs = timeoutMs; +} +uint32_t EspnowMeshBackend::getEspnowTransmissionTimeout() {return _espnowTransmissionTimeoutMs;} + +void EspnowMeshBackend::setEspnowRetransmissionInterval(uint32_t intervalMs) +{ + _espnowRetransmissionIntervalMs = intervalMs; +} +uint32_t EspnowMeshBackend::getEspnowRetransmissionInterval() {return _espnowRetransmissionIntervalMs;} + +void EspnowMeshBackend::setEncryptionRequestTimeout(uint32_t timeoutMs) +{ + _encryptionRequestTimeoutMs = timeoutMs; +} +uint32_t EspnowMeshBackend::getEncryptionRequestTimeout() {return _encryptionRequestTimeoutMs;} + +void EspnowMeshBackend::setAutoEncryptionDuration(uint32_t duration) +{ + _autoEncryptionDuration = duration; +} +uint32_t EspnowMeshBackend::getAutoEncryptionDuration() {return _autoEncryptionDuration;} + +bool EspnowMeshBackend::usesConstantSessionKey(char messageType) +{ + return messageType == 'A' || messageType == 'C'; +} + +transmission_status_t EspnowMeshBackend::espnowSendToNode(const String &message, const uint8_t *targetBSSID, char messageType, EspnowMeshBackend *espnowInstance) +{ + using EspnowProtocolInterpreter::synchronizationRequestHeader; + + EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(targetBSSID); + + if(encryptedConnection) + { + uint8_t encryptedMac[6] {0}; + encryptedConnection->getEncryptedPeerMac(encryptedMac); + + if(encryptedConnection->desync()) + { + espnowSendToNodeUnsynchronized(synchronizationRequestHeader, encryptedMac, 'S', generateMessageID(encryptedConnection), espnowInstance); + + if(encryptedConnection->desync()) + { + return TS_TRANSMISSION_FAILED; + } + } + + return espnowSendToNodeUnsynchronized(message, encryptedMac, messageType, generateMessageID(encryptedConnection), espnowInstance); + } + else + { + return espnowSendToNodeUnsynchronized(message, targetBSSID, messageType, generateMessageID(encryptedConnection), espnowInstance); + } +} + +static const uint8_t broadcastMac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // Saved for future use. TODO +transmission_status_t EspnowMeshBackend::espnowSendToNodeUnsynchronized(const String message, const uint8_t *targetBSSID, char messageType, uint64_t messageID, EspnowMeshBackend *espnowInstance) +{ + using namespace EspnowProtocolInterpreter; + + MutexTracker mutexTracker(_espnowSendToNodeMutex); + if(!mutexTracker.mutexCaptured()) + { + assert(false && "ERROR! espnowSendToNode already in progress. Don't call espnowSendToNode from callbacks as this will make it impossible to know which transmissions succeed! Aborting."); + return TS_TRANSMISSION_FAILED; + } + + // We copy the message String and bssid array from the arguments in this method to make sure they are + // not modified by a callback during the delay(1) calls further down. + // This also makes it possible to get the current _transmissionTargetBSSID outside of the method. + std::copy_n(targetBSSID, 6, _transmissionTargetBSSID); + + EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(_transmissionTargetBSSID); + + int32_t transmissionsRequired = ceil((double)message.length() / getMaxMessageBytesPerTransmission()); + int32_t transmissionsRemaining = transmissionsRequired > 1 ? transmissionsRequired - 1 : 0; + + _transmissionsTotal++; + + // Though it is possible to handle messages requiring more than 3 transmissions with the current design, transmission fail rates would increase dramatically. + // Messages composed of up to 128 transmissions can be handled without modification, but RAM limitations on the ESP8266 would make this hard in practice. + // We thus prefer to keep the code simple and performant instead. + // Very large messages can always be split by the user as required. + assert(transmissionsRequired <= getMaxTransmissionsPerMessage()); + assert(messageType == 'Q' || messageType == 'A' || messageType == 'S' || messageType == 'P' || messageType == 'C'); + if(messageType == 'P' || messageType == 'C') + { + assert(transmissionsRequired == 1); // These messages are assumed to be contained in one message by the receive callbacks. + } + + uint8_t transmissionSize = 0; + bool messageStart = true; + uint8_t sizeOfProtocolBytes = espnowProtocolBytesSize(); + + do + { + ////// Manage logs ////// + + if(transmissionsRemaining == 0 && messageType == 'Q') + { + assert(espnowInstance); // espnowInstance required when transmitting 'Q' type messages. + // If we are sending the last transmission of a request we should store the sent request in the log no matter if we receive an ack for the final transmission or not. + // That way we will always be ready to receive the response to the request when there is a chance the request message was transmitted successfully, + // even if the final ack for the request message was lost. + storeSentRequest(macToUint64(_transmissionTargetBSSID), messageID, RequestData(*espnowInstance)); + } + + ////// Create transmission array ////// + + if(transmissionsRemaining > 0) + transmissionSize = getMaxBytesPerTransmission(); + else if(message.length() == 0) + transmissionSize = sizeOfProtocolBytes; + else + transmissionSize = sizeOfProtocolBytes + (message.length() % getMaxMessageBytesPerTransmission() == 0 ? + getMaxMessageBytesPerTransmission() : message.length() % getMaxMessageBytesPerTransmission()); + + uint8_t transmission[transmissionSize]; + + ////// Fill protocol bytes ////// + + transmission[espnowMessageTypeIndex] = messageType; + + if(messageStart) + { + transmission[espnowTransmissionsRemainingIndex] = (char)(transmissionsRemaining | 0x80); + } + else + { + transmission[espnowTransmissionsRemainingIndex] = (char)transmissionsRemaining; + } + + // Fills indicies in range [espnowTransmissionMacIndex, espnowTransmissionMacIndex + 5] (6 bytes) with the MAC address of the WiFi AP interface. + // We always transmit from the station interface (due to using ESP_NOW_ROLE_CONTROLLER), so this makes it possible to always know both interface MAC addresses of a node that sends a transmission. + WiFi.softAPmacAddress(transmission + espnowTransmissionMacIndex); + + espnowSetMessageID(transmission, messageID); + + ////// Fill message bytes ////// + + int32_t transmissionStartIndex = (transmissionsRequired - transmissionsRemaining - 1) * getMaxMessageBytesPerTransmission(); + std::copy_n(message.substring(transmissionStartIndex, transmissionStartIndex + transmissionSize - sizeOfProtocolBytes).c_str(), + transmissionSize - sizeOfProtocolBytes, transmission + sizeOfProtocolBytes); + + ////// Transmit ////// + + _espnowSendConfirmed = false; + uint32_t transmissionStartTime = millis(); + + while(!_espnowSendConfirmed && millis() - transmissionStartTime < getEspnowTransmissionTimeout()) + { + if(esp_now_send(_transmissionTargetBSSID, transmission, transmissionSize) == 0) // == 0 => Success + { + uint32_t transmissionAttemptStart = millis(); + while(!_espnowSendConfirmed + && (millis() - transmissionAttemptStart < getEspnowRetransmissionInterval()) + && (millis() - transmissionStartTime < getEspnowTransmissionTimeout())) + { + delay(1); // Note that callbacks can be called during delay time, so it is possible to receive a transmission during this delay. + } + } + + if(_espnowSendConfirmed) + { + if(messageStart) + { + if(encryptedConnection && !usesConstantSessionKey(messageType) && encryptedConnection->getOwnSessionKey() == messageID) + { + encryptedConnection->setDesync(false); + encryptedConnection->incrementOwnSessionKey(); + } + + messageStart = false; + } + + break; + } + } + + if(!_espnowSendConfirmed) + { + _transmissionsFailed++; + + staticVerboseModePrint("espnowSendToNode failed!"); + staticVerboseModePrint("Transmission #: " + String(transmissionsRequired - transmissionsRemaining) + "/" + String(transmissionsRequired)); + staticVerboseModePrint("Transmission fail rate (up) " + String(getTransmissionFailRate())); + + if(messageStart && encryptedConnection && !usesConstantSessionKey(messageType) && encryptedConnection->getOwnSessionKey() == messageID) + encryptedConnection->setDesync(true); + + return TS_TRANSMISSION_FAILED; + } + + transmissionsRemaining--; // This is used when transfering multi-transmission messages. + + } while(transmissionsRemaining >= 0); + + // Useful when debugging the protocol + //staticVerboseModePrint("Sent to Mac: " + macToString(_transmissionTargetBSSID) + " ID: " + uint64ToString(messageID)); + + return TS_TRANSMISSION_COMPLETE; +} + +transmission_status_t EspnowMeshBackend::sendRequest(const String &message, const uint8_t *targetBSSID) +{ + transmission_status_t transmissionStatus = espnowSendToNode(message, targetBSSID, 'Q', this); + + return transmissionStatus; +} + +transmission_status_t EspnowMeshBackend::sendResponse(const String &message, uint64_t requestID, const uint8_t *targetBSSID) +{ + EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(targetBSSID); + uint8_t encryptedMac[6] {0}; + + if(encryptedConnection) + { + encryptedConnection->getEncryptedPeerMac(encryptedMac); + } + + return espnowSendToNodeUnsynchronized(message, encryptedConnection ? encryptedMac : targetBSSID, 'A', requestID, this); +} + +bool EspnowMeshBackend::transmissionInProgress(){return _espnowTransmissionMutex;} + +EspnowMeshBackend::macAndType_td EspnowMeshBackend::createMacAndTypeValue(uint64_t uint64Mac, char messageType) +{ + return static_cast(uint64Mac << 8 | (uint64_t)messageType); +} + +uint64_t EspnowMeshBackend::macAndTypeToUint64Mac(const macAndType_td &macAndTypeValue) +{ + return static_cast(macAndTypeValue) >> 8; +} + +void EspnowMeshBackend::espnowReceiveCallback(uint8_t *macaddr, uint8_t *dataArray, uint8_t len) +{ + using namespace EspnowProtocolInterpreter; + + ////// ////// + /* + if(messageStart) + { + storeTransmission + } + else + { + if(messageFound) + storeTransmission or (erase and return) + else + return + } + + if(transmissionsRemaining != 0) + return + + processMessage + */ + ////// ////// + + char messageType = espnowGetMessageType(dataArray); + uint8_t transmissionsRemaining = espnowGetTransmissionsRemaining(dataArray); + uint64_t uint64Mac = macToUint64(macaddr); + + // The MAC is 6 bytes so two bytes of uint64Mac are free. We must include the messageType there since it is possible that we will + // receive both a request and a response that shares the same messageID from the same uint64Mac, being distinguished only by the messageType. + // This would otherwise potentially cause the request and response to be mixed into one message when they are multi-part transmissions sent roughly at the same time. + macAndType_td macAndType = createMacAndTypeValue(uint64Mac, messageType); + uint64_t messageID = espnowGetMessageID(dataArray); + + //uint32_t methodStart = millis(); + + if(espnowIsMessageStart(dataArray)) + { + // Does nothing if key already in receivedEspnowTransmissions + receivedEspnowTransmissions.insert(std::make_pair(std::make_pair(macAndType, messageID), MessageData(dataArray, len))); + } + else + { + std::map, MessageData>::iterator storedMessageIterator = receivedEspnowTransmissions.find(std::make_pair(macAndType, messageID)); + + if(storedMessageIterator != receivedEspnowTransmissions.end()) // If we have not stored the key already, we missed the first message part. + { + if(!storedMessageIterator->second.addToMessage(dataArray, len)) + { + // If we received the wrong message part, remove the whole message if we have missed a part. + // Otherwise just ignore the received part since it has already been stored. + + uint8_t transmissionsRemainingExpected = storedMessageIterator->second.getTransmissionsRemaining() - 1; + + if(transmissionsRemaining < transmissionsRemainingExpected) + { + receivedEspnowTransmissions.erase(storedMessageIterator); + return; + } + } + } + else + { + return; + } + } + + //Serial.println("methodStart storage done " + String(millis() - methodStart)); + + if(transmissionsRemaining != 0) + { + return; + } + + std::map, MessageData>::iterator storedMessageIterator = receivedEspnowTransmissions.find(std::make_pair(macAndType, messageID)); + assert(storedMessageIterator != receivedEspnowTransmissions.end()); + + // Copy totalMessage in case user callbacks (request/responseHandler) do something odd with receivedEspnowTransmissions list. + String totalMessage = storedMessageIterator->second.getTotalMessage(); // https://stackoverflow.com/questions/134731/returning-a-const-reference-to-an-object-instead-of-a-copy It is likely that most compilers will perform Named Value Return Value Optimisation in this case + + receivedEspnowTransmissions.erase(storedMessageIterator); // Erase the extra copy of the totalMessage, to save RAM. + + //Serial.println("methodStart erase done " + String(millis() - methodStart)); + + if(messageType == 'Q') // Question (request) + { + storeReceivedRequest(uint64Mac, messageID, TimeTracker(millis())); + //Serial.println("methodStart request stored " + String(millis() - methodStart)); + + setSenderMac(macaddr); + setReceivedEncryptedMessage(usesEncryption(messageID)); + String response = getRequestHandler()(totalMessage, *this); + //Serial.println("methodStart response acquired " + String(millis() - methodStart)); + + if(response.length() > 0) + { + responsesToSend.push_back(ResponseData(response, macaddr, messageID)); + + //Serial.println("methodStart Q done " + String(millis() - methodStart)); + } + } + else if(messageType == 'A') // Answer (response) + { + deleteSentRequest(uint64Mac, messageID); // Request has been answered, so stop accepting new answers about it. + + if(EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(macaddr)) + { + if(encryptedConnection->getOwnSessionKey() == messageID) + { + encryptedConnection->setDesync(false); // We just received an answer to the latest request we sent to the node, so the node sending the answer must now be in sync. + encryptedConnection->incrementOwnSessionKey(); + } + } + + setSenderMac(macaddr); + setReceivedEncryptedMessage(usesEncryption(messageID)); + getResponseHandler()(totalMessage, *this); + } + else + { + assert(messageType == 'Q' || messageType == 'A'); + } + + ESP.wdtFeed(); // Prevents WDT reset in case we receive a lot of transmissions without break. + + //Serial.println("methodStart wdtFeed done " + String(millis() - methodStart)); +} + +void EspnowMeshBackend::setEspnowEncryptionKey(const uint8_t espnowEncryptionKey[espnowEncryptionKeyLength]) +{ + assert(espnowEncryptionKey != nullptr); + + for(int i = 0; i < espnowEncryptionKeyLength; i++) + { + _espnowEncryptionKey[i] = espnowEncryptionKey[i]; + } +} + +const uint8_t *EspnowMeshBackend::getEspnowEncryptionKey() +{ + return _espnowEncryptionKey; +} + +uint8_t *EspnowMeshBackend::getEspnowEncryptionKey(uint8_t resultArray[espnowEncryptionKeyLength]) +{ + std::copy_n(_espnowEncryptionKey, espnowEncryptionKeyLength, resultArray); + return resultArray; +} + +bool EspnowMeshBackend::setEspnowEncryptionKok(uint8_t espnowEncryptionKok[espnowEncryptionKeyLength]) +{ + if(espnowEncryptionKok == nullptr || esp_now_set_kok(espnowEncryptionKok, espnowEncryptionKeyLength)) // esp_now_set_kok failed if not == 0 + return false; + + for(int i = 0; i < espnowEncryptionKeyLength; i++) + { + _espnowEncryptionKok[i] = espnowEncryptionKok[i]; + } + + _espnowEncryptionKokSet = true; + + return true; +} + +const uint8_t *EspnowMeshBackend::getEspnowEncryptionKok() +{ + if(_espnowEncryptionKokSet) + return _espnowEncryptionKok; + else + return nullptr; +} + +void EspnowMeshBackend::setEspnowHashKey(const uint8_t espnowHashKey[espnowHashKeyLength]) +{ + assert(espnowHashKey != nullptr); + + for(int i = 0; i < espnowHashKeyLength; i++) + { + _espnowHashKey[i] = espnowHashKey[i]; + } +} + +const uint8_t *EspnowMeshBackend::getEspnowHashKey() +{ + return _espnowHashKey; +} + +bool EspnowMeshBackend::verifyPeerSessionKey(uint64_t sessionKey, const uint8_t *peerMac, char messageType) +{ + if(EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(peerMac)) + { + return verifyPeerSessionKey(sessionKey, *encryptedConnection, macToUint64(peerMac), messageType); + } + + return false; +} + +bool EspnowMeshBackend::verifyPeerSessionKey(uint64_t sessionKey, EncryptedConnectionLog &encryptedConnection, uint64_t uint64PeerMac, char messageType) +{ + if(EspnowProtocolInterpreter::usesEncryption(sessionKey)) + { + if(sessionKey == encryptedConnection.getPeerSessionKey() + || receivedEspnowTransmissions.find(std::make_pair(createMacAndTypeValue(uint64PeerMac, messageType), sessionKey)) + != receivedEspnowTransmissions.end()) + { + // If sessionKey is correct or sessionKey is one part of a multi-part transmission. + return true; + } + } + + return false; +} + +bool EspnowMeshBackend::synchronizePeerSessionKey(uint64_t sessionKey, const uint8_t *peerMac) +{ + if(EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(peerMac)) + { + return synchronizePeerSessionKey(sessionKey, *encryptedConnection); + } + + return false; +} + +bool EspnowMeshBackend::synchronizePeerSessionKey(uint64_t sessionKey, EncryptedConnectionLog &encryptedConnection) +{ + if(EspnowProtocolInterpreter::usesEncryption(sessionKey)) + { + if(sessionKey == encryptedConnection.getPeerSessionKey()) + { + uint8_t hashKey[espnowHashKeyLength] {0}; + encryptedConnection.setPeerSessionKey(EncryptedConnectionLog::incrementSessionKey(sessionKey, encryptedConnection.getHashKey(hashKey), espnowHashKeyLength)); + return true; + } + } + + return false; +} + +std::list::const_iterator EspnowMeshBackend::getScheduledResponse(uint32_t responseIndex) +{ + assert(responseIndex < numberOfScheduledResponses()); + + bool startFromBeginning = responseIndex < numberOfScheduledResponses()/2; + auto responseIterator = startFromBeginning ? responsesToSend.cbegin() : responsesToSend.cend(); + uint32_t stepsToTarget = startFromBeginning ? responseIndex : numberOfScheduledResponses() - responseIndex; // cend is one element beyond the last + + while(stepsToTarget > 0) + { + startFromBeginning ? ++responseIterator : --responseIterator; + stepsToTarget--; + } + + return responseIterator; +} + +String EspnowMeshBackend::getScheduledResponseMessage(uint32_t responseIndex) +{ + return getScheduledResponse(responseIndex)->getMessage(); +} + +const uint8_t *EspnowMeshBackend::getScheduledResponseRecipient(uint32_t responseIndex) +{ + return getScheduledResponse(responseIndex)->getRecipientMac(); +} + +void EspnowMeshBackend::clearAllScheduledResponses() +{ + responsesToSend.clear(); +} + +uint32_t EspnowMeshBackend::numberOfScheduledResponses() {return responsesToSend.size();} + + +void EspnowMeshBackend::deleteScheduledResponsesByRecipient(const uint8_t *recipientMac, bool encryptedOnly) +{ + for(auto responseIterator = responsesToSend.begin(); responseIterator != responsesToSend.end(); ) + { + if(macEqual(responseIterator->getRecipientMac(), recipientMac) && (!encryptedOnly || EspnowProtocolInterpreter::usesEncryption(responseIterator->getRequestID()))) + { + responseIterator = responsesToSend.erase(responseIterator); + } + else + ++responseIterator; + } +} + +void EspnowMeshBackend::setSenderMac(uint8_t *macArray) +{ + std::copy_n(macArray, 6, _senderMac); +} + +String EspnowMeshBackend::getSenderMac() {return macToString(_senderMac);} +uint8_t *EspnowMeshBackend::getSenderMac(uint8_t *macArray) +{ + std::copy_n(_senderMac, 6, macArray); + return macArray; +} + +void EspnowMeshBackend::setReceivedEncryptedMessage(bool receivedEncryptedMessage) { _receivedEncryptedMessage = receivedEncryptedMessage; } +bool EspnowMeshBackend::receivedEncryptedMessage() {return _receivedEncryptedMessage;} + +encrypted_connection_status_t EspnowMeshBackend::addEncryptedConnection(uint8_t *peerStaMac, uint8_t *peerApMac, uint64_t peerSessionKey, uint64_t ownSessionKey) +{ + assert(encryptedConnections.size() <= maxEncryptedConnections); // If this is not the case, ESP-NOW is no longer in sync with the library + + uint8_t encryptionKeyArray[espnowEncryptionKeyLength] = { 0 }; + + if(EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(peerStaMac)) + { + // Encrypted connection with MAC already exists, so no need to replace it, just updating is enough. + temporaryEncryptedConnectionToPermanent(peerStaMac); + encryptedConnection->setPeerSessionKey(peerSessionKey); + encryptedConnection->setOwnSessionKey(ownSessionKey); + esp_now_set_peer_key(peerStaMac, getEspnowEncryptionKey(encryptionKeyArray), espnowEncryptionKeyLength); + encryptedConnection->setHashKey(getEspnowHashKey()); + + return ECS_CONNECTION_ESTABLISHED; + } + + if(encryptedConnections.size() == maxEncryptedConnections) + { + // No capacity for more encrypted connections. + return ECS_MAX_CONNECTIONS_REACHED_SELF; + } + // int esp_now_add_peer(u8 *mac_addr, u8 role, u8 channel, u8 *key, u8 key_len), returns 0 on success + // Only MAC, encryption key and key length (16) actually matter. The rest is not used by ESP-NOW. + else if(0 == esp_now_add_peer(peerStaMac, ESP_NOW_ROLE_CONTROLLER, getWiFiChannel(), getEspnowEncryptionKey(encryptionKeyArray), espnowEncryptionKeyLength)) + { + encryptedConnections.emplace_back(peerStaMac, peerApMac, peerSessionKey, ownSessionKey, getEspnowHashKey()); + return ECS_CONNECTION_ESTABLISHED; + } + else + { + return ECS_API_CALL_FAILED; + } +} + +encrypted_connection_status_t EspnowMeshBackend::addEncryptedConnection(const String &serializedConnectionState, bool ignoreDuration) +{ + uint32_t duration = 0; + bool desync = false; + uint64_t ownSessionKey = 0; + uint64_t peerSessionKey = 0; + uint8_t peerStaMac[6] = { 0 }; + uint8_t peerApMac[6] = { 0 }; + + if(JsonTranslator::getDesync(serializedConnectionState, desync) + && JsonTranslator::getOwnSessionKey(serializedConnectionState, ownSessionKey) && JsonTranslator::getPeerSessionKey(serializedConnectionState, peerSessionKey) + && JsonTranslator::getPeerStaMac(serializedConnectionState, peerStaMac) && JsonTranslator::getPeerApMac(serializedConnectionState, peerApMac)) + { + encrypted_connection_status_t result = ECS_API_CALL_FAILED; + + if(!ignoreDuration && JsonTranslator::getDuration(serializedConnectionState, duration)) + { + result = addTemporaryEncryptedConnection(peerStaMac, peerApMac, peerSessionKey, ownSessionKey, duration); + } + else + { + result = addEncryptedConnection(peerStaMac, peerApMac, peerSessionKey, ownSessionKey); + } + + if(result == ECS_CONNECTION_ESTABLISHED) + { + EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(peerStaMac); + encryptedConnection->setDesync(desync); + } + + return result; + } + else + { + return ECS_REQUEST_TRANSMISSION_FAILED; + } +} + +encrypted_connection_status_t EspnowMeshBackend::addTemporaryEncryptedConnection(uint8_t *peerStaMac, uint8_t *peerApMac, uint64_t peerSessionKey, uint64_t ownSessionKey, uint32_t duration) +{ + assert(encryptedConnections.size() <= maxEncryptedConnections); // If this is not the case, ESP-NOW is no longer in sync with the library + + uint8_t encryptionKeyArray[espnowEncryptionKeyLength] = { 0 }; + + connectionLogIterator encryptedConnection = connectionLogEndIterator(); + + if(getEncryptedConnectionIterator(peerStaMac, encryptedConnection)) + { + // There is already an encrypted connection to this mac, so no need to replace it, just updating is enough. + encryptedConnection->setPeerSessionKey(peerSessionKey); + encryptedConnection->setOwnSessionKey(ownSessionKey); + esp_now_set_peer_key(peerStaMac, getEspnowEncryptionKey(encryptionKeyArray), espnowEncryptionKeyLength); + encryptedConnection->setHashKey(getEspnowHashKey()); + + if(encryptedConnection->temporary()) + { + encryptedConnection->setRemainingDuration(duration); + } + + return ECS_CONNECTION_ESTABLISHED; + } + + encrypted_connection_status_t result = addEncryptedConnection(peerStaMac, peerApMac, peerSessionKey, ownSessionKey); + + if(result == ECS_CONNECTION_ESTABLISHED) + { + if(!getEncryptedConnectionIterator(peerStaMac, encryptedConnection)) + assert(false && "No connection found despite being added in addTemporaryEncryptedConnection."); + + encryptedConnection->setRemainingDuration(duration); + } + + return result; +} + +encrypted_connection_status_t EspnowMeshBackend::addTemporaryEncryptedConnection(const String &serializedConnectionState, uint32_t duration) +{ + bool desync = false; + uint64_t ownSessionKey = 0; + uint64_t peerSessionKey = 0; + uint8_t peerStaMac[6] = { 0 }; + uint8_t peerApMac[6] = { 0 }; + + if(JsonTranslator::getDesync(serializedConnectionState, desync) + && JsonTranslator::getOwnSessionKey(serializedConnectionState, ownSessionKey) && JsonTranslator::getPeerSessionKey(serializedConnectionState, peerSessionKey) + && JsonTranslator::getPeerStaMac(serializedConnectionState, peerStaMac) && JsonTranslator::getPeerApMac(serializedConnectionState, peerApMac)) + { + encrypted_connection_status_t result = addTemporaryEncryptedConnection(peerStaMac, peerApMac, peerSessionKey, ownSessionKey, duration); + + if(result == ECS_CONNECTION_ESTABLISHED) + { + EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(peerStaMac); + encryptedConnection->setDesync(desync); + } + + return result; + } + else + { + return ECS_REQUEST_TRANSMISSION_FAILED; + } +} + +void EspnowMeshBackend::handlePostponedRemovals() +{ + MutexTracker mutexTracker(_espnowTransmissionMutex); + if(!mutexTracker.mutexCaptured()) + { + assert(false && "ERROR! Transmission in progress. Don't call handlePostponedRemovals from callbacks as this may corrupt program state! Aborting."); + return; + } + + if(EncryptedConnectionLog::newRemovalsScheduled()) + { + updateTemporaryEncryptedConnections(true); + } +} + +encrypted_connection_status_t EspnowMeshBackend::requestEncryptedConnectionKernel(uint8_t *peerMac, const encryptionRequestBuilderType &encryptionRequestBuilder) +{ + using namespace EspnowProtocolInterpreter; + + assert(encryptedConnections.size() <= maxEncryptedConnections); // If this is not the case, ESP-NOW is no longer in sync with the library + + MutexTracker mutexTracker(_espnowTransmissionMutex, handlePostponedRemovals); + if(!mutexTracker.mutexCaptured()) + { + assert(false && "ERROR! Transmission in progress. Don't call requestEncryptedConnection from callbacks as this may corrupt program state! Aborting."); + return ECS_REQUEST_TRANSMISSION_FAILED; + } + + EncryptedConnectionLog *existingEncryptedConnection = getEncryptedConnection(peerMac); + ExpiringTimeTracker existingTimeTracker = existingEncryptedConnection && existingEncryptedConnection->temporary() ? + *existingEncryptedConnection->temporary() : ExpiringTimeTracker(0); + + if(!existingEncryptedConnection && encryptedConnections.size() >= maxEncryptedConnections) + { + assert(encryptedConnections.size() == maxEncryptedConnections); + + // No capacity for more encrypted connections. + return ECS_MAX_CONNECTIONS_REACHED_SELF; + } + + String requestNonce = macToString(peerMac) + uint64ToString(randomUint64()) + uint64ToString(randomUint64()); + _ongoingPeerRequestResult = ECS_REQUEST_TRANSMISSION_FAILED; + _ongoingPeerRequestNonce = requestNonce; + _ongoingPeerRequester = this; + String requestMessage = encryptionRequestBuilder(requestNonce, existingTimeTracker); + + verboseModePrint("Sending encrypted connection request to: " + macToString(peerMac)); + + if(espnowSendToNode(requestMessage, peerMac, 'P') == TS_TRANSMISSION_COMPLETE) + { + uint32_t startTime = millis(); + + // _ongoingPeerRequestNonce is set to "" when a peer confirmation response from the mac is received + while(millis() - startTime < getEncryptionRequestTimeout() && _ongoingPeerRequestNonce != "") + { + delay(1); + } + } + + if(_ongoingPeerRequestNonce != "") + { + // If nonce != "" we only received the basic connection info, so the pairing process is incomplete + _ongoingPeerRequestResult = ECS_REQUEST_TRANSMISSION_FAILED; + _ongoingPeerRequestNonce = ""; + } + else if(_ongoingPeerRequestResult == ECS_CONNECTION_ESTABLISHED) + { + requestMessage = encryptionRequestBuilder(requestNonce, existingTimeTracker); // Give the builder a chance to update the message + + int32_t messageHeaderEndIndex = requestMessage.indexOf(':'); + String messageHeader = requestMessage.substring(0, messageHeaderEndIndex + 1); + String messageBody = requestMessage.substring(messageHeaderEndIndex + 1); + + // If we do not get an ack within getEncryptionRequestTimeout() the peer has probably had the time to delete the temporary encrypted connection. + if(espnowSendToNode(encryptedConnectionVerificationHeader + requestMessage, peerMac, 'P') == TS_TRANSMISSION_COMPLETE + && millis() - _ongoingPeerRequestEncryptionStart < getEncryptionRequestTimeout()) + { + EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(peerMac); + if(!encryptedConnection || encryptedConnection->removalScheduled()) + { + assert(encryptedConnection && "requestEncryptedConnectionKernel cannot find an encrypted connection!"); + // requestEncryptedConnectionRemoval received. + _ongoingPeerRequestResult = ECS_REQUEST_TRANSMISSION_FAILED; + } + else + { + // Finalize connection + if(messageHeader == encryptionRequestHeader) + { + temporaryEncryptedConnectionToPermanent(peerMac); + } + else if(messageHeader == temporaryEncryptionRequestHeader) + { + if(!existingEncryptedConnection || existingEncryptedConnection->temporary()) + { + // Should not change duration of existing permanent connections. + uint32_t connectionDuration = 0; + bool durationFound = JsonTranslator::getDuration(messageBody, connectionDuration); + assert(durationFound); + encryptedConnection->setRemainingDuration(connectionDuration); + } + } + else + { + assert(false && "Unknown messageHeader during encrypted connection finalization!"); + _ongoingPeerRequestResult = ECS_API_CALL_FAILED; + } + } + } + else + { + _ongoingPeerRequestResult = ECS_REQUEST_TRANSMISSION_FAILED; + } + } + + if(_ongoingPeerRequestResult != ECS_CONNECTION_ESTABLISHED) + { + if(!existingEncryptedConnection) + { + // Remove any connection that was added during the request attempt. + removeEncryptedConnectionUnprotected(peerMac); + } + } + + _ongoingPeerRequester = nullptr; + + return _ongoingPeerRequestResult; +} + +String EspnowMeshBackend::defaultEncryptionRequestBuilder(const String &requestHeader, const uint32_t durationMs, + const String &requestNonce, const ExpiringTimeTracker &existingTimeTracker) +{ + using namespace JsonTranslator; + using EspnowProtocolInterpreter::temporaryEncryptionRequestHeader; + + (void)existingTimeTracker; // This removes a "unused parameter" compiler warning. Does nothing else. + + String requestMessage = ""; + + if(requestHeader == temporaryEncryptionRequestHeader) + { + requestMessage += createEncryptionRequestIntro(requestHeader, durationMs); + } + else + { + requestMessage += createEncryptionRequestIntro(requestHeader); + } + + requestMessage += createEncryptionRequestEnding(requestNonce); + + return requestMessage; +} + +String EspnowMeshBackend::flexibleEncryptionRequestBuilder(const uint32_t minDurationMs, const String &requestNonce, const ExpiringTimeTracker &existingTimeTracker) +{ + using namespace JsonTranslator; + using EspnowProtocolInterpreter::temporaryEncryptionRequestHeader; + + uint32_t connectionDuration = minDurationMs >= existingTimeTracker.remainingDuration() ? + minDurationMs : existingTimeTracker.remainingDuration(); + + return createEncryptionRequestIntro(temporaryEncryptionRequestHeader, connectionDuration) + createEncryptionRequestEnding(requestNonce); +} + +encrypted_connection_status_t EspnowMeshBackend::requestEncryptedConnection(uint8_t *peerMac) +{ + using namespace std::placeholders; + return requestEncryptedConnectionKernel(peerMac, std::bind(defaultEncryptionRequestBuilder, EspnowProtocolInterpreter::encryptionRequestHeader, 0, _1, _2)); +} + +encrypted_connection_status_t EspnowMeshBackend::requestTemporaryEncryptedConnection(uint8_t *peerMac, uint32_t durationMs) +{ + using namespace std::placeholders; + return requestEncryptedConnectionKernel(peerMac, std::bind(defaultEncryptionRequestBuilder, EspnowProtocolInterpreter::temporaryEncryptionRequestHeader, durationMs, _1, _2)); +} + +encrypted_connection_status_t EspnowMeshBackend::requestFlexibleTemporaryEncryptedConnection(uint8_t *peerMac, uint32_t minDurationMs) +{ + using namespace std::placeholders; + return requestEncryptedConnectionKernel(peerMac, std::bind(flexibleEncryptionRequestBuilder, minDurationMs, _1, _2)); +} + +bool EspnowMeshBackend::temporaryEncryptedConnectionToPermanent(uint8_t *peerMac) +{ + if(EncryptedConnectionLog *temporaryConnection = getTemporaryEncryptedConnection(peerMac)) + { + temporaryConnection->removeDuration(); + return true; + } + else + { + return false; + } +} + +encrypted_connection_removal_outcome_t EspnowMeshBackend::removeEncryptedConnection(uint8_t *peerMac) +{ + auto connectionIterator = getEncryptedConnectionIterator(peerMac, encryptedConnections); + if(connectionIterator != encryptedConnections.end()) + { + MutexTracker mutexTracker(_espnowTransmissionMutex); + if(!mutexTracker.mutexCaptured()) + { + // We should not remove an encrypted connection while there is a transmission in progress, since that may cause encrypted data to be sent unencrypted. + // Thus when a transmission is in progress we just schedule the encrypted connection for removal, so it will be removed during the next updateTemporaryEncryptedConnections() call. + connectionIterator->scheduleForRemoval(); + return ECRO_REMOVAL_SCHEDULED; + } + else + { + return removeEncryptedConnectionUnprotected(peerMac); + } + } + else + { + // peerMac is already removed + return ECRO_REMOVAL_SUCCEEDED; + } +} + +encrypted_connection_removal_outcome_t EspnowMeshBackend::removeEncryptedConnectionUnprotected(uint8_t *peerMac, std::vector::iterator *resultingIterator) +{ + connectionLogIterator connectionIterator = getEncryptedConnectionIterator(peerMac, encryptedConnections); + return removeEncryptedConnectionUnprotected(connectionIterator, resultingIterator); +} + +encrypted_connection_removal_outcome_t EspnowMeshBackend::removeEncryptedConnectionUnprotected(connectionLogIterator &connectionIterator, std::vector::iterator *resultingIterator) +{ + assert(encryptedConnections.size() <= maxEncryptedConnections); // If this is not the case, ESP-NOW is no longer in sync with the library + + if(connectionIterator != connectionLogEndIterator()) + { + uint8_t encryptedMac[6] {0}; + connectionIterator->getEncryptedPeerMac(encryptedMac); + staticVerboseModePrint("Removing connection " + macToString(encryptedMac) + "... ", false); + bool removalSucceeded = esp_now_del_peer(encryptedMac) == 0; + + if(removalSucceeded) + { + if(resultingIterator != nullptr) + { + *resultingIterator = encryptedConnections.erase(connectionIterator); + } + else + { + encryptedConnections.erase(connectionIterator); + } + staticVerboseModePrint("Removal succeeded"); + + // Not deleting encrypted responses here would cause them to be sent unencrypted, + // exposing the peer session key which can be misused later if the encrypted connection is re-established. + deleteScheduledResponsesByRecipient(encryptedMac, true); + + // Not deleting these entries here may cause issues if the encrypted connection is quickly re-added + // and happens to get the same session keys as before (e.g. requestReceived() could then give false positives). + deleteEntriesByMac(receivedEspnowTransmissions, encryptedMac, true); + deleteEntriesByMac(sentRequests, encryptedMac, true); + deleteEntriesByMac(receivedRequests, encryptedMac, true); + + return ECRO_REMOVAL_SUCCEEDED; + } + else + { + staticVerboseModePrint("Removal failed"); + return ECRO_REMOVAL_FAILED; + } + } + else + { + // connection is already removed + return ECRO_REMOVAL_SUCCEEDED; + } +} + +template +void EspnowMeshBackend::deleteEntriesByMac(std::map, T> &logEntries, const uint8_t *peerMac, bool encryptedOnly) +{ + bool macFound = false; + + for(typename std::map, T>::iterator entryIterator = logEntries.begin(); + entryIterator != logEntries.end(); ) + { + if(macAndTypeToUint64Mac(entryIterator->first.first) == macToUint64(peerMac)) + { + macFound = true; + + if(!encryptedOnly || EspnowProtocolInterpreter::usesEncryption(entryIterator->first.second)) + { + entryIterator = logEntries.erase(entryIterator); + continue; + } + } + else if(macFound) + { + // Since the map is sorted by MAC, we know here that no more matching MAC will be found. + return; + } + + ++entryIterator; + } +} + +template +void EspnowMeshBackend::deleteEntriesByMac(std::map, T> &logEntries, const uint8_t *peerMac, bool encryptedOnly) +{ + bool macFound = false; + + for(typename std::map, T>::iterator entryIterator = logEntries.begin(); + entryIterator != logEntries.end(); ) + { + if(entryIterator->first.first == macToUint64(peerMac)) + { + macFound = true; + + if(!encryptedOnly || EspnowProtocolInterpreter::usesEncryption(entryIterator->first.second)) + { + entryIterator = logEntries.erase(entryIterator); + continue; + } + } + else if(macFound) + { + // Since the map is sorted by MAC, we know here that no more matching MAC will be found. + return; + } + + ++entryIterator; + } +} + +encrypted_connection_removal_outcome_t EspnowMeshBackend::requestEncryptedConnectionRemoval(uint8_t *peerMac) +{ + using EspnowProtocolInterpreter::encryptedConnectionRemovalRequestHeader; + + assert(encryptedConnections.size() <= maxEncryptedConnections); // If this is not the case, ESP-NOW is no longer in sync with the library + + MutexTracker mutexTracker(_espnowTransmissionMutex, handlePostponedRemovals); + if(!mutexTracker.mutexCaptured()) + { + assert(false && "ERROR! Transmission in progress. Don't call requestEncryptedConnectionRemoval from callbacks as this may corrupt program state! Aborting."); + return ECRO_REMOVAL_REQUEST_FAILED; + } + + if(EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(peerMac)) + { + if(espnowSendToNode(encryptedConnectionRemovalRequestHeader, peerMac, 'P') == TS_TRANSMISSION_COMPLETE) + { + return removeEncryptedConnectionUnprotected(peerMac); + } + else + { + if(encryptedConnection->removalScheduled()) + return ECRO_REMOVAL_SUCCEEDED; // Removal will be completed by mutex destructorHook. + else + return ECRO_REMOVAL_REQUEST_FAILED; + } + } + else + { + // peerMac is already removed + return ECRO_REMOVAL_SUCCEEDED; + } +} + +void EspnowMeshBackend::setAcceptsUnencryptedRequests(bool acceptsUnencryptedRequests) { _acceptsUnencryptedRequests = acceptsUnencryptedRequests; } +bool EspnowMeshBackend::acceptsUnencryptedRequests() { return _acceptsUnencryptedRequests; } + +template +typename std::vector::iterator EspnowMeshBackend::getEncryptedConnectionIterator(const uint8_t *peerMac, typename std::vector &connectionVector) +{ + typename std::vector::iterator connectionIterator = connectionVector.begin(); + + while(connectionIterator != connectionVector.end()) + { + if(connectionIterator->connectedTo(peerMac)) + break; + else + ++connectionIterator; + } + + return connectionIterator; +} + +EspnowMeshBackend::connectionLogIterator EspnowMeshBackend::connectionLogEndIterator() +{ + return encryptedConnections.end(); +} + +bool EspnowMeshBackend::getEncryptedConnectionIterator(const uint8_t *peerMac, connectionLogIterator &iterator) +{ + connectionLogIterator result = getEncryptedConnectionIterator(peerMac, encryptedConnections); + + if(result != connectionLogEndIterator()) + { + iterator = result; + return true; + } + else + { + return false; + } +} + +bool EspnowMeshBackend::getTemporaryEncryptedConnectionIterator(const uint8_t *peerMac, connectionLogIterator &iterator) +{ + connectionLogIterator result = connectionLogEndIterator(); + + if(getEncryptedConnectionIterator(peerMac, result) && result->temporary()) + { + iterator = result; + return true; + } + else + { + return false; + } +} + +EncryptedConnectionLog *EspnowMeshBackend::getEncryptedConnection(const uint8_t *peerMac) +{ + auto connectionIterator = getEncryptedConnectionIterator(peerMac, encryptedConnections); + if(connectionIterator != encryptedConnections.end()) + { + return &(*connectionIterator); + } + else + { + return nullptr; + } +} + +EncryptedConnectionLog *EspnowMeshBackend::getTemporaryEncryptedConnection(const uint8_t *peerMac) +{ + connectionLogIterator connectionIterator = connectionLogEndIterator(); + if(getTemporaryEncryptedConnectionIterator(peerMac, connectionIterator)) + { + return &(*connectionIterator); + } + else + { + return nullptr; + } +} + + +uint8_t *EspnowMeshBackend::getEncryptedMac(const uint8_t *peerMac, uint8_t *resultArray) +{ + if(EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(peerMac)) + { + return encryptedConnection->getEncryptedPeerMac(resultArray); + } + else + { + return nullptr; + } +} + +void EspnowMeshBackend::attemptTransmission(const String &message, bool scan, bool scanAllWiFiChannels) +{ + MutexTracker mutexTracker(_espnowTransmissionMutex, handlePostponedRemovals); + if(!mutexTracker.mutexCaptured()) + { + assert(false && "ERROR! Transmission in progress. Don't call attemptTransmission from callbacks as this may corrupt program state! Aborting."); + return; + } + + setMessage(message); + + latestTransmissionOutcomes.clear(); + + if(scan) + { + scanForNetworks(scanAllWiFiChannels); + } + + for(NetworkInfo ¤tNetwork : connectionQueue) + { + String currentSSID = ""; + int currentWiFiChannel = NETWORK_INFO_DEFAULT_INT; + uint8_t *currentBSSID = NULL; + + // If a BSSID has been assigned, it is prioritized over an assigned networkIndex since the networkIndex is more likely to change. + if(currentNetwork.BSSID != NULL) + { + currentSSID = currentNetwork.SSID; + currentWiFiChannel = currentNetwork.wifiChannel; + currentBSSID = currentNetwork.BSSID; + } + else // Use only networkIndex + { + currentSSID = WiFi.SSID(currentNetwork.networkIndex); + currentWiFiChannel = WiFi.channel(currentNetwork.networkIndex); + currentBSSID = WiFi.BSSID(currentNetwork.networkIndex); + } + + if(verboseMode()) // Avoid string generation if not required + { + printAPInfo(currentNetwork.networkIndex, currentSSID, currentWiFiChannel); + verboseModePrint(F("")); + } + + uint32_t transmissionStartTime = millis(); + transmission_status_t transmissionResult = sendRequest(getMessage(), currentBSSID); + + uint32_t transmissionDuration = millis() - transmissionStartTime; + + if(verboseMode() && transmissionResult == TS_TRANSMISSION_COMPLETE) // Avoid calculations if not required + { + totalDurationWhenSuccessful_AT += transmissionDuration; + successfulTransmissions_AT++; + if(transmissionDuration > maxTransmissionDuration_AT) + { + maxTransmissionDuration_AT = transmissionDuration; + } + } + + latestTransmissionOutcomes.push_back(TransmissionResult{.origin = currentNetwork, .transmissionStatus = transmissionResult}); + } + + if(verboseMode() && successfulTransmissions_AT > 0) // Avoid calculations if not required + { + verboseModePrint("Average duration of successful transmissions: " + String(totalDurationWhenSuccessful_AT/successfulTransmissions_AT) + " ms."); + verboseModePrint("Maximum duration of successful transmissions: " + String(maxTransmissionDuration_AT) + " ms."); + } + else + { + verboseModePrint("No successful transmission."); + } +} + +void EspnowMeshBackend::attemptAutoEncryptingTransmission(const String &message, bool scan, bool scanAllWiFiChannels) +{ + MutexTracker outerMutexTracker(_espnowTransmissionMutex, handlePostponedRemovals); + if(!outerMutexTracker.mutexCaptured()) + { + assert(false && "ERROR! Transmission in progress. Don't call attemptTransmission from callbacks as this may corrupt program state! Aborting."); + return; + } + + setMessage(message); + + latestTransmissionOutcomes.clear(); + + if(scan) + { + scanForNetworks(scanAllWiFiChannels); + } + + outerMutexTracker.releaseMutex(); + + for(NetworkInfo ¤tNetwork : connectionQueue) + { + MutexTracker innerMutexTracker = MutexTracker(_espnowTransmissionMutex); + if(!innerMutexTracker.mutexCaptured()) + { + assert(false && "ERROR! Unable to recapture Mutex in attemptAutoEncryptingTransmission. Aborting."); + return; + } + + String currentSSID = ""; + int currentWiFiChannel = NETWORK_INFO_DEFAULT_INT; + uint8_t *currentBSSID = NULL; + + // If a BSSID has been assigned, it is prioritized over an assigned networkIndex since the networkIndex is more likely to change. + if(currentNetwork.BSSID != NULL) + { + currentSSID = currentNetwork.SSID; + currentWiFiChannel = currentNetwork.wifiChannel; + currentBSSID = currentNetwork.BSSID; + } + else // Use only networkIndex + { + currentSSID = WiFi.SSID(currentNetwork.networkIndex); + currentWiFiChannel = WiFi.channel(currentNetwork.networkIndex); + currentBSSID = WiFi.BSSID(currentNetwork.networkIndex); + } + + if(verboseMode()) // Avoid string generation if not required + { + printAPInfo(currentNetwork.networkIndex, currentSSID, currentWiFiChannel); + verboseModePrint(F("")); + } + + EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(currentBSSID); + encrypted_connection_status_t connectionStatus = ECS_MAX_CONNECTIONS_REACHED_SELF; + + innerMutexTracker.releaseMutex(); + + connectionStatus = requestFlexibleTemporaryEncryptedConnection(currentBSSID, getAutoEncryptionDuration()); + + innerMutexTracker = MutexTracker(_espnowTransmissionMutex); + if(!innerMutexTracker.mutexCaptured()) + { + assert(false && "ERROR! Unable to recapture Mutex in attemptAutoEncryptingTransmission. Aborting."); + return; + } + + if(connectionStatus == ECS_CONNECTION_ESTABLISHED) + { + uint32_t transmissionStartTime = millis(); + transmission_status_t transmissionResult = sendRequest(getMessage(), currentBSSID); + + uint32_t transmissionDuration = millis() - transmissionStartTime; + + if(verboseMode() && transmissionResult == TS_TRANSMISSION_COMPLETE) // Avoid calculations if not required + { + totalDurationWhenSuccessful_AT += transmissionDuration; + successfulTransmissions_AT++; + if(transmissionDuration > maxTransmissionDuration_AT) + { + maxTransmissionDuration_AT = transmissionDuration; + } + } + + latestTransmissionOutcomes.push_back(TransmissionResult{.origin = currentNetwork, .transmissionStatus = transmissionResult}); + } + else + { + latestTransmissionOutcomes.push_back(TransmissionResult{.origin = currentNetwork, .transmissionStatus = TS_CONNECTION_FAILED}); + } + + if(!encryptedConnection) + { + // Remove any connection that was added during the transmission attempt. + removeEncryptedConnectionUnprotected(currentBSSID); + } + } + + if(verboseMode() && successfulTransmissions_AT > 0) // Avoid calculations if not required + { + verboseModePrint("Average duration of successful transmissions: " + String(totalDurationWhenSuccessful_AT/successfulTransmissions_AT) + " ms."); + verboseModePrint("Maximum duration of successful transmissions: " + String(maxTransmissionDuration_AT) + " ms."); + } + else + { + verboseModePrint("No successful transmission."); + } +} + +void EspnowMeshBackend::sendEspnowResponses() +{ + //uint32_t startTime = millis(); + + uint32_t bufferedCriticalHeapLevel = criticalHeapLevel() + criticalHeapLevelBuffer(); // We preferably want to start clearing the logs a bit before things get critical. + + for(std::list::iterator confirmationsIterator = peerRequestConfirmationsToSend.begin(); confirmationsIterator != peerRequestConfirmationsToSend.end(); ) + { + using namespace EspnowProtocolInterpreter; + + auto timeTrackerPointer = confirmationsIterator->temporary(); + assert(timeTrackerPointer); // peerRequestConfirmations should always expire and so should always have a timeTracker + if(timeTrackerPointer->timeSinceCreation() > getEncryptionRequestTimeout()) + { + ++confirmationsIterator; + continue; + } + + uint8_t defaultBSSID[6] {0}; + confirmationsIterator->getEncryptedPeerMac(defaultBSSID); + uint8_t unencryptedBSSID[6] {0}; + confirmationsIterator->getUnencryptedPeerMac(unencryptedBSSID); + uint8_t hashKey[espnowHashKeyLength] {0}; + confirmationsIterator->getHashKey(hashKey); + + EncryptedConnectionLog *existingEncryptedConnection = getEncryptedConnection(defaultBSSID); + + // If we receive a non-encrypted request for encrypted connection from a node that already exists as an encrypted peer for us we cannot send a response to the encrypted MAC + // since that transmission will then be encrypted and impossible for the request sender to read. Of course, removing the existing encrypted connection would also work, + // but make it very simple for a third party to disrupt an encrypted connection by just sending random requests for encrypted connection. + bool sendToDefaultBSSID = confirmationsIterator->requestEncrypted() || !existingEncryptedConnection; + + // Note that callbacks can be called during delay time, so it is possible to receive a transmission during espnowSendToNode + // (which may add an element to the peerRequestConfirmationsToSend list). + + staticVerboseModePrint("Responding to encrypted connection request from MAC " + macToString(defaultBSSID)); + + if(!existingEncryptedConnection && encryptedConnections.size() >= maxEncryptedConnections) + { + espnowSendToNodeUnsynchronized(JsonTranslator::createEncryptionRequestHmacMessage(maxConnectionsReachedHeader, + confirmationsIterator->getPeerRequestNonce(), hashKey, espnowHashKeyLength), + defaultBSSID, 'C', generateMessageID(nullptr)); // Generates a new message ID to avoid sending encrypted sessionKeys over unencrypted connections. + + confirmationsIterator = peerRequestConfirmationsToSend.erase(confirmationsIterator); + } + else if(espnowSendToNodeUnsynchronized(JsonTranslator::createEncryptionRequestHmacMessage(basicConnectionInfoHeader, + confirmationsIterator->getPeerRequestNonce(), hashKey, espnowHashKeyLength), + sendToDefaultBSSID ? defaultBSSID : unencryptedBSSID, 'C', generateMessageID(nullptr)) // Generates a new message ID to avoid sending encrypted sessionKeys over unencrypted connections. + == TS_TRANSMISSION_COMPLETE) + { + // Try to add encrypted connection. If connection added send confirmation with encryptedConnection->getOwnSessionKey() as session key and C type message (won't increment key). Then proceed with next request (no need to wait for answer). + if(existingEncryptedConnection) + { + if(auto timeTrackerPointer = existingEncryptedConnection->temporary()) + { + if(getEncryptionRequestTimeout() > timeTrackerPointer->remainingDuration()) + { + existingEncryptedConnection->setRemainingDuration(getEncryptionRequestTimeout()); + } + } + } + else if(EspnowMeshBackend *currentEspnowRequestManager = getEspnowRequestManager()) + { + uint8_t staMacArray[6] = { 0 }; + uint8_t apMacArray[6] = { 0 }; + currentEspnowRequestManager->addTemporaryEncryptedConnection(confirmationsIterator->getPeerStaMac(staMacArray), confirmationsIterator->getPeerApMac(apMacArray), + createSessionKey(), createSessionKey(), getEncryptionRequestTimeout()); + existingEncryptedConnection = getEncryptedConnection(defaultBSSID); + } + else + { + warningPrint("WARNING! Ignoring received encrypted connection request since no EspnowRequestManager is assigned."); + } + + if(!existingEncryptedConnection) + { + // Send "node full" message + espnowSendToNodeUnsynchronized(JsonTranslator::createEncryptionRequestHmacMessage(maxConnectionsReachedHeader, + confirmationsIterator->getPeerRequestNonce(), hashKey, espnowHashKeyLength), + defaultBSSID, 'C', generateMessageID(nullptr)); // Generates a new message ID to avoid sending encrypted sessionKeys over unencrypted connections. + } + else + { + delay(1); // Give some time for the peer to add an encrypted connection + + // Send password and keys. + // Probably no need to know which connection type to use, that is stored in request node and will be sent over for finalization. + espnowSendToNodeUnsynchronized(JsonTranslator::createEncryptedConnectionInfo( + confirmationsIterator->getPeerRequestNonce(), confirmationsIterator->getAuthenticationPassword(), + existingEncryptedConnection->getOwnSessionKey(), existingEncryptedConnection->getPeerSessionKey()), + defaultBSSID, 'C', generateMessageID(nullptr)); // Generates a new message ID to avoid sending encrypted sessionKeys over unencrypted connections. + } + + confirmationsIterator = peerRequestConfirmationsToSend.erase(confirmationsIterator); + } + else + { + ++confirmationsIterator; + } + + if(ESP.getFreeHeap() <= bufferedCriticalHeapLevel) + { + // Heap is getting very low, which probably means we are receiving a lot of transmissions while trying to transmit responses. + // Clear all old data to try to avoid running out of memory. + warningPrint("WARNING! Free heap below chosen minimum. Performing emergency log clearing."); + clearOldLogEntries(); + return; // confirmationsIterator may be invalid now. Also, we should give the main loop a chance to respond to the situation. + } + } + + for(std::list::iterator responseIterator = responsesToSend.begin(); responseIterator != responsesToSend.end(); ) + { + if(responseIterator->timeSinceCreation() > logEntryLifetimeMs()) + { + // If the response is older than logEntryLifetimeMs(), the corresponding request log entry has been deleted at the request sender, + // so the request sender will not accept our response any more. + // This probably happens because we have a high transmission activity and more requests coming in than we can handle. + ++responseIterator; + continue; + } + + // Note that callbacks can be called during delay time, so it is possible to receive a transmission during espnowSendToNode + // (which may add an element to the responsesToSend list). + if(espnowSendToNodeUnsynchronized(responseIterator->getMessage(), responseIterator->getRecipientMac(), 'A', responseIterator->getRequestID()) + == TS_TRANSMISSION_COMPLETE) + { + responseIterator = responsesToSend.erase(responseIterator); + } + else + { + ++responseIterator; + } + + if(ESP.getFreeHeap() <= bufferedCriticalHeapLevel) + { + // Heap is getting very low, which probably means we are receiving a lot of transmissions while trying to transmit responses. + // Clear all old data to try to avoid running out of memory. + warningPrint("WARNING! Free heap below chosen minimum. Performing emergency log clearing."); + clearOldLogEntries(); + return; // responseIterator may be invalid now. Also, we should give the main loop a chance to respond to the situation. + } + } +} + +uint32_t EspnowMeshBackend::getMaxBytesPerTransmission() +{ + return _maxBytesPerTransmission; +} + +uint32_t EspnowMeshBackend::getMaxMessageBytesPerTransmission() +{ + return getMaxBytesPerTransmission() - EspnowProtocolInterpreter::espnowProtocolBytesSize(); +} + +void EspnowMeshBackend::setMaxTransmissionsPerMessage(uint8_t maxTransmissionsPerMessage) +{ + assert(1 <= maxTransmissionsPerMessage && maxTransmissionsPerMessage <= 128); + + _maxTransmissionsPerMessage = maxTransmissionsPerMessage; +} + +uint8_t EspnowMeshBackend::getMaxTransmissionsPerMessage() {return _maxTransmissionsPerMessage;} + +uint32_t EspnowMeshBackend::getMaxMessageLength() +{ + return getMaxTransmissionsPerMessage() * getMaxMessageBytesPerTransmission(); +} + +uint8_t EspnowMeshBackend::numberOfEncryptedConnections() +{ + return encryptedConnections.size(); +} + +espnow_connection_type_t EspnowMeshBackend::getConnectionInfoHelper(const EncryptedConnectionLog *encryptedConnection, uint32_t *remainingDuration, uint8_t *peerMac) +{ + if(!encryptedConnection) + { + return ECT_NO_CONNECTION; + } + else + { + if(peerMac) + encryptedConnection->getEncryptedPeerMac(peerMac); + + if(const ExpiringTimeTracker *timeTracker = encryptedConnection->temporary()) + { + if(remainingDuration) + *remainingDuration = timeTracker->remainingDuration(); + + return ECT_TEMPORARY_CONNECTION; + } + else + { + return ECT_PERMANENT_CONNECTION; + } + } +} + +espnow_connection_type_t EspnowMeshBackend::getConnectionInfo(uint8_t *peerMac, uint32_t *remainingDuration) +{ + EncryptedConnectionLog *encryptedConnection = nullptr; + + if(peerMac) + encryptedConnection = getEncryptedConnection(peerMac); + + return getConnectionInfoHelper(encryptedConnection, remainingDuration); +} + +espnow_connection_type_t EspnowMeshBackend::getConnectionInfo(uint32_t connectionIndex, uint32_t *remainingDuration, uint8_t *peerMac) +{ + EncryptedConnectionLog *encryptedConnection = nullptr; + + if(connectionIndex < numberOfEncryptedConnections()) + encryptedConnection = &encryptedConnections[connectionIndex]; + + return getConnectionInfoHelper(encryptedConnection, remainingDuration, peerMac); +} + +double EspnowMeshBackend::getTransmissionFailRate() +{ + if(_transmissionsTotal == 0) + return 0; + else + return _transmissionsFailed/_transmissionsTotal; +} + +void EspnowMeshBackend::resetTransmissionFailRate() +{ + _transmissionsFailed = 0; + _transmissionsTotal = 0; +} + +String EspnowMeshBackend::serializeEncryptedConnection(const uint8_t *peerMac) +{ + EncryptedConnectionLog *encryptedConnection = nullptr; + + if(peerMac) + encryptedConnection = getEncryptedConnection(peerMac); + + if(encryptedConnection) + return encryptedConnection->serialize(); + else + return ""; +} + +String EspnowMeshBackend::serializeEncryptedConnection(uint32_t connectionIndex) +{ + if(connectionIndex < numberOfEncryptedConnections()) + return encryptedConnections[connectionIndex].serialize(); + else + return ""; +} diff --git a/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.h b/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.h new file mode 100644 index 000000000..a340029a2 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.h @@ -0,0 +1,764 @@ +/* + EspnowMeshBackend + + Copyright (C) 2019 Anders Löfgren + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +// ESP-NOW is faster for small data payloads (up to a few kB, split over multiple messages). Transfer of up to 234 bytes takes 4 ms. +// In general ESP-NOW transfer time can be approximated with the following function: transferTime = ceil(bytesToTransfer / 234.0)*3 ms. +// If you only transfer 234 bytes at a time, this adds up to around 56kB/s. Finally a chance to relive the glory of the olden days +// when people were restricted to V90 dial-up modems for internet access! +// TCP-IP takes longer to connect (around 1000 ms), and an AP has to disconnect all connected stations in order to transfer data to another AP, +// but this backend has a much higher data transfer speed than ESP-NOW once connected (100x faster or so). + +/** + * Encryption pairing process, schematic overview: + * + * Connection | Peer sends: | Peer requester sends: | Connection + * encrypted: | | | encrypted: + * | | Peer request + Nonce | + * | StaMac + Nonce + HMAC | | + * | | Ack | + * X | SessionKeys + Nonce + Password | | X + * X | | Ack | X + * X | | SessionKey | X + * X | Ack | | X + * | | | + * + * + * The ESP-NOW CCMP encryption should have replay attack protection built in, + * but since there is no official documentation from Espressif about this a 128 bit random nonce is included in encrypted connection requests. + */ + +#ifndef __ESPNOWMESHBACKEND_H__ +#define __ESPNOWMESHBACKEND_H__ + +#include "MeshBackendBase.h" +#include "EspnowProtocolInterpreter.h" +#include "EncryptedConnectionLog.h" +#include "PeerRequestLog.h" +#include "RequestData.h" +#include "ResponseData.h" +#include "MessageData.h" +#include +#include +#include "Crypto.h" + +typedef enum +{ + ECT_NO_CONNECTION = 0, + ECT_TEMPORARY_CONNECTION = 1, + ECT_PERMANENT_CONNECTION = 2 +} espnow_connection_type_t; + +typedef enum +{ + ECS_MAX_CONNECTIONS_REACHED_SELF = -3, + ECS_REQUEST_TRANSMISSION_FAILED = -2, + ECS_MAX_CONNECTIONS_REACHED_PEER = -1, + ECS_API_CALL_FAILED = 0, + ECS_CONNECTION_ESTABLISHED = 1 +} encrypted_connection_status_t; + +typedef enum +{ + ECRO_REMOVAL_REQUEST_FAILED = -1, + ECRO_REMOVAL_FAILED = 0, + ECRO_REMOVAL_SUCCEEDED = 1, + ECRO_REMOVAL_SCHEDULED = 2 +} encrypted_connection_removal_outcome_t; + + +/** + * An alternative to standard delay(). Will continuously call performEspnowMaintainance() during the waiting time, so that the ESP-NOW node remains responsive. + * Note that if there is a lot of ESP-NOW transmission activity to the node during the espnowDelay, the desired duration may be overshot by several ms. + * Thus, if precise timing is required, use standard delay() instead. + * + * Should not be used inside responseHandler, requestHandler or networkFilter callbacks since performEspnowMaintainance() can alter the ESP-NOW state. + * + * @param durationMs The shortest allowed delay duration, in milliseconds. + */ +void espnowDelay(uint32_t durationMs); + +class RequestData; + +class EspnowMeshBackend : public MeshBackendBase { + +public: + + /** + * WiFiMesh Constructor method. Creates a WiFi Mesh 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. + * @param responseHandler The callback handler for dealing with received responses. Takes a string as an argument which + * is the response string received from another node. Returns a transmission status code as a transmission_status_t. + * @param networkFilter The callback handler for deciding which WiFi networks to connect to. + * @param meshPassword The WiFi password for the mesh network. + * @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. + * + */ + EspnowMeshBackend(requestHandlerType requestHandler, responseHandlerType responseHandler, networkFilterType networkFilter, + 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); + + ~EspnowMeshBackend() override; + + /** + * Initialises the node. + */ + void begin() override; + + /** + * This method performs all the background operations for the EspnowMeshBackend. + * It is recommended to place it in the beginning of the loop(), unless there is a need to put it elsewhere. + * Among other things, the method cleans up old Espnow log entries (freeing up RAM) and sends the responses you provide to Espnow requests. + * Note that depending on the amount of responses to send and their length, this method can take tens or even hundreds of milliseconds to complete. + * More intense transmission activity and less frequent calls to performEspnowMaintainance will likely cause the method to take longer to complete, so plan accordingly. + * + * Should not be used inside responseHandler, requestHandler or networkFilter callbacks since performEspnowMaintainance() can alter the ESP-NOW state. + */ + static void performEspnowMaintainance(); + + /** + * At critical heap level no more incoming requests are accepted. + */ + static uint32_t criticalHeapLevel(); + + /** + * At critical heap level no more incoming requests are accepted. + * This method sets the maximum number of bytes above the critical heap level that will trigger an early ESP-NOW log clearing in an attempt to increase available heap size. + * A too high value may cause very frequent early log clearings, which will slow things down. Especially if you are using a lot of heap in other parts of your program. + * A too low value may cause some incoming requests to be lost and/or an increase in heap fragmentation, + * especially if you quickly fill the heap by receiving a lot of large ESP-NOW messages or sending a lot of large ESP-NOW responses. + * The buffer is set to 6000 bytes by default, which should be enough to prevent lost incoming requests while giving plenty of heap to fill up before early clearing in most circumstances. + * + * The buffer can be set lower than the default if you are running low on heap, since it may otherwise be hard to get responses sent. + * However, lower values tend to result in more heap fragmentation during intense transmission activity. + * Depending on your situation (message size, transmission frequency), values below 2000-3000 bytes will also start to cause lost incoming requests due to heap shortage. + * + * If the buffer is set to 0 bytes a significant number of incoming requests are likely to be lost during intense transmission activity, + * and there is a greater risk of heap space completely running out before log clearing occurs (which may cause crashes or empty transmissions). + */ + static void setCriticalHeapLevelBuffer(uint32_t bufferInBytes); + static uint32_t criticalHeapLevelBuffer(); + + /** + * Deactivates Espnow for this node. Call begin() on a EspnowMeshBackend instance to reactivate Espnow. + * + * @returns True if deactivation was successful. False otherwise. + */ + static bool deactivateEspnow(); + + void attemptTransmission(const String &message, bool scan = true, bool scanAllWiFiChannels = false) override; + + // Will ensure that an encrypted connection exists to each target node before sending the message, + // establishing a temporary encrypted connection with duration getAutoEncryptionDuration() first if neccessary. + // If an encrypted connection cannot be established to a target node, no message will be sent to that node. + // Note that if an encrypted connection to a target node is not present before this method is called, the response from said node will likely not be received + // since it will be encrypted and the auto encrypted connection to the node is immediately removed after transmission. + // Also note that if a temporary encrypted connection already exists to a target node, this method will slightly extend the connection duration + // depending on the time it takes to verify the connection to the node. This can substantially increase the connection duration if many auto encrypting + // transmissions occurs. + void attemptAutoEncryptingTransmission(const String &message, bool scan = true, bool scanAllWiFiChannels = false); + + /** + * Set the EspnowMeshBackend instance responsible for handling incoming requests. The requestHandler of the instance will be called upon receiving ESP-NOW requests. + * + * Set to nullptr to stop processing the ESP-NOW requests received by this node (requests will be ignored, but still received (ack will be sent)). + * The node can still send ESP-NOW transmissions to other nodes, even when the espnowRequestManager is nullptr. + */ + static void setEspnowRequestManager(EspnowMeshBackend *espnowMeshInstance); + + static EspnowMeshBackend *getEspnowRequestManager(); + + /** + * Check if this EspnowMeshBackend instance is the espnowRequestManager. + * + * @returns True if this EspnowMeshBackend is the espnowRequestManager. False otherwise. + */ + bool isEspnowRequestManager(); + + /** + * Change the key used by this EspnowMeshBackend instance for creating encrypted ESP-NOW connections. + * Will apply to any new received requests for encrypted connection if this EspnowMeshBackend instance is the current request manager. + * Will apply to any new encrypted connections requested or added by this EspnowMeshBackend instance. + * + * NOTE: Encrypted connections added before the encryption key change will retain their old encryption key. + * Only changes the encryption key used by this EspnowMeshBackend instance, so each instance can use a separate key. + * Both Kok and encryption key must match in an encrypted connection pair for encrypted communication to be possible. + * Otherwise the transmissions will never reach the recipient, even though acks are received by the sender. + * + * @param espnowEncryptionKey An array containing the espnowEncryptionKeyLength bytes that will be used as the encryption key. + */ + void setEspnowEncryptionKey(const uint8_t espnowEncryptionKey[EspnowProtocolInterpreter::espnowEncryptionKeyLength]); + + /** + * Get the encryption key used by this EspnowMeshBackend instance for creating encrypted ESP-NOW connections. + * + * @return The current espnowEncryptionKey for this EspnowMeshBackend instance. + */ + const uint8_t *getEspnowEncryptionKey(); + uint8_t *getEspnowEncryptionKey(uint8_t resultArray[EspnowProtocolInterpreter::espnowEncryptionKeyLength]); + + /** + * Change the key used to encrypt/decrypt the encryption key when creating encrypted ESP-NOW connections. (Kok = key of keys, perhaps) If no Kok is provided by the user, a default Kok is used. + * Will apply to any new encrypted connections. + * Must be called after begin() to take effect. + * + * NOTE: Encrypted connections added before the Kok change will retain their old Kok. + * This changes the Kok for all EspnowMeshBackend instances on this ESP8266. + * Both Kok and encryption key must match in an encrypted connection pair for encrypted communication to be possible. + * Otherwise the transmissions will never reach the recipient, even though acks are received by the sender. + * + * @param espnowEncryptionKok An array containing the espnowEncryptionKeyLength bytes that will be used as the Kok. + * @return True if Kok was changed successfully. False if Kok was not changed. + */ + static bool setEspnowEncryptionKok(uint8_t espnowEncryptionKok[EspnowProtocolInterpreter::espnowEncryptionKeyLength]); + + /** + * Get the key used to encrypt the encryption keys when creating encrypted ESP-NOW connections. (Kok = key of keys, perhaps) Returns nullptr if no Kok has been provided by the user. + * + * @return nullptr if default Kok is used, or current espnowEncryptionKok if a custom Kok has been set via the setEspnowEncryptionKok method. + */ + static const uint8_t *getEspnowEncryptionKok(); + + /** + * Change the secret key used to generate HMACs for encrypted ESP-NOW connections. + * Will apply to any new received requests for encrypted connection if this EspnowMeshBackend instance is the current request manager. + * Will apply to any new encrypted connections requested or added by this EspnowMeshBackend instance. + * + * NOTE: Encrypted connections added before the key change will retain their old key. + * Only changes the secret hash key used by this EspnowMeshBackend instance, so each instance can use a separate secret key. + * + * @param espnowHashKey An array containing the espnowHashKeyLength bytes that will be used as the HMAC key. + */ + void setEspnowHashKey(const uint8_t espnowHashKey[EspnowProtocolInterpreter::espnowHashKeyLength]); + const uint8_t *getEspnowHashKey(); + + /** + * Hint: Use String.length() to get the ASCII length of a String. + * + * @returns The maximum number of bytes (or ASCII characters) a transmission can contain. Note that non-ASCII characters usually require the space of at least two ASCII characters each. + */ + static uint32_t getMaxMessageBytesPerTransmission(); + + /** + * Set the maximum acceptable message length, in terms of transmissions, when sending a message from this node. + * This has no effect when receiving messages, the limit for receiving is always 256 transmissions per message. + * Note that although values up to 128 are possible, this would in practice fill almost all the RAM available on the ESP8266 with just one message. + * Thus, if this value is set higher than the default, make sure there is enough heap available to store the messages + * and don't send messages more frequently than they can be processed. + * Also note that a higher value will make the node less responsive as it will be spending a long time transmitting. + * + * Typical symptoms of running out of heap are crashes and messages that become empty even though they shouldn't be. Keep this in mind if going beyond the default. + * + * @param maxTransmissionsPerMessage The maximum acceptable message length, in terms of transmissions, when sending a message from this node. Valid values are 1 to 128. Defaults to 3. + */ + static void setMaxTransmissionsPerMessage(uint8_t maxTransmissionsPerMessage); + static uint8_t getMaxTransmissionsPerMessage(); + + /** + * Hint: Use String.length() to get the ASCII length of a String. + * + * @returns The maximum length in bytes an ASCII message is allowed to be when transmitted by this node. Note that non-ASCII characters usually require at least two bytes each. + */ + static uint32_t getMaxMessageLength(); + + /** + * Set whether the normal events occurring in the library will be printed to Serial or not. Off by default. + * This setting is shared by all EspnowMeshBackend instances. + * + * @param enabled If true, library Serial prints are activated. + */ + void setVerboseModeState(bool enabled) override; + bool verboseMode() override; + + /** + * Only print stringToPrint if verboseMode() returns true. + * + * @param stringToPrint String to print. + * @param newline If true, will end the print with a newline. True by default. + */ + void verboseModePrint(const String &stringToPrint, bool newline = true) override; + + /** + * Same as verboseMode(), but used for printing from static functions. + * + * @returns True if the normal events occurring in the library will be printed to Serial. False otherwise. + */ + static bool staticVerboseMode(); + + /** + * Only print stringToPrint if staticVerboseMode() returns true. + * + * @param stringToPrint String to print. + * @param newline If true, will end the print with a newline. True by default. + */ + static void staticVerboseModePrint(const String &stringToPrint, bool newline = true); + + /** + * Get the message of the response at responseIndex among the responses that are scheduled for transmission from this node. + * + * @param responseIndex The index of the response. Must be lower than numberOfScheduledResponses(). + * @returns A String containing the message of the response at responseIndex. + */ + static String getScheduledResponseMessage(uint32_t responseIndex); + + /** + * Get the MAC address for the recipient of the response at responseIndex among the responses that are scheduled for transmission from this node. + * + * @param responseIndex The index of the response. Must be lower than numberOfScheduledResponses(). + * @returns An array with six bytes containing the MAC address for the recipient of the response at responseIndex. + */ + static const uint8_t *getScheduledResponseRecipient(uint32_t responseIndex); + + /** + * Get the number of ESP-NOW responses that are scheduled for transmission from this node. + * + * @returns The number of ESP-NOW responses scheduled for transmission. + */ + static uint32_t numberOfScheduledResponses(); + + /** + * Remove all responses that have been scheduled for transmission but are not yet transmitted. + * Note that cleared responses will not be received by their recipient. + */ + static void clearAllScheduledResponses(); + + /** + * Remove all responses targeting recipientMac that have been scheduled for transmission but are not yet transmitted. + * Optionally deletes only responses to encrypted requests. + * Note that deleted responses will not be received by their recipient. + * + * @param recipientMac The MAC address of the response recipient. + * @param encryptedOnly If true, only responses to encrypted requests will be deleted. + */ + static void deleteScheduledResponsesByRecipient(const uint8_t *recipientMac, bool encryptedOnly); + + /** + * Set the timeout to use for each ESP-NOW transmission when transmitting. + * Note that for multi-part transmissions (where message length is greater than getMaxMessageBytesPerTransmission()), the timeout is reset for each transmission part. + * The default timeouts should fit most use cases, but in case you do a lot of time consuming processing when the node receives a message, you may need to relax them a bit. + * + * @param timeoutMs The timeout that should be used for each ESP-NOW transmission, in milliseconds. Defaults to 40 ms. + */ + static void setEspnowTransmissionTimeout(uint32_t timeoutMs); + static uint32_t getEspnowTransmissionTimeout(); + + /** + * Set the time to wait for an ack after having made an ESP-NOW transmission. If no ack is received within said time, a new transmission attempt is made. + * Note that if a retransmission causes duplicate transmissions to reach the receiver, the duplicates will be detected and ignored automatically. + * The default timeouts should fit most use cases, but in case you do a lot of time consuming processing when the node receives a message, you may need to relax them a bit. + * + * @param intervalMs The time to wait for an ack after having made an ESP-NOW transmission, in milliseconds. Defaults to 15 ms. + */ + static void setEspnowRetransmissionInterval(uint32_t intervalMs); + static uint32_t getEspnowRetransmissionInterval(); + + // The maximum amount of time each of the two stages in an encrypted connection request may take. + static void setEncryptionRequestTimeout(uint32_t timeoutMs); + static uint32_t getEncryptionRequestTimeout(); + + void setAutoEncryptionDuration(uint32_t duration); + uint32_t getAutoEncryptionDuration(); + + /** + * Get the MAC address of the sender of the most recently received ESP-NOW request or response to this EspnowMeshBackend instance. + * Returns a String. + * By default the MAC will be that of the sender's station interface. The only exception is for unencrypted + * responses to requests sent to an AP interface, which will return the response sender's AP interface MAC. + * + * @returns A String filled with a hexadecimal representation of the MAC, without delimiters. + */ + String getSenderMac(); + + /** + * Get the MAC address of the sender of the most recently received ESP-NOW request or response to this EspnowMeshBackend instance. + * Returns a uint8_t array. + * By default the MAC will be that of the sender's station interface. The only exception is for unencrypted + * responses to requests sent to an AP interface, which will return the response sender's AP interface MAC. + * + * @param macArray The array that should store the MAC address. Must be at least 6 bytes. + * @returns macArray filled with the sender MAC. + */ + uint8_t *getSenderMac(uint8_t *macArray); + + /** + * Get whether the ESP-NOW request or response which was most recently received by this EspnowMeshBackend instance was encrypted or not. + * + * @returns If true, the request or response was encrypted. If false, it was unencrypted. + */ + bool receivedEncryptedMessage(); + + // Updates connection with current stored encryption key. + // At least one of the leftmost 32 bits in each of the session keys should be 1, since the key otherwise indicates the connection is unencrypted. + encrypted_connection_status_t addEncryptedConnection(uint8_t *peerStaMac, uint8_t *peerApMac, uint64_t peerSessionKey, uint64_t ownSessionKey); + // Note that the espnowEncryptionKey, espnowEncryptionKok and espnowHashKey are not serialized. + // These will be set to the values of the EspnowMeshBackend instance that is adding the serialized encrypted connection. + // @param ignoreDuration Ignores any stored duration serializedConnectionState, guaranteeing that the created connection will be permanent. Returns: ECS_REQUEST_TRANSMISSION_FAILED indicates malformed serializedConnectionState. + encrypted_connection_status_t addEncryptedConnection(const String &serializedConnectionState, bool ignoreDuration = false); + + // Adds a new temporary encrypted connection, or changes the duration of an existing temporary connection (only updates keys, not duration, for existing permanent connections). + // As with all these methods, changes will only take effect once the requester proves it has the ability to decrypt the session key. + // At least one of the leftmost 32 bits in each of the session keys should be 1, since the key otherwise indicates the connection is unencrypted. + encrypted_connection_status_t addTemporaryEncryptedConnection(uint8_t *peerStaMac, uint8_t *peerApMac, uint64_t peerSessionKey, uint64_t ownSessionKey, uint32_t duration); + // Note that the espnowEncryptionKey, espnowEncryptionKok and espnowHashKey are not serialized. + // These will be set to the values of the EspnowMeshBackend instance that is adding the serialized encrypted connection. + // Uses duration argument instead of any stored duration in serializedConnectionState. Returns: ECS_REQUEST_TRANSMISSION_FAILED indicates malformed serializedConnectionState. + encrypted_connection_status_t addTemporaryEncryptedConnection(const String &serializedConnectionState, uint32_t duration); + + // If an encrypted connection to peerMac already exists, only connection duration is updated. All other settings are kept as is. Use removeEncryptedConnection/requestEncryptedConnectionRemoval first if encryption keys should be updated. + // Makes sure both nodes have an encrypted connection to each other that's permanent. + encrypted_connection_status_t requestEncryptedConnection(uint8_t *peerMac); + // Makes sure both nodes have an encrypted connection to each other that's either permanent or has the duration specified. + encrypted_connection_status_t requestTemporaryEncryptedConnection(uint8_t *peerMac, uint32_t durationMs); + // Makes sure both nodes have an encrypted connection to each other that's either permanent or has at least the duration specified. + // Note that if a temporary encrypted connection already exists to a target node, this method will slightly extend the connection duration + // depending on the time it takes to verify the connection to the node. + encrypted_connection_status_t requestFlexibleTemporaryEncryptedConnection(uint8_t *peerMac, uint32_t minDurationMs); + static encrypted_connection_removal_outcome_t removeEncryptedConnection(uint8_t *peerMac); + encrypted_connection_removal_outcome_t requestEncryptedConnectionRemoval(uint8_t *peerMac); + + /** + * Set whether this EspnowMeshBackend instance will accept unencrypted ESP-NOW requests or not, when acting as EspnowRequestManager. + * When set to false and combined with already existing encrypted connections, this can be used to ensure only encrypted transmissions are processed. + * When set to false it will also make it impossible to send unencrypted requests for encrypted connection to the node, + * which can be useful if too many such requests could otherwise be expected. + * + * @param acceptsUnencryptedRequests If and only if true, unencrypted requests will be processed when this EspnowMeshBackend instance is acting as EspnowRequestManager. True by default. + */ + void setAcceptsUnencryptedRequests(bool acceptsUnencryptedRequests); + bool acceptsUnencryptedRequests(); + + /** + * @ returns The current number of encrypted ESP-NOW connections. + */ + static uint8_t numberOfEncryptedConnections(); + + // @returns resultArray filled with the MAC to the encrypted interface of the node, if an encrypted connection exists. nulltpr otherwise. + static uint8_t *getEncryptedMac(const uint8_t *peerMac, uint8_t *resultArray); + + // Create a string containing the current state of the encrypted connection for this node. The result can be used as input to addEncryptedConnection. + // Note that transferring the serialized state over an unencrypted connection will compromise the security of the stored connection. + // @ returns A String containing the serialized encrypted connection, or an empty String if there is no matching encrypted connection. + static String serializeEncryptedConnection(const uint8_t *peerMac); + static String serializeEncryptedConnection(uint32_t connectionIndex); + + /** + * Get information about any current ESP-NOW connection with another node. + * + * @param peerMac The node MAC for which to get information. Both MAC for AP interface and MAC for STA interface can be used (and will yield the same result). + * Use the getEncryptedMac method or the indexed based getConnectionInfo if there is a need to find the actual encrypted interface. + * @param remainingDuration An optional pointer to a uint32_t variable. + * If supplied and the connection type is ECT_TEMPORARY_CONNECTION the variable will be set to the remaining duration of the connection. + * Otherwise the variable value is not modified. + * @ returns The espnow_connection_type_t of the connection with peerMac. + */ + static espnow_connection_type_t getConnectionInfo(uint8_t *peerMac, uint32_t *remainingDuration = nullptr); + + /** + * Get information about any current ESP-NOW connection with another node. + * + * @param connectionIndex The connection index of the node for which to get information. Valid values are limited by numberOfEncryptedConnections(). + * @param remainingDuration An optional pointer to a uint32_t variable. + * If supplied and the connection type is ECT_TEMPORARY_CONNECTION the variable will be set to the remaining duration of the connection. + * Otherwise the variable value is not modified. + * @param peerMac An optional pointer to an uint8_t array with at least size 6. It will be filled with the MAC of the encrypted peer interface if an encrypted connection exists. + * Otherwise the array is not modified. + * @ returns The espnow_connection_type_t of the connection given by connectionIndex. + */ + static espnow_connection_type_t getConnectionInfo(uint32_t connectionIndex, uint32_t *remainingDuration = nullptr, uint8_t *peerMac = nullptr); + + /** + * @returns The proportion of ESP-NOW requests made by this node that have failed, since power on or latest reset. + */ + static double getTransmissionFailRate(); + + /** + * Reset TransmissionFailRate back to 0. + */ + static void resetTransmissionFailRate(); + +protected: + + typedef std::vector::iterator connectionLogIterator; + static connectionLogIterator connectionLogEndIterator(); + + bool activateEspnow(); + + /* + * Note that ESP-NOW is not perfect and in rare cases messages may be dropped. + * This needs to be compensated for in the application via extra verification + * (e.g. by always sending a response such as a message hash), if message delivery must be guaranteed. + * + * Note that although responses will generally be sent in the order they were created, this is not guaranteed to be the case. + * For example, response order will be mixed up if some responses fail to transmit while others transmit successfully. + */ + static void sendEspnowResponses(); + static void clearOldLogEntries(); + + static uint32_t getMaxBytesPerTransmission(); + + static std::list::const_iterator getScheduledResponse(uint32_t responseIndex); + + // Note that removing an encrypted connection while there are encrypted responses scheduled for transmission to the encrypted peer will cause these encrypted responses to be removed without being sent. + // Also note that removing an encrypted connection while there is encrypted data to be received will make the node unable to decrypt that data (although an ack will still be sent to confirm data reception). + // In other words, it is good to use these methods with care and to make sure that both nodes in an encrypted pair are in a state where it is safe for the encrypted connection to be removed before using them. + // Consider using getScheduledResponseRecipient and similar methods for this preparation. + // Should only be used when there is no transmissions in progress. In practice when _espnowTransmissionMutex is free. + // @param resultingIterator Will be set to the iterator position after the removed element, if an element to remove was found. Otherwise no change will occur. + static encrypted_connection_removal_outcome_t removeEncryptedConnectionUnprotected(uint8_t *peerMac, std::vector::iterator *resultingIterator = nullptr); + static encrypted_connection_removal_outcome_t removeEncryptedConnectionUnprotected(connectionLogIterator &connectionIterator, std::vector::iterator *resultingIterator); + + /** + * Set the MAC address considered to be the sender of the most recently received ESP-NOW request or response. + * + * @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 setSenderMac(uint8_t *macArray); + + /** + * Set whether the most recently received ESP-NOW request or response is presented as having been encrypted or not. + * + * @param receivedEncryptedMessage If true, the request or response is presented as having been encrypted. + */ + void setReceivedEncryptedMessage(bool receivedEncryptedMessage); + + static bool temporaryEncryptedConnectionToPermanent(uint8_t *peerMac); + + /** + * Will be true if a transmission initiated by a public method is in progress. + */ + static bool _espnowTransmissionMutex; + + /** + * Check if there is an ongoing ESP-NOW transmission in the library. Used to avoid interrupting transmissions. + * + * @returns True if a transmission initiated by a public method is in progress. + */ + static bool transmissionInProgress(); + + enum class macAndType_td : uint64_t {}; + typedef uint64_t messageID_td; + typedef uint64_t peerMac_td; + + static macAndType_td createMacAndTypeValue(uint64_t uint64Mac, char messageType); + static uint64_t macAndTypeToUint64Mac(const macAndType_td &macAndTypeValue); + + /** + * Remove all entries which target peerMac in the logEntries map. + * Optionally deletes only entries sent/received by encrypted transmissions. + * + * @param logEntries The map to process. + * @param peerMac The MAC address of the peer node. + * @param encryptedOnly If true, only entries sent/received by encrypted transmissions will be deleted. + */ + template + static void deleteEntriesByMac(std::map, T> &logEntries, const uint8_t *peerMac, bool encryptedOnly); + + template + static void deleteEntriesByMac(std::map, T> &logEntries, const uint8_t *peerMac, bool encryptedOnly); + + static bool requestReceived(uint64_t requestMac, uint64_t requestID); + + /** + * Send an ESP-NOW message to the ESP8266 that has the MAC address specified in targetBSSID. + * + * @param messageType The identifier character for the type of message to send. Choices are 'Q' for question (request), + * 'A' for answer (response), 'S' for synchronization request, 'P' for peer request and 'C' for peer request confirmation. + * @returns The transmission status for the transmission. + */ + // Send a message to the node having targetBSSID as mac, changing targetBSSID to the mac of the encrypted connection if it exists and ensuring such an encrypted connection is synchronized. + static transmission_status_t espnowSendToNode(const String &message, const uint8_t *targetBSSID, char messageType, EspnowMeshBackend *espnowInstance = nullptr); + // Send a message using exactly the arguments given, without consideration for any encrypted connections. + static transmission_status_t espnowSendToNodeUnsynchronized(const String message, const uint8_t *targetBSSID, char messageType, uint64_t messageID, EspnowMeshBackend *espnowInstance = nullptr); + + transmission_status_t sendRequest(const String &message, const uint8_t *targetBSSID); + transmission_status_t sendResponse(const String &message, uint64_t requestID, const uint8_t *targetBSSID); + +private: + + typedef std::function encryptionRequestBuilderType; + static String defaultEncryptionRequestBuilder(const String &requestHeader, const uint32_t durationMs, const String &requestNonce, const ExpiringTimeTracker &existingTimeTracker); + static String flexibleEncryptionRequestBuilder(const uint32_t minDurationMs, const String &requestNonce, const ExpiringTimeTracker &existingTimeTracker); + + /** + * We can't feed esp_now_register_recv_cb our EspnowMeshBackend instance's espnowReceiveCallback method directly, so this callback wrapper is a workaround. + * + * This method is very time critical so avoid Serial printing in it and in methods called from it (such as espnowReceiveCallback) as much as possible. + * Otherwise transmission fail rate is likely to skyrocket. + */ + static void espnowReceiveCallbackWrapper(uint8_t *macaddr, uint8_t *dataArray, uint8_t len); + void espnowReceiveCallback(uint8_t *macaddr, uint8_t *data, uint8_t len); + + static void handlePeerRequest(uint8_t *macaddr, uint8_t *dataArray, uint8_t len, uint64_t uint64StationMac, uint64_t receivedMessageID); + static void handlePeerRequestConfirmation(uint8_t *macaddr, uint8_t *dataArray, uint8_t len); + + static void handlePostponedRemovals(); + + static bool verifyPeerSessionKey(uint64_t sessionKey, const uint8_t *peerMac, char messageType); + static bool verifyPeerSessionKey(uint64_t sessionKey, EncryptedConnectionLog &encryptedConnection, uint64_t uint64PeerMac, char messageType); + + static bool synchronizePeerSessionKey(uint64_t sessionKey, const uint8_t *peerMac); + static bool synchronizePeerSessionKey(uint64_t sessionKey, EncryptedConnectionLog &encryptedConnection); + + static const uint32_t _maxBytesPerTransmission = 250; + static uint8_t _maxTransmissionsPerMessage; + + static uint32_t _espnowTransmissionTimeoutMs; + static uint32_t _espnowRetransmissionIntervalMs; + + uint32_t _autoEncryptionDuration = 50; + + static bool _staticVerboseMode; + + static EspnowMeshBackend *_espnowRequestManager; + + static std::map, MessageData> receivedEspnowTransmissions; + static std::map, RequestData> sentRequests; + static std::map, TimeTracker> receivedRequests; + + static std::list responsesToSend; + static std::list peerRequestConfirmationsToSend; + + static std::vector encryptedConnections; + + static EncryptedConnectionLog *getEncryptedConnection(const uint8_t *peerMac); + static EncryptedConnectionLog *getTemporaryEncryptedConnection(const uint8_t *peerMac); + + //@returns iterator to connection in connectionVector, or connectionVector.end() if element not found + template + static typename std::vector::iterator getEncryptedConnectionIterator(const uint8_t *peerMac, typename std::vector &connectionVector); + static bool getEncryptedConnectionIterator(const uint8_t *peerMac, connectionLogIterator &iterator); + // @returns true if an encrypted connection to peerMac is found and the found connection is temporary. Only changes iterator if true is returned. + static bool getTemporaryEncryptedConnectionIterator(const uint8_t *peerMac, connectionLogIterator &iterator); + + static espnow_connection_type_t getConnectionInfoHelper(const EncryptedConnectionLog *encryptedConnection, uint32_t *remainingDuration, uint8_t *peerMac = nullptr); + + // Should only be used when there is no transmissions in progress, so it is safe to remove encrypted connections. In practice when _espnowTransmissionMutex is free. + // @param scheduledRemovalOnly If true, only deletes encrypted connections where removalScheduled() is true. This means only connections which have been requested for removal will be deleted, + // not other connections which have expired. + static void updateTemporaryEncryptedConnections(bool scheduledRemovalOnly = false); + + template + static void deleteExpiredLogEntries(std::map, T> &logEntries, uint32_t maxEntryLifetimeMs); + + template + static void deleteExpiredLogEntries(std::list &logEntries, uint32_t maxEntryLifetimeMs); + + static uint32_t _logEntryLifetimeMs; + static uint32_t logEntryLifetimeMs(); + static uint32_t _responseTimeoutMs; + static uint32_t responseTimeoutMs(); + + static uint32_t _encryptionRequestTimeoutMs; + + static uint32_t _timeOfLastLogClear; + static uint32_t _criticalHeapLevel; + static uint32_t _criticalHeapLevelBuffer; + + static bool _espnowSendConfirmed; + + static String _ongoingPeerRequestNonce; + static EspnowMeshBackend *_ongoingPeerRequester; + static encrypted_connection_status_t _ongoingPeerRequestResult; + static uint32_t _ongoingPeerRequestEncryptionStart; + + template + static T *getMapValue(std::map &mapIn, uint64_t keyIn); + + static bool usesConstantSessionKey(char messageType); + + bool _acceptsUnencryptedRequests = true; + + uint8_t _espnowEncryptionKey[EspnowProtocolInterpreter::espnowEncryptionKeyLength] {0}; + uint8_t _espnowHashKey[EspnowProtocolInterpreter::espnowHashKeyLength] {0}; + static uint8_t _espnowEncryptionKok[EspnowProtocolInterpreter::espnowEncryptionKeyLength]; + static bool _espnowEncryptionKokSet; + static uint32_t _unencryptedMessageID; + + uint8_t _senderMac[6] = {0}; + bool _receivedEncryptedMessage = false; + + static bool _espnowSendToNodeMutex; + static uint8_t _transmissionTargetBSSID[6]; + + static void storeSentRequest(const uint64_t targetBSSID, const uint64_t messageID, const RequestData &requestData); + static void storeReceivedRequest(const uint64_t senderBSSID, const uint64_t messageID, const TimeTracker &timeTracker); + + /** + * Get a pointer to the EspnowMeshBackend instance that sent a request with the given requestID to the specified mac address. + * + * @returns A valid EspnowMeshBackend pointer if a matching entry is found in the EspnowMeshBackend sentRequests container. nullptr otherwise. + */ + static EspnowMeshBackend *getOwnerOfSentRequest(uint64_t requestMac, uint64_t requestID); + + /** + * Delete all entries in the sentRequests container where requestMac is noted as having received requestID. + * + * @returns The number of entries deleted. + */ + static size_t deleteSentRequest(uint64_t requestMac, uint64_t requestID); + + static size_t deleteSentRequestsByOwner(EspnowMeshBackend *instancePointer); + + /** + * Contains the core logic used for requesting an encrypted connection to a peerMac. + * + * @param peerMac The MAC of the node with which an encrypted connection should be established. + * @param encryptionRequestBuilder A function which is responsible for constructing the request message to send. + * Called twice when the request is successful. First to build the initial request message and then to build the connection verification message. + * The request message should typically be of the form: JsonTranslator::createEncryptionRequestIntro() + JsonTranslator::createEncryptionRequestEnding(). + * @returns The ultimate status of the requested encrypted connection, as encrypted_connection_status_t. + */ + encrypted_connection_status_t requestEncryptedConnectionKernel(uint8_t *peerMac, const encryptionRequestBuilderType &encryptionRequestBuilder); + + /** + * Generate a new message ID to be used when making a data transmission. The generated ID will be different depending on whether an encrypted connection exists or not. + * + * @param encryptedConnection A pointer to the EncryptedConnectionLog of the encrypted connection. Can be set to nullptr if the connection is unecrypted. + * @returns The generated message ID. + */ + static uint64_t generateMessageID(EncryptedConnectionLog *encryptedConnection); + + /** + * Create a new session key for an encrypted connection using the built in RANDOM_REG32 of the ESP8266. + * Should only be used when initializing a new connection. + * Use generateMessageID instead when the encrypted connection is already initialized to keep the connection synchronized. + * + * @returns A uint64_t containing a new session key for an encrypted connection. + */ + static uint64_t createSessionKey(); + + // Used for verboseMode printing in attemptTransmission, _AT suffix used to reduce namespace clutter + uint32_t totalDurationWhenSuccessful_AT = 0; + uint32_t successfulTransmissions_AT = 0; + uint32_t maxTransmissionDuration_AT = 0; + + static double _transmissionsTotal; + static double _transmissionsFailed; +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/EspnowProtocolInterpreter.cpp b/libraries/ESP8266WiFiMesh/src/EspnowProtocolInterpreter.cpp new file mode 100644 index 000000000..b5b284fa9 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/EspnowProtocolInterpreter.cpp @@ -0,0 +1,105 @@ +/* + * 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 "EspnowProtocolInterpreter.h" +#include "TypeConversionFunctions.h" +#include + +namespace EspnowProtocolInterpreter +{ + const uint64_t uint64LeftmostBits = 0xFFFFFFFF00000000; + + uint8_t espnowProtocolBytesSize() + { + return 16; + } + + String espnowGetMessageContent(uint8_t *transmission, uint8_t transmissionLength) + { + if(transmissionLength < espnowProtocolBytesSize()) + { + return ""; + } + else + { + // Ensure we have a NULL terminated character array so the String() constructor knows where to stop. + uint8_t bufferedTransmission[transmissionLength + 1]; + std::copy_n(transmission, transmissionLength, bufferedTransmission); + bufferedTransmission[transmissionLength] = 0; + return String((char *)(bufferedTransmission + espnowProtocolBytesSize())); + } + } + + char espnowGetMessageType(const uint8_t *transmissionDataArray) + { + return char(transmissionDataArray[espnowMessageTypeIndex]); + } + + uint8_t espnowGetTransmissionsRemaining(const uint8_t *transmissionDataArray) + { + return (transmissionDataArray[espnowTransmissionsRemainingIndex] & 0x7F); + } + + bool espnowIsMessageStart(const uint8_t *transmissionDataArray) + { + return (transmissionDataArray[espnowTransmissionsRemainingIndex] & 0x80); // If MSB is one we have messageStart + } + + uint64_t espnowGetTransmissionMac(const uint8_t *transmissionDataArray) + { + return macToUint64(transmissionDataArray + espnowTransmissionMacIndex); + } + + uint8_t *espnowGetTransmissionMac(const uint8_t *transmissionDataArray, uint8_t *resultArray) + { + std::copy_n((transmissionDataArray + espnowTransmissionMacIndex), 6, resultArray); + return resultArray; + } + + uint64_t espnowGetMessageID(const uint8_t *transmissionDataArray) + { + uint64_t outcome = 0; + for(int shiftingFortune = 56; shiftingFortune >= 0; shiftingFortune -= 8) + { + outcome |= ((uint64_t)transmissionDataArray[espnowMessageIDIndex + 7 - shiftingFortune/8] << shiftingFortune); + } + + return outcome; + } + + uint8_t *espnowSetMessageID(uint8_t *transmissionDataArray, uint64_t messageID) + { + for(int shiftingFortune = 56; shiftingFortune >= 0; shiftingFortune -= 8) + { + transmissionDataArray[espnowMessageIDIndex + 7 - shiftingFortune/8] = messageID >> shiftingFortune & 0xFF; + } + return transmissionDataArray; + } + + bool usesEncryption(uint64_t messageID) + { + // At least one of the leftmost half of bits in messageID is 1 if the transmission is encrypted. + return messageID & uint64LeftmostBits; + } +} diff --git a/libraries/ESP8266WiFiMesh/src/EspnowProtocolInterpreter.h b/libraries/ESP8266WiFiMesh/src/EspnowProtocolInterpreter.h new file mode 100644 index 000000000..45fa46b55 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/EspnowProtocolInterpreter.h @@ -0,0 +1,74 @@ +/* + * 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 __ESPNOWPROTOCOLINTERPRETER_H__ +#define __ESPNOWPROTOCOLINTERPRETER_H__ + +#include + +// The following protocol is used on top of ESP-NOW (for the bits and bytes in each transmission): +// Bit 0-7: Message type. The type for requests must be different from the type for responses if they may require more than one transmission. Otherwise multi-part requests and responses with the same ID may be mixed together. +// Bit 8: Flag for message start. +// Bit 9-15: Transmissions remaining for the message. +// Byte 2-7: Transmission sender MAC address for AP interface. Since we always transmit from the station interface, this ensures both sender MAC addresses are available to the receiver. +// Byte 8-15: Message ID. 32 rightmost bits used for unencrypted messages (the rest is 0). 64 bits used for encrypted messages (with at least one of the leftmost 32 bits set to 1). +// This distinction based on encryption is required since the ESP-NOW API does not provide information about whether a received transmission is encrypted or not. +// Byte 16-249: The message. +// Each message can be split in up to EspnowMeshBackend::getMaxTransmissionsPerMessage() transmissions, based on message size. (max three transmissions per message is the default) + +namespace EspnowProtocolInterpreter +{ + const String synchronizationRequestHeader = "Synchronization request."; + const String encryptionRequestHeader = "AddEC:"; // Add encrypted connection + const String temporaryEncryptionRequestHeader = "AddTEC:"; // Add temporary encrypted connection + const String basicConnectionInfoHeader = "BasicCI:"; // Basic connection info + const String encryptedConnectionInfoHeader = "EncryptedCI:"; // Encrypted connection info + const String maxConnectionsReachedHeader = "ECS_MAX_CONNECTIONS_REACHED_PEER:"; + const String encryptedConnectionVerificationHeader = "ECVerified:"; // Encrypted connection verified + const String encryptedConnectionRemovalRequestHeader = "RemoveEC:"; // Remove encrypted connection + + const uint8_t espnowMessageTypeIndex = 0; + const uint8_t espnowTransmissionsRemainingIndex = 1; + const uint8_t espnowTransmissionMacIndex = 2; + const uint8_t espnowMessageIDIndex = 8; + + uint8_t espnowProtocolBytesSize(); + + const uint8_t espnowEncryptionKeyLength = 16; // This is restricted to exactly 16 bytes by the ESP-NOW API. It should not be changed unless the ESP-NOW API is changed. + const uint8_t espnowHashKeyLength = 16; // This can be changed to any value up to 255. Common values are 16 and 32. + + String espnowGetMessageContent(uint8_t *transmission, uint8_t transmissionLength); + char espnowGetMessageType(const uint8_t *transmissionDataArray); + uint8_t espnowGetTransmissionsRemaining(const uint8_t *transmissionDataArray); + bool espnowIsMessageStart(const uint8_t *transmissionDataArray); + uint64_t espnowGetTransmissionMac(const uint8_t *transmissionDataArray); + uint8_t *espnowGetTransmissionMac(const uint8_t *transmissionDataArray, uint8_t *resultArray); + uint64_t espnowGetMessageID(const uint8_t *transmissionDataArray); + // @return a pointer to transmissionDataArray + uint8_t *espnowSetMessageID(uint8_t *transmissionDataArray, uint64_t messageID); + + bool usesEncryption(uint64_t messageID); +} + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/ExpiringTimeTracker.cpp b/libraries/ESP8266WiFiMesh/src/ExpiringTimeTracker.cpp new file mode 100644 index 000000000..6c17d2dc2 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/ExpiringTimeTracker.cpp @@ -0,0 +1,59 @@ +/* + * 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 "ExpiringTimeTracker.h" + +ExpiringTimeTracker::ExpiringTimeTracker(uint32_t duration, uint32_t creationTimeMs) : + TimeTracker(creationTimeMs), _duration(duration) +{ } + +uint32_t ExpiringTimeTracker::duration() const +{ + return _duration; +} + +void ExpiringTimeTracker::setRemainingDuration(uint32_t remainingDuration) +{ + _duration = timeSinceCreation() + remainingDuration; +} + +uint32_t ExpiringTimeTracker::remainingDuration() const +{ + uint32_t remainingDuration = duration() - timeSinceCreation(); + + if(expired()) + { + // Overflow probably occured for remainingDuration calculation. + return 0; + } + else + { + return remainingDuration; + } +} + +bool ExpiringTimeTracker::expired() const +{ + return timeSinceCreation() > duration(); +} \ No newline at end of file diff --git a/libraries/ESP8266WiFiMesh/src/ExpiringTimeTracker.h b/libraries/ESP8266WiFiMesh/src/ExpiringTimeTracker.h new file mode 100644 index 000000000..32fdd9440 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/ExpiringTimeTracker.h @@ -0,0 +1,48 @@ +/* + * 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 __EXPIRINGTIMETRACKER_H__ +#define __EXPIRINGTIMETRACKER_H__ + +#include "TimeTracker.h" +#include + +class ExpiringTimeTracker : public TimeTracker { + +public: + + ~ExpiringTimeTracker() override = default; + + ExpiringTimeTracker(uint32_t duration, uint32_t creationTimeMs = millis()); + uint32_t duration() const; + void setRemainingDuration(uint32_t remainingDuration); + uint32_t remainingDuration() const; + bool expired() const; + +private: + + uint32_t _duration; +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/JsonTranslator.cpp b/libraries/ESP8266WiFiMesh/src/JsonTranslator.cpp new file mode 100644 index 000000000..c92f5f157 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/JsonTranslator.cpp @@ -0,0 +1,269 @@ +/* + * 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 "JsonTranslator.h" +#include "Crypto.h" +#include "EspnowProtocolInterpreter.h" +#include "TypeConversionFunctions.h" + +namespace JsonTranslator +{ + String createJsonPair(const String &valueIdentifier, const String &value) + { + return valueIdentifier + "\"" + value + "\","; + } + + String createJsonEndPair(const String &valueIdentifier, const String &value) + { + return valueIdentifier + "\"" + value + "\"}}"; + } + + uint8_t *createHmac(const String &message, const uint8_t *hashKey, uint8_t hashKeyLength, uint8_t resultArray[SHA256HMAC_SIZE]) + { + // Create the HMAC instance with our key + SHA256HMAC hmac(hashKey, hashKeyLength); + + // Update the HMAC with our message + hmac.doUpdate(message.c_str()); + + // Finish the HMAC calculation and return the authentication code + hmac.doFinal(resultArray); + + // resultArray now contains our SHA256HMAC_SIZE byte authentication code + return resultArray; + } + + String createHmac(const String &message, const uint8_t *hashKey, uint8_t hashKeyLength) + { + byte hmac[SHA256HMAC_SIZE]; + createHmac(message, hashKey, hashKeyLength, hmac); + return uint8ArrayToHexString(hmac, SHA256HMAC_SIZE); + } + + bool verifyHmac(const String &message, const String &messageHmac, const uint8_t *hashKey, uint8_t hashKeyLength) + { + if(messageHmac.length() != 2*SHA256HMAC_SIZE) // We know that each HMAC byte should become 2 String characters due to uint8ArrayToHexString. + return false; + + String generatedHmac = createHmac(message, hashKey, hashKeyLength); + if(generatedHmac == messageHmac) + return true; + else + return false; + } + + bool verifyHmac(const String &encryptionRequestHmacMessage, const uint8_t *hashKey, uint8_t hashKeyLength) + { + String hmac = ""; + if(getHmac(encryptionRequestHmacMessage, hmac)) + { + int32_t hmacStartIndex = encryptionRequestHmacMessage.indexOf(jsonHmac); + if(hmacStartIndex < 0) + return false; + + if(verifyHmac(encryptionRequestHmacMessage.substring(0, hmacStartIndex), hmac, hashKey, hashKeyLength)) + { + return true; + } + } + + return false; + } + + String createEncryptedConnectionInfo(const String &requestNonce, const String &authenticationPassword, uint64_t ownSessionKey, uint64_t peerSessionKey) + { + // Returns: Encrypted connection info:{"arguments":{"nonce":"1F2","password":"abc","ownSessionKey":"3B4","peerSessionKey":"1A2"}} + + + return + EspnowProtocolInterpreter::encryptedConnectionInfoHeader + "{\"arguments\":{" + + createJsonPair(jsonNonce, requestNonce) + + createJsonPair(jsonPassword, authenticationPassword) + + createJsonPair(jsonOwnSessionKey, uint64ToString(peerSessionKey)) // Exchanges session keys since it should be valid for the receiver. + + createJsonEndPair(jsonPeerSessionKey, uint64ToString(ownSessionKey)); + } + + String createEncryptionRequestIntro(const String &requestHeader, uint32_t duration) + { + return + requestHeader + "{\"arguments\":{" + + (requestHeader == EspnowProtocolInterpreter::temporaryEncryptionRequestHeader ? createJsonPair(jsonDuration, String(duration)) : ""); + } + + String createEncryptionRequestEnding(const String &requestNonce) + { + return createJsonEndPair(jsonNonce, requestNonce); + } + + String createEncryptionRequestMessage(const String &requestHeader, const String &requestNonce, uint32_t duration) + { + return createEncryptionRequestIntro(requestHeader, duration) + createEncryptionRequestEnding(requestNonce); + } + + String createEncryptionRequestHmacMessage(const String &requestHeader, const String &requestNonce, const uint8_t *hashKey, uint8_t hashKeyLength, uint32_t duration) + { + String mainMessage = createEncryptionRequestIntro(requestHeader, duration) + createJsonPair(jsonNonce, requestNonce); + String hmac = createHmac(mainMessage, hashKey, hashKeyLength); + return mainMessage + createJsonEndPair(jsonHmac, hmac); + } + + int32_t getStartIndex(const String &jsonString, const String &valueIdentifier, int32_t searchStartIndex) + { + int32_t startIndex = jsonString.indexOf(valueIdentifier, searchStartIndex); + if(startIndex < 0) + return startIndex; + + startIndex += valueIdentifier.length() + 1; // Do not include valueIdentifier and initial quotation mark + return startIndex; + } + + int32_t getEndIndex(const String &jsonString, int32_t searchStartIndex) + { + int32_t endIndex = jsonString.indexOf(',', searchStartIndex); + if(endIndex < 0) + endIndex = jsonString.indexOf('}', searchStartIndex); + + endIndex -= 1; // End index will be at the character after the closing quotation mark, so need to subtract 1. + + return endIndex; + } + + bool getPassword(const String &jsonString, String &result) + { + int32_t startIndex = getStartIndex(jsonString, jsonPassword); + if(startIndex < 0) + return false; + + int32_t endIndex = getEndIndex(jsonString, startIndex); + if(endIndex < 0) + return false; + + result = jsonString.substring(startIndex, endIndex); + return true; + } + + bool getOwnSessionKey(const String &jsonString, uint64_t &result) + { + int32_t startIndex = getStartIndex(jsonString, jsonOwnSessionKey); + if(startIndex < 0) + return false; + + int32_t endIndex = getEndIndex(jsonString, startIndex); + if(endIndex < 0) + return false; + + result = stringToUint64(jsonString.substring(startIndex, endIndex)); + return true; + } + + bool getPeerSessionKey(const String &jsonString, uint64_t &result) + { + int32_t startIndex = getStartIndex(jsonString, jsonPeerSessionKey); + if(startIndex < 0) + return false; + + int32_t endIndex = getEndIndex(jsonString, startIndex); + if(endIndex < 0) + return false; + + result = stringToUint64(jsonString.substring(startIndex, endIndex)); + return true; + } + + bool getPeerStaMac(const String &jsonString, uint8_t *resultArray) + { + int32_t startIndex = getStartIndex(jsonString, jsonPeerStaMac); + if(startIndex < 0) + return false; + + int32_t endIndex = getEndIndex(jsonString, startIndex); + if(endIndex < 0 || endIndex - startIndex != 12) // Mac String is always 12 characters long + return false; + + stringToMac(jsonString.substring(startIndex, endIndex), resultArray); + return true; + } + + bool getPeerApMac(const String &jsonString, uint8_t *resultArray) + { + int32_t startIndex = getStartIndex(jsonString, jsonPeerApMac); + if(startIndex < 0) + return false; + + int32_t endIndex = getEndIndex(jsonString, startIndex); + if(endIndex < 0 || endIndex - startIndex != 12) // Mac String is always 12 characters long + return false; + + stringToMac(jsonString.substring(startIndex, endIndex), resultArray); + return true; + } + + bool getDuration(const String &jsonString, uint32_t &result) + { + int32_t startIndex = getStartIndex(jsonString, jsonDuration); + if(startIndex < 0) + return false; + + result = strtoul(jsonString.substring(startIndex).c_str(), nullptr, 0); // strtoul stops reading input when an invalid character is discovered. + return true; + } + + bool getNonce(const String &jsonString, String &result) + { + int32_t startIndex = getStartIndex(jsonString, jsonNonce); + if(startIndex < 0) + return false; + + int32_t endIndex = getEndIndex(jsonString, startIndex); + if(endIndex < 0) + return false; + + result = jsonString.substring(startIndex, endIndex); + return true; + } + + bool getHmac(const String &jsonString, String &result) + { + int32_t startIndex = getStartIndex(jsonString, jsonHmac); + if(startIndex < 0) + return false; + + int32_t endIndex = getEndIndex(jsonString, startIndex); + if(endIndex < 0) + return false; + + result = jsonString.substring(startIndex, endIndex); + return true; + } + + bool getDesync(const String &jsonString, bool &result) + { + int32_t startIndex = getStartIndex(jsonString, jsonDesync); + if(startIndex < 0) + return false; + + result = bool(strtoul(jsonString.substring(startIndex).c_str(), nullptr, 0)); // strtoul stops reading input when an invalid character is discovered. + return true; + } +} diff --git a/libraries/ESP8266WiFiMesh/src/JsonTranslator.h b/libraries/ESP8266WiFiMesh/src/JsonTranslator.h new file mode 100644 index 000000000..e03b468b0 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/JsonTranslator.h @@ -0,0 +1,109 @@ +/* + * 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 __ESPNOWJSONTRANSLATOR_H__ +#define __ESPNOWJSONTRANSLATOR_H__ + +#include +#include "Crypto.h" + +namespace JsonTranslator +{ + const String jsonPassword = "\"password\":"; + const String jsonOwnSessionKey = "\"ownSK\":"; + const String jsonPeerSessionKey = "\"peerSK\":"; + const String jsonPeerStaMac = "\"peerStaMac\":"; + const String jsonPeerApMac = "\"peerApMac\":"; + const String jsonDuration = "\"duration\":"; + const String jsonNonce = "\"nonce\":"; + const String jsonHmac = "\"hmac\":"; + const String jsonDesync = "\"desync\":"; + + String createJsonPair(const String &valueIdentifier, const String &value); + String createJsonEndPair(const String &valueIdentifier, const String &value); + + uint8_t *createHmac(const String &message, const uint8_t *hashKey, uint8_t hashKeyLength, uint8_t resultArray[SHA256HMAC_SIZE]); + String createHmac(const String &message, const uint8_t *hashKey, uint8_t hashKeyLength); + + bool verifyHmac(const String &message, const String &messageHmac, const uint8_t *hashKey, uint8_t hashKeyLength); + bool verifyHmac(const String &encryptionRequestHmacMessage, const uint8_t *hashKey, uint8_t hashKeyLength); + + String createEncryptedConnectionInfo(const String &requestNonce, const String &authenticationPassword, uint64_t ownSessionKey, uint64_t peerSessionKey); + String createEncryptionRequestIntro(const String &requestHeader, uint32_t duration = 0); + String createEncryptionRequestEnding(const String &requestNonce); + String createEncryptionRequestMessage(const String &requestHeader, const String &requestNonce, uint32_t duration = 0); + String createEncryptionRequestHmacMessage(const String &requestHeader, const String &requestNonce, const uint8_t *hashKey, uint8_t hashKeyLength, uint32_t duration = 0); + + /** + * Provides the index within jsonString where the value of valueIdentifier starts. + * + * @param jsonString The String to search within. + * @param valueIdentifier The identifier to search for. + * @param searchStartIndex Optional argument that makes it possible to decide at which index of jsonString the search starts. Search will begin at index 0 if not provided. + * + * @returns An int32_t containing the index within jsonString where the value of valueIdentifier starts, or a negative value if valueIdentifier was not found. + */ + int32_t getStartIndex(const String &jsonString, const String &valueIdentifier, int32_t searchStartIndex = 0); + + /** + * Provides the index within jsonString where the next JSON termination character (',' or '}') is found, starting from searchStartIndex. + * + * @param jsonString The String to search within. + * @param searchStartIndex The index of jsonString where the search will start. + * + * @returns An int32_t containing the index within jsonString where the next JSON termination character is found, or a negative value if no such character was found. + */ + int32_t getEndIndex(const String &jsonString, int32_t searchStartIndex); + + /** + * Stores the value of the password field within jsonString into the result variable. + * No changes to the result variable are made if jsonString does not contain a password. + * + * @param jsonString The String to search within. + * @param result The String where the value should be stored. + * + * @returns True if a value was found. False otherwise. + */ + bool getPassword(const String &jsonString, String &result); + bool getOwnSessionKey(const String &jsonString, uint64_t &result); + bool getPeerSessionKey(const String &jsonString, uint64_t &result); + + /** + * Stores the value of the peerStaMac field within jsonString into the resultArray. + * No changes to the resultArray are made if jsonString does not contain a peerStaMac. + * + * @param jsonString The String to search within. + * @param resultArray The uint8_t array where the value should be stored. Must be at least 6 bytes. + * + * @returns True if a value was found. False otherwise. + */ + bool getPeerStaMac(const String &jsonString, uint8_t *resultArray); + bool getPeerApMac(const String &jsonString, uint8_t *resultArray); + bool getDuration(const String &jsonString, uint32_t &result); + bool getNonce(const String &jsonString, String &result); + bool getHmac(const String &jsonString, String &result); + bool getDesync(const String &jsonString, bool &result); +} + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/MeshBackendBase.cpp b/libraries/ESP8266WiFiMesh/src/MeshBackendBase.cpp new file mode 100644 index 000000000..f9454ddbd --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/MeshBackendBase.cpp @@ -0,0 +1,276 @@ +/* + MeshBackendBase + + Copyright (c) 2015 Julian Fell and 2019 Anders Löfgren. All rights reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "MeshBackendBase.h" + +#include + +MeshBackendBase *MeshBackendBase::apController = nullptr; + +std::vector MeshBackendBase::connectionQueue = {}; +std::vector MeshBackendBase::latestTransmissionOutcomes = {}; + +bool MeshBackendBase::_printWarnings = true; + +MeshBackendBase::MeshBackendBase(requestHandlerType requestHandler, responseHandlerType responseHandler, networkFilterType networkFilter, mesh_backend_t classType) +{ + setRequestHandler(requestHandler); + setResponseHandler(responseHandler); + setNetworkFilter(networkFilter); + setClassType(classType); +} + +MeshBackendBase::~MeshBackendBase() +{ + deactivateAP(); +} + +void MeshBackendBase::setClassType(mesh_backend_t classType) +{ + _classType = classType; +} + +mesh_backend_t MeshBackendBase::getClassType() {return _classType;} + +void MeshBackendBase::activateAP() +{ + // Deactivate active AP to avoid two servers using the same port, which can lead to crashes. + if(MeshBackendBase *currentAPController = MeshBackendBase::getAPController()) + currentAPController->deactivateAP(); + + activateAPHook(); + + WiFi.mode(WIFI_AP_STA); + + apController = this; +} + +void MeshBackendBase::activateAPHook() +{ + WiFi.softAP( getSSID().c_str(), getMeshPassword().c_str(), getWiFiChannel(), getAPHidden() ); // Note that a maximum of 8 TCP/IP stations can be connected at a time to each AP, max 4 by default. +} + +void MeshBackendBase::deactivateAP() +{ + if(isAPController()) + { + deactivateAPHook(); + + WiFi.softAPdisconnect(); + WiFi.mode(WIFI_STA); + + // Since there is no active AP controller now, make the apController variable point to nothing. + apController = nullptr; + } +} + +void MeshBackendBase::deactivateAPHook() +{ +} + +void MeshBackendBase::restartAP() +{ + deactivateAP(); + yield(); + activateAP(); + yield(); +} + +MeshBackendBase *MeshBackendBase::getAPController() +{ + return apController; +} + +bool MeshBackendBase::isAPController() +{ + return (this == getAPController()); +} + +void MeshBackendBase::setWiFiChannel(uint8 newWiFiChannel) +{ + assert(1 <= newWiFiChannel && newWiFiChannel <= 13); + + _meshWiFiChannel = newWiFiChannel; + + // WiFi.channel() will change if this node connects to an AP with another channel, + // so there is no guarantee we are using _meshWiFiChannel. + // Also, we cannot change the WiFi channel while we are still connected to the other AP. + if(WiFi.channel() != getWiFiChannel() && WiFi.status() != WL_CONNECTED) + { + // Apply changes to active AP. + if(isAPController()) + restartAP(); + } +} + +uint8 MeshBackendBase::getWiFiChannel() +{ + return _meshWiFiChannel; +} + +void MeshBackendBase::setSSID(const String &newSSIDPrefix, const String &newSSIDRoot, const String &newSSIDSuffix) +{ + if(newSSIDPrefix != "") + _SSIDPrefix = newSSIDPrefix; + if(newSSIDRoot != "") + _SSIDRoot = newSSIDRoot; + if(newSSIDSuffix != "") + _SSIDSuffix = newSSIDSuffix; + + String newSSID = _SSIDPrefix + _SSIDRoot + _SSIDSuffix; + + if(getSSID() != newSSID) + { + _SSID = newSSID; + + // Apply SSID changes to active AP. + if(isAPController()) + restartAP(); + } +} + +String MeshBackendBase::getSSID() {return _SSID;} + +void MeshBackendBase::setSSIDPrefix(const String &newSSIDPrefix) +{ + setSSID(newSSIDPrefix); +} + +String MeshBackendBase::getSSIDPrefix() {return _SSIDPrefix;} + +void MeshBackendBase::setSSIDRoot(const String &newSSIDRoot) +{ + setSSID("", newSSIDRoot); +} + +String MeshBackendBase::getSSIDRoot() {return _SSIDRoot;} + +void MeshBackendBase::setSSIDSuffix(const String &newSSIDSuffix) +{ + setSSID("", "", newSSIDSuffix); +} + +String MeshBackendBase::getSSIDSuffix() {return _SSIDSuffix;} + +void MeshBackendBase::setMeshName(const String &newMeshName) +{ + setSSIDPrefix(newMeshName); +} + +String MeshBackendBase::getMeshName() {return getSSIDPrefix();} + +void MeshBackendBase::setNodeID(const String &newNodeID) +{ + setSSIDSuffix(newNodeID); +} + +String MeshBackendBase::getNodeID() {return getSSIDSuffix();} + +void MeshBackendBase::setMeshPassword(const String &newMeshPassword) +{ + assert(8 <= newMeshPassword.length() && newMeshPassword.length() <= 64); // Limited by the ESP8266 API. + + _meshPassword = newMeshPassword; + + // Apply changes to active AP. + if(isAPController()) + restartAP(); +} + +String MeshBackendBase::getMeshPassword() {return _meshPassword;} + +void MeshBackendBase::setMessage(const String &newMessage) {_message = newMessage;} +String MeshBackendBase::getMessage() {return _message;} + +void MeshBackendBase::setRequestHandler(MeshBackendBase::requestHandlerType requestHandler) {_requestHandler = requestHandler;} +MeshBackendBase::requestHandlerType MeshBackendBase::getRequestHandler() {return _requestHandler;} + +void MeshBackendBase::setResponseHandler(MeshBackendBase::responseHandlerType responseHandler) {_responseHandler = responseHandler;} +MeshBackendBase::responseHandlerType MeshBackendBase::getResponseHandler() {return _responseHandler;} + +void MeshBackendBase::setNetworkFilter(MeshBackendBase::networkFilterType networkFilter) {_networkFilter = networkFilter;} +MeshBackendBase::networkFilterType MeshBackendBase::getNetworkFilter() {return _networkFilter;} + +void MeshBackendBase::setScanHidden(bool scanHidden) +{ + _scanHidden = scanHidden; +} + +bool MeshBackendBase::getScanHidden() {return _scanHidden;} + +void MeshBackendBase::setAPHidden(bool apHidden) +{ + if(getAPHidden() != apHidden) + { + _apHidden = apHidden; + + // Apply changes to active AP. + if(isAPController()) + restartAP(); + } +} + +bool MeshBackendBase::getAPHidden() {return _apHidden;} + +bool MeshBackendBase::latestTransmissionSuccessful() +{ + if(MeshBackendBase::latestTransmissionOutcomes.empty()) + return false; + else + for(TransmissionResult &transmissionResult : MeshBackendBase::latestTransmissionOutcomes) + if(transmissionResult.transmissionStatus != TS_TRANSMISSION_COMPLETE) + return false; + + return true; +} + +void MeshBackendBase::scanForNetworks(bool scanAllWiFiChannels) +{ + verboseModePrint(F("Scanning... "), false); + + /* Scan for APs */ + connectionQueue.clear(); + + // If scanAllWiFiChannels is true, scanning will cause the WiFi radio to cycle through all WiFi channels. + // This means existing WiFi connections are likely to break or work poorly if done frequently. + int n = 0; + if(scanAllWiFiChannels) + { + n = WiFi.scanNetworks(false, getScanHidden()); + } + else + { + // Scan function argument overview: scanNetworks(bool async = false, bool show_hidden = false, uint8 channel = 0, uint8* ssid = NULL) + n = WiFi.scanNetworks(false, getScanHidden(), getWiFiChannel()); + } + + getNetworkFilter()(n, *this); // Update the connectionQueue. +} + +void MeshBackendBase::printAPInfo(const int apNetworkIndex, const String &apSSID, const int apWiFiChannel) +{ + verboseModePrint(String(F("AP acquired: ")) + apSSID + String(F(", Ch:")) + String(apWiFiChannel) + " ", false); + + if(apNetworkIndex != NETWORK_INFO_DEFAULT_INT) + { + verboseModePrint("(" + String(WiFi.RSSI(apNetworkIndex)) + String(F("dBm) ")) + + (WiFi.encryptionType(apNetworkIndex) == ENC_TYPE_NONE ? String(F("open")) : ""), false); + } + + verboseModePrint(F("... "), false); +} diff --git a/libraries/ESP8266WiFiMesh/src/MeshBackendBase.h b/libraries/ESP8266WiFiMesh/src/MeshBackendBase.h new file mode 100644 index 000000000..cfaae01e8 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/MeshBackendBase.h @@ -0,0 +1,302 @@ +/* + MeshBackendBase + + Copyright (c) 2015 Julian Fell and 2019 Anders Löfgren. All rights reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef __MESHBACKENDBASE_H__ +#define __MESHBACKENDBASE_H__ + +#include +#include "TransmissionResult.h" + +const String ESP8266_MESH_EMPTY_STRING = ""; + +typedef enum +{ + MB_TCP_IP = 0, + MB_ESP_NOW = 1 +} mesh_backend_t; + +class MeshBackendBase { + +protected: + + typedef std::function requestHandlerType; + typedef std::function responseHandlerType; + typedef std::function networkFilterType; + +public: + + MeshBackendBase(requestHandlerType requestHandler, responseHandlerType responseHandler, networkFilterType networkFilter, mesh_backend_t classType); + + virtual ~MeshBackendBase(); + + /** + * A vector that contains the NetworkInfo for each WiFi network to connect to. + * The connectionQueue vector is cleared before each new scan and filled via the networkFilter callback function once the scan completes. + * WiFi connections will start with connectionQueue[0] and then incrementally proceed to higher vector positions. + * Note that old network indicies often are invalidated whenever a new WiFi network scan occurs. + */ + static std::vector connectionQueue; + + /** + * A vector with the TransmissionResult for each AP to which a transmission was attempted during the latest attemptTransmission call. + * The latestTransmissionOutcomes vector is cleared before each new transmission attempt. + * Connection attempts are indexed in the same order they were attempted. + * Note that old network indicies often are invalidated whenever a new WiFi network scan occurs. + */ + static std::vector latestTransmissionOutcomes; + + /** + * @returns True if latest transmission was successful (i.e. latestTransmissionOutcomes is not empty and all entries have transmissionStatus TS_TRANSMISSION_COMPLETE). False otherwise. + */ + static bool latestTransmissionSuccessful(); + + /** + * Initialises the node. + */ + virtual void begin() = 0; + + /** + * Each AP requires a separate server port. If two AP:s are using the same server port, they will not be able to have both server instances active at the same time. + * This is managed automatically by the activateAP method. + */ + void activateAP(); + void deactivateAP(); + void restartAP(); + + /** + * Get the MeshBackendBase instance currently in control of the ESP8266 AP. + * Note that the result will be nullptr when there is no active AP controller. + * If another instance takes control over the AP after the pointer is created, + * the created pointer will still point to the old AP instance. + * + * @returns A pointer to the MeshBackendBase instance currently in control of the ESP8266 AP, + * or nullptr if there is no active AP controller. + */ + static MeshBackendBase *getAPController(); + + /** + * Check if this MeshBackendBase instance is in control of the ESP8266 AP. + * + * @returns True if this MeshBackendBase instance is in control of the ESP8266 AP. False otherwise. + */ + bool isAPController(); + + /** + * Change the WiFi channel used by this MeshBackendBase instance. + * Will also change the WiFi channel for the active AP if this MeshBackendBase instance is the current AP controller and it is possible to change channel. + * + * 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 MeshBackendBase instances exist on the same ESP8266 and use different WiFi channels. + * In such a case, whenever the station of one MeshBackendBase 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. + * + * @param newWiFiChannel The WiFi channel to change to. Valid values are integers from 1 to 13. + * + */ + void setWiFiChannel(uint8 newWiFiChannel); + uint8 getWiFiChannel(); + + /** + * Change the SSID used by this MeshBackendBase instance. + * Will also change the SSID for the active AP if this MeshBackendBase instance is the current AP controller. + * + * @param newSSIDPrefix The first part of the new SSID. + * @param newSSIDRoot The middle part of the new SSID. + * @param newSSIDSuffix The last part of the new SSID. + */ + void setSSID(const String &newSSIDPrefix = ESP8266_MESH_EMPTY_STRING, const String &newSSIDRoot = ESP8266_MESH_EMPTY_STRING, + const String &newSSIDSuffix = ESP8266_MESH_EMPTY_STRING); + String getSSID(); + + /** + * Change the first part of the SSID used by this MeshBackendBase instance. + * Will also change the first part of the SSID for the active AP if this MeshBackendBase instance is the current AP controller. + * + * @param newSSIDPrefix The new first part of the SSID. + */ + void setSSIDPrefix(const String &newSSIDPrefix); + String getSSIDPrefix(); + + /** + * Change the middle part of the SSID used by this MeshBackendBase instance. + * Will also change the middle part of the SSID for the active AP if this MeshBackendBase instance is the current AP controller. + * + * @param newSSIDPrefix The new middle part of the SSID. + */ + void setSSIDRoot(const String &newSSIDRoot); + String getSSIDRoot(); + + /** + * Change the last part of the SSID used by this MeshBackendBase instance. + * Will also change the last part of the SSID for the active AP if this MeshBackendBase instance is the current AP controller. + * + * @param newSSIDSuffix The new last part of the SSID. + */ + void setSSIDSuffix(const String &newSSIDSuffix); + String getSSIDSuffix(); + + /** + * Change the mesh name used by this MeshBackendBase instance. + * Will also change the mesh name for the active AP if this MeshBackendBase instance is the current AP controller. + * Used as alias for setSSIDPrefix by default. Feel free to override this method in a subclass if your mesh name is not equal to SSIDPrefix. + * + * @param newMeshName The mesh name to change to. + */ + virtual void setMeshName(const String &newMeshName); + virtual String getMeshName(); + + /** + * Change the node id used by this MeshBackendBase instance. + * Will also change the node id for the active AP if this MeshBackendBase instance is the current AP controller. + * Used as alias for setSSIDSuffix by default. Feel free to override this method in a subclass if your node id is not equal to SSIDSuffix. + * + * @param newNodeID The node id to change to. + */ + virtual void setNodeID(const String &newNodeID); + virtual String getNodeID(); + + /** + * Set the password used when connecting to other AP:s and when other nodes connect to the AP of this node. + * Will also change the setting for the active AP if this MeshBackendBase instance is the current AP controller. + * + * @param newMeshPassword The password to use. + */ + void setMeshPassword(const String &newMeshPassword); + String getMeshPassword(); + + /** + * Set the message that will be sent to other nodes when calling attemptTransmission. + * + * @param newMessage The message to send. + */ + void setMessage(const String &newMessage); + String getMessage(); + + virtual void attemptTransmission(const String &message, bool scan = true, bool scanAllWiFiChannels = false) = 0; + + void setRequestHandler(requestHandlerType requestHandler); + requestHandlerType getRequestHandler(); + + void setResponseHandler(responseHandlerType responseHandler); + responseHandlerType getResponseHandler(); + + void setNetworkFilter(networkFilterType networkFilter); + networkFilterType getNetworkFilter(); + + /** + * Set whether scan results from this MeshBackendBase instance will include WiFi networks with hidden SSIDs. + * This is false by default. + * The SSID field of a found hidden network will be blank in the scan results. + * WiFi.isHidden(networkIndex) can be used to verify that a found network is hidden. + * + * @param scanHidden If true, WiFi networks with hidden SSIDs will be included in scan results. + */ + void setScanHidden(bool scanHidden); + bool getScanHidden(); + + /** + * Set whether the AP controlled by this MeshBackendBase instance will have a WiFi network with hidden SSID. + * This is false by default. + * Will also change the setting for the active AP if this MeshBackendBase instance is the current AP controller. + * + * @param apHidden If true, the WiFi network created will have a hidden SSID. + */ + void setAPHidden(bool apHidden); + bool getAPHidden(); + + /** + * Set whether the normal events occurring in the library will be printed to Serial or not. Off by default. + * This setting is separate for each mesh instance. + * + * @param enabled If true, library Serial prints are activated. + */ + virtual void setVerboseModeState(bool enabled); + virtual bool verboseMode(); + + /** + * Only print stringToPrint if verboseMode() returns true. + * + * @param stringToPrint String to print. + * @param newline If true, will end the print with a newline. True by default. + */ + virtual void verboseModePrint(const String &stringToPrint, bool newline = true); + + /** + * Set whether the warnings occurring in the library will be printed to Serial or not. On by default. + * This setting will affect all mesh instances. + * + * @param printEnabled If true, warning Serial prints from the library are activated. + */ + static void setPrintWarnings(bool printEnabled); + static bool printWarnings(); + + /** + * Only print stringToPrint if printWarnings() returns true. + * + * @param stringToPrint String to print. + * @param newline If true, will end the print with a newline. True by default. + */ + static void warningPrint(const String &stringToPrint, bool newline = true); + + mesh_backend_t getClassType(); + +protected: + + virtual void scanForNetworks(bool scanAllWiFiChannels); + virtual void printAPInfo(const int apNetworkIndex, const String &apSSID, const int apWiFiChannel); + + /** + * Called just before we activate the AP. + * Put _server.stop() in deactivateAPHook() in case you use _server.begin() here. + */ + virtual void activateAPHook(); + + /** + * Called just before we deactivate the AP. + * Put _server.stop() here in case you use _server.begin() in activateAPHook(). + */ + virtual void deactivateAPHook(); + + void setClassType(mesh_backend_t classType); + +private: + + mesh_backend_t _classType; + + static MeshBackendBase *apController; + + String _SSID; + String _SSIDPrefix; + String _SSIDRoot; + String _SSIDSuffix; + String _meshPassword; + uint8 _meshWiFiChannel; + bool _verboseMode; + String _message = ESP8266_MESH_EMPTY_STRING; + bool _scanHidden = false; + bool _apHidden = false; + + requestHandlerType _requestHandler; + responseHandlerType _responseHandler; + networkFilterType _networkFilter; + + static bool _printWarnings; +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/MessageData.cpp b/libraries/ESP8266WiFiMesh/src/MessageData.cpp new file mode 100644 index 000000000..f0070c2f5 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/MessageData.cpp @@ -0,0 +1,69 @@ +/* + * 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 "MessageData.h" +#include "EspnowProtocolInterpreter.h" +#include "EspnowMeshBackend.h" +#include + +MessageData::MessageData(uint8_t *initialTransmission, uint8_t transmissionLength,uint32_t creationTimeMs) : + TimeTracker(creationTimeMs) +{ + _transmissionsExpected = EspnowProtocolInterpreter::espnowGetTransmissionsRemaining(initialTransmission) + 1; + addToMessage(initialTransmission, transmissionLength); +} + +bool MessageData::addToMessage(uint8_t *transmission, uint8_t transmissionLength) +{ + if(EspnowProtocolInterpreter::espnowGetTransmissionsRemaining(transmission) == getTransmissionsRemaining() - 1) + { + String message = EspnowProtocolInterpreter::espnowGetMessageContent(transmission, transmissionLength); + assert(message.length() <= EspnowMeshBackend::getMaxMessageBytesPerTransmission()); // Should catch some cases where transmission is not null terminated. + _totalMessage += message; + _transmissionsReceived++; + return true; + } + + return false; +} + +uint8_t MessageData::getTransmissionsReceived() +{ + return _transmissionsReceived; +} + +uint8_t MessageData::getTransmissionsExpected() +{ + return _transmissionsExpected; +} + +uint8_t MessageData::getTransmissionsRemaining() +{ + return getTransmissionsExpected() - getTransmissionsReceived(); +} + +String MessageData::getTotalMessage() +{ + return _totalMessage; +} diff --git a/libraries/ESP8266WiFiMesh/src/MessageData.h b/libraries/ESP8266WiFiMesh/src/MessageData.h new file mode 100644 index 000000000..2d9ad9da7 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/MessageData.h @@ -0,0 +1,54 @@ +/* + * 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 __ESPNOWMESSAGEDATA_H__ +#define __ESPNOWMESSAGEDATA_H__ + +#include "TimeTracker.h" +#include + +class MessageData : public TimeTracker { + +public: + + MessageData(uint8_t *initialTransmission, uint8_t transmissionLength, uint32_t creationTimeMs = millis()); + /** + * @transmission A string of characters, including initial protocol bytes. + * @transmissionLength Length of transmission. + */ + bool addToMessage(uint8_t *transmission, uint8_t transmissionLength); + uint8_t getTransmissionsReceived(); + uint8_t getTransmissionsExpected(); + uint8_t getTransmissionsRemaining(); + String getTotalMessage(); + +private: + + uint8_t _transmissionsReceived = 0; + uint8_t _transmissionsExpected; + String _totalMessage = ""; + +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/MutexTracker.cpp b/libraries/ESP8266WiFiMesh/src/MutexTracker.cpp new file mode 100644 index 000000000..97e84a75e --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/MutexTracker.cpp @@ -0,0 +1,72 @@ +/* + * 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 "MutexTracker.h" + +MutexTracker::MutexTracker(bool &mutexToCapture) +{ + attemptMutexCapture(mutexToCapture); +} + +MutexTracker::MutexTracker(bool &mutexToCapture, std::function destructorHook) : MutexTracker(mutexToCapture) +{ + _destructorHook = destructorHook; +} + +MutexTracker::~MutexTracker() +{ + releaseMutex(); + _destructorHook(); +} + +bool MutexTracker::mutexCaptured() +{ + if(_capturedMutex) + return true; + else + return false; +} + +void MutexTracker::releaseMutex() +{ + if(mutexCaptured()) + { + *_capturedMutex = false; + _capturedMutex = nullptr; + } +} + +bool MutexTracker::attemptMutexCapture(bool &mutexToCapture) +{ + if(!mutexToCapture) + { + _capturedMutex = &mutexToCapture; + *_capturedMutex = true; + return true; + } + else + { + return false; + } +} diff --git a/libraries/ESP8266WiFiMesh/src/MutexTracker.h b/libraries/ESP8266WiFiMesh/src/MutexTracker.h new file mode 100644 index 000000000..b7144cb27 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/MutexTracker.h @@ -0,0 +1,71 @@ +/* + * 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 __MUTEXTRACKER_H__ +#define __MUTEXTRACKER_H__ + +#include + +/** + * A SLIM (Scope LImited Manager)/Scope-Bound Resource Management/RAII class to manage the state of a mutex. + */ +class MutexTracker +{ + public: + + /** + * Attempts to capture the mutex. Use the mutexCaptured() method to check success. + */ + MutexTracker(bool &mutexToCapture); + + /** + * Attempts to capture the mutex. Use the mutexCaptured() method to check success. + * + * @param destructorHook A function to hook into the MutexTracker destructor. Will be called when the MutexTracker instance is being destroyed, after the mutex has been released. + */ + MutexTracker(bool &mutexToCapture, std::function destructorHook); + + ~MutexTracker(); + + bool mutexCaptured(); + + /** + * Set the mutex free to roam the binary plains, giving new MutexTrackers a chance to capture it. + */ + void releaseMutex(); + + private: + + bool *_capturedMutex = nullptr; + std::function _destructorHook = [](){ }; + + /** + * Attempt to capture the mutex. + * + * @returns True if mutex was caught (meaning no other instance is holding the mutex). False otherwise. + */ + bool attemptMutexCapture(bool &mutexToCapture); +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/PeerRequestLog.cpp b/libraries/ESP8266WiFiMesh/src/PeerRequestLog.cpp new file mode 100644 index 000000000..dd9d33dbd --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/PeerRequestLog.cpp @@ -0,0 +1,50 @@ +/* + * 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 "PeerRequestLog.h" +#include "EspnowMeshBackend.h" + +using EspnowProtocolInterpreter::espnowHashKeyLength; + +PeerRequestLog::PeerRequestLog(uint64_t requestID, bool requestEncrypted, const String &authenticationPassword, const String &peerRequestNonce, const uint8_t peerStaMac[6], const uint8_t peerApMac[6], const uint8_t hashKey[espnowHashKeyLength]) + : EncryptedConnectionData(peerStaMac, peerApMac, 0, 0, EspnowMeshBackend::getEncryptionRequestTimeout(), hashKey), + _requestID(requestID), _requestEncrypted(requestEncrypted), _authenticationPassword(authenticationPassword), _peerRequestNonce(peerRequestNonce) +{ } + +PeerRequestLog::PeerRequestLog(uint64_t requestID, bool requestEncrypted, const String &authenticationPassword, const String &peerRequestNonce, const uint8_t peerStaMac[6], const uint8_t peerApMac[6], uint64_t peerSessionKey, uint64_t ownSessionKey, const uint8_t hashKey[espnowHashKeyLength]) + : EncryptedConnectionData(peerStaMac, peerApMac, peerSessionKey, ownSessionKey, EspnowMeshBackend::getEncryptionRequestTimeout(), hashKey), + _requestID(requestID), _requestEncrypted(requestEncrypted), _authenticationPassword(authenticationPassword), _peerRequestNonce(peerRequestNonce) +{ } + +void PeerRequestLog::setRequestID(uint64_t requestID) { _requestID = requestID; } +uint64_t PeerRequestLog::getRequestID() { return _requestID; } + +void PeerRequestLog::setRequestEncrypted(bool requestEncrypted) { _requestEncrypted = requestEncrypted; } +bool PeerRequestLog::requestEncrypted() { return _requestEncrypted; } + +void PeerRequestLog::setAuthenticationPassword(const String &password) { _authenticationPassword = password; } +String PeerRequestLog::getAuthenticationPassword() { return _authenticationPassword; } + +void PeerRequestLog::setPeerRequestNonce(const String &nonce) { _peerRequestNonce = nonce; } +String PeerRequestLog::getPeerRequestNonce() { return _peerRequestNonce; } diff --git a/libraries/ESP8266WiFiMesh/src/PeerRequestLog.h b/libraries/ESP8266WiFiMesh/src/PeerRequestLog.h new file mode 100644 index 000000000..7f058f171 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/PeerRequestLog.h @@ -0,0 +1,60 @@ +/* + * 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 __ESPNOWPEERREQUESTLOG_H__ +#define __ESPNOWPEERREQUESTLOG_H__ + +#include "EncryptedConnectionData.h" +#include "EspnowProtocolInterpreter.h" + +class PeerRequestLog : public EncryptedConnectionData { + +public: + + PeerRequestLog(uint64_t requestID, bool requestEncrypted, const String &authenticationPassword, const String &peerRequestNonce, const uint8_t peerStaMac[6], + const uint8_t peerApMac[6], const uint8_t hashKey[EspnowProtocolInterpreter::espnowHashKeyLength]); + PeerRequestLog(uint64_t requestID, bool requestEncrypted, const String &authenticationPassword, const String &peerRequestNonce, const uint8_t peerStaMac[6], + const uint8_t peerApMac[6], uint64_t peerSessionKey, uint64_t ownSessionKey, const uint8_t hashKey[EspnowProtocolInterpreter::espnowHashKeyLength]); + + void setRequestID(uint64_t requestID); + uint64_t getRequestID(); + + void setRequestEncrypted(bool requestEncrypted); + bool requestEncrypted(); + + void setAuthenticationPassword(const String &password); + String getAuthenticationPassword(); + + void setPeerRequestNonce(const String &nonce); + String getPeerRequestNonce(); + +private: + + uint64_t _requestID; + bool _requestEncrypted; + String _authenticationPassword = ""; + String _peerRequestNonce = ""; +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/RequestData.cpp b/libraries/ESP8266WiFiMesh/src/RequestData.cpp new file mode 100644 index 000000000..a45b46051 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/RequestData.cpp @@ -0,0 +1,32 @@ +/* + * 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 "RequestData.h" + +RequestData::RequestData(EspnowMeshBackend &meshInstance, uint32_t creationTimeMs) : + TimeTracker(creationTimeMs), _meshInstance(meshInstance) +{ } + +void RequestData::setMeshInstance(EspnowMeshBackend &meshInstance) { _meshInstance = meshInstance; } +EspnowMeshBackend &RequestData::getMeshInstance() { return _meshInstance; } diff --git a/libraries/ESP8266WiFiMesh/src/RequestData.h b/libraries/ESP8266WiFiMesh/src/RequestData.h new file mode 100644 index 000000000..77f373245 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/RequestData.h @@ -0,0 +1,47 @@ +/* + * 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 __ESPNOWREQUESTDATA_H__ +#define __ESPNOWREQUESTDATA_H__ + +#include "TimeTracker.h" +#include "EspnowMeshBackend.h" + +class EspnowMeshBackend; + +class RequestData : public TimeTracker { + +public: + + RequestData(EspnowMeshBackend &meshInstance, uint32_t creationTimeMs = millis()); + + void setMeshInstance(EspnowMeshBackend &meshInstance); + EspnowMeshBackend &getMeshInstance(); + +private: + + EspnowMeshBackend &_meshInstance; +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/ResponseData.cpp b/libraries/ESP8266WiFiMesh/src/ResponseData.cpp new file mode 100644 index 000000000..b3678d246 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/ResponseData.cpp @@ -0,0 +1,79 @@ +/* + * 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 "ResponseData.h" + +ResponseData::ResponseData(const String &message, const uint8_t recipientMac[6], uint64_t requestID, uint32_t creationTimeMs) : + TimeTracker(creationTimeMs), _message(message), _requestID(requestID) +{ + storeRecipientMac(recipientMac); +} + +ResponseData::ResponseData(const ResponseData &other) + : TimeTracker(other), _message(other.getMessage()), _requestID(other.getRequestID()) +{ + storeRecipientMac(other.getRecipientMac()); +} + +ResponseData & ResponseData::operator=(const ResponseData &other) +{ + if(this != &other) + { + TimeTracker::operator=(other); + _message = other.getMessage(); + _requestID = other.getRequestID(); + storeRecipientMac(other.getRecipientMac()); + } + + return *this; +} + +void ResponseData::storeRecipientMac(const uint8_t newRecipientMac[6]) +{ + if(newRecipientMac != nullptr) + { + if(_recipientMac == nullptr) + { + _recipientMac = _recipientMacArray; + } + + for(int i = 0; i < 6; i++) + { + _recipientMac[i] = newRecipientMac[i]; + } + } + else + { + _recipientMac = nullptr; + } +} + +void ResponseData::setRecipientMac(const uint8_t recipientMac[6]) { storeRecipientMac(recipientMac); } +const uint8_t *ResponseData::getRecipientMac() const { return _recipientMac; } + +void ResponseData::setMessage(String &message) { _message = message; } +String ResponseData::getMessage() const { return _message; } + +void ResponseData::setRequestID(uint64_t requestID) { _requestID = requestID; } +uint64_t ResponseData::getRequestID() const { return _requestID; } diff --git a/libraries/ESP8266WiFiMesh/src/ResponseData.h b/libraries/ESP8266WiFiMesh/src/ResponseData.h new file mode 100644 index 000000000..7bd527d6e --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/ResponseData.h @@ -0,0 +1,59 @@ +/* + * 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 __ESPNOWRESPONSEDATA_H__ +#define __ESPNOWRESPONSEDATA_H__ + +#include "TimeTracker.h" +#include + +class ResponseData : public TimeTracker { + +public: + + ResponseData(const String &message, const uint8_t recipientMac[6], uint64_t requestID, uint32_t creationTimeMs = millis()); + ResponseData(const ResponseData &other); + ResponseData & operator=(const ResponseData &other); + // No need for explicit destructor with current class design + + void setRecipientMac(const uint8_t recipientMac[6]); + const uint8_t *getRecipientMac() const; + + void setMessage(String &message); + String getMessage() const; + + void setRequestID(uint64_t requestID); + uint64_t getRequestID() const; + +private: + + void storeRecipientMac(const uint8_t newRecipientMac[6]); + + uint8_t _recipientMacArray[6] {0}; + uint8_t *_recipientMac = nullptr; + String _message = ""; + uint64_t _requestID = 0; +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/TcpIpMeshBackend.cpp b/libraries/ESP8266WiFiMesh/src/TcpIpMeshBackend.cpp new file mode 100644 index 000000000..b00f8744a --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/TcpIpMeshBackend.cpp @@ -0,0 +1,469 @@ +/* + TcpIpMeshBackend + + Copyright (c) 2015 Julian Fell and 2019 Anders Löfgren. All rights reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include +#include +#include +#include + +#include "TcpIpMeshBackend.h" +#include "TypeConversionFunctions.h" +#include "MutexTracker.h" + +#define SERVER_IP_ADDR "192.168.4.1" + +const IPAddress TcpIpMeshBackend::emptyIP = IPAddress(); + +bool TcpIpMeshBackend::_tcpIpTransmissionMutex = false; + +String TcpIpMeshBackend::lastSSID = ""; +bool TcpIpMeshBackend::staticIPActivated = false; + +// IP needs to be at the same subnet as server gateway (192.168.4 in this case). Station gateway ip must match ip for server. +IPAddress TcpIpMeshBackend::staticIP = emptyIP; +IPAddress TcpIpMeshBackend::gateway = IPAddress(192,168,4,1); +IPAddress TcpIpMeshBackend::subnetMask = IPAddress(255,255,255,0); + +TcpIpMeshBackend::TcpIpMeshBackend(requestHandlerType requestHandler, responseHandlerType responseHandler, + networkFilterType networkFilter, const String &meshPassword, const String &ssidPrefix, + const String &ssidSuffix, bool verboseMode, uint8 meshWiFiChannel, uint16_t serverPort) + : MeshBackendBase(requestHandler, responseHandler, networkFilter, MB_TCP_IP), _server(serverPort) +{ + setSSID(ssidPrefix, "", ssidSuffix); + setMeshPassword(meshPassword); + setVerboseModeState(verboseMode); + setWiFiChannel(meshWiFiChannel); + setServerPort(serverPort); +} + +void TcpIpMeshBackend::begin() +{ + if(!TcpIpMeshBackend::getAPController()) // If there is no active AP controller + WiFi.mode(WIFI_STA); // WIFI_AP_STA mode automatically sets up an AP, so we can't use that as default. + + #if LWIP_VERSION_MAJOR >= 2 + verboseModePrint(F("lwIP version is at least 2. Static ip optimizations enabled.\n")); + #else + verboseModePrint(F("lwIP version is less than 2. Static ip optimizations DISABLED.\n")); + #endif +} + +void TcpIpMeshBackend::activateAPHook() +{ + WiFi.softAP( getSSID().c_str(), getMeshPassword().c_str(), getWiFiChannel(), getAPHidden(), _maxAPStations ); // Note that a maximum of 8 TCP/IP stations can be connected at a time to each AP, max 4 by default. + + _server = WiFiServer(getServerPort()); // Fixes an occasional crash bug that occurs when using the copy constructor to duplicate the AP controller. + _server.begin(); // Actually calls _server.stop()/_server.close() first. +} + +void TcpIpMeshBackend::deactivateAPHook() +{ + _server.stop(); +} + +bool TcpIpMeshBackend::transmissionInProgress(){return _tcpIpTransmissionMutex;} + +void TcpIpMeshBackend::setStaticIP(const IPAddress &newIP) +{ + // Comment out the line below to remove static IP and use DHCP instead. + // DHCP makes WiFi connection happen slower, but there is no need to care about manually giving different IPs to the nodes and less need to worry about used IPs giving "Server unavailable" issues. + // Static IP has faster connection times (50 % of DHCP) and will make sending of data to a node that is already transmitting data happen more reliably. + // Note that after WiFi.config(staticIP, gateway, subnetMask) is used, static IP will always be active, even for new connections, unless WiFi.config(0u,0u,0u); is called. + WiFi.config(newIP, gateway, subnetMask); + staticIPActivated = true; + staticIP = newIP; +} + +IPAddress TcpIpMeshBackend::getStaticIP() +{ + if(staticIPActivated) + return staticIP; + + return emptyIP; +} + +void TcpIpMeshBackend::disableStaticIP() +{ + WiFi.config(0u,0u,0u); + yield(); + staticIPActivated = false; +} + +void TcpIpMeshBackend::setServerPort(uint16_t serverPort) +{ + _serverPort = serverPort; + + // Apply changes to active AP. + if(isAPController()) + restartAP(); +} + +uint16_t TcpIpMeshBackend::getServerPort() {return _serverPort;} + +void TcpIpMeshBackend::setMaxAPStations(uint8_t maxAPStations) +{ + assert(maxAPStations <= 8); // Valid values are 0 to 8, but uint8_t is always at least 0. + + if(_maxAPStations != maxAPStations) + { + _maxAPStations = maxAPStations; + + // Apply changes to active AP. + if(isAPController()) + restartAP(); + } +} + +bool TcpIpMeshBackend::getMaxAPStations() {return _maxAPStations;} + +void TcpIpMeshBackend::setConnectionAttemptTimeout(int32_t connectionAttemptTimeoutMs) +{ + _connectionAttemptTimeoutMs = connectionAttemptTimeoutMs; +} + +int32_t TcpIpMeshBackend::getConnectionAttemptTimeout() {return _connectionAttemptTimeoutMs;} + +void TcpIpMeshBackend::setStationModeTimeout(int stationModeTimeoutMs) +{ + _stationModeTimeoutMs = stationModeTimeoutMs; +} + +int TcpIpMeshBackend::getStationModeTimeout() {return _stationModeTimeoutMs;} + +void TcpIpMeshBackend::setAPModeTimeout(uint32_t apModeTimeoutMs) +{ + _apModeTimeoutMs = apModeTimeoutMs; +} + +uint32_t TcpIpMeshBackend::getAPModeTimeout() {return _apModeTimeoutMs;} + +/** + * Disconnect completely from a network. + */ +void TcpIpMeshBackend::fullStop(WiFiClient &currClient) +{ + currClient.stop(); + yield(); + WiFi.disconnect(); + yield(); +} + +/** + * Wait for a WiFiClient to transmit + * + * @returns: True if the client is ready, false otherwise. + * + */ +bool TcpIpMeshBackend::waitForClientTransmission(WiFiClient &currClient, uint32_t maxWait) +{ + uint32_t connectionStartTime = millis(); + uint32_t waitingTime = millis() - connectionStartTime; + while(currClient.connected() && !currClient.available() && waitingTime < maxWait) + { + delay(1); + waitingTime = millis() - connectionStartTime; + } + + /* Return false if the client isn't ready to communicate */ + if (WiFi.status() == WL_DISCONNECTED && !currClient.available()) + { + verboseModePrint(F("Disconnected!")); + return false; + } + + return true; +} + +/** + * Send the mesh instance's current message then read back the other node's response + * and pass that to the user-supplied responseHandler. + * + * @param currClient The client to which the message should be transmitted. + * @returns: A status code based on the outcome of the exchange. + * + */ +transmission_status_t TcpIpMeshBackend::exchangeInfo(WiFiClient &currClient) +{ + verboseModePrint("Transmitting"); // Not storing strings in flash (via F()) to avoid performance impacts when using the string. + + currClient.print(getMessage() + "\r"); + yield(); + + if (!waitForClientTransmission(currClient, _stationModeTimeoutMs)) + { + fullStop(currClient); + return TS_CONNECTION_FAILED; + } + + if (!currClient.available()) + { + verboseModePrint(F("No response!")); + return TS_TRANSMISSION_FAILED; // WiFi.status() != WL_DISCONNECTED so we do not want to use fullStop(currClient) here since that would force the node to scan for WiFi networks. + } + + String response = currClient.readStringUntil('\r'); + yield(); + currClient.flush(); + + /* Pass data to user callback */ + return getResponseHandler()(response, *this); +} + +/** + * Handle data transfer process with a connected AP. + * + * @returns: A status code based on the outcome of the data transfer attempt. + */ +transmission_status_t TcpIpMeshBackend::attemptDataTransfer() +{ + // Unlike WiFi.mode(WIFI_AP);, WiFi.mode(WIFI_AP_STA); allows us to stay connected to the AP we connected to in STA mode, at the same time as we can receive connections from other stations. + // We cannot send data to the AP in STA_AP mode though, that requires STA mode. + // Switching to STA mode will disconnect all stations connected to the node AP (though they can request a reconnect even while we are in STA mode). + WiFiMode_t storedWiFiMode = WiFi.getMode(); + WiFi.mode(WIFI_STA); + delay(1); + transmission_status_t transmissionOutcome = attemptDataTransferKernel(); + WiFi.mode(storedWiFiMode); + delay(1); + + return transmissionOutcome; +} + +/** + * Helper function that contains the core functionality for the data transfer process with a connected AP. + * + * @returns: A status code based on the outcome of the data transfer attempt. + */ +transmission_status_t TcpIpMeshBackend::attemptDataTransferKernel() +{ + WiFiClient currClient; + currClient.setTimeout(_stationModeTimeoutMs); + + /* Connect to the node's server */ + if (!currClient.connect(SERVER_IP_ADDR, getServerPort())) + { + fullStop(currClient); + verboseModePrint(F("Server unavailable")); + return TS_CONNECTION_FAILED; + } + + transmission_status_t transmissionOutcome = exchangeInfo(currClient); + if (transmissionOutcome <= 0) + { + verboseModePrint(F("Transmission failed during exchangeInfo.")); + return transmissionOutcome; + } + + currClient.stop(); + yield(); + + return transmissionOutcome; +} + +void TcpIpMeshBackend::initiateConnectionToAP(const String &targetSSID, int targetChannel, uint8_t *targetBSSID) +{ + if(targetChannel == NETWORK_INFO_DEFAULT_INT) + WiFi.begin( targetSSID.c_str(), getMeshPassword().c_str() ); // Without giving channel and BSSID, connection time is longer. + else if(targetBSSID == NULL) + WiFi.begin( targetSSID.c_str(), getMeshPassword().c_str(), targetChannel ); // Without giving channel and BSSID, connection time is longer. + else + WiFi.begin( targetSSID.c_str(), getMeshPassword().c_str(), targetChannel, targetBSSID ); +} + +/** + * Connect to the AP at SSID and transmit the mesh instance's current message. + * + * @param targetSSID The name of the AP the other node has set up. + * @param targetChannel The WiFI channel of the AP the other node has set up. + * @param targetBSSID The MAC address of the AP the other node has set up. + * @returns: A status code based on the outcome of the connection and data transfer process. + * + */ +transmission_status_t TcpIpMeshBackend::connectToNode(const String &targetSSID, int targetChannel, uint8_t *targetBSSID) +{ + if(staticIPActivated && lastSSID != "" && lastSSID != targetSSID) // So we only do this once per connection, in case there is a performance impact. + { + #if LWIP_VERSION_MAJOR >= 2 + // Can be used with Arduino core for ESP8266 version 2.4.2 or higher with lwIP2 enabled to keep static IP on even during network switches. + WiFiMode_t storedWiFiMode = WiFi.getMode(); + WiFi.mode(WIFI_OFF); + WiFi.mode(storedWiFiMode); + yield(); + + #else + // Disable static IP so that we can connect to other servers via DHCP (DHCP is slower but required for connecting to more than one server, it seems (possible bug?)). + disableStaticIP(); + verboseModePrint(F("\nConnecting to a different network. Static IP deactivated to make this possible.")); + + #endif + } + lastSSID = targetSSID; + + verboseModePrint(F("Connecting... "), false); + initiateConnectionToAP(targetSSID, targetChannel, targetBSSID); + + int connectionStartTime = millis(); + int attemptNumber = 1; + + int waitingTime = millis() - connectionStartTime; + while((WiFi.status() == WL_DISCONNECTED) && waitingTime <= _connectionAttemptTimeoutMs) + { + if(waitingTime > attemptNumber * _connectionAttemptTimeoutMs) // _connectionAttemptTimeoutMs can be replaced (lowered) if you want to limit the time allowed for each connection attempt. + { + verboseModePrint(F("... "), false); + WiFi.disconnect(); + yield(); + initiateConnectionToAP(targetSSID, targetChannel, targetBSSID); + attemptNumber++; + } + delay(1); + waitingTime = millis() - connectionStartTime; + } + + verboseModePrint(String(waitingTime)); + + /* If the connection timed out */ + if (WiFi.status() != WL_CONNECTED) + { + verboseModePrint(F("Timeout")); + return TS_CONNECTION_FAILED; + } + + return attemptDataTransfer(); +} + +void TcpIpMeshBackend::attemptTransmission(const String &message, bool scan, bool scanAllWiFiChannels, bool concludingDisconnect, bool initialDisconnect ) +{ + MutexTracker mutexTracker(_tcpIpTransmissionMutex); + if(!mutexTracker.mutexCaptured()) + { + assert(false && "ERROR! TCP/IP transmission in progress. Don't call attemptTransmission from callbacks as this may corrupt program state! Aborting."); + return; + } + + if(initialDisconnect) + { + WiFi.disconnect(); + yield(); + } + + setMessage(message); + + latestTransmissionOutcomes.clear(); + + if(WiFi.status() == WL_CONNECTED) + { + transmission_status_t transmissionResult = attemptDataTransfer(); + latestTransmissionOutcomes.push_back(TransmissionResult(connectionQueue.back(), transmissionResult)); + } + else + { + if(scan) + { + scanForNetworks(scanAllWiFiChannels); + } + + for(NetworkInfo ¤tNetwork : connectionQueue) + { + WiFi.disconnect(); + yield(); + + String currentSSID = ""; + int currentWiFiChannel = NETWORK_INFO_DEFAULT_INT; + uint8_t *currentBSSID = NULL; + + // If an SSID has been assigned, it is prioritized over an assigned networkIndex since the networkIndex is more likely to change. + if(currentNetwork.SSID != "") + { + currentSSID = currentNetwork.SSID; + currentWiFiChannel = currentNetwork.wifiChannel; + currentBSSID = currentNetwork.BSSID; + } + else // Use only networkIndex + { + currentSSID = WiFi.SSID(currentNetwork.networkIndex); + currentWiFiChannel = WiFi.channel(currentNetwork.networkIndex); + currentBSSID = WiFi.BSSID(currentNetwork.networkIndex); + } + + if(verboseMode()) // Avoid string generation if not required + { + printAPInfo(currentNetwork.networkIndex, currentSSID, currentWiFiChannel); + } + + transmission_status_t transmissionResult = connectToNode(currentSSID, currentWiFiChannel, currentBSSID); + + latestTransmissionOutcomes.push_back(TransmissionResult{.origin = currentNetwork, .transmissionStatus = transmissionResult}); + } + } + + if(WiFi.status() == WL_CONNECTED && staticIP != emptyIP && !staticIPActivated) + { + verboseModePrint(F("Reactivating static IP to allow for faster re-connects.")); + setStaticIP(staticIP); + } + + // If we do not want to be connected at end of transmission, disconnect here so we can re-enable static IP first (above). + if(concludingDisconnect) + { + WiFi.disconnect(); + yield(); + } +} + +void TcpIpMeshBackend::attemptTransmission(const String &message, bool scan, bool scanAllWiFiChannels) +{ + attemptTransmission(message, scan, scanAllWiFiChannels, true, false); +} + +void TcpIpMeshBackend::acceptRequest() +{ + MutexTracker mutexTracker(_tcpIpTransmissionMutex); + if(!mutexTracker.mutexCaptured()) + { + assert(false && "ERROR! TCP/IP transmission in progress. Don't call acceptRequest from TCP/IP callbacks as this may corrupt program state! Aborting."); + return; + } + + while (true) { + WiFiClient _client = _server.available(); + + if (!_client) + break; + + if (!waitForClientTransmission(_client, _apModeTimeoutMs) || !_client.available()) { + continue; + } + + /* Read in request and pass it to the supplied requestHandler */ + String request = _client.readStringUntil('\r'); + yield(); + _client.flush(); + + String response = getRequestHandler()(request, *this); + + /* Send the response back to the client */ + if (_client.connected()) + { + verboseModePrint("Responding"); // Not storing strings in flash (via F()) to avoid performance impacts when using the string. + _client.print(response + "\r"); + _client.flush(); + yield(); + } + } +} diff --git a/libraries/ESP8266WiFiMesh/src/TcpIpMeshBackend.h b/libraries/ESP8266WiFiMesh/src/TcpIpMeshBackend.h new file mode 100644 index 000000000..86dc7ce03 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/TcpIpMeshBackend.h @@ -0,0 +1,214 @@ +/* + TcpIpMeshBackend + + Copyright (c) 2015 Julian Fell and 2019 Anders Löfgren. All rights reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +// ESP-NOW is faster for small data payloads (up to a few kB, split over multiple messages). Transfer of up to 234 bytes takes 4 ms. +// In general ESP-NOW transfer time can be approximated with the following function: transferTime = ceil(bytesToTransfer / 234.0)*3 ms. +// If you only transfer 234 bytes at a time, this adds up to around 56kB/s. Finally a chance to relive the glory of the olden days +// when people were restricted to V90 dial-up modems for internet access! +// TCP-IP takes longer to connect (around 1000 ms), and an AP has to disconnect all connected stations in order to transfer data to another AP, +// but this backend has a much higher data transfer speed than ESP-NOW once connected (100x faster or so). + +#ifndef __TCPIPMESHBACKEND_H__ +#define __TCPIPMESHBACKEND_H__ + +#include +#include +#include +#include +#include "MeshBackendBase.h" +#include "NetworkInfo.h" + +class TcpIpMeshBackend : public MeshBackendBase { + +public: + + /** + * WiFiMesh Constructor method. Creates a WiFi Mesh 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. + * @param responseHandler The callback handler for dealing with received responses. Takes a string as an argument which + * is the response string received from another node. Returns a transmission status code as a transmission_status_t. + * @param networkFilter The callback handler for deciding which WiFi networks to connect to. + * @param meshPassword The WiFi password for the mesh network. + * @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 separate for each TcpIpMeshBackend instance. + * @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. + * @param serverPort The server port used both by the AP of the TcpIpMeshBackend instance and when the instance connects to other APs. + * If multiple APs exist on a single ESP8266, each requires a separate server port. + * If two AP:s on the same ESP8266 are using the same server port, they will not be able to have both server instances active at the same time. + * This is managed automatically by the activateAP method. + * + */ + TcpIpMeshBackend(requestHandlerType requestHandler, responseHandlerType responseHandler, networkFilterType networkFilter, + const String &meshPassword, const String &ssidPrefix, const String &ssidSuffix, bool verboseMode = false, + uint8 meshWiFiChannel = 1, uint16_t serverPort = 4011); + + /** + * Initialises the node. + */ + void begin() override; + + /** + * If AP connection already exists, and the initialDisconnect argument is set to false, send message only to the already connected AP. + * Otherwise, scan for other networks, send the scan result to networkFilter and then transmit the message to the networks found in connectionQueue. + * + * @param message The message to send to other nodes. It will be stored in the class instance until replaced via attemptTransmission or setMessage. + * @param concludingDisconnect Disconnect from AP once transmission is complete. Defaults to true. + * @param initialDisconnect Disconnect from any currently connected AP before attempting transmission. Defaults to false. + * @param scan Scan for new networks and call the networkFilter function with the scan results. When set to false, only the data already in connectionQueue will be used for the transmission. + * @param scanAllWiFiChannels Scan all WiFi channels during a WiFi scan, instead of just the channel the MeshBackendBase instance is using. + * Scanning all WiFi channels takes about 2100 ms, compared to just 60 ms if only channel 1 (standard) is scanned. + * Note that if the ESP8266 has an active AP, that AP will switch WiFi channel to match that of any other AP the ESP8266 connects to. + * This can make it impossible for other nodes to detect the AP if they are scanning the wrong WiFi channel. + */ + void attemptTransmission(const String &message, bool scan, bool scanAllWiFiChannels, bool concludingDisconnect, bool initialDisconnect = false); + + void attemptTransmission(const String &message, bool scan = true, bool scanAllWiFiChannels = false) override; + + /** + * If any clients are connected, accept their requests and call the requestHandler function for each one. + */ + void acceptRequest(); + + /** + * Set a static IP address for the ESP8266 and activate use of static IP. + * The static IP needs to be at the same subnet as the server's gateway. + */ + void setStaticIP(const IPAddress &newIP); + IPAddress getStaticIP(); + void disableStaticIP(); + + /** + * An empty IPAddress. Used as default when no IP is set. + */ + static const IPAddress emptyIP; + + /** + * Set the server port used both by the AP of the TcpIpMeshBackend instance and when the instance connects to other APs. + * If multiple APs exist on a single ESP8266, each requires a separate server port. + * If two AP:s on the same ESP8266 are using the same server port, they will not be able to have both server instances active at the same time. + * This is managed automatically by the activateAP method. + * Will also change the setting for the active AP if this TcpIpMeshBackend instance is the current AP controller. + * + * @param serverPort The server port to use. + * + */ + void setServerPort(uint16_t serverPort); + uint16_t getServerPort(); + + /** + * Set the maximum number of stations that can simultaneously be connected to the AP controlled by this TcpIpMeshBackend instance. + * This number is 4 by default. + * Once the max number has been reached, any other station that wants to connect will be forced to wait until an already connected station disconnects. + * The more stations that are connected, the more memory is required. + * Will also change the setting for the active AP if this TcpIpMeshBackend instance is the current AP controller. + * + * @param maxAPStations The maximum number of simultaneous station connections allowed. Valid values are 0 to 8. + */ + void setMaxAPStations(uint8_t maxAPStations); + bool getMaxAPStations(); + + /** + * Set the timeout for each attempt to connect to another AP that occurs through the attemptTransmission method by this TcpIpMeshBackend instance. + * The timeout is 10 000 ms by default. + * + * @param connectionAttemptTimeoutMs The timeout for each connection attempt, in milliseconds. + */ + void setConnectionAttemptTimeout(int32_t connectionAttemptTimeoutMs); + int32_t getConnectionAttemptTimeout(); + + /** + * Set the timeout to use for transmissions when this TcpIpMeshBackend instance acts as a station (i.e. when connected to another AP). + * This will affect the timeout of the attemptTransmission method once a connection to an AP has been established. + * The timeout is 5 000 ms by default. + * + * @param stationModeTimeoutMs The timeout to use, in milliseconds. + */ + void setStationModeTimeout(int stationModeTimeoutMs); + int getStationModeTimeout(); + + /** + * Set the timeout to use for transmissions when this TcpIpMeshBackend instance acts as an AP (i.e. when receiving connections from other stations). + * This will affect the timeout of the acceptRequest method. + * The timeout is 4 500 ms by default. + * Will also change the setting for the active AP if this TcpIpMeshBackend instance is the current AP controller. + * + * @param apModeTimeoutMs The timeout to use, in milliseconds. + */ + void setAPModeTimeout(uint32_t apModeTimeoutMs); + uint32_t getAPModeTimeout(); + +protected: + + /** + * Called just before we activate the AP. + * Put _server.stop() in deactivateAPHook() in case you use _server.begin() here. + */ + void activateAPHook() override; + + /** + * Called just before we deactivate the AP. + * Put _server.stop() here in case you use _server.begin() in activateAPHook(). + */ + void deactivateAPHook() override; + + /** + * Will be true if a transmission initiated by a public method is in progress. + */ + static bool _tcpIpTransmissionMutex; + + /** + * Check if there is an ongoing TCP/IP transmission in the library. Used to avoid interrupting transmissions. + * + * @returns True if a transmission initiated by a public method is in progress. + */ + static bool transmissionInProgress(); + +private: + + uint16_t _serverPort; + WiFiServer _server; + uint8_t _maxAPStations = 4; // Only affects TCP/IP connections, not ESP-NOW connections + int32_t _connectionAttemptTimeoutMs = 10000; + int _stationModeTimeoutMs = 5000; // int is the type used in the Arduino core for this particular API, not uint32_t, which is why we use int here. + uint32_t _apModeTimeoutMs = 4500; + + static String lastSSID; + static bool staticIPActivated; + bool useStaticIP; + static IPAddress staticIP; + static IPAddress gateway; + static IPAddress subnetMask; + + void fullStop(WiFiClient &currClient); + void initiateConnectionToAP(const String &targetSSID, int targetChannel = NETWORK_INFO_DEFAULT_INT, uint8_t *targetBSSID = NULL); + transmission_status_t connectToNode(const String &targetSSID, int targetChannel = NETWORK_INFO_DEFAULT_INT, uint8_t *targetBSSID = NULL); + transmission_status_t exchangeInfo(WiFiClient &currClient); + bool waitForClientTransmission(WiFiClient &currClient, uint32_t maxWait); + transmission_status_t attemptDataTransfer(); + transmission_status_t attemptDataTransferKernel(); +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/TimeTracker.cpp b/libraries/ESP8266WiFiMesh/src/TimeTracker.cpp new file mode 100644 index 000000000..1656e185e --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/TimeTracker.cpp @@ -0,0 +1,39 @@ +/* + * 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 "TimeTracker.h" +#include + +TimeTracker::TimeTracker(uint32_t creationTimeMs) : _creationTimeMs(creationTimeMs) +{ } + +uint32_t TimeTracker::timeSinceCreation() const +{ + return millis() - creationTimeMs(); // Will work even when millis() overflow: http://forum.arduino.cc/index.php/topic,42997.0.html +} + +uint32_t TimeTracker::creationTimeMs() const +{ + return _creationTimeMs; +} diff --git a/libraries/ESP8266WiFiMesh/src/TimeTracker.h b/libraries/ESP8266WiFiMesh/src/TimeTracker.h new file mode 100644 index 000000000..76e5eafe4 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/TimeTracker.h @@ -0,0 +1,45 @@ +/* + * 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 __TIMETRACKER_H__ +#define __TIMETRACKER_H__ + +#include + +class TimeTracker { + +public: + + virtual ~TimeTracker() = default; + + TimeTracker(uint32_t creationTimeMs); + uint32_t timeSinceCreation() const; + uint32_t creationTimeMs() const; + +private: + + uint32_t _creationTimeMs; +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/TypeConversionFunctions.cpp b/libraries/ESP8266WiFiMesh/src/TypeConversionFunctions.cpp index be908b556..0458cc54c 100644 --- a/libraries/ESP8266WiFiMesh/src/TypeConversionFunctions.cpp +++ b/libraries/ESP8266WiFiMesh/src/TypeConversionFunctions.cpp @@ -24,6 +24,7 @@ */ #include "TypeConversionFunctions.h" +#include "Crypto.h" String uint64ToString(uint64_t number, byte base) { @@ -56,3 +57,89 @@ uint64_t stringToUint64(const String &string, byte base) return result; } + +String uint8ArrayToHexString(const uint8_t *uint8Array, uint32_t arrayLength) +{ + char hexString[2*arrayLength + 1]; // Each uint8_t will become two characters (00 to FF) and we want a null terminated char array. + hexString[arrayLength + 1] = { 0 }; + for(uint32_t i = 0; i < arrayLength; i++) + { + sprintf(hexString + 2*i, "%02X", uint8Array[i]); + } + + return String(hexString); +} + +uint8_t *hexStringToUint8Array(const String &hexString, uint8_t *uint8Array, uint32_t arrayLength) +{ + assert(hexString.length() >= arrayLength*2); // Each array element can hold two hexString characters + + for(uint32_t i = 0; i < arrayLength; i++) + { + uint8Array[i] = strtoul(hexString.substring(i*2, (i+1)*2).c_str(), nullptr, 16); + } + + return uint8Array; +} + +String macToString(const uint8_t *mac) +{ + char macString[13] = { 0 }; + sprintf(macString, "%02X%02X%02X%02X%02X%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + return String(macString); +} + +uint8_t *stringToMac(const String &macString, uint8_t *macArray) +{ + return hexStringToUint8Array(macString, macArray, 6); +} + +uint64_t macToUint64(const uint8_t *macArray) +{ + uint64_t outcome = 0; + for(int shiftingFortune = 40; shiftingFortune >= 0; shiftingFortune -= 8) + { + outcome |= ((uint64_t)macArray[5 - shiftingFortune/8] << shiftingFortune); + } + + return outcome; +} + +uint8_t *uint64ToMac(uint64_t macValue, uint8_t *macArray) +{ + assert(macValue <= 0xFFFFFFFFFFFF); // Overflow will occur if value can't fit within 6 bytes + + for(int shiftingFortune = 40; shiftingFortune >= 0; shiftingFortune -= 8) + { + macArray[5 - shiftingFortune/8] = macValue >> shiftingFortune & 0xFF; + } + return macArray; +} + +/** + * Helper function for meshBackendCast. + */ +template +T attemptPointerCast(MeshBackendBase *meshBackendBaseInstance, mesh_backend_t resultClassType) +{ + if(meshBackendBaseInstance && meshBackendBaseInstance->getClassType() == resultClassType) + { + return static_cast(meshBackendBaseInstance); + } + else + { + return nullptr; + } +} + +template <> +EspnowMeshBackend *meshBackendCast(MeshBackendBase *meshBackendBaseInstance) +{ + return attemptPointerCast(meshBackendBaseInstance, MB_ESP_NOW); +} + +template <> +TcpIpMeshBackend *meshBackendCast(MeshBackendBase *meshBackendBaseInstance) +{ + return attemptPointerCast(meshBackendBaseInstance, MB_TCP_IP); +} diff --git a/libraries/ESP8266WiFiMesh/src/TypeConversionFunctions.h b/libraries/ESP8266WiFiMesh/src/TypeConversionFunctions.h index 5d42e414c..a6fa11465 100644 --- a/libraries/ESP8266WiFiMesh/src/TypeConversionFunctions.h +++ b/libraries/ESP8266WiFiMesh/src/TypeConversionFunctions.h @@ -28,6 +28,9 @@ #include #include +#include "MeshBackendBase.h" +#include "TcpIpMeshBackend.h" +#include "EspnowMeshBackend.h" /** * Note that using a base higher than 16 increases likelihood of randomly generating SSID strings containing controversial words. @@ -47,4 +50,66 @@ String uint64ToString(uint64_t number, byte base = 16); */ uint64_t stringToUint64(const String &string, byte base = 16); +// All array elements will be padded with zeroes to ensure they are converted to 2 string characters each. +String uint8ArrayToHexString(const uint8_t *uint8Array, uint32_t arrayLength); + +// There must be 2 string characters for each array element. Use padding with zeroes where required. +uint8_t *hexStringToUint8Array(const String &hexString, uint8_t *uint8Array, uint32_t arrayLength); + +/** + * Takes a uint8_t array and converts the first 6 bytes to a hexadecimal string. + * + * @param mac A uint8_t array with the mac address to convert to a string. Should be 6 bytes in total. + * @returns A hexadecimal string representation of the mac. + */ +String macToString(const uint8_t *mac); + +/** + * Takes a String and converts the first 12 characters to uint8_t numbers which are stored in the macArray from low to high index. Assumes hexadecimal number encoding. + * + * @param macString A String which begins with the mac address to store in the array as a hexadecimal number. + * @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. + * @returns The macArray. + */ +uint8_t *stringToMac(const String &macString, uint8_t *macArray); + +/** + * Takes a uint8_t array and converts the first 6 bytes to a uint64_t. Assumes index 0 of the array contains MSB. + * + * @param macArray A uint8_t array with the mac address to convert to a uint64_t. Should be 6 bytes in total. + * @returns A uint64_t representation of the mac. + */ +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. + * + * @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. + * @returns The macArray. + */ +uint8_t *uint64ToMac(uint64_t macValue, uint8_t *macArray); + +/** + * Conversion function that can be used on MeshBackend classes instead of dynamic_cast since RTTI is disabled. + * + * @param T The MeshBackend class pointer type to cast the meshBackendBaseInstance pointer into. + * @param meshBackendBaseInstance The instance pointer to cast. + * @returns A pointer of type T to meshBackendBaseInstance if meshBackendBaseInstance is of type T. nullptr otherwise. + */ +template +T meshBackendCast(MeshBackendBase *meshBackendBaseInstance) +{ + // The only valid template arguments are handled by the template specializations below, so ending up here is an error. + static_assert(std::is_same::value || std::is_same::value, + "Error: Invalid MeshBackend class type. Make sure the template argument to meshBackendCast is supported!"); +} + +// These template specializations allow us to put the main template functionality in the .cpp file (which gives better encapsulation). +template <> +EspnowMeshBackend *meshBackendCast(MeshBackendBase *meshBackendBaseInstance); + +template <> +TcpIpMeshBackend *meshBackendCast(MeshBackendBase *meshBackendBaseInstance); + #endif diff --git a/libraries/ESP8266WiFiMesh/src/UtilityFunctions.cpp b/libraries/ESP8266WiFiMesh/src/UtilityFunctions.cpp new file mode 100644 index 000000000..6b4af4c01 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/UtilityFunctions.cpp @@ -0,0 +1,45 @@ +/* + * UtilityFunctions + * 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 "UtilityFunctions.h" +#include + +bool macEqual(const uint8_t *macOne, const uint8_t *macTwo) +{ + for(int i = 0; i <= 5; i++) + { + if(macOne[i] != macTwo[i]) + { + return false; + } + } + + return true; +} + +uint64_t randomUint64() +{ + return (((uint64_t)RANDOM_REG32 << 32) | (uint64_t)RANDOM_REG32); +} diff --git a/libraries/ESP8266WiFiMesh/src/UtilityFunctions.h b/libraries/ESP8266WiFiMesh/src/UtilityFunctions.h new file mode 100644 index 000000000..6fa0afb61 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/UtilityFunctions.h @@ -0,0 +1,35 @@ +/* + * UtilityFunctions + * 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 __UTILITYFUNCTIONS_H__ +#define __UTILITYFUNCTIONS_H__ + +#include + +bool macEqual(const uint8_t *macOne, const uint8_t *macTwo); + +uint64_t randomUint64(); + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/UtilityMethods.cpp b/libraries/ESP8266WiFiMesh/src/UtilityMethods.cpp index 795eacb63..9425d87ed 100644 --- a/libraries/ESP8266WiFiMesh/src/UtilityMethods.cpp +++ b/libraries/ESP8266WiFiMesh/src/UtilityMethods.cpp @@ -1,5 +1,5 @@ /* - * TransmissionResult + * UtilityMethods * Copyright (C) 2018 Anders Löfgren * * License (MIT license): @@ -24,11 +24,15 @@ */ #include "TypeConversionFunctions.h" -#include "ESP8266WiFiMesh.h" +#include "MeshBackendBase.h" +#include "EspnowMeshBackend.h" -void ESP8266WiFiMesh::verboseModePrint(const String &stringToPrint, bool newline) +void MeshBackendBase::setVerboseModeState(bool enabled) {_verboseMode = enabled;} +bool MeshBackendBase::verboseMode() {return _verboseMode;} + +void MeshBackendBase::verboseModePrint(const String &stringToPrint, bool newline) { - if(_verboseMode) + if(verboseMode()) { if(newline) Serial.println(stringToPrint); @@ -37,45 +41,43 @@ void ESP8266WiFiMesh::verboseModePrint(const String &stringToPrint, bool newline } } -/** - * Calculate the current lwIP version number and store the numbers in the _lwipVersion array. - * lwIP version can be changed in the "Tools" menu of Arduino IDE. - */ -void ESP8266WiFiMesh::storeLwipVersion() +void EspnowMeshBackend::setVerboseModeState(bool enabled) {MeshBackendBase::setVerboseModeState(enabled); _staticVerboseMode = enabled;} +bool EspnowMeshBackend::verboseMode() {return staticVerboseMode();} + +void EspnowMeshBackend::verboseModePrint(const String &stringToPrint, bool newline) { - // ESP.getFullVersion() looks something like: - // SDK:2.2.1(cfd48f3)/Core:win-2.5.0-dev/lwIP:2.0.3(STABLE-2_0_3_RELEASE/glue:arduino-2.4.1-10-g0c0d8c2)/BearSSL:94e9704 - String fullVersion = ESP.getFullVersion(); - - int i = fullVersion.indexOf("lwIP:") + 5; - char currentChar = fullVersion.charAt(i); - - for(int versionPart = 0; versionPart < 3; versionPart++) + if(verboseMode()) { - while(!isdigit(currentChar)) - { - currentChar = fullVersion.charAt(++i); - } - while(isdigit(currentChar)) - { - _lwipVersion[versionPart] = 10 * _lwipVersion[versionPart] + (currentChar - '0'); // Left shift and add digit value, in base 10. - currentChar = fullVersion.charAt(++i); - } + if(newline) + Serial.println(stringToPrint); + else + Serial.print(stringToPrint); } } -/** - * Check if the code is running on a version of lwIP that is at least minLwipVersion. - */ -bool ESP8266WiFiMesh::atLeastLwipVersion(const uint32_t minLwipVersion[3]) -{ - for(int versionPart = 0; versionPart < 3; versionPart++) - { - if(_lwipVersion[versionPart] > minLwipVersion[versionPart]) - return true; - else if(_lwipVersion[versionPart] < minLwipVersion[versionPart]) - return false; - } +bool EspnowMeshBackend::staticVerboseMode() {return _staticVerboseMode;} - return true; +void EspnowMeshBackend::staticVerboseModePrint(const String &stringToPrint, bool newline) +{ + if(staticVerboseMode()) + { + if(newline) + Serial.println(stringToPrint); + else + Serial.print(stringToPrint); + } +} + +void MeshBackendBase::setPrintWarnings(bool printEnabled) {_printWarnings = printEnabled;} +bool MeshBackendBase::printWarnings() {return _printWarnings;} + +void MeshBackendBase::warningPrint(const String &stringToPrint, bool newline) +{ + if(printWarnings()) + { + if(newline) + Serial.println(stringToPrint); + else + Serial.print(stringToPrint); + } }