diff --git a/libraries/ESP8266WiFiMesh/README.md b/libraries/ESP8266WiFiMesh/README.md index 8d955220ca..4bfd019928 100644 --- a/libraries/ESP8266WiFiMesh/README.md +++ b/libraries/ESP8266WiFiMesh/README.md @@ -1,81 +1,398 @@ -ESP8266 WiFi Mesh -================= +# ESP8266 WiFi Mesh -A library for turning your ESP8266 into a mesh network node. +## Contents +1. [Overview](#Overview) +2. [How does it work?](#Work) +3. [The first step](#Start) +4. [TcpIpMeshBackend](#TcpIpMeshBackendMore) + * [Usage](#TcpIpMeshBackendUsage) + * [Note](#TcpIpMeshBackendNote) + * [General Information](#TcpIpMeshBackendGeneral) +5. [EspnowMeshBackend](#EspnowMeshBackendMore) + * [Usage](#EspnowMeshBackendUsage) + * [Note](#EspnowMeshBackendNote) + * [Callbacks](#EspnowMeshBackendCallbacks) + * [Encryption](#EspnowMeshBackendEncryption) + * [CCMP](#CCMP) + * [AEAD](#AEAD) +6. [FloodingMesh](#FloodingMeshMore) + * [Usage](#FloodingMeshUsage) + * [Note](#FloodingMeshNote) + * [Serialization and the internal state of a node](#FloodingMeshSerialization) +7. [FAQ](#FAQ) + * [My ESP8266 crashes on start-up when I use the library!](#FAQStartupCrash) + * [The node does not remember the SSID I assign to it!](#FAQSSIDAmnesia) + * [I want to control the WiFi mode myself.](#FAQModeControl) + * [I have a lot of interference from all the nodes that are close to each other. What can I do?](#FAQInterference) + * [How do I change the interval of the WiFi AP beacon broadcast?](#FAQBeaconInterval) + * [My ESP is ignoring the WiFi AP beacon broadcast interval settings you just told me about above! (a.k.a. How do I change the WiFi scan mode to passive?)](#FAQPassiveScan) + * [My internet is slower when I connect the ESP8266 to my router!](#FAQSlowRouter) -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). -**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. +## Overview -Usage ------ +This is a library for creating a mesh network using the ESP8266. -The basic operation of a mesh node is as follows: +The library has been tested and works with Arduino Core for ESP8266 version 3.0.0 (with lwIP2). It may work with earlier and later core releases, but this has not been tested during development. -The `attemptTransmission` method of the ESP8266WiFiMesh instance is called with a message to send to other nodes in the mesh network. If the node is already connected to an AP, the message is sent only to that AP. Otherwise a WiFi scan is performed. The scan results are sent to the `networkFilter` callback function of the ESP8266WiFiMesh instance which adds the AP:s of interest to the `connectionQueue` vector. The message is then transmitted to the networks in the `connectionQueue`, and the response from each AP is sent to the `responseHandler` callback of the ESP8266WiFiMesh instance. The outcome from each transmission attempt can be found in the `latestTransmissionOutcomes` vector. +**Note:** This mesh library has been extensively rewritten for core release 3.0.0. The old method signatures have been retained for compatibility purposes, but will be removed in core release 3.0.X. 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. -The node receives messages from other nodes by calling the `acceptRequest` method of the ESP8266WiFiMesh instance. These received messages are passed to the `requestHandler` callback of the mesh instance. For each received message the return value of `requestHandler` is sent to the other node as a response to the message. +## How does it work? -For more details, see the included example. The main functions to modify in the example are `manageRequest` (`requestHandler`), `manageResponse` (`responseHandler`) and `networkFilter`. There is also more information to be found in the source code comments. An example is the ESP8266WiFiMesh constructor comment, which is shown below for reference: -``` -/** -* 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 meshName The name of the mesh network. Used as prefix for the node SSID and to find other network nodes in the example network filter function. -* @param nodeID The id for this mesh node. Used as suffix for the node SSID. If set to "", the id will default to ESP.getChipId(). -* @param verboseMode Determines if we should print the events occurring in the library to Serial. Off by default. -* @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 ESP8266WiFiMesh instances exist on the same ESP8266 and use different WiFi channels. -* In such a case, whenever the station of one ESP8266WiFiMesh 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 by the AP of the ESP8266WiFiMesh instance. 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. -* -*/ -ESP8266WiFiMesh(requestHandlerType requestHandler, responseHandlerType responseHandler, networkFilterType networkFilter, - const String &meshPassword, const String &meshName = "MeshNode_", const String &nodeID = WIFI_MESH_EMPTY_STRING, bool verboseMode = false, - uint8 meshWiFiChannel = 1, int serverPort = 4011); -``` +The ESP8266 WiFi Mesh library is a cake, metaphorically speaking. At the bottom you have the general ESP8266 Arduino Core WiFi functionality. On top of this two mesh backends have been created (`EspnowMeshBackend` and `TcpIpMeshBackend`), a yummy filling that completely covers the bottom. Then at the very top over the backends is the beautiful and delicious frosting: `FloodingMesh`. `FloodingMesh` is an actual mesh network implementation that uses the `EspnowMeshBackend`. + +Eating the cake in its current form is a process which involves all the layers. However, if you prefer to be your own pastry chef it is easy to use both the `EspnowMeshBackend` and the `TcpIpMeshBackend` separately from `FloodingMesh`, perhaps to construct your own mesh network architecture or just to simplify the usage of TCP/IP or ESP-NOW. If you have made a nice mesh architecture with this library that you would like to share with the rest of the world, feel free to make a PR with it! + +In general ESP-NOW is faster than TCP/IP for small data payloads (up to a few kB). The data segment of a standard ESP-NOW transmission is 234 bytes, which takes around 2-4 ms to transmit. + +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. However, this backend has a much higher data transfer speed than ESP-NOW once connected (100x faster or so). + +## The first step + +There are plenty of details to the operations of the library, but if you want to get started quickly you really only need to know this: In the example folder of the library there is a file called `HelloMesh.ino`. Upload it to a few ESP8266 and you have a working mesh network. Change the `useLED` variable to `true` if you have built-in LEDs on your ESP8266s to illustrate how the message is spread through the network. Change the `floodingMesh.broadcast` calls to modify what the mesh nodes are transmitting to each other. Change the code of the `meshMessageHandler` to modify how mesh nodes react to received transmissions. + +Finally, three things are important to note: + +1. This library uses the standard Arduino Core for ESP8266 WiFi functions. Therefore, other code that also uses these WiFi functions (e.g. `WiFi.mode()`) may cause conflicts with the library, resulting in strange behaviour. See "[I want to control the WiFi mode myself](#FAQModeControl)" in the FAQ for ideas on how to work around this. +2. Both the `EspnowMeshBackend` and the `TcpIpMeshBackend` can be used simultaneously on the same node. However, since there is only one WiFi radio on the ESP8266, only one backend at a time will be responsible for the settings of this radio (SSID, WiFi channel etc.). The backend in control is known as the `APController` in the library. Both backends can still send messages, regardless of who is `APController`. +3. The `MeshBackendBase`, `EspnowMeshBackend`, `TcpIpMeshBackend` and `FloodingMesh` source files are meant to be the main front-ends of the library and are all extensively documented. If you wonder about how something is working, chances are good that you will find an answer in the documentation of those files. + +## TcpIpMeshBackend -### Note +### Usage -* 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. +The basic operation of the TCP/IP mesh backend is as follows: - At the moment static IP is a global setting, meaning that all ESP8266WiFiMesh instances on the same ESP8266 share the same static IP settings. +The `attemptTransmission` method of the TcpIpMeshBackend instance is called with a message to send to other nodes in the mesh network. If the node is already connected to an AP, the message is sent only to that AP. Otherwise the default behaviour is for a WiFi scan to be performed. The scan results are sent to the `networkFilter` callback function of the TcpIpMeshBackend instance which adds the AP:s of interest to the `connectionQueue` vector. The message is then transmitted to the networks in the `connectionQueue`, and the response from each AP is sent to the `responseHandler` callback of the TcpIpMeshBackend instance. The outcome from each transmission attempt can be found in the `latestTransmissionOutcomes` vector. -* 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. +The node receives messages from other TCP/IP nodes by calling the `acceptRequests` method of the TcpIpMeshBackend instance. These received messages are passed to the `requestHandler` callback of the mesh instance. For each received message the return value of `requestHandler` is sent to the other node as a response to the message. - 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. +For more details, see the included HelloTcpIp example. The main functions to modify in the example are `manageRequest` (`requestHandler`), `manageResponse` (`responseHandler`), `networkFilter` and `exampleTransmissionOutcomesUpdateHook`. There is also much more information to be found in the source code comments. -* 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: +### 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. When using static IP, the following is good to keep in mind: + + 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 TcpIpMeshBackend instances on the same ESP8266 share the same static IP settings. + +* Scanning all WiFi channels (e.g. via the `attemptTransmission` method with the `scanAllWiFiChannels` argument set to `true`) will slow down scans considerably and make it more likely that existing WiFi connections will break during scans. + +* If the ESP8266 has an active AP, that AP will switch WiFi channel to match that of any other AP the TcpIpMeshBackend of 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: ``` - if(ESP8266WiFiMesh *apController = ESP8266WiFiMesh::getAPController()) // Make sure apController is not nullptr + if(MeshBackendBase *apController = MeshBackendBase::getAPController()) // Make sure apController is not nullptr apController->restartAP(); ``` -* It is possible to have several ESP8266WiFiMesh instances running on every ESP8266 (e.g. to communicate with different mesh networks). However, because the ESP8266 has one WiFi radio only one AP per ESP8266 can be active at a time. Also note that if the ESP8266WiFiMesh instances use different WiFi channels, active APs are forced to use the same WiFi channel as active stations, possibly causing AP disconnections. +* It is possible to have several TcpIpMeshBackend instances running on every ESP8266 (e.g. to communicate with different mesh networks). However, because the ESP8266 has one WiFi radio only one AP per ESP8266 can be active at a time. Also note that if the TcpIpMeshBackend instances use different WiFi channels, active APs are forced to use the same WiFi channel as active stations, possibly causing AP disconnections. -* While it is possible to connect to other nodes by only giving their SSID, e.g. `ESP8266WiFiMesh::connectionQueue.push_back(NetworkInfo("NodeSSID"));`, it is recommended that AP WiFi channel and AP BSSID are given as well, to minimize connection delay. +* While it is possible to connect to other nodes by only giving their SSID, e.g. `TcpIpMeshBackend::connectionQueue().emplace_back("NodeSSID");`, it is recommended that AP WiFi channel and AP BSSID are given as well, to minimize connection delay. * Also, remember to change the default mesh network WiFi password! -General Information ---------------------------- - -* This library uses the standard Arduino core for ESP8266 WiFi functions. Therefore, other code that also uses these WiFi functions may cause conflicts with the library, resulting in strange behaviour. +### General Information * By default, a maximum of 4 stations can be connected at a time to each AP. This can be changed to a value in the range 0 to 8 via the `setMaxAPStations` method. 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. -* Unlike `WiFi.mode(WIFI_AP)`, the `WiFi.mode(WIFI_AP_STA)` which is used in this library allows nodes to stay connected to an AP they connect to while in STA mode, at the same time as they can receive connections from other stations. Nodes cannot send data to an AP while in STA_AP mode though, that requires STA mode. Switching to STA mode will sometimes disconnect stations connected to the node AP (though they can request a reconnect even while the previous AP node is in STA mode). +* Unlike `WiFi.mode(WIFI_AP)`, the `WiFi.mode(WIFI_AP_STA)` which is used in this library allows TCP/IP nodes to stay connected to an AP they connect to while in STA mode, at the same time as they can receive connections from other stations. Nodes cannot send data to an AP while in STA_AP mode though, that requires STA mode. Switching to STA mode will sometimes disconnect stations connected to the node AP (though they can request a reconnect even while the previous AP node is in STA mode). + +## EspnowMeshBackend + +Unlike the TcpIpMeshBackend, the ESP-NOW backend uses pure callbacks even for message reception. This means that whenever `delay()` is called or the `loop()` function returns, the ESP-NOW backend will automatically check if an ESP-NOW message has been received and send it to the correct callback. There is no need to call `acceptRequests` as for the TcpIpMeshBackend. As a result of this, it is possible to receive an ingoing ESP-NOW transmission at the same time as an outgoing ESP-NOW transmission is in progress. This will likely be noted as a spike in the usual transmission time, the size of which will depend on the execution time of `requestHandler`/`responseHandler` (determined by transmission type). + +Some ESP-NOW tasks cannot be securely handled via callbacks. To manage this there are `espnowDelay` and `performEspnowMaintenance` functions available which handle these tasks separately. Either of these methods should be called regularly when your node has some time over for handling background tasks. + +### Usage + +There are two primary ways to send an ESP-NOW message: `broadcast` and `attemptTransmission`. + +If `broadcast` is used, the message is sent to all surrounding nodes in one transmission without any WiFi scan. When the surrounding nodes receive the broadcast they will send it to the `broadcastFilter` callback of the EspnowMeshBackend instance, and based on the return value of this callback either accept or reject the broadcast. The `broadcastFilter` callback is also responsible for removing any metadata from the broadcast. + +If `attemptTransmission` is used, a WiFi scan is by default performed before the transmission. The scan results are sent to the `networkFilter` callback function of the EspnowMeshBackend instance which adds the AP:s of interest to the `connectionQueue` vector. The message is then transmitted to the nodes in the `connectionQueue`. The outcome from each transmission attempt can be found in the `latestTransmissionOutcomes` vector. + +Regardless of whether `broadcast` or `attemptTransmission` is used, when a node receives a message (and it is accepted), the message is passed to the `requestHandler` callback of the EspnowMeshBackend instance. For each received message the return value of `requestHandler` is stored as a response in the `responsesToSend` waiting list. These stored responses will then be sent whenever `performEspnowMaintenance` (or `espnowDelay`) is called. + +When the response is received by the node that sent the request, the response message is forwarded to the `responseHandler` callback of the EspnowMeshBackend instance that sent the request. + +To be completely clear, requests are actually passed to the `broadcastFilter` and `requestHandler` callbacks belonging to the `EspnowRequestManager` of the node, but as long as there is only one EspnowMeshBackend instance on the node this will be the `EspnowRequestManager`. Also, since received ESP-NOW messages are handled via a callback, there is no need to call `acceptRequests` to receive messages, unlike with the TcpIpMeshBackend. + +The EspnowMeshBackend has a few different options for encrypting messages. This is described in greater detail in the [Encryption](#EspnowMeshBackendEncryption) section below. + +More information can be found in the source code comments and in the included HelloEspnow example. The main functions to modify in the example are `manageRequest` (`requestHandler`), `manageResponse` (`responseHandler`), `networkFilter` and `broadcastFilter`. + +### Note + +* `yield()` can cause crashes when using ESP-NOW, since the command requires code to be run in the CONT context. If you are having problems with this, use `delay()` instead. + +* This library uses the ESP8266 modules' MAC addresses to keep track of transmissions. So if you need to change the MAC addresses do so with care and preferably before any transmission is made. +Turning the AP off will make it impossible to send information to the node AP mac. However, it will still be possible to send the data to the station mac. +To do this, send over the station mac to the transmitting node and then manually add it to the `connectionQueue` whenever a transmission should be made to that node. + +* If the available heap goes under `criticalHeapLevel()` bytes (6000 bytes by default), the ESP-NOW backend will temporarily cease accepting new incoming ESP-NOW requests in an attempt to avoid running out of RAM. Warning messages about this will also be printed to the Serial Monitor, assuming `printWarnings()` is `true` (this is the default value). + +* During very heavy load the `performEspnowMaintenance` method may occasionally need to process requests for tens of milliseconds. Since this won't happen until the method is called, you can choose when this is done. Callbacks can be executed while the request processing is ongoing, but note that they should have a very fast execution time in this case. Also be sure to take into account the callback restrictions mentioned [below](#EspnowMeshBackendCallbacks). + +* When `WiFi.mode(WIFI_STA)` is used, nodes are unable to receive ESP-NOW broadcast messages. All nodes can however still receive direct ESP-NOW messages to their STA mac. Nodes seem to continue transmitting successfully to the correct (broadcast) MAC regardless of WiFi mode, only message reception is affected. Different combinations of ESP-NOW roles do not seem to have any influence on the outcome. Stripping out all library code and only using the bare minimum required for a broadcast does not change the outcome. Thus, this issue seems to be unfixable until corrected by Espressif. + + During testing it seemed for a while as though some nodes were able to receive ESP-NOW broadcasts even when in STA mode. There was no obvious difference between the nodes for which this worked and those for which it did not, so what caused this is unknown. Possibly the issue could have been caused by something stored on the nodes, perhaps a different persistent WiFi config or something similar. It is of course also possible that there was an error made during testing, but the event is noted here as it could be an avenue for further investigation. + +* Although ESP-NOW 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 first fail to transmit while others transmit successfully. Use the `ResponseTransmittedHook`callback if this behaviour should be modified. + +### Callbacks + +For maximum performance and minimum RAM usage it is very important that your callbacks and hooks can be handled quickly (within a few milliseconds, preferably), as node performance can start to suffer quickly otherwise, particularly if transmission intensity is high. Be especially wary of long Serial prints, as these require a lot of time to complete. If transmission activity is very low, it is however possible to have callbacks which take a long time to complete. In these cases, even a callback execution time of multiple seconds can be acceptable. Of course, you would get problems with other parts of the Arduino Core framework (like watch dog timer resets) if you don't call `delay()` or `ESP.wdtFeed()` within that time. + +Certain methods of the EspnowMeshBackend (e.g. `attemptTransmission`, `broadcast`, `espnowDelay` and `performEspnowMaintenance`) should not be used within callbacks, since this can mess with the internal state of the backend. These methods are all using a `MutexTracker` component to enforce this requirement via asserts, so if your nodes are crashing for unknown reasons when using callbacks, make sure to check the Serial Monitor to see if there are any mutex error messages! + +One way to resolve such errors is to simply always call the sensitive methods from the `loop()` instead of from a callback, possibly just storing the received value for later inside the callback. [PolledTimeout](https://github.com/esp8266/Arduino/blob/master/cores/esp8266/PolledTimeout.h) can be helpful for time tracking in this case. + +If a callback with the sensitive methods is required, it has been reported that the methods in `TaskScheduler.h` of the [TaskScheduler library](https://github.com/arkhipenko/TaskScheduler) work well when scheduling tasks. It can in this role be used as a replacement of the [Ticker](https://arduino-esp8266.readthedocs.io/en/latest/libraries.html#ticker) functionality in the Arduino Core. + +The reason the callback limitations exist is that during a transmission the library will only get an ack from the receiver when `delay()` is used. Yet `delay()` also calls all other background tasks, including user callbacks, and these must thus be safe to execute during ongoing transmissions. + +### Encryption + +There are two separate methods for encrypting a message with the ESP-NOW backend. One method creates an encrypted connection between two nodes using the built-in CCMP encryption of the ESP8266. The other method simply uses software AEAD to encrypt and decrypt the messages sent. + +More in-depth information about the encryption methods of the framework can be found at the top of the EspnowMeshBackend.h and EspnowProtocolInterpreter.h files. + +A brief overview of the advantages of each method: + +AEAD + +* The AEAD encryption does not require any pairing, and is thus faster for single messages than establishing a new encrypted connection before transfer. + +* AEAD encryption also works with ESP-NOW broadcasts and supports an unlimited number of nodes, which is not true for encrypted connections. + +CCMP + +* Using AEAD will only encrypt the message content, not the transmission metadata. CCMP encryption covers both. + +* Encrypted ESP-NOW connections come with built in replay attack protection, which is not provided by the framework when using AEAD encryption. + +* Encrypted ESP-NOW connections also allow `EspnowProtocolInterpreter::aeadMetadataSize` extra message bytes per transmission. + +* Transmissions via encrypted connections are also slightly faster than via AEAD once a connection has been established. + +#### CCMP + +For encrypted connections (managed via such methods as `addEncryptedConnection`, `requestEncryptedConnection` and `requestEncryptedConnectionRemoval`), ESP-NOW [uses](https://www.espressif.com/sites/default/files/documentation/esp-now_user_guide_en.pdf) [CCMP encryption](https://en.wikipedia.org/wiki/CCMP_(cryptography)). +To handle some idiosyncrasies of ESP-NOW (like having no way in the application layer to know if received information is encrypted or not), a separate API layer has been built on top. +This API layer is provided in the hope that it will be useful, but has not been subject to any cryptographic validation (yet, feel free to have a go at it if you have the knowledge). +The goal of the API layer is to ensure that when an encrypted connection is established, the received encrypted messages will both be marked as encrypted and be trustworthy. + +Established encrypted connections can be either permanent or temporary. A permanent encrypted connection can only be removed by explicitly calling `removeEncryptedConnection` or `requestEncryptedConnectionRemoval`. A temporary encrypted connection will expire once the duration has passed, although this duration can be updated through the methods used for adding new encrypted connections. + +The maximum number of simultaneous encrypted connections is restricted by the ESP-NOW specifications and is `EspnowProtocolInterpreter::maxEncryptedConnections` (6 by default). If required, a stricter soft upper limit can be used for the number of encrypted connections a node can have when receiving encrypted connection requests, to ensure there is normally some margin to the hard maximum. This is handled via the`setEncryptedConnectionsSoftLimit` method. + +The internal state of an encrypted connection will be lost if the ESP8266 is restarted or loses power, meaning encrypted messages will no longer be received. There is however functionality available to serialize the state of an encrypted connection so it can be restored later. The HelloEspnow.ino example file shows how this is done. Of course, a stored state should only be used once, since the communication will otherwise be susceptible to replay attacks. See "[Serialization and the internal state of a node](#FloodingMeshSerialization)" in the FloodingMesh docs for more info. + +Some security considerations for CCMP encrypted connections are listed below. + +* Part of the separate API layer uses the internal hardware random number generator of the ESP8266 (via `ESP.random()`) to initialize the connection state. This may or may not have enough entropy for your security needs. +For an even more random (but slower) number generation, you may want to replace the use of plain `ESP.random()` with something else. + +* Since there is no way to know whether a received transmission is encrypted or not via the default ESP-NOW API, an attacker can send unencrypted ESP-NOW messages which pretend to be encrypted without this being detected by the application. To prevent such attacks from happening, this framework uses an extra 64 bit session key for all encrypted connections. A message is only accepted as encrypted if it has the correct session key. 64 bits are used mainly because the uint64_t datatype is the largest natively supported by the ESP8266 Arduino Core, and because each ESP-NOW transmission has a relatively small maximum capacity of 250 bytes. + +* The ESP-NOW CCMP encryption should according to the standard have replay attack protection built in, but there is no official documentation from Espressif about this. The 64 bit session key used for encrypted connections, as described above, will however also ensure replay protection. + +* The maximum rate at which a potential attacker can poll a session key (via unencrypted transmissions pretending to be encrypted transmissions) is around 0.3 keys per ms, but in practice this rate would render the node completely unresponsive and is thus easily detected. +Assuming the rate above is used that would mean that an attacker in one day could try 0.3 x 1000 x 60 x 60 x 24 = 25 920 000 keys, which is roughly 1/711 600 000 000 of the total (total is 2^(64) - 2^(32), the top 32 session key bits are all 0 when the transmission is unencrypted). + +* Should there be a need for even more security, the user could enhance the library with 128 bit (or more) session keys, or ensure CCMP encrypted messages are sent frequently since this will rehash the session key every time, or frequently remove and re-add the encrypted connections (which will cause the session keys to be randomized or set to the supplied values). + +#### Authenticated Encryption with Associated Data (AEAD) + +In addition to using encrypted ESP-NOW connections the framework can send automatically encrypted messages (using AEAD) over both encrypted and unencrypted connections. This message encryption is conditioned on the `useEncryptedMessages()` flag of the EspnowMeshBackend. Typically, activating the AEAD encryption would be done like so: +``` +espnowBackendInstance.setEspnowMessageEncryptionKey(F("ChangeThisKeySeed_TODO")); // The message encryption key should always be set manually. Otherwise a default key (all zeroes) is used. +espnowBackendInstance.setUseEncryptedMessages(true); +``` + +The AEAD protocol uses the ChaCha20 stream cipher with Poly1305 for message authentication. +More information about this encryption standard can be found here: https://tools.ietf.org/html/rfc7539 , https://tools.ietf.org/html/rfc8439 + +## FloodingMesh + +**Important:** As of now, the ESP8266 must have the AP active to receive mesh messages (either via AP mode (use only if CCMP encryption is not required) or AP+STA mode). Messages can however be transmitted even when the AP is turned off. This is limited by the Espressif binary in the ESP8266 Arduino Core and so cannot be corrected by the library code. + +*** + +As the name implies, FloodingMesh is a simple flooding mesh architecture, which means it stores no mesh network routing data in the nodes but only passes new messages on to all surrounding nodes. It therefore has no RAM overhead for network size, which is important for the ESP8266 since available RAM is very limited. The downside is that there is a lot of network traffic for each sent message, and all nodes use the same WiFi channel, so especially for dense networks a lot of interference will be created. Based on tests, a mesh with 30 nodes close together (-44 dBm RSSI) will work well (1-2 dropped messages of 1000). A mesh with around 160 nodes close together will not work at all (though this would probably be solved by spreading out the nodes more, so the interference is reduced). + +The FloodingMesh exclusively uses the `EspnowMeshBackend`. The mesh network size is only limited by available MAC addresses, so the maximum is (2^48)/2 = 140 trillion give or take. However, the maximum throughput of the FloodingMesh is around 100 messages per second with 234 bytes per message, so using the maximum number of nodes is not recommended in most cases. Note that while ASCII characters require 1 message byte each, non-ASCII characters usually require 2 message bytes each. + +### Usage + +There are two primary ways to send a message in FloodingMesh: `broadcast` and `encryptedBroadcast`. + +Messages sent via `encryptedBroadcast` use CCMP encryption. Messages sent via `broadcast` are by default unencrypted, but can optionally be encrypted with AEAD encryption. See the "[Encryption](#EspnowMeshBackendEncryption)" segment of the EspnowMeshBackend documentation for more information on the forms of encryption. + +The main advantage of `encryptedBroadcast` over `broadcast` is that replay attack protection comes built-in. However, `encryptedBroadcast` is currently slow and experimental so for now `broadcast` is the recommended method to use. This means that replay attacks must be handled separately in a manner suitable for your application (e.g. by adding a counter to your messages or just by designing your application so repeated messages is not an issue). + +When `broadcast` is used, the message is sent to all surrounding nodes in one transmission without any WiFi scan. + +When a FloodingMesh node receives a message it will first check in its logs to see if the message ID has been received before. If the message ID is not found, the message will be passed to the `meshMessageHandler` of the FloodingMesh instance. + +If `meshMessageHandler` returns `false`, the message will not be propagated from the node. If `meshMessageHandler` returns `true`, the message (including any modifications made to it by the `meshMessageHandler`) will be stored in the `forwardingBacklog`. Messages stored in this way are automatically sent to all surrounding nodes via a new `broadcast` or `encryptedBroadcast` (same method as used for the received message) whenever `performMeshMaintenance()`, `performMeshInstanceMaintenance()` or `floodingMeshDelay` is called. + +For advanced users, the behaviour of FloodingMesh can easily be modified on the fly by changing the callbacks of the EspnowMeshBackend instance used by the FloodingMesh. The default behaviour can then be restored by calling the `restore` method for the respective callbacks. E.g. messages to forward in the FloodingMesh are by default stored in the `_defaultRequestHandler`, so call `floodingMeshInstance.getEspnowMeshBackend().setRequestHandler` with your own `requestHandler` function to modify this behaviour. + +More details can be found in the source code comments of both FloodingMesh and EspnowMeshBackend, as well as in the included HelloMesh example. The main function to modify in the example is `meshMessageHandler`. You can also change the `useLED` variable in the example to `true` if you have built-in LEDs on your ESP8266s to get visual feedback on how the message is spread through the mesh network. + +Note that there is no mesh recovery code in the HelloMesh example. It only selects one node (which is marked via the onboard LED if the `useLED` variable is `true`) and makes it continuously transmit. So if the selected node goes offline, no new transmissions will be made. One way to make the example mesh recover is to add a timeout to re-start the selection process if no message is received after a while. However, in practice you will probably want most or all nodes to broadcast their own messages, not just one selected node, so such a recovery timeout will not be useful in that context. + +**I want to know all the nodes in my FloodingMesh. What do I do?** + +To get a list of all nodes in the HelloMesh.ino example, you will have to make broadcast transmissions such as `floodingMesh.broadcast("Register MAC");` and then add code to register previously unknown `meshInstance.getOriginMac()` in the `meshMessageHandler`. + +**What's the best method to get the number of FloodingMesh nodes around me?** + +You could do a WiFi scan if you just want to see the nodes around you (if WiFi AP is enabled). Or you could make the nodes transmit and pick up the MACs with `meshInstance.getEspnowMeshBackend().getSenderMac()` in the `meshMessageHandler`. + +### Note + +Since FloodingMesh is based on EspnowMeshBackend, it shares all the limitations described for that backend above. In addition there are some more specific issues to keep in mind. + +* The network needs enough time to re-broadcast messages. In practice, if the mesh transmits more than 100 new messages per second (in total), there is a risk of running out of RAM since more messages will be received by the nodes than they can re-transmit. + +* A too low value for `messageLogSize` can result in a broadcast storm since the number of "active" messages will be greater than the log size, resulting in messages that bounce around in the network without end. The message log stores all unique FloodingMesh message IDs seen by a node, with more recent IDs replacing the older ones when `messageLogSize` is reached. This means that a node in a mesh network containing 2 nodes will have to send `messageLogSize + 1` transmissions to cause the message log of the other node to forget the first message, while a node in a mesh network containing 101 nodes will have to send 1 % as many messages (on average) to do the same. + + Use `FloodingMesh::setMessageLogSize` to adapt the log size to your needs. A larger log size will of course lead to a higher RAM usage. + +### Serialization and the internal state of a node + +The internal state of a node will be lost if it is restarted or loses power. There is however a method called `serializeMeshState()` available in FloodingMesh to serialize the state of a node so it can be restored later. Of course, a stored state should only be used once, since the communication will otherwise be susceptible to replay attacks. + +For the node state of FloodingMesh there are a few things to keep in mind. + +1. If you use the serialization functionality everything should just work. +2. If all nodes go to sleep without serializing, they will of course lose their memory but the network will be recreated and work as normal when the nodes wake up. +3. If only some nodes go to sleep without serializing the state, things get more complicated. The following is possible: + * If you use `encryptedBroadcast`, the nodes that wake up may silently ignore messages forever from the nodes they used to have an encrypted connection with. + * If you do not use `encryptedBroadcast` the ESP-NOW backend will by default clear its message ID logs in 2.5 seconds (`logEntryLifetimeMs`) and FloodingMesh will have done the same after 100 new message IDs have been received (`messageLogSize`). Once the logs of both classes have been cleared, things will work as normal. Before that, any new message the awoken node sends may have the same ID as an old message, and will then be silently ignored by the receiver. + +The messageID is always used together with the node MAC of the sender. For details on how the ID is generated, check out the `generateMessageID` methods. + +It is important to realize that there is no global message ID counter, only the local received message IDs for each node in the network. Automatic resynchronizing with this local value is currently only supported for encrypted connections, which exist exclusively between two nodes. For unencrypted connections, `addUnencryptedConnection` may be used manually for similar purposes. + +## FAQ + +### My ESP8266 crashes on start-up when I use the library! + +This could be caused by incorrect arguments to the constructors of the library. Usually you would get a Serial Monitor print of the error in question, but if the constructor is called before you call `Serial.begin(115200)` then there will be nothing to print to. The solution is first to check so that all constructor arguments are valid, e.g. that the mesh password has the correct length and does not contain any forbidden characters. If everything checks out you can try to move all the library contructors you use into the `setup()` function of your sketch, after the position where `Serial.begin(115200)` is called. That should give you a proper error message in the Serial Monitor, so you can locate the problem. + +### The node does not remember the SSID I assign to it! + +All example files use `WiFi.persistent(false)` in the `setup()` function, so if you switch the AP off and on again only by using `WiFi.mode()` without the framework methods (`activateAP`/`deactivateAP`), it is likely your last persisted SSID is used, not the one you set in the FloodingMesh/EspnowMeshBackend/TcpIpMeshBackend constructor. The solution is to always use the framework methods to turn the AP on and off, or to follow the instructions below for controlling WiFi mode. + +### I want to control the WiFi mode myself. + +By default the mesh library assumes it is the only code in charge of managing the WiFi. So it expects to be the middle man when the user wants to do something WiFi related. + +That being said, there are some relatively simple ways to go around this. Note that the steps below are not officially supported and may break in future library versions. + +The key to solving this is to note that the only methods of EspnowMeshBackend and FloodingMesh which interact with the WiFi mode is `begin()`, `activateAP()` and `deactivateAP()` (for TcpIpMeshBackend `attemptTransmission` should be added to this list). Let's take a look at the methods: + +``` +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 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() +{ +} +``` + +As you can see, there is nothing in `activateAP` and `deactivateAP` that you cannot do yourself. You do not have to worry about`apController` since it is only used if the mesh library is actually managing an AP (i.e. if `activateAP()` has been called), and the rest is standard Arduino Core WiFi calls. All you have to do then is to call `begin()` once when your program starts and then take responsibility yourself for activating and deactivating an AP with the correct SSID. Essentially, you would create the following function: + +``` +void myActivateAP() +{ + WiFi.softAP( SSID, password, WiFiChannel ); // You can store these values in the mesh backend and call the respective getters, but then you also have to set the backend values whenever they change. + WiFi.mode(WIFI_AP_STA); // Can also be WiFi.mode(WIFI_AP) +} +``` + +Please note that having an AP active is required when receiving broadcasts with FloodingMesh and EspnowMeshBackend (transmitting broadcasts work even when the AP is off). The regular `attemptTransmission` method will transmit even to nodes that have their AP turned off if the recipient STA MAC is already known (then you can set WiFi mode to any mode you like, apart from `WIFI_OFF`). + +When an AP is required, AP+STA mode is used in the ESP-NOW backend to keep compatibility with the TCP/IP backend (both backends can be used at the same time). The reason AP+STA mode is used in the TCP/IP backend can be found in TcpIpMeshBackend.cpp : "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." +Also, AP+STA mode allows encrypted ESP-NOW connections to recover from failure in some cases. + +So in summary, you can solve this by calling `begin()` once and then only using the library methods that do not interact with the WiFi mode. As long as you manage your own AP. + +### I have a lot of interference from all the nodes that are close to each other. What can I do? + +In general, you can switch WiFi channel for some nodes (use only channel 1, 6 and 11 for optimal spread, remember that nodes on different WiFi channels cannot communicate directly with each other), try to improve signal quality, or try to reduce interference by reducing the amount of transmissions in the network. + +If using FloodingMesh you can try to experiment with reducing error rates by using the mesh method `void setBroadcastReceptionRedundancy(uint8_t redundancy);` (default 2) at the cost of more RAM. + + +With both FloodingMesh and the EspnowMeshBackend it is possible to use `floodingMesh.getEspnowMeshBackend().setBroadcastTransmissionRedundancy(uint8_t redundancy)` (default 1) to increase the chance of a message arriving, at the cost of longer transmission times. + +For reducing the amount of transmissions in the network, that will either require you to optimize your transmission usage or reduce the amount of background protocol transmissions. The latter option is described in greater detail in the two answers below. + +### How do I change the interval of the WiFi AP beacon broadcast? + +Currently this requires hacking your Arduino Core source files. At [line 122](https://github.com/esp8266/Arduino/blob/8ee67ab2b53463466fd9f035eef2c542ad9a6775/libraries/ESP8266WiFi/src/ESP8266WiFiAP.cpp#L122) in `ESP8266WiFiAP.cpp` you will find the following line `conf.beacon_interval = 100;` (within the `softAp` method). You can change 100 to any value in the range [100, 60000] ms. If you are having problems with too many AP beacon broadcasts in a mesh network, increasing this value should help you with that. To prevent all nodes from beaconing at the same time, delay initial AP activation by a random value in the range [0, x] and then change `conf.beacon_interval` to x, for some large value x <= 60000 ms (same for all nodes). + +### My ESP is ignoring the WiFi AP beacon broadcast interval settings you just told me about above! (a.k.a. How do I change the WiFi scan mode to passive?) + +The default WiFi scan mode of the ESP8266 is active. This triggers a probe response by all AP:s that receives the probe request from the scan. So setting a different beacon interval time has little effect on the background transmission activity if a lot of active scans happen, since all nodes will start performing probe responses (at the same time) in response to the scans. + +However, we can change the scan mode so it is passive instead! That will avoid a flood of probe responses after every scan. The downside is that your scan will only detect the nodes that happen to beacon during the scan time. Since you may be able to use ESP-NOW broadcasts instead of AP beacons for node detection, this is perhaps not a problem if you just want to reduce background transmission activity as much as possible to reduce interference. + +Note though, that any device that uses active WiFi scans will trigger probe responses from the ESP8266, including smartphones and laptops. So even if you make all ESPs use passive scans, you can still end up with a lot of probe responses from the ESPs if they are close to other devices. The only way to fix this would be to disable the AP of the ESP8266, which of course will make it impossible to find the node via a WiFi scan, and also seems to make it impossible to receive ESP-NOW broadcasts (sending ESP-NOW broadcasts still work though, see the "[Note](#EspnowMeshBackendNote)" section of the EspnowMeshBackend documentation for more on this). + +To change the WiFi scan mode to passive, the following information is helpful: +1. A `scan_config` struct is found in `user_interface.h` (and the ESP8266 API documentation). We want to modify `scan_type`, but note that `scan_time` can also be set here if we want faster or slower scans. +2. In `ESP8266WiFiScan.cpp` one can find the following variable declaration: `struct scan_config config;` around line 87. Adding `config.scan_type = WIFI_SCAN_TYPE_PASSIVE;` after `memset(&config, 0, sizeof(config));` on line 88 will ensure passive scans are used. + +### My internet is slower when I connect the ESP8266 to my router! +There has been some reports about this happening when the ESP8266 is in AP+STA mode while connected to the router. The ESP8266 automatically switches to 802.11g in AP+STA mode, so if your router normally uses a faster WiFi standard such as 802.11n or 802.11ac the router may change mode of operation to 802.11g. Typically this would result in a maximum WiFi speed of around 30 Mbit/s. + +A possible workaround is to use only AP mode or STA mode (see "[I want to control the WiFi mode myself](#FAQModeControl)"), perhaps with an extra ESP8266 in one of these modes as a buffer between your ESP8266 mesh network and your router. Remember that the ESP8266 must have the AP active in order to receive ESP-NOW broadcast messages. -* Scanning for networks (e.g. via the `attemptTransmission` method) without the WiFi scan optimizations for core version 2.4.2 mentioned above, causes the WiFi radio to cycle through all WiFi channels which means existing WiFi connections are likely to break or work poorly if done frequently. \ No newline at end of file +Another possible workaround is to try with a different router or router firmware. \ No newline at end of file diff --git a/libraries/ESP8266WiFiMesh/examples/HelloEspnow/HelloEspnow.ino b/libraries/ESP8266WiFiMesh/examples/HelloEspnow/HelloEspnow.ino new file mode 100644 index 0000000000..3b90a6ef11 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/examples/HelloEspnow/HelloEspnow.ino @@ -0,0 +1,454 @@ +#define ESP8266WIFIMESH_DISABLE_COMPATIBILITY // Excludes redundant compatibility code. TODO: Should be used for new code until the compatibility code is removed with release 3.0.0 of the Arduino core. + +#include +#include +#include +#include +#include + +namespace TypeCast = MeshTypeConversionFunctions; + +/** + 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 +*/ +constexpr char exampleMeshName[] PROGMEM = "MeshNode_"; // The name of the mesh network. Used as prefix for the node SSID and to find other network nodes in the example networkFilter and broadcastFilter functions below. +constexpr char exampleWiFiPassword[] PROGMEM = "ChangeThisWiFiPassword_TODO"; // Note: " is an illegal character. 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. +// Note that it is also possible to use Strings as key seeds instead of arrays. +uint8_t espnowEncryptedConnectionKey[16] = {0x33, 0x44, 0x33, 0x44, 0x33, 0x44, 0x33, 0x44, // This is the key for encrypting transmissions of encrypted connections. + 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 encrypted connection 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; + +const char broadcastMetadataDelimiter = 23; // 23 = End-of-Transmission-Block (ETB) control character in ASCII + +String manageRequest(const String &request, MeshBackendBase &meshInstance); +TransmissionStatusType manageResponse(const String &response, MeshBackendBase &meshInstance); +void networkFilter(int numberOfNetworks, MeshBackendBase &meshInstance); +bool broadcastFilter(String &firstTransmission, EspnowMeshBackend &meshInstance); + +/* Create the mesh node object */ +EspnowMeshBackend espnowNode = EspnowMeshBackend(manageRequest, manageResponse, networkFilter, broadcastFilter, FPSTR(exampleWiFiPassword), espnowEncryptedConnectionKey, espnowHashKey, FPSTR(exampleMeshName), TypeCast::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. + @return 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) { + // To get the actual class of the polymorphic meshInstance, do as follows (meshBackendCast replaces dynamic_cast since RTTI is disabled) + if (EspnowMeshBackend *espnowInstance = TypeCast::meshBackendCast(&meshInstance)) { + String transmissionEncrypted = espnowInstance->receivedEncryptedTransmission() ? F(", Encrypted transmission") : F(", Unencrypted transmission"); + Serial.print(String(F("ESP-NOW (")) + espnowInstance->getSenderMac() + transmissionEncrypted + F("): ")); + } else if (TcpIpMeshBackend *tcpIpInstance = TypeCast::meshBackendCast(&meshInstance)) { + (void)tcpIpInstance; // This is useful to remove a "unused parameter" compiler warning. Does nothing else. + Serial.print(F("TCP/IP: ")); + } else { + Serial.print(F("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. + // Note that request.substring will not work as expected if the String contains null values as data. + Serial.print(F("Request received: ")); + + if (request.charAt(0) == 0) { + Serial.println(request); // substring will not work for multiStrings. + } else { + Serial.println(request.substring(0, 100)); + } + + /* return a string to send back */ + return (String(F("Hello world response #")) + String(responseNumber++) + F(" from ") + meshInstance.getMeshName() + meshInstance.getNodeID() + F(" with AP MAC ") + WiFi.softAPmacAddress() + String('.')); +} + +/** + Callback for when you get a response from other nodes + + @param response The response string received from another node in the mesh + @param meshInstance The MeshBackendBase instance that called the function. + @return The status code resulting from the response, as an int +*/ +TransmissionStatusType manageResponse(const String &response, MeshBackendBase &meshInstance) { + TransmissionStatusType statusCode = TransmissionStatusType::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 = TypeCast::meshBackendCast(&meshInstance)) { + String transmissionEncrypted = espnowInstance->receivedEncryptedTransmission() ? F(", Encrypted transmission") : F(", Unencrypted transmission"); + Serial.print(String(F("ESP-NOW (")) + espnowInstance->getSenderMac() + transmissionEncrypted + F("): ")); + } else if (TcpIpMeshBackend *tcpIpInstance = TypeCast::meshBackendCast(&meshInstance)) { + Serial.print(F("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->getCurrentMessage().substring(0, 100)); + } else { + Serial.print(F("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. + // Note that response.substring will not work as expected if the String contains null values as data. + 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 = TypeCast::stringToUint64(currentSSID.substring(meshNameIndex + meshInstance.getMeshName().length())); + + if (targetNodeID < TypeCast::stringToUint64(meshInstance.getNodeID())) { + if (EspnowMeshBackend *espnowInstance = TypeCast::meshBackendCast(&meshInstance)) { + espnowInstance->connectionQueue().emplace_back(networkIndex); + } else if (TcpIpMeshBackend *tcpIpInstance = TypeCast::meshBackendCast(&meshInstance)) { + tcpIpInstance->connectionQueue().emplace_back(networkIndex); + } else { + Serial.println(F("Invalid mesh backend!")); + } + } + } + } +} + +/** + Callback used to decide which broadcast messages to accept. Only called for the first transmission in each broadcast. + If true is returned from this callback, the first broadcast transmission is saved until the entire broadcast message has been received. + The complete broadcast message will then be sent to the requestHandler (manageRequest in this example). + If false is returned from this callback, the broadcast message is discarded. + Note that the BroadcastFilter may be called multiple times for messages that are discarded in this way, but is only called once for accepted messages. + + @param firstTransmission The first transmission of the broadcast. Modifications to this String are passed on to the broadcast message. + @param meshInstance The EspnowMeshBackend instance that called the function. + + @return True if the broadcast should be accepted. False otherwise. +*/ +bool broadcastFilter(String &firstTransmission, EspnowMeshBackend &meshInstance) { + // This example broadcastFilter will accept a transmission if it contains the broadcastMetadataDelimiter + // and as metaData either no targetMeshName or a targetMeshName that matches the MeshName of meshInstance. + + int32_t metadataEndIndex = firstTransmission.indexOf(broadcastMetadataDelimiter); + + if (metadataEndIndex == -1) { + return false; // broadcastMetadataDelimiter not found + } + + String targetMeshName = firstTransmission.substring(0, metadataEndIndex); + + if (!targetMeshName.isEmpty() && meshInstance.getMeshName() != targetMeshName) { + return false; // Broadcast is for another mesh network + } else { + // Remove metadata from message and mark as accepted broadcast. + // Note that when you modify firstTransmission it is best to avoid using substring or other String methods that rely on null values for String length determination. + // Otherwise your broadcasts cannot include null values in the message bytes. + firstTransmission.remove(0, metadataEndIndex + 1); + return true; + } +} + +/** + Once passed to the setTransmissionOutcomesUpdateHook method of the ESP-NOW backend, + this function will be called after each update of the latestTransmissionOutcomes vector during attemptTransmission. + (which happens after each individual transmission has finished) + + Example use cases is modifying getMessage() between transmissions, or aborting attemptTransmission before all nodes in the connectionQueue have been contacted. + + @param meshInstance The MeshBackendBase instance that called the function. + + @return True if attemptTransmission should continue with the next entry in the connectionQueue. False if attemptTransmission should stop. +*/ +bool exampleTransmissionOutcomesUpdateHook(MeshBackendBase &meshInstance) { + // Currently this is exactly the same as the default hook, but you can modify it to alter the behaviour of attemptTransmission. + + (void)meshInstance; // This is useful to remove a "unused parameter" compiler warning. Does nothing else. + + return true; +} + +/** + Once passed to the setResponseTransmittedHook method of the ESP-NOW backend, + this function will be called after each attempted ESP-NOW response transmission. + In case of a successful response transmission, this happens just before the response is removed from the waiting list. + Only the hook of the EspnowMeshBackend instance that is getEspnowRequestManager() will be called. + + @param transmissionSuccessful True if the response was transmitted successfully. False otherwise. + @param response The sent response. + @param recipientMac The MAC address the response was sent to. + @param responseIndex The index of the response in the waiting list. + @param meshInstance The EspnowMeshBackend instance that called the function. + + @return True if the response transmission process should continue with the next response in the waiting list. + False if the response transmission process should stop once processing of the just sent response is complete. +*/ +bool exampleResponseTransmittedHook(bool transmissionSuccessful, const String &response, const uint8_t *recipientMac, uint32_t responseIndex, EspnowMeshBackend &meshInstance) { + // Currently this is exactly the same as the default hook, but you can modify it to alter the behaviour of sendEspnowResponses. + + (void)transmissionSuccessful; // This is useful to remove a "unused parameter" compiler warning. Does nothing else. + (void)response; + (void)recipientMac; + (void)responseIndex; + (void)meshInstance; + + return true; +} + +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); + + 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 encrypted connection 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.setEspnowEncryptedConnectionKey(espnowEncryptedConnectionKey); + + // Makes it possible to find the node through scans, makes it possible to recover from an encrypted connection where only the other node is encrypted, and also makes it possible to receive broadcast transmissions. + // 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 the multi-recipient versions of espnowNode.attemptTransmission and espnowNode.attemptAutoEncryptingTransmission will replace the stored message with whatever message is transmitted. + // Also note that the maximum allowed number of ASCII characters in a ESP-NOW message is given by EspnowMeshBackend::getMaxMessageLength(). + espnowNode.setMessage(String(F("Hello world request #")) + String(requestNumber) + F(" from ") + espnowNode.getMeshName() + espnowNode.getNodeID() + String('.')); + + espnowNode.setTransmissionOutcomesUpdateHook(exampleTransmissionOutcomesUpdateHook); + espnowNode.setResponseTransmittedHook(exampleResponseTransmittedHook); + + // In addition to using encrypted ESP-NOW connections the framework can also send automatically encrypted messages (AEAD) over both encrypted and unencrypted connections. + // Using AEAD will only encrypt the message content, not the transmission metadata. + // The AEAD encryption does not require any pairing, and is thus faster for single messages than establishing a new encrypted connection before transfer. + // AEAD encryption also works with ESP-NOW broadcasts and supports an unlimited number of nodes, which is not true for encrypted connections. + // Encrypted ESP-NOW connections do however come with built in replay attack protection, which is not provided by the framework when using AEAD encryption, + // and allow EspnowProtocolInterpreter::aeadMetadataSize extra message bytes per transmission. + // Transmissions via encrypted connections are also slightly faster than via AEAD once a connection has been established. + // + // Uncomment the lines below to use automatic AEAD encryption/decryption of messages sent/received. + // All nodes this node wishes to communicate with must then also use encrypted messages with the same getEspnowMessageEncryptionKey(), or messages will not be accepted. + // Note that using AEAD encrypted messages will reduce the number of message bytes that can be transmitted. + //espnowNode.setEspnowMessageEncryptionKey(F("ChangeThisKeySeed_TODO")); // The message encryption key should always be set manually. Otherwise a default key (all zeroes) is used. + //espnowNode.setUseEncryptedMessages(true); +} + +int32_t timeOfLastScan = -10000; +void loop() { + // The performEspnowMaintenance() 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 performEspnowMaintenance will likely cause the method to take longer to complete, so plan accordingly. + + //Should not be used inside responseHandler, requestHandler, networkFilter or broadcastFilter callbacks since performEspnowMaintenance() can alter the ESP-NOW state. + EspnowMeshBackend::performEspnowMaintenance(); + + if (millis() - timeOfLastScan > 10000) { // Give other nodes some time to connect between data transfers. + Serial.println(F("\nPerforming unencrypted ESP-NOW transmissions.")); + + uint32_t startTime = millis(); + espnowNode.attemptTransmission(espnowNode.getMessage()); + Serial.println(String(F("Scan and ")) + String(espnowNode.latestTransmissionOutcomes().size()) + F(" transmissions done in ") + String(millis() - startTime) + F(" ms.")); + + timeOfLastScan = millis(); + + // Wait for response. espnowDelay continuously calls performEspnowMaintenance() so we will respond to ESP-NOW request while waiting. + // Should not be used inside responseHandler, requestHandler, networkFilter or broadcastFilter callbacks since performEspnowMaintenance() can alter the ESP-NOW state. + espnowDelay(100); + + // One way to check how attemptTransmission worked out + if (espnowNode.latestTransmissionSuccessful()) { + Serial.println(F("Transmission successful.")); + } + + // Another way to check how attemptTransmission worked out + if (espnowNode.latestTransmissionOutcomes().empty()) { + Serial.println(F("No mesh AP found.")); + } else { + for (TransmissionOutcome &transmissionOutcome : espnowNode.latestTransmissionOutcomes()) { + if (transmissionOutcome.transmissionStatus() == TransmissionStatusType::TRANSMISSION_FAILED) { + Serial.println(String(F("Transmission failed to mesh AP ")) + transmissionOutcome.SSID()); + } else if (transmissionOutcome.transmissionStatus() == TransmissionStatusType::CONNECTION_FAILED) { + Serial.println(String(F("Connection failed to mesh AP ")) + transmissionOutcome.SSID()); + } else if (transmissionOutcome.transmissionStatus() == TransmissionStatusType::TRANSMISSION_COMPLETE) { + // No need to do anything, transmission was successful. + } else { + Serial.println(String(F("Invalid transmission status for ")) + transmissionOutcome.SSID() + String('!')); + assert(F("Invalid transmission status returned from responseHandler!") && false); + } + } + + Serial.println(F("\nPerforming ESP-NOW broadcast.")); + + startTime = millis(); + + // Remove espnowNode.getMeshName() from the broadcastMetadata below to broadcast to all ESP-NOW nodes regardless of MeshName. + // Note that data that comes before broadcastMetadataDelimiter should not contain any broadcastMetadataDelimiter characters, + // otherwise the broadcastFilter function used in this example file will not work. + String broadcastMetadata = espnowNode.getMeshName() + String(broadcastMetadataDelimiter); + String broadcastMessage = String(F("Broadcast #")) + String(requestNumber) + F(" from ") + espnowNode.getMeshName() + espnowNode.getNodeID() + String('.'); + espnowNode.broadcast(broadcastMetadata + broadcastMessage); + Serial.println(String(F("Broadcast to all mesh nodes done in ")) + String(millis() - startTime) + F(" ms.")); + + espnowDelay(100); // Wait for responses (broadcasts can receive an unlimited number of responses, other transmissions can only receive one response). + + // If you have a data array containing null values it is possible to transmit the raw data by making the array into a multiString as shown below. + // You can use String::c_str() or String::begin() to retreive the data array later. + // Note that certain String methods such as String::substring use null values to determine String length, which means they will not work as normal with multiStrings. + uint8_t dataArray[] = {0, '\'', 0, '\'', ' ', '(', 'n', 'u', 'l', 'l', ')', ' ', 'v', 'a', 'l', 'u', 'e'}; + String espnowMessage = TypeCast::uint8ArrayToMultiString(dataArray, sizeof dataArray) + F(" from ") + espnowNode.getMeshName() + espnowNode.getNodeID() + String('.'); + Serial.println(String(F("\nTransmitting: ")) + espnowMessage); + espnowNode.attemptTransmission(espnowMessage, false); + espnowDelay(100); // Wait for response. + + Serial.println(F("\nPerforming encrypted ESP-NOW transmissions.")); + + uint8_t targetBSSID[6] {0}; + + // We can create encrypted connections to individual nodes so that all ESP-NOW communication with the node will be encrypted. + if (espnowNode.constConnectionQueue()[0].getBSSID(targetBSSID) && espnowNode.requestEncryptedConnection(targetBSSID) == EncryptedConnectionStatus::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 = TypeCast::macToString(targetBSSID); + + Serial.println(String(F("Encrypted ESP-NOW connection with ")) + peerMac + F(" established!")); + + // Making a transmission now will cause messages to targetBSSID to be encrypted. + String espnowMessage = String(F("This message is encrypted only when received by node ")) + peerMac; + Serial.println(String(F("\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(targetBSSID); + + Serial.println(); + // We can remove an encrypted connection like so. + espnowNode.removeEncryptedConnection(targetBSSID); + + // 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 = String(F("This message is no longer encrypted when received by node ")) + peerMac; + Serial.println(String(F("\nTransmitting: ")) + espnowMessage); + espnowNode.attemptTransmission(espnowMessage, false); + espnowDelay(100); // Wait for response. + Serial.println(F("Cannot read the encrypted response...")); + + // Let's re-add our stored connection so we can communicate properly with targetBSSID again! + espnowNode.addEncryptedConnection(serializedEncryptedConnection); + + espnowMessage = String(F("This message is once again encrypted when received by node ")) + peerMac; + Serial.println(String(F("\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. + EncryptedConnectionRemovalOutcome removalOutcome = espnowNode.requestEncryptedConnectionRemoval(targetBSSID); + if (removalOutcome == EncryptedConnectionRemovalOutcome::REMOVAL_SUCCEEDED) { + Serial.println(peerMac + F(" is no longer encrypted!")); + + espnowMessage = String(F("This message is only received by node ")) + peerMac + F(". Transmitting in this way will not change the transmission state of the sender."); + Serial.println(String(F("Transmitting: ")) + espnowMessage); + espnowNode.attemptTransmission(espnowMessage, EspnowNetworkInfo(targetBSSID)); + espnowDelay(100); // Wait for response. + + Serial.println(); + + // Of course, we can also just create a temporary encrypted connection that will remove itself once its duration has passed. + if (espnowNode.requestTemporaryEncryptedConnection(targetBSSID, 1000) == EncryptedConnectionStatus::CONNECTION_ESTABLISHED) { + espnowDelay(42); + uint32_t remainingDuration = 0; + EspnowMeshBackend::getConnectionInfo(targetBSSID, &remainingDuration); + + espnowMessage = String(F("Messages this node sends to ")) + peerMac + F(" will be encrypted for ") + String(remainingDuration) + F(" ms more."); + Serial.println(String(F("\nTransmitting: ")) + espnowMessage); + espnowNode.attemptTransmission(espnowMessage, false); + + EspnowMeshBackend::getConnectionInfo(targetBSSID, &remainingDuration); + espnowDelay(remainingDuration + 100); + + espnowMessage = String(F("Due to encrypted connection expiration, this message is no longer encrypted when received by node ")) + peerMac; + Serial.println(String(F("\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. + // This can be remedied via the requestPermanentConnections argument, though it must be noted that the maximum number of encrypted connections supported at a time is 6. + espnowMessage = F("This message is always encrypted, regardless of receiver."); + Serial.println(String(F("\nTransmitting: ")) + espnowMessage); + espnowNode.attemptAutoEncryptingTransmission(espnowMessage); + espnowDelay(100); // Wait for response. + } else { + Serial.println(String(F("Ooops! Encrypted connection removal failed. Status: ")) + String(static_cast(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(F("\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) + F(" from ") + + espnowNode.getMeshName() + espnowNode.getNodeID() + String('.')); + } + + Serial.println(); + } +} diff --git a/libraries/ESP8266WiFiMesh/examples/HelloMesh/HelloMesh.ino b/libraries/ESP8266WiFiMesh/examples/HelloMesh/HelloMesh.ino index cdd3e6d5c9..3ae8567e16 100644 --- a/libraries/ESP8266WiFiMesh/examples/HelloMesh/HelloMesh.ino +++ b/libraries/ESP8266WiFiMesh/examples/HelloMesh/HelloMesh.ino @@ -1,7 +1,21 @@ +/** + This example makes every node broadcast their AP MAC to the rest of the network during the first 28 seconds, as long as the node thinks it has the highest AP MAC in the network. + Once 28 seconds have passed, the node that has the highest AP MAC will start broadcasting benchmark messages, which will allow you to see how many messages are lost at the other nodes. + If you have an onboard LED on your ESP8266 it is recommended that you change the useLED variable below to true. + That way you will get instant confirmation of the mesh communication without checking the Serial Monitor. + + If you want to experiment with reducing error rates you can use the mesh method "void setBroadcastReceptionRedundancy(uint8_t redundancy);" (default 2) at the cost of more RAM. + Or "floodingMesh.getEspnowMeshBackend().setBroadcastTransmissionRedundancy(uint8_t redundancy)" (default 1) at the cost of longer transmission times. +*/ + +#define ESP8266WIFIMESH_DISABLE_COMPATIBILITY // Excludes redundant compatibility code. TODO: Should be used for new code until the compatibility code is removed with release 3.0.0 of the Arduino core. + #include -#include #include #include +#include + +namespace TypeCast = MeshTypeConversionFunctions; /** NOTE: Although we could define the strings below as normal String variables, @@ -14,84 +28,95 @@ https://github.com/esp8266/Arduino/issues/1143 https://arduino-esp8266.readthedocs.io/en/latest/PROGMEM.html */ -const char exampleMeshName[] PROGMEM = "MeshNode_"; -const char exampleWiFiPassword[] PROGMEM = "ChangeThisWiFiPassword_TODO"; +constexpr char exampleMeshName[] PROGMEM = "MeshNode_"; // The name of the mesh network. Used as prefix for the node SSID and to find other network nodes in the example networkFilter and broadcastFilter functions below. +constexpr char exampleWiFiPassword[] PROGMEM = "ChangeThisWiFiPassword_TODO"; // Note: " is an illegal character. The password has to be min 8 and max 64 characters long, otherwise an AP which uses it will not be found during scans. -unsigned int requestNumber = 0; -unsigned int responseNumber = 0; +// A custom encryption key is required when using encrypted ESP-NOW transmissions. There is always a default Kok set, but it can be replaced if desired. +// All ESP-NOW keys below must match in an encrypted connection pair for encrypted communication to be possible. +// Note that it is also possible to use Strings as key seeds instead of arrays. +uint8_t espnowEncryptedConnectionKey[16] = {0x33, 0x44, 0x33, 0x44, 0x33, 0x44, 0x33, 0x44, // This is the key for encrypting transmissions of encrypted connections. + 0x33, 0x44, 0x33, 0x44, 0x33, 0x44, 0x32, 0x11 + }; +uint8_t espnowHashKey[16] = {0xEF, 0x44, 0x33, 0x0C, 0x33, 0x44, 0xFE, 0x44, // This is the secret key used for HMAC during encrypted connection requests. + 0x33, 0x44, 0x33, 0xB0, 0x33, 0x44, 0x32, 0xAD + }; -String manageRequest(const String &request, ESP8266WiFiMesh &meshInstance); -transmission_status_t manageResponse(const String &response, ESP8266WiFiMesh &meshInstance); -void networkFilter(int numberOfNetworks, ESP8266WiFiMesh &meshInstance); +bool meshMessageHandler(String &message, FloodingMesh &meshInstance); /* Create the mesh node object */ -ESP8266WiFiMesh meshNode = ESP8266WiFiMesh(manageRequest, manageResponse, networkFilter, FPSTR(exampleWiFiPassword), FPSTR(exampleMeshName), "", true); +FloodingMesh floodingMesh = FloodingMesh(meshMessageHandler, FPSTR(exampleWiFiPassword), espnowEncryptedConnectionKey, espnowHashKey, FPSTR(exampleMeshName), TypeCast::uint64ToString(ESP.getChipId()), true); -/** - Callback for when other nodes send you a request +bool theOne = true; +String theOneMac; - @param request The request string received from another node in the mesh - @param meshInstance The ESP8266WiFiMesh instance that called the function. - @returns The string to send back to the other node -*/ -String manageRequest(const String &request, ESP8266WiFiMesh &meshInstance) { - // We do not store strings in flash (via F()) in this function. - // The reason is that the other node will be waiting for our response, - // so keeping the strings in RAM will give a (small) improvement in response time. - // Of course, it is advised to adjust this approach based on RAM requirements. - - /* Print out received message */ - Serial.print("Request received: "); - Serial.println(request); - - /* return a string to send back */ - return ("Hello world response #" + String(responseNumber++) + " from " + meshInstance.getMeshName() + meshInstance.getNodeID() + "."); -} +bool useLED = false; // Change this to true if you wish the onboard LED to mark The One. /** - 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 ESP8266WiFiMesh instance that called the function. - @returns The status code resulting from the response, as an int + Callback for when a message is received from the mesh network. + + @param message The message String received from the mesh. + Modifications to this String are passed on when the message is forwarded from this node to other nodes. + However, the forwarded message will still use the same messageID. + Thus it will not be sent to nodes that have already received this messageID. + If you want to send a new message to the whole network, use a new broadcast from within the loop() instead. + @param meshInstance The FloodingMesh instance that received the message. + @return True if this node should forward the received message to other nodes. False otherwise. */ -transmission_status_t manageResponse(const String &response, ESP8266WiFiMesh &meshInstance) { - transmission_status_t statusCode = TS_TRANSMISSION_COMPLETE; +bool meshMessageHandler(String &message, FloodingMesh &meshInstance) { + int32_t delimiterIndex = message.indexOf(meshInstance.metadataDelimiter()); + if (delimiterIndex == 0) { + Serial.print(String(F("Message received from STA MAC ")) + meshInstance.getEspnowMeshBackend().getSenderMac() + F(": ")); + Serial.println(message.substring(2, 102)); + + String potentialMac = message.substring(2, 14); + + if (potentialMac >= theOneMac) { + if (potentialMac > theOneMac) { + theOne = false; + theOneMac = potentialMac; + } - /* Print out received message */ - Serial.print(F("Request sent: ")); - Serial.println(meshInstance.getMessage()); - Serial.print(F("Response received: ")); - Serial.println(response); + if (useLED && !theOne) { + bool ledState = message.charAt(1) == '1'; + digitalWrite(LED_BUILTIN, ledState); // Turn LED on/off (LED_BUILTIN is active low) + } - // Our last request got a response, so time to create a new request. - meshInstance.setMessage(String(F("Hello world request #")) + String(++requestNumber) + String(F(" from ")) - + meshInstance.getMeshName() + meshInstance.getNodeID() + String(F("."))); + return true; + } else { + return false; + } + } else if (delimiterIndex > 0) { + if (meshInstance.getOriginMac() == theOneMac) { + uint32_t totalBroadcasts = strtoul(message.c_str(), nullptr, 0); // strtoul stops reading input when an invalid character is discovered. - // (void)meshInstance; // This is useful to remove a "unused parameter" compiler warning. Does nothing else. - return statusCode; -} + // Static variables are only initialized once. + static uint32_t firstBroadcast = totalBroadcasts; -/** - Callback used to decide which networks to connect to once a WiFi scan has been completed. + if (totalBroadcasts - firstBroadcast >= 100) { // Wait a little to avoid start-up glitches + static uint32_t missedBroadcasts = 1; // Starting at one to compensate for initial -1 below. + static uint32_t previousTotalBroadcasts = totalBroadcasts; + static uint32_t totalReceivedBroadcasts = 0; + totalReceivedBroadcasts++; - @param numberOfNetworks The number of networks found in the WiFi scan. - @param meshInstance The ESP8266WiFiMesh instance that called the function. -*/ -void networkFilter(int numberOfNetworks, ESP8266WiFiMesh &meshInstance) { - for (int networkIndex = 0; networkIndex < numberOfNetworks; ++networkIndex) { - String currentSSID = WiFi.SSID(networkIndex); - int meshNameIndex = currentSSID.indexOf(meshInstance.getMeshName()); + missedBroadcasts += totalBroadcasts - previousTotalBroadcasts - 1; // We expect an increment by 1. + previousTotalBroadcasts = totalBroadcasts; - /* Connect to any _suitable_ APs which contain meshInstance.getMeshName() */ - if (meshNameIndex >= 0) { - uint64_t targetNodeID = stringToUint64(currentSSID.substring(meshNameIndex + meshInstance.getMeshName().length())); - - if (targetNodeID < stringToUint64(meshInstance.getNodeID())) { - ESP8266WiFiMesh::connectionQueue.push_back(NetworkInfo(networkIndex)); + if (totalReceivedBroadcasts % 50 == 0) { + Serial.println(String(F("missed/total: ")) + String(missedBroadcasts) + '/' + String(totalReceivedBroadcasts)); + } + if (totalReceivedBroadcasts % 500 == 0) { + Serial.println(String(F("Benchmark message: ")) + message.substring(0, 100)); + } } } + } else { + // Only show first 100 characters because printing a large String takes a lot of time, which is a bad thing for a callback function. + // If you need to print the whole String it is better to store it and print it in the loop() later. + Serial.print(String(F("Message with origin ")) + meshInstance.getOriginMac() + F(" received: ")); + Serial.println(message.substring(0, 100)); } + + return true; } void setup() { @@ -100,63 +125,73 @@ void setup() { WiFi.persistent(false); Serial.begin(115200); - delay(50); // Wait for Serial. - - //yield(); // Use this if you don't want to wait for Serial. - - // 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 to speed up connection times.\n" - "Use the setStaticIP method as shown in this example to enable this.\n" - "Ensure that nodes connecting to the same AP have distinct static IP:s.\n" - "Also, remember to change the default mesh network password!\n\n")); + Serial.println(F("If you have an onboard LED on your ESP8266 it is recommended that you change the useLED variable to true.\n" + "That way you will get instant confirmation of the mesh communication.\n" + "Also, remember to change the default mesh network password and ESP-NOW keys!\n")); Serial.println(F("Setting up mesh node...")); - /* Initialise the mesh node */ - meshNode.begin(); - meshNode.activateAP(); // Each AP requires a separate server port. - meshNode.setStaticIP(IPAddress(192, 168, 4, 22)); // Activate static IP mode to speed up connection times. + floodingMesh.begin(); + floodingMesh.activateAP(); // Required to receive messages + + uint8_t apMacArray[6] {0}; + theOneMac = TypeCast::macToString(WiFi.softAPmacAddress(apMacArray)); + + if (useLED) { + pinMode(LED_BUILTIN, OUTPUT); // Initialize the LED_BUILTIN pin as an output + digitalWrite(LED_BUILTIN, LOW); // Turn LED on (LED_BUILTIN is active low) + } + + // Uncomment the lines below to use automatic AEAD encryption/decryption of messages sent/received via broadcast() and encryptedBroadcast(). + // The main benefit of AEAD encryption is that it can be used with normal broadcasts (which are substantially faster than encryptedBroadcasts). + // The main drawbacks are that AEAD only encrypts the message data (not transmission metadata), transfers less data per message and lacks replay attack protection. + // When using AEAD, potential replay attacks must thus be handled manually. + //floodingMesh.getEspnowMeshBackend().setEspnowMessageEncryptionKey(F("ChangeThisKeySeed_TODO")); // The message encryption key should always be set manually. Otherwise a default key (all zeroes) is used. + //floodingMesh.getEspnowMeshBackend().setUseEncryptedMessages(true); + + floodingMeshDelay(5000); // Give some time for user to start the nodes } -int32_t timeOfLastScan = -10000; +int32_t timeOfLastProclamation = -10000; void loop() { - if (millis() - timeOfLastScan > 3000 // Give other nodes some time to connect between data transfers. - || (WiFi.status() != WL_CONNECTED && millis() - timeOfLastScan > 2000)) { // Scan for networks with two second intervals when not already connected. - String request = String(F("Hello world request #")) + String(requestNumber) + String(F(" from ")) + meshNode.getMeshName() + meshNode.getNodeID() + String(F(".")); - meshNode.attemptTransmission(request, false); - timeOfLastScan = millis(); - - // One way to check how attemptTransmission worked out - if (ESP8266WiFiMesh::latestTransmissionSuccessful()) { - Serial.println(F("Transmission successful.")); + static bool ledState = 1; + static uint32_t benchmarkCount = 0; + static uint32_t loopStart = millis(); + + // The floodingMeshDelay() method performs all the background operations for the FloodingMesh (via FloodingMesh::performMeshMaintenance()). + // It is recommended to place one of these methods in the beginning of the loop(), unless there is a need to put them elsewhere. + // Among other things, the method cleans up old ESP-NOW log entries (freeing up RAM) and forwards received mesh messages. + // Note that depending on the amount of messages to forward and their length, this method can take tens or even hundreds of milliseconds to complete. + // More intense transmission activity and less frequent calls to performMeshMaintenance will likely cause the method to take longer to complete, so plan accordingly. + // The maintenance methods should not be used inside the meshMessageHandler callback, since they can alter the mesh node state. The framework will alert you during runtime if you make this mistake. + floodingMeshDelay(1); + + // If you wish to transmit only to a single node, try using one of the following methods (requires the node to be within range and know the MAC of the recipient): + // Unencrypted: TransmissionStatusType floodingMesh.getEspnowMeshBackend().attemptTransmission(message, EspnowNetworkInfo(recipientMac)); + // Encrypted (slow): floodingMesh.getEspnowMeshBackend().attemptAutoEncryptingTransmission(message, EspnowNetworkInfo(recipientMac)); + + if (theOne) { + if (millis() - timeOfLastProclamation > 10000) { + uint32_t startTime = millis(); + ledState = ledState ^ bool(benchmarkCount); // Make other nodes' LEDs alternate between on and off once benchmarking begins. + + // Note: The maximum length of an unencrypted broadcast message is given by floodingMesh.maxUnencryptedMessageLength(). It is around 670 bytes by default. + floodingMesh.broadcast(String(floodingMesh.metadataDelimiter()) + String(ledState) + theOneMac + F(" is The One.")); + Serial.println(String(F("Proclamation broadcast done in ")) + String(millis() - startTime) + F(" ms.")); + + timeOfLastProclamation = millis(); + floodingMeshDelay(20); } - // Another way to check how attemptTransmission worked out - if (ESP8266WiFiMesh::latestTransmissionOutcomes.empty()) { - Serial.println(F("No mesh AP found.")); - } else { - for (TransmissionResult &transmissionResult : ESP8266WiFiMesh::latestTransmissionOutcomes) { - if (transmissionResult.transmissionStatus == TS_TRANSMISSION_FAILED) { - Serial.println(String(F("Transmission failed to mesh AP ")) + transmissionResult.SSID); - } else if (transmissionResult.transmissionStatus == TS_CONNECTION_FAILED) { - Serial.println(String(F("Connection failed to mesh AP ")) + transmissionResult.SSID); - } else if (transmissionResult.transmissionStatus == TS_TRANSMISSION_COMPLETE) { - // No need to do anything, transmission was successful. - } else { - Serial.println(String(F("Invalid transmission status for ")) + transmissionResult.SSID + String(F("!"))); - assert(F("Invalid transmission status returned from responseHandler!") && false); - } - } + if (millis() - loopStart > 23000) { // Start benchmarking the mesh once three proclamations have been made + uint32_t startTime = millis(); + floodingMesh.broadcast(String(benchmarkCount++) + String(floodingMesh.metadataDelimiter()) + F(": Not a spoon in sight.")); + Serial.println(String(F("Benchmark broadcast done in ")) + String(millis() - startTime) + F(" ms.")); + floodingMeshDelay(20); } - Serial.println(); - } else { - /* Accept any incoming connections */ - meshNode.acceptRequest(); } } diff --git a/libraries/ESP8266WiFiMesh/examples/HelloTcpIp/HelloTcpIp.ino b/libraries/ESP8266WiFiMesh/examples/HelloTcpIp/HelloTcpIp.ino new file mode 100644 index 0000000000..661f2249cd --- /dev/null +++ b/libraries/ESP8266WiFiMesh/examples/HelloTcpIp/HelloTcpIp.ino @@ -0,0 +1,223 @@ +#define ESP8266WIFIMESH_DISABLE_COMPATIBILITY // Excludes redundant compatibility code. TODO: Should be used for new code until the compatibility code is removed with release 3.0.0 of the Arduino core. + +#include +#include +#include +#include +#include + +namespace TypeCast = MeshTypeConversionFunctions; + +/** + 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 +*/ +constexpr char exampleMeshName[] PROGMEM = "MeshNode_"; +constexpr char exampleWiFiPassword[] PROGMEM = "ChangeThisWiFiPassword_TODO"; // Note: " is an illegal character. The password has to be min 8 and max 64 characters long, otherwise an AP which uses it will not be found during scans. + +unsigned int requestNumber = 0; +unsigned int responseNumber = 0; + +String manageRequest(const String &request, MeshBackendBase &meshInstance); +TransmissionStatusType manageResponse(const String &response, MeshBackendBase &meshInstance); +void networkFilter(int numberOfNetworks, MeshBackendBase &meshInstance); + +/* Create the mesh node object */ +TcpIpMeshBackend tcpIpNode = TcpIpMeshBackend(manageRequest, manageResponse, networkFilter, FPSTR(exampleWiFiPassword), FPSTR(exampleMeshName), TypeCast::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. + @return 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) { + // To get the actual class of the polymorphic meshInstance, do as follows (meshBackendCast replaces dynamic_cast since RTTI is disabled) + if (EspnowMeshBackend *espnowInstance = TypeCast::meshBackendCast(&meshInstance)) { + String transmissionEncrypted = espnowInstance->receivedEncryptedTransmission() ? F(", Encrypted transmission") : F(", Unencrypted transmission"); + Serial.print(String(F("ESP-NOW (")) + espnowInstance->getSenderMac() + transmissionEncrypted + F("): ")); + } else if (TcpIpMeshBackend *tcpIpInstance = TypeCast::meshBackendCast(&meshInstance)) { + (void)tcpIpInstance; // This is useful to remove a "unused parameter" compiler warning. Does nothing else. + Serial.print(F("TCP/IP: ")); + } else { + Serial.print(F("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. + // Note that request.substring will not work as expected if the String contains null values as data. + Serial.print(F("Request received: ")); + Serial.println(request.substring(0, 100)); + + /* return a string to send back */ + return (String(F("Hello world response #")) + String(responseNumber++) + F(" from ") + meshInstance.getMeshName() + meshInstance.getNodeID() + F(" with AP MAC ") + WiFi.softAPmacAddress() + String('.')); +} + +/** + Callback for when you get a response from other nodes + + @param response The response string received from another node in the mesh + @param meshInstance The MeshBackendBase instance that called the function. + @return The status code resulting from the response, as an int +*/ +TransmissionStatusType manageResponse(const String &response, MeshBackendBase &meshInstance) { + TransmissionStatusType statusCode = TransmissionStatusType::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 = TypeCast::meshBackendCast(&meshInstance)) { + String transmissionEncrypted = espnowInstance->receivedEncryptedTransmission() ? F(", Encrypted transmission") : F(", Unencrypted transmission"); + Serial.print(String(F("ESP-NOW (")) + espnowInstance->getSenderMac() + transmissionEncrypted + F("): ")); + } else if (TcpIpMeshBackend *tcpIpInstance = TypeCast::meshBackendCast(&meshInstance)) { + Serial.print(F("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->getCurrentMessage().substring(0, 100)); + } else { + Serial.print(F("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. + // Note that response.substring will not work as expected if the String contains null values as data. + 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 = TypeCast::stringToUint64(currentSSID.substring(meshNameIndex + meshInstance.getMeshName().length())); + + if (targetNodeID < TypeCast::stringToUint64(meshInstance.getNodeID())) { + if (EspnowMeshBackend *espnowInstance = TypeCast::meshBackendCast(&meshInstance)) { + espnowInstance->connectionQueue().emplace_back(networkIndex); + } else if (TcpIpMeshBackend *tcpIpInstance = TypeCast::meshBackendCast(&meshInstance)) { + tcpIpInstance->connectionQueue().emplace_back(networkIndex); + } else { + Serial.println(F("Invalid mesh backend!")); + } + } + } + } +} + +/** + Once passed to the setTransmissionOutcomesUpdateHook method of the TCP/IP backend, + this function will be called after each update of the latestTransmissionOutcomes vector during attemptTransmission. + (which happens after each individual transmission has finished) + + Example use cases is modifying getMessage() between transmissions, or aborting attemptTransmission before all nodes in the connectionQueue have been contacted. + + @param meshInstance The MeshBackendBase instance that called the function. + + @return True if attemptTransmission should continue with the next entry in the connectionQueue. False if attemptTransmission should stop. +*/ +bool exampleTransmissionOutcomesUpdateHook(MeshBackendBase &meshInstance) { + // The default hook only returns true and does nothing else. + + if (TcpIpMeshBackend *tcpIpInstance = TypeCast::meshBackendCast(&meshInstance)) { + if (tcpIpInstance->latestTransmissionOutcomes().back().transmissionStatus() == TransmissionStatusType::TRANSMISSION_COMPLETE) { + // Our last request got a response, so time to create a new request. + meshInstance.setMessage(String(F("Hello world request #")) + String(++requestNumber) + F(" from ") + + meshInstance.getMeshName() + meshInstance.getNodeID() + String('.')); + } + } else { + Serial.println(F("Invalid mesh backend!")); + } + + return true; +} + +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); + + Serial.println(); + Serial.println(); + + Serial.println(F("Note that this library can use static IP:s for the nodes to speed up connection times.\n" + "Use the setStaticIP method as shown in this example to enable this.\n" + "Ensure that nodes connecting to the same AP have distinct static IP:s.\n" + "Also, remember to change the default mesh network password!\n\n")); + + Serial.println(F("Setting up mesh node...")); + + /* Initialise the mesh node */ + tcpIpNode.begin(); + tcpIpNode.activateAP(); // Each AP requires a separate server port. + tcpIpNode.setStaticIP(IPAddress(192, 168, 4, 22)); // Activate static IP mode to speed up connection times. + + // Storing our message in the TcpIpMeshBackend instance is not required, but can be useful for organizing code, especially when using many TcpIpMeshBackend instances. + // Note that calling the multi-recipient tcpIpNode.attemptTransmission will replace the stored message with whatever message is transmitted. + tcpIpNode.setMessage(String(F("Hello world request #")) + String(requestNumber) + F(" from ") + tcpIpNode.getMeshName() + tcpIpNode.getNodeID() + String('.')); + + tcpIpNode.setTransmissionOutcomesUpdateHook(exampleTransmissionOutcomesUpdateHook); +} + +int32_t timeOfLastScan = -10000; +void loop() { + if (millis() - timeOfLastScan > 3000 // Give other nodes some time to connect between data transfers. + || (WiFi.status() != WL_CONNECTED && millis() - timeOfLastScan > 2000)) { // Scan for networks with two second intervals when not already connected. + + // attemptTransmission(message, scan, scanAllWiFiChannels, concludingDisconnect, initialDisconnect = false) + tcpIpNode.attemptTransmission(tcpIpNode.getMessage(), true, false, false); + timeOfLastScan = millis(); + + // One way to check how attemptTransmission worked out + if (tcpIpNode.latestTransmissionSuccessful()) { + Serial.println(F("Transmission successful.")); + } + + // Another way to check how attemptTransmission worked out + if (tcpIpNode.latestTransmissionOutcomes().empty()) { + Serial.println(F("No mesh AP found.")); + } else { + for (TransmissionOutcome &transmissionOutcome : tcpIpNode.latestTransmissionOutcomes()) { + if (transmissionOutcome.transmissionStatus() == TransmissionStatusType::TRANSMISSION_FAILED) { + Serial.println(String(F("Transmission failed to mesh AP ")) + transmissionOutcome.SSID()); + } else if (transmissionOutcome.transmissionStatus() == TransmissionStatusType::CONNECTION_FAILED) { + Serial.println(String(F("Connection failed to mesh AP ")) + transmissionOutcome.SSID()); + } else if (transmissionOutcome.transmissionStatus() == TransmissionStatusType::TRANSMISSION_COMPLETE) { + // No need to do anything, transmission was successful. + } else { + Serial.println(String(F("Invalid transmission status for ")) + transmissionOutcome.SSID() + String('!')); + assert(F("Invalid transmission status returned from responseHandler!") && false); + } + } + } + Serial.println(); + } else { + /* Accept any incoming connections */ + tcpIpNode.acceptRequests(); + } +} diff --git a/libraries/ESP8266WiFiMesh/keywords.txt b/libraries/ESP8266WiFiMesh/keywords.txt index 2ea9f96aad..db8d7cd3be 100644 --- a/libraries/ESP8266WiFiMesh/keywords.txt +++ b/libraries/ESP8266WiFiMesh/keywords.txt @@ -12,51 +12,96 @@ ESP8266WiFiMesh KEYWORD3 # Datatypes (KEYWORD1) ####################################### -ESP8266WiFiMesh KEYWORD1 -NetworkInfo KEYWORD1 -TransmissionResult KEYWORD1 -transmission_status_t KEYWORD1 +MeshBackendBase KEYWORD1 +MeshBackendType KEYWORD1 +requestHandlerType KEYWORD1 +responseHandlerType KEYWORD1 +networkFilterType KEYWORD1 +transmissionOutcomesUpdateHookType KEYWORD1 + +TcpIpMeshBackend KEYWORD1 + +EspnowMeshBackend KEYWORD1 +broadcastFilterType KEYWORD1 +ConnectionType KEYWORD1 +EncryptedConnectionStatus KEYWORD1 +EncryptedConnectionRemovalOutcome KEYWORD1 +responseTransmittedHookType KEYWORD1 + +FloodingMesh KEYWORD1 +messageHandlerType KEYWORD1 + +TransmissionOutcome KEYWORD1 +TransmissionStatusType KEYWORD1 + +NetworkInfoBase KEYWORD1 +TcpIpNetworkInfo KEYWORD1 +EspnowNetworkInfo KEYWORD1 ####################################### # Methods and Functions (KEYWORD2) ####################################### -connectionQueue KEYWORD2 -latestTransmissionOutcomes KEYWORD2 -latestTransmissionSuccessful KEYWORD2 +# MeshBackendBase begin KEYWORD2 activateAP KEYWORD2 deactivateAP KEYWORD2 +deactivateControlledAP KEYWORD2 restartAP KEYWORD2 getAPController KEYWORD2 isAPController KEYWORD2 setWiFiChannel KEYWORD2 getWiFiChannel KEYWORD2 +setSSID KEYWORD2 +getSSID KEYWORD2 +setSSIDPrefix KEYWORD2 +getSSIDPrefix KEYWORD2 +setSSIDRoot KEYWORD2 +getSSIDRoot KEYWORD2 +setSSIDSuffix KEYWORD2 +getSSIDSuffix KEYWORD2 setMeshName KEYWORD2 getMeshName KEYWORD2 setNodeID KEYWORD2 getNodeID KEYWORD2 -setSSID KEYWORD2 -getSSID KEYWORD2 +setMeshPassword KEYWORD2 +getMeshPassword KEYWORD2 setMessage KEYWORD2 getMessage KEYWORD2 attemptTransmission KEYWORD2 -acceptRequest KEYWORD2 -setStaticIP KEYWORD2 -getStaticIP KEYWORD2 -disableStaticIP->KEYWORD2 -uint64ToString KEYWORD2 -stringToUint64 KEYWORD2 setRequestHandler KEYWORD2 getRequestHandler KEYWORD2 setResponseHandler KEYWORD2 getResponseHandler KEYWORD2 setNetworkFilter KEYWORD2 getNetworkFilter KEYWORD2 +setTransmissionOutcomesUpdateHook KEYWORD2 +getTransmissionOutcomesUpdateHook KEYWORD2 setScanHidden KEYWORD2 getScanHidden KEYWORD2 setAPHidden KEYWORD2 getAPHidden KEYWORD2 +setVerboseModeState KEYWORD2 +verboseMode KEYWORD2 +verboseModePrint KEYWORD2 +setPrintWarnings KEYWORD2 +printWarnings KEYWORD2 +warningPrint KEYWORD2 +getClassType KEYWORD2 +printAPInfo KEYWORD2 + +# TcpIpMeshBackend +connectionQueue KEYWORD2 +constConnectionQueue KEYWORD2 +latestTransmissionOutcomes KEYWORD2 +latestTransmissionSuccessful KEYWORD2 +acceptRequests KEYWORD2 +getCurrentMessage KEYWORD2 +setStaticIP KEYWORD2 +getStaticIP KEYWORD2 +disableStaticIP->KEYWORD2 +setServerPort KEYWORD2 +getServerPort KEYWORD2 setMaxAPStations KEYWORD2 getMaxAPStations KEYWORD2 setConnectionAttemptTimeout KEYWORD2 @@ -66,10 +111,152 @@ getStationModeTimeout KEYWORD2 setAPModeTimeout KEYWORD2 getAPModeTimeout KEYWORD2 +# EspnowMeshBackend +espnowDelay KEYWORD2 +performEspnowMaintenance KEYWORD2 +criticalHeapLevel KEYWORD2 +setCriticalHeapLevelBuffer KEYWORD2 +criticalHeapLevelBuffer KEYWORD2 +deactivateEspnow KEYWORD2 +attemptAutoEncryptingTransmission KEYWORD2 +broadcast KEYWORD2 +setBroadcastTransmissionRedundancy KEYWORD2 +getBroadcastTransmissionRedundancy KEYWORD2 +setEspnowRequestManager KEYWORD2 +getEspnowRequestManager KEYWORD2 +isEspnowRequestManager KEYWORD2 +setLogEntryLifetimeMs KEYWORD2 +logEntryLifetimeMs KEYWORD2 +setBroadcastResponseTimeoutMs KEYWORD2 +broadcastResponseTimeoutMs KEYWORD2 +setEspnowEncryptedConnectionKey KEYWORD2 +getEspnowEncryptedConnectionKey KEYWORD2 +setEspnowEncryptionKok KEYWORD2 +getEspnowEncryptionKok KEYWORD2 +setEspnowHashKey KEYWORD2 +getEspnowHashKey KEYWORD2 +setUseEncryptedMessages KEYWORD2 +useEncryptedMessages KEYWORD2 +setEspnowMessageEncryptionKey KEYWORD2 +getEspnowMessageEncryptionKey KEYWORD2 +getMaxMessageBytesPerTransmission KEYWORD2 +setMaxTransmissionsPerMessage KEYWORD2 +getMaxTransmissionsPerMessage KEYWORD2 +getMaxMessageLength KEYWORD2 +staticVerboseMode KEYWORD2 +staticVerboseModePrint KEYWORD2 +getScheduledResponseMessage KEYWORD2 +getScheduledResponseRecipient KEYWORD2 +numberOfScheduledResponses KEYWORD2 +clearAllScheduledResponses KEYWORD2 +deleteScheduledResponsesByRecipient KEYWORD2 +setEspnowTransmissionTimeout KEYWORD2 +getEspnowTransmissionTimeout KEYWORD2 +setEspnowRetransmissionInterval KEYWORD2 +getEspnowRetransmissionInterval KEYWORD2 +setEncryptionRequestTimeout KEYWORD2 +getEncryptionRequestTimeout KEYWORD2 +setAutoEncryptionDuration KEYWORD2 +getAutoEncryptionDuration KEYWORD2 +setBroadcastFilter KEYWORD2 +getBroadcastFilter KEYWORD2 +setResponseTransmittedHook KEYWORD2 +getResponseTransmittedHook KEYWORD2 +getSenderMac KEYWORD2 +getSenderAPMac KEYWORD2 +receivedEncryptedTransmission KEYWORD2 +addUnencryptedConnection KEYWORD2 +addEncryptedConnection KEYWORD2 +addTemporaryEncryptedConnection KEYWORD2 +requestEncryptedConnection KEYWORD2 +requestTemporaryEncryptedConnection KEYWORD2 +requestFlexibleTemporaryEncryptedConnection KEYWORD2 +removeEncryptedConnection KEYWORD2 +requestEncryptedConnectionRemoval KEYWORD2 +setAcceptsUnverifiedRequests KEYWORD2 +acceptsUnverifiedRequests KEYWORD2 +setEncryptedConnectionsSoftLimit KEYWORD2 +encryptedConnectionsSoftLimit KEYWORD2 +numberOfEncryptedConnections KEYWORD2 +getEncryptedMac KEYWORD2 +serializeUnencryptedConnection KEYWORD2 +serializeEncryptedConnection KEYWORD2 +serializeEncryptedConnection KEYWORD2 +getConnectionInfo KEYWORD2 +getTransmissionFailRate KEYWORD2 +resetTransmissionFailRate KEYWORD2 + +# FloodingMesh +floodingMeshDelay KEYWORD2 +performMeshMaintenance KEYWORD2 +performMeshInstanceMaintenance KEYWORD2 +serializeMeshState KEYWORD2 +setBroadcastReceptionRedundancy KEYWORD2 +getBroadcastReceptionRedundancy KEYWORD2 +encryptedBroadcast KEYWORD2 +clearMessageLogs KEYWORD2 +clearForwardingBacklog KEYWORD2 +setMessageHandler KEYWORD2 +getMessageHandler KEYWORD2 +getOriginMac KEYWORD2 +setMessageLogSize KEYWORD2 +messageLogSize KEYWORD2 +maxUnencryptedMessageLength KEYWORD2 +maxEncryptedMessageLength KEYWORD2 +setMetadataDelimiter KEYWORD2 +metadataDelimiter KEYWORD2 +getEspnowMeshBackend KEYWORD2 +getEspnowMeshBackendConst KEYWORD2 +restoreDefaultRequestHandler KEYWORD2 +restoreDefaultResponseHandler KEYWORD2 +restoreDefaultNetworkFilter KEYWORD2 +restoreDefaultBroadcastFilter KEYWORD2 +restoreDefaultTransmissionOutcomesUpdateHook KEYWORD2 +restoreDefaultResponseTransmittedHook KEYWORD2 + +# NetworkInfoBase +setBSSID KEYWORD2 +getBSSID KEYWORD2 +setWifiChannel KEYWORD2 +wifiChannel KEYWORD2 +setEncryptionType KEYWORD2 +setRSSI KEYWORD2 +setIsHidden KEYWORD2 + +# TransmissionOutcome +setTransmissionStatus KEYWORD2 +transmissionStatus KEYWORD2 + +# TypeConversionFunctions +uint64ToString KEYWORD2 +stringToUint64 KEYWORD2 +uint8ArrayToHexString KEYWORD2 +hexStringToUint8Array KEYWORD2 +uint8ArrayToMultiString KEYWORD2 +bufferedUint8ArrayToMultiString KEYWORD2 +macToString KEYWORD2 +stringToMac KEYWORD2 +macToUint64 KEYWORD2 +uint64ToMac KEYWORD2 +uint64ToUint8Array KEYWORD2 +uint8ArrayToUint64 KEYWORD2 +meshBackendCast KEYWORD2 + +# UtilityFunctions +macEqual KEYWORD2 +randomUint64 KEYWORD2 +getMapValue KEYWORD2 + ####################################### # Constants (LITERAL1) ####################################### emptyIP LITERAL1 + NETWORK_INFO_DEFAULT_INT LITERAL1 -WIFI_MESH_EMPTY_STRING LITERAL1 +defaultBSSID LITERAL1 +defaultEncryptionType LITERAL1 +defaultIsHidden LITERAL1 +defaultSSID LITERAL1 +defaultWifiChannel LITERAL1 +defaultRSSI LITERAL1 diff --git a/libraries/ESP8266WiFiMesh/library.properties b/libraries/ESP8266WiFiMesh/library.properties index ddfea96a58..2ff47b0ead 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/CompatibilityLayer.cpp b/libraries/ESP8266WiFiMesh/src/CompatibilityLayer.cpp index fd926a9351..0bb480acd0 100644 --- a/libraries/ESP8266WiFiMesh/src/CompatibilityLayer.cpp +++ b/libraries/ESP8266WiFiMesh/src/CompatibilityLayer.cpp @@ -27,9 +27,9 @@ /******************************************************************************************** * NOTE! * -* All method signatures in this file are deprecated and will be removed in core version 2.5.0. +* All method signatures in this file are deprecated and will be removed in core version 3.0.0. * If you are still using these methods, please consider migrating to the new API shown in -* the ESP8266WiFiMesh.h source file. +* the EspnowMeshBackend.h or TcpIpMeshBackend.h source files. * * TODO: delete this file. ********************************************************************************************/ diff --git a/libraries/ESP8266WiFiMesh/src/ConditionalPrinter.cpp b/libraries/ESP8266WiFiMesh/src/ConditionalPrinter.cpp new file mode 100644 index 0000000000..15b08195b8 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/ConditionalPrinter.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2020 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 "TypeConversionFunctions.h" +#include "MeshBackendBase.h" +#include "EspnowMeshBackend.h" + +namespace +{ + bool _staticVerboseMode = false; + bool _printWarnings = true; +} + +void ConditionalPrinter::setVerboseModeState(const bool enabled) {_verboseMode = enabled;} +bool ConditionalPrinter::verboseMode() const {return _verboseMode;} + +void ConditionalPrinter::verboseModePrint(const String &stringToPrint, const bool newline) const +{ + if(verboseMode()) + { + if(newline) + Serial.println(stringToPrint); + else + Serial.print(stringToPrint); + } +} + +void ConditionalPrinter::setStaticVerboseModeState(const bool enabled) {_staticVerboseMode = enabled;}; +bool ConditionalPrinter::staticVerboseMode() {return _staticVerboseMode;} + +void ConditionalPrinter::staticVerboseModePrint(const String &stringToPrint, const bool newline) +{ + if(staticVerboseMode()) + { + if(newline) + Serial.println(stringToPrint); + else + Serial.print(stringToPrint); + } +} + +void ConditionalPrinter::setPrintWarnings(const bool printEnabled) {_printWarnings = printEnabled;} +bool ConditionalPrinter::printWarnings() {return _printWarnings;} + +void ConditionalPrinter::warningPrint(const String &stringToPrint, const bool newline) +{ + if(printWarnings()) + { + if(newline) + Serial.println(stringToPrint); + else + Serial.print(stringToPrint); + } +} diff --git a/libraries/ESP8266WiFiMesh/src/ConditionalPrinter.h b/libraries/ESP8266WiFiMesh/src/ConditionalPrinter.h new file mode 100644 index 0000000000..bf66c6652d --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/ConditionalPrinter.h @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2020 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 __CONDITIONALPRINTER_H__ +#define __CONDITIONALPRINTER_H__ + +class ConditionalPrinter +{ + +public: + + /** + * Set whether the normal events occurring in the library will be printed to Serial or not. + * + * @param enabled If true, library Serial prints are activated. + */ + void setVerboseModeState(const bool enabled); + bool verboseMode() const; + + /** + * 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, const bool newline = true) const; + + /** + * Same as verboseMode(), but used for printing from static functions. + * + * @param enabled If true, the normal events occurring in the library will be printed to Serial. + */ + static void setStaticVerboseModeState(const bool enabled); + 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, const bool newline = true); + + /** + * Set whether the warnings occurring in the library will be printed to Serial or not. On by default. + * + * @param printEnabled If true, warning Serial prints from the library are activated. + */ + static void setPrintWarnings(const 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, const bool newline = true); + +private: + + bool _verboseMode = false; + +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/ESP8266WiFiMesh.cpp b/libraries/ESP8266WiFiMesh/src/ESP8266WiFiMesh.cpp index e35cac33b9..5775fa3009 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 @@ -26,10 +48,11 @@ #include "ESP8266WiFiMesh.h" #include "TypeConversionFunctions.h" +namespace TypeCast = MeshTypeConversionFunctions; + #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,11 +74,9 @@ 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(); - - updateNetworkNames(meshName, (!nodeID.isEmpty() ? nodeID : uint64ToString(ESP.getChipId()))); + : _server(serverPort) +{ + updateNetworkNames(meshName, (!nodeID.isEmpty() ? nodeID : TypeCast::uint64ToString(ESP.getChipId()))); _requestHandler = requestHandler; _responseHandler = responseHandler; setWiFiChannel(meshWiFiChannel); @@ -99,15 +120,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 } } @@ -323,7 +339,7 @@ void ESP8266WiFiMesh::fullStop(WiFiClient &currClient) /** * Wait for a WiFiClient to transmit * - * @returns: True if the client is ready, false otherwise. + * @return: True if the client is ready, false otherwise. * */ bool ESP8266WiFiMesh::waitForClientTransmission(WiFiClient &currClient, uint32_t maxWait) @@ -351,7 +367,7 @@ bool ESP8266WiFiMesh::waitForClientTransmission(WiFiClient &currClient, uint32_t * 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. + * @return: A status code based on the outcome of the exchange. * */ transmission_status_t ESP8266WiFiMesh::exchangeInfo(WiFiClient &currClient) @@ -384,7 +400,7 @@ transmission_status_t ESP8266WiFiMesh::exchangeInfo(WiFiClient &currClient) /** * Handle data transfer process with a connected AP. * - * @returns: A status code based on the outcome of the data transfer attempt. + * @return: A status code based on the outcome of the data transfer attempt. */ transmission_status_t ESP8266WiFiMesh::attemptDataTransfer() { @@ -404,7 +420,7 @@ transmission_status_t ESP8266WiFiMesh::attemptDataTransfer() /** * 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. + * @return: A status code based on the outcome of the data transfer attempt. */ transmission_status_t ESP8266WiFiMesh::attemptDataTransferKernel() { @@ -448,32 +464,25 @@ void ESP8266WiFiMesh::initiateConnectionToAP(const String &targetSSID, int targe * @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. + * @return: A status code based on the outcome of the connection and data transfer process. * */ transmission_status_t ESP8266WiFiMesh::connectToNode(const String &targetSSID, int targetChannel, uint8_t *targetBSSID) { if(staticIPActivated && !lastSSID.isEmpty() && 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 +546,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 +558,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,12 +655,12 @@ 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 */ @@ -669,3 +674,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 ceca8f0ff4..8647502bb3 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__ @@ -25,11 +47,9 @@ #include #include #include -#include "NetworkInfo.h" #include "TransmissionResult.h" +#include "NetworkInfo.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 = ""; @@ -44,8 +64,6 @@ class ESP8266WiFiMesh { 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 +74,7 @@ class ESP8266WiFiMesh { static String lastSSID; static bool staticIPActivated; + bool useStaticIP; static IPAddress staticIP; static IPAddress gateway; static IPAddress subnetMask; @@ -78,8 +97,6 @@ class ESP8266WiFiMesh { 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 +150,7 @@ class ESP8266WiFiMesh { //////////////////////////// TODO: REMOVE IN 2.5.0//////////////////////////// ~ESP8266WiFiMesh(); - + /** * WiFiMesh Constructor method. Creates a WiFi Mesh Node, ready to be initialised. * @@ -159,7 +176,7 @@ class ESP8266WiFiMesh { */ ESP8266WiFiMesh(requestHandlerType requestHandler, responseHandlerType responseHandler, networkFilterType networkFilter, const String &meshPassword, const String &meshName = "MeshNode_", const String &nodeID = WIFI_MESH_EMPTY_STRING, bool verboseMode = false, - uint8 meshWiFiChannel = 1, uint16_t serverPort = 4011); + uint8 meshWiFiChannel = 1, uint16_t serverPort = 4011) __attribute__((deprecated)); /** * A vector that contains the NetworkInfo for each WiFi network to connect to. @@ -178,7 +195,7 @@ class ESP8266WiFiMesh { 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. + * @return 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(); @@ -201,7 +218,7 @@ class ESP8266WiFiMesh { * 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 ESP8266WiFiMesh instance currently in control of the ESP8266 AP, + * @return A pointer to the ESP8266WiFiMesh instance currently in control of the ESP8266 AP, * or nullptr if there is no active AP controller. */ static ESP8266WiFiMesh * getAPController(); @@ -209,7 +226,7 @@ class ESP8266WiFiMesh { /** * Check if this ESP8266WiFiMesh instance is in control of the ESP8266 AP. * - * @returns True if this ESP8266WiFiMesh instance is in control of the ESP8266 AP. False otherwise. + * @return True if this ESP8266WiFiMesh instance is in control of the ESP8266 AP. False otherwise. */ bool isAPController(); diff --git a/libraries/ESP8266WiFiMesh/src/EncryptedConnectionData.cpp b/libraries/ESP8266WiFiMesh/src/EncryptedConnectionData.cpp new file mode 100644 index 0000000000..03a8a89fcd --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/EncryptedConnectionData.cpp @@ -0,0 +1,186 @@ +/* + * 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" +#include "MeshCryptoInterface.h" +#include "Serializer.h" + +namespace +{ + using EspnowProtocolInterpreter::hashKeyLength; + namespace TypeCast = MeshTypeConversionFunctions; +} + +EncryptedConnectionData::EncryptedConnectionData(const uint8_t peerStaMac[6], const uint8_t peerApMac[6], const uint64_t peerSessionKey, const uint64_t ownSessionKey, const uint8_t hashKey[hashKeyLength]) + : _peerSessionKey(peerSessionKey), _ownSessionKey(ownSessionKey) +{ + std::copy_n(peerStaMac, 6, _peerStaMac); + std::copy_n(peerApMac, 6, _peerApMac); + std::copy_n(hashKey, hashKeyLength, _hashKey); +} + +EncryptedConnectionData::EncryptedConnectionData(const uint8_t peerStaMac[6], const uint8_t peerApMac[6], const uint64_t peerSessionKey, const uint64_t ownSessionKey, const uint32_t duration, const uint8_t hashKey[hashKeyLength]) + : EncryptedConnectionData(peerStaMac, peerApMac, peerSessionKey, ownSessionKey, hashKey) +{ + setRemainingDuration(duration); +} + +EncryptedConnectionData::EncryptedConnectionData(const EncryptedConnectionData &other) + : _peerSessionKey(other.getPeerSessionKey()), _ownSessionKey(other.getOwnSessionKey()), + _timeTracker(other.temporary() ? new ExpiringTimeTracker(*other.temporary()) : nullptr), + _desync(other.desync()) +{ + 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; +} + +void EncryptedConnectionData::setPeerApMac(const uint8_t *peerApMac) +{ + std::copy_n(peerApMac, 6, _peerApMac); +} + +bool EncryptedConnectionData::connectedTo(const uint8_t *peerMac) const +{ + if(MeshUtilityFunctions::macEqual(peerMac, _peerStaMac) || MeshUtilityFunctions::macEqual(peerMac, _peerApMac)) + { + return true; + } + + return false; +} + +void EncryptedConnectionData::setHashKey(const uint8_t hashKey[hashKeyLength]) +{ + assert(hashKey != nullptr); + + std::copy_n(hashKey, hashKeyLength, _hashKey); +} + +uint8_t *EncryptedConnectionData::getHashKey(uint8_t *resultArray) const +{ + std::copy_n(_hashKey, hashKeyLength, resultArray); + return resultArray; +} + +void EncryptedConnectionData::setPeerSessionKey(const uint64_t sessionKey) { _peerSessionKey = sessionKey; } +uint64_t EncryptedConnectionData::getPeerSessionKey() const { return _peerSessionKey; } + +void EncryptedConnectionData::setOwnSessionKey(const uint64_t sessionKey) { _ownSessionKey = sessionKey; } +uint64_t EncryptedConnectionData::getOwnSessionKey() const { return _ownSessionKey; } + +uint64_t EncryptedConnectionData::incrementSessionKey(const uint64_t sessionKey, const uint8_t *hashKey, const uint8_t hashKeyLength) +{ + uint8_t inputArray[8] {0}; + uint8_t hmacArray[experimental::crypto::SHA256::NATURAL_LENGTH] {0}; + experimental::crypto::SHA256::hmac(TypeCast::uint64ToUint8Array(sessionKey, inputArray), 8, hashKey, hashKeyLength, hmacArray, experimental::crypto::SHA256::NATURAL_LENGTH); + + /* 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 = TypeCast::uint8ArrayToUint64(hmacArray) & EspnowProtocolInterpreter::uint64LeftmostBits; + + if(newLeftmostBits == 0) + newLeftmostBits = ((uint64_t)ESP.random() | (1 << 31)) << 32; // We never want newLeftmostBits == 0 since that would indicate an unencrypted transmission. + + uint64_t newRightmostBits = (uint32_t)(sessionKey + 1); + + return newLeftmostBits | newRightmostBits; +} + +void EncryptedConnectionData::incrementOwnSessionKey() +{ + setOwnSessionKey(incrementSessionKey(getOwnSessionKey(), _hashKey, EspnowProtocolInterpreter::hashKeyLength)); +} + +void EncryptedConnectionData::setDesync(const bool desync) { _desync = desync; } +bool EncryptedConnectionData::desync() const { return _desync; } + +String EncryptedConnectionData::serialize() const +{ + return Serializer:: serializeEncryptedConnection((temporary() ? String(temporary()->remainingDuration()) : emptyString), String(desync()), TypeCast::uint64ToString(getOwnSessionKey()), + TypeCast::uint64ToString(getPeerSessionKey()), TypeCast::macToString(_peerStaMac), TypeCast::macToString(_peerApMac)); +} + +const ExpiringTimeTracker *EncryptedConnectionData::temporary() const +{ + return _timeTracker.get(); +} + +void EncryptedConnectionData::setRemainingDuration(const 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 0000000000..cb9ac95593 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/EncryptedConnectionData.h @@ -0,0 +1,98 @@ +/* + * 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], const uint64_t peerSessionKey, const uint64_t ownSessionKey, + const uint8_t hashKey[EspnowProtocolInterpreter::hashKeyLength]); + EncryptedConnectionData(const uint8_t peerStaMac[6], const uint8_t peerApMac[6], const uint64_t peerSessionKey, const uint64_t ownSessionKey, + const uint32_t duration, const uint8_t hashKey[EspnowProtocolInterpreter::hashKeyLength]); + + EncryptedConnectionData(const EncryptedConnectionData &other); + + EncryptedConnectionData & operator=(const EncryptedConnectionData &other); + + /** + * @param resultArray An uint8_t array with at least size 6. + * + * @return 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; + void setPeerStaMac(const uint8_t *peerStaMac) = delete; // A method for setPeerStaMac would sometimes require interacting with the ESP-NOW API to change encrypted connections, so it is not implemented. + uint8_t *getPeerApMac(uint8_t *resultArray) const; + void setPeerApMac(const uint8_t *peerApMac); + + bool connectedTo(const uint8_t *peerMac) const; + + void setHashKey(const uint8_t hashKey[EspnowProtocolInterpreter::hashKeyLength]); + // @param resultArray At least size hashKeyLength. + uint8_t *getHashKey(uint8_t *resultArray) const; + + void setPeerSessionKey(const uint64_t sessionKey); + uint64_t getPeerSessionKey() const; + void setOwnSessionKey(const uint64_t sessionKey); + uint64_t getOwnSessionKey() const; + + static uint64_t incrementSessionKey(const uint64_t sessionKey, const uint8_t *hashKey, const uint8_t hashKeyLength); + void incrementOwnSessionKey(); + + void setDesync(const bool desync); + bool desync() const; + + // Note that the espnowEncryptedConnectionKey, espnowEncryptionKok, espnowHashKey and espnowMessageEncryptionKey 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(const uint32_t remainingDuration); + virtual void removeDuration(); + +private: + + uint8_t _peerStaMac[6] {0}; + uint8_t _peerApMac[6] {0}; + uint64_t _peerSessionKey; + uint64_t _ownSessionKey; + std::unique_ptr _timeTracker = nullptr; + uint8_t _hashKey[EspnowProtocolInterpreter::hashKeyLength] {0}; + bool _desync = false; +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/EncryptedConnectionLog.cpp b/libraries/ESP8266WiFiMesh/src/EncryptedConnectionLog.cpp new file mode 100644 index 0000000000..6b979575c4 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/EncryptedConnectionLog.cpp @@ -0,0 +1,95 @@ +/* + * 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" + +namespace +{ + using EspnowProtocolInterpreter::hashKeyLength; +} + +EncryptedConnectionLog::EncryptedConnectionLog(const uint8_t peerStaMac[6], const uint8_t peerApMac[6], const uint64_t peerSessionKey, const uint64_t ownSessionKey, const uint8_t hashKey[hashKeyLength]) + : EncryptedConnectionData(peerStaMac, peerApMac, peerSessionKey, ownSessionKey, hashKey) +{ } + +EncryptedConnectionLog::EncryptedConnectionLog(const uint8_t peerStaMac[6], const uint8_t peerApMac[6], const uint64_t peerSessionKey, const uint64_t ownSessionKey, const uint32_t duration, const uint8_t hashKey[hashKeyLength]) + : EncryptedConnectionData(peerStaMac, peerApMac, peerSessionKey, ownSessionKey, duration, hashKey) +{ } + +std::unique_ptr EncryptedConnectionLog::_soonestExpiringConnectionTracker = nullptr; + +bool EncryptedConnectionLog::_newRemovalsScheduled = false; + +void EncryptedConnectionLog::setRemainingDuration(const 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 performEspnowMaintenance() 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(const bool scheduledForRemoval) +{ + _scheduledForRemoval = scheduledForRemoval; + + if(scheduledForRemoval) + setNewRemovalsScheduled(true); +} +bool EncryptedConnectionLog::removalScheduled() const { return _scheduledForRemoval; } + +void EncryptedConnectionLog::setNewRemovalsScheduled(const bool newRemovalsScheduled) { _newRemovalsScheduled = newRemovalsScheduled; } +bool EncryptedConnectionLog::newRemovalsScheduled( ) { return _newRemovalsScheduled; } + +const ExpiringTimeTracker *EncryptedConnectionLog::getSoonestExpiringConnectionTracker() +{ + return _soonestExpiringConnectionTracker.get(); +} + +void EncryptedConnectionLog::updateSoonestExpiringConnectionTracker(const 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 0000000000..91386c75f1 --- /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], const uint64_t peerSessionKey, const uint64_t ownSessionKey, + const uint8_t hashKey[EspnowProtocolInterpreter::hashKeyLength]); + EncryptedConnectionLog(const uint8_t peerStaMac[6], const uint8_t peerApMac[6], const uint64_t peerSessionKey, const uint64_t ownSessionKey, + const uint32_t duration, const uint8_t hashKey[EspnowProtocolInterpreter::hashKeyLength]); + + // 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(const uint32_t remainingDuration) override; + void removeDuration() override; + + void scheduleForRemoval(); + bool removalScheduled() const; + + static void setNewRemovalsScheduled(const bool newRemovalsScheduled); + static bool newRemovalsScheduled(); + + static const ExpiringTimeTracker *getSoonestExpiringConnectionTracker(); + static void updateSoonestExpiringConnectionTracker(const uint32_t remainingDuration); + static void clearSoonestExpiringConnectionTracker(); + +private: + + bool _scheduledForRemoval = false; + void setScheduledForRemoval(const bool scheduledForRemoval); +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/EspnowConnectionManager.cpp b/libraries/ESP8266WiFiMesh/src/EspnowConnectionManager.cpp new file mode 100644 index 0000000000..c0534d269c --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/EspnowConnectionManager.cpp @@ -0,0 +1,585 @@ +/* + Copyright (C) 2020 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 "EspnowConnectionManager.h" +#include "JsonTranslator.h" +#include "MeshCryptoInterface.h" +#include "Serializer.h" +#include "EspnowTransmitter.h" + +namespace +{ + using EspnowProtocolInterpreter::encryptedConnectionKeyLength; + using EspnowProtocolInterpreter::hashKeyLength; + using EspnowProtocolInterpreter::maxEncryptedConnections; + + namespace TypeCast = MeshTypeConversionFunctions; + + std::vector _encryptedConnections = {}; + + uint32_t _unsynchronizedMessageID = 0; + + uint8_t _espnowEncryptionKok[encryptedConnectionKeyLength] = { 0 }; + bool _espnowEncryptionKokSet = false; +} + +EspnowConnectionManager::EspnowConnectionManager(ConditionalPrinter &conditionalPrinterInstance, EspnowDatabase &databaseInstance) + : _conditionalPrinter(conditionalPrinterInstance), _database(databaseInstance) +{ + // Reserve the maximum possible usage early on to prevent heap fragmentation later. + encryptedConnections().reserve(maxEncryptedConnections); +} + +std::vector & EspnowConnectionManager::encryptedConnections() { return _encryptedConnections; } + +uint8_t EspnowConnectionManager::numberOfEncryptedConnections() +{ + return encryptedConnections().size(); +} + +ConnectionType EspnowConnectionManager::getConnectionInfo(uint8_t *peerMac, uint32_t *remainingDuration) +{ + EncryptedConnectionLog *encryptedConnection = nullptr; + + if(peerMac) + encryptedConnection = getEncryptedConnection(peerMac); + + return getConnectionInfoHelper(encryptedConnection, remainingDuration); +} + +ConnectionType EspnowConnectionManager::getConnectionInfo(const uint32_t connectionIndex, uint32_t *remainingDuration, uint8_t *peerMac) +{ + EncryptedConnectionLog *encryptedConnection = nullptr; + + if(connectionIndex < numberOfEncryptedConnections()) + encryptedConnection = &encryptedConnections()[connectionIndex]; + + return getConnectionInfoHelper(encryptedConnection, remainingDuration, peerMac); +} + +EspnowConnectionManager::connectionLogIterator EspnowConnectionManager::connectionLogEndIterator() +{ + return encryptedConnections().end(); +} + +void EspnowConnectionManager::setEspnowEncryptedConnectionKey(const uint8_t espnowEncryptedConnectionKey[encryptedConnectionKeyLength]) +{ + assert(espnowEncryptedConnectionKey != nullptr); + + for(int i = 0; i < encryptedConnectionKeyLength; ++i) + { + _espnowEncryptedConnectionKey[i] = espnowEncryptedConnectionKey[i]; + } +} + +void EspnowConnectionManager::setEspnowEncryptedConnectionKey(const String &espnowEncryptedConnectionKeySeed) +{ + MeshCryptoInterface::initializeKey(_espnowEncryptedConnectionKey, encryptedConnectionKeyLength, espnowEncryptedConnectionKeySeed); +} + +const uint8_t *EspnowConnectionManager::getEspnowEncryptedConnectionKey() const +{ + return _espnowEncryptedConnectionKey; +} + +uint8_t *EspnowConnectionManager::getEspnowEncryptedConnectionKey(uint8_t resultArray[encryptedConnectionKeyLength]) const +{ + std::copy_n(_espnowEncryptedConnectionKey, encryptedConnectionKeyLength, resultArray); + return resultArray; +} + +bool EspnowConnectionManager::initializeEncryptionKok() +{ + // esp_now_set_kok returns 0 on success. + return !(_espnowEncryptionKokSet && esp_now_set_kok(_espnowEncryptionKok, encryptedConnectionKeyLength)); +} + +bool EspnowConnectionManager::setEspnowEncryptionKok(uint8_t espnowEncryptionKok[encryptedConnectionKeyLength]) +{ + if(espnowEncryptionKok == nullptr || esp_now_set_kok(espnowEncryptionKok, encryptedConnectionKeyLength)) // esp_now_set_kok failed if not == 0 + return false; + + for(int i = 0; i < encryptedConnectionKeyLength; ++i) + { + _espnowEncryptionKok[i] = espnowEncryptionKok[i]; + } + + _espnowEncryptionKokSet = true; + + return true; +} + +bool EspnowConnectionManager::setEspnowEncryptionKok(const String &espnowEncryptionKokSeed) +{ + uint8_t espnowEncryptionKok[encryptedConnectionKeyLength] {}; + MeshCryptoInterface::initializeKey(espnowEncryptionKok, encryptedConnectionKeyLength, espnowEncryptionKokSeed); + + return setEspnowEncryptionKok(espnowEncryptionKok); +} + +const uint8_t *EspnowConnectionManager::getEspnowEncryptionKok() +{ + if(_espnowEncryptionKokSet) + return _espnowEncryptionKok; + else + return nullptr; +} + +void EspnowConnectionManager::setEspnowHashKey(const uint8_t espnowHashKey[hashKeyLength]) +{ + assert(espnowHashKey != nullptr); + + for(int i = 0; i < hashKeyLength; ++i) + { + _espnowHashKey[i] = espnowHashKey[i]; + } +} + +void EspnowConnectionManager::setEspnowHashKey(const String &espnowHashKeySeed) +{ + MeshCryptoInterface::initializeKey(_espnowHashKey, hashKeyLength, espnowHashKeySeed); +} + +const uint8_t *EspnowConnectionManager::getEspnowHashKey() const +{ + return _espnowHashKey; +} + +uint8_t *EspnowConnectionManager::getEncryptedMac(const uint8_t *peerMac, uint8_t *resultArray) +{ + if(EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(peerMac)) + { + return encryptedConnection->getEncryptedPeerMac(resultArray); + } + + return nullptr; +} + +EncryptedConnectionLog *EspnowConnectionManager::getEncryptedConnection(const uint8_t *peerMac) +{ + auto connectionIterator = getEncryptedConnectionIterator(peerMac, encryptedConnections()); + if(connectionIterator != encryptedConnections().end()) + { + return &(*connectionIterator); + } + + return nullptr; +} + +EncryptedConnectionLog *EspnowConnectionManager::getEncryptedConnection(const uint32_t connectionIndex) +{ + if(connectionIndex < numberOfEncryptedConnections()) + return &encryptedConnections()[connectionIndex]; + + return nullptr; +} + +EncryptedConnectionLog *EspnowConnectionManager::getTemporaryEncryptedConnection(const uint8_t *peerMac) +{ + connectionLogIterator connectionIterator = connectionLogEndIterator(); + if(getTemporaryEncryptedConnectionIterator(peerMac, connectionIterator)) + { + return &(*connectionIterator); + } + + return nullptr; +} + +template +typename T::iterator EspnowConnectionManager::getEncryptedConnectionIterator(const uint8_t *peerMac, T &connectionContainer) +{ + typename T::iterator connectionIterator = connectionContainer.begin(); + + while(connectionIterator != connectionContainer.end()) + { + if(connectionIterator->connectedTo(peerMac)) + break; + else + ++connectionIterator; + } + + return connectionIterator; +} + +bool EspnowConnectionManager::getEncryptedConnectionIterator(const uint8_t *peerMac, connectionLogIterator &iterator) +{ + connectionLogIterator result = getEncryptedConnectionIterator(peerMac, encryptedConnections()); + + if(result != connectionLogEndIterator()) + { + iterator = result; + return true; + } + + return false; +} + +bool EspnowConnectionManager::getTemporaryEncryptedConnectionIterator(const uint8_t *peerMac, connectionLogIterator &iterator) +{ + connectionLogIterator result = connectionLogEndIterator(); + + if(getEncryptedConnectionIterator(peerMac, result) && result->temporary()) + { + iterator = result; + return true; + } + + return false; +} + +ConnectionType EspnowConnectionManager::getConnectionInfoHelper(const EncryptedConnectionLog *encryptedConnection, uint32_t *remainingDuration, uint8_t *peerMac) +{ + if(!encryptedConnection) + { + return ConnectionType::NO_CONNECTION; + } + + if(peerMac) + encryptedConnection->getEncryptedPeerMac(peerMac); + + if(const ExpiringTimeTracker *timeTracker = encryptedConnection->temporary()) + { + if(remainingDuration) + *remainingDuration = timeTracker->remainingDuration(); + + return ConnectionType::TEMPORARY_CONNECTION; + } + + return ConnectionType::PERMANENT_CONNECTION; +} + + +void EspnowConnectionManager::setEncryptedConnectionsSoftLimit(const uint8_t softLimit) +{ + assert(softLimit <= 6); // Valid values are 0 to 6, but uint8_t is always at least 0. + _encryptedConnectionsSoftLimit = softLimit; +} + +uint8_t EspnowConnectionManager::encryptedConnectionsSoftLimit() const { return _encryptedConnectionsSoftLimit; } + +bool EspnowConnectionManager::addUnencryptedConnection(const String &serializedConnectionState) +{ + return JsonTranslator::getUnsynchronizedMessageID(serializedConnectionState, _unsynchronizedMessageID); +} + +EncryptedConnectionStatus EspnowConnectionManager::addEncryptedConnection(uint8_t *peerStaMac, uint8_t *peerApMac, const uint64_t peerSessionKey, const 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[encryptedConnectionKeyLength] = { 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, getEspnowEncryptedConnectionKey(encryptionKeyArray), encryptedConnectionKeyLength); + encryptedConnection->setHashKey(getEspnowHashKey()); + + return EncryptedConnectionStatus::CONNECTION_ESTABLISHED; + } + + if(encryptedConnections().size() == maxEncryptedConnections) + { + // No capacity for more encrypted connections. + return EncryptedConnectionStatus::MAX_CONNECTIONS_REACHED_SELF; + } + // returns 0 on success: int esp_now_add_peer(u8 *mac_addr, u8 role, u8 channel, u8 *key, u8 key_len) + // Only MAC, encryption key and key length (16) actually matter. The rest is not used by ESP-NOW. + else if(0 == esp_now_add_peer(peerStaMac, ESP_NOW_ROLE_CONTROLLER, _database.getWiFiChannel(), getEspnowEncryptedConnectionKey(encryptionKeyArray), encryptedConnectionKeyLength)) + { + encryptedConnections().emplace_back(peerStaMac, peerApMac, peerSessionKey, ownSessionKey, getEspnowHashKey()); + return EncryptedConnectionStatus::CONNECTION_ESTABLISHED; + } + else + { + return EncryptedConnectionStatus::API_CALL_FAILED; + } +} + +EncryptedConnectionStatus EspnowConnectionManager::addEncryptedConnection(const String &serializedConnectionState, const 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)) + { + EncryptedConnectionStatus result = EncryptedConnectionStatus::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 == EncryptedConnectionStatus::CONNECTION_ESTABLISHED) + { + EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(peerStaMac); + encryptedConnection->setDesync(desync); + } + + return result; + } + + return EncryptedConnectionStatus::REQUEST_TRANSMISSION_FAILED; +} + +EncryptedConnectionStatus EspnowConnectionManager::addTemporaryEncryptedConnection(uint8_t *peerStaMac, uint8_t *peerApMac, const uint64_t peerSessionKey, const uint64_t ownSessionKey, const 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[encryptedConnectionKeyLength] = { 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, getEspnowEncryptedConnectionKey(encryptionKeyArray), encryptedConnectionKeyLength); + encryptedConnection->setHashKey(getEspnowHashKey()); + + if(encryptedConnection->temporary()) + { + encryptedConnection->setRemainingDuration(duration); + } + + return EncryptedConnectionStatus::CONNECTION_ESTABLISHED; + } + + EncryptedConnectionStatus result = addEncryptedConnection(peerStaMac, peerApMac, peerSessionKey, ownSessionKey); + + if(result == EncryptedConnectionStatus::CONNECTION_ESTABLISHED) + { + if(!getEncryptedConnectionIterator(peerStaMac, encryptedConnection)) + assert(false && String(F("No connection found despite being added in addTemporaryEncryptedConnection."))); + + encryptedConnection->setRemainingDuration(duration); + } + + return result; +} + +EncryptedConnectionStatus EspnowConnectionManager::addTemporaryEncryptedConnection(const String &serializedConnectionState, const 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)) + { + EncryptedConnectionStatus result = addTemporaryEncryptedConnection(peerStaMac, peerApMac, peerSessionKey, ownSessionKey, duration); + + if(result == EncryptedConnectionStatus::CONNECTION_ESTABLISHED) + { + EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(peerStaMac); + encryptedConnection->setDesync(desync); + } + + return result; + } + + return EncryptedConnectionStatus::REQUEST_TRANSMISSION_FAILED; +} + +EncryptedConnectionRemovalOutcome EspnowConnectionManager::removeEncryptedConnection(const uint8_t *peerMac) +{ + auto connectionIterator = getEncryptedConnectionIterator(peerMac, encryptedConnections()); + if(connectionIterator != encryptedConnections().end()) + { + MutexTracker mutexTracker(EspnowTransmitter::captureEspnowTransmissionMutex()); + 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 EncryptedConnectionRemovalOutcome::REMOVAL_SCHEDULED; + } + else + { + return removeEncryptedConnectionUnprotected(peerMac); + } + } + + // peerMac is already removed + return EncryptedConnectionRemovalOutcome::REMOVAL_SUCCEEDED; +} + + +EncryptedConnectionRemovalOutcome EspnowConnectionManager::removeEncryptedConnectionUnprotected(const uint8_t *peerMac, std::vector::iterator *resultingIterator) +{ + connectionLogIterator connectionIterator = getEncryptedConnectionIterator(peerMac, encryptedConnections()); + return removeEncryptedConnectionUnprotected(connectionIterator, resultingIterator); +} + +EncryptedConnectionRemovalOutcome EspnowConnectionManager::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); + ConditionalPrinter::staticVerboseModePrint(String(F("Removing connection ")) + TypeCast::macToString(encryptedMac) + String(F("... ")), false); + bool removalSucceeded = esp_now_del_peer(encryptedMac) == 0; + + if(removalSucceeded) + { + if(resultingIterator != nullptr) + { + *resultingIterator = encryptedConnections().erase(connectionIterator); + } + else + { + encryptedConnections().erase(connectionIterator); + } + ConditionalPrinter::staticVerboseModePrint(String(F("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. + EspnowDatabase::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). + EspnowDatabase::deleteEntriesByMac(EspnowDatabase::receivedEspnowTransmissions(), encryptedMac, true); + EspnowDatabase::deleteEntriesByMac(EspnowDatabase::sentRequests(), encryptedMac, true); + EspnowDatabase::deleteEntriesByMac(EspnowDatabase::receivedRequests(), encryptedMac, true); + + return EncryptedConnectionRemovalOutcome::REMOVAL_SUCCEEDED; + } + else + { + ConditionalPrinter::staticVerboseModePrint(String(F("Removal failed"))); + return EncryptedConnectionRemovalOutcome::REMOVAL_FAILED; + } + } + + // connection is already removed + return EncryptedConnectionRemovalOutcome::REMOVAL_SUCCEEDED; +} + +bool EspnowConnectionManager::temporaryEncryptedConnectionToPermanent(const uint8_t *peerMac) +{ + if(EncryptedConnectionLog *temporaryConnection = getTemporaryEncryptedConnection(peerMac)) + { + temporaryConnection->removeDuration(); + return true; + } + + return false; +} + +String EspnowConnectionManager::serializeUnencryptedConnection() +{ + return Serializer::serializeUnencryptedConnection(String(_unsynchronizedMessageID)); +} + +String EspnowConnectionManager::serializeEncryptedConnection(const uint8_t *peerMac) +{ + String serializedConnection(emptyString); + + EncryptedConnectionLog *encryptedConnection = nullptr; + + if(peerMac) + encryptedConnection = getEncryptedConnection(peerMac); + + if(encryptedConnection) + serializedConnection = encryptedConnection->serialize(); + + return serializedConnection; +} + +String EspnowConnectionManager::serializeEncryptedConnection(const uint32_t connectionIndex) +{ + String serializedConnection(emptyString); + + if(EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(connectionIndex)) + serializedConnection = encryptedConnection->serialize(); + + return serializedConnection; +} + +void EspnowConnectionManager::handlePostponedRemovals() +{ + MutexTracker mutexTracker(EspnowTransmitter::captureEspnowTransmissionMutex()); + if(!mutexTracker.mutexCaptured()) + { + assert(false && String(F("ERROR! Transmission in progress. Don't call handlePostponedRemovals from callbacks as this may corrupt program state! Aborting."))); + return; + } + + if(EncryptedConnectionLog::newRemovalsScheduled()) + { + updateTemporaryEncryptedConnections(true); + } +} + +void EspnowConnectionManager::updateTemporaryEncryptedConnections(const 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); +} + +uint64_t EspnowConnectionManager::generateMessageID(const EncryptedConnectionLog *encryptedConnection) +{ + if(encryptedConnection) + { + return encryptedConnection->getOwnSessionKey(); + } + + return _unsynchronizedMessageID++; +} diff --git a/libraries/ESP8266WiFiMesh/src/EspnowConnectionManager.h b/libraries/ESP8266WiFiMesh/src/EspnowConnectionManager.h new file mode 100644 index 0000000000..7edcfcb930 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/EspnowConnectionManager.h @@ -0,0 +1,152 @@ +/* + Copyright (C) 2020 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 +*/ + +#ifndef __ESPNOWCONNECTIONMANAGER_H__ +#define __ESPNOWCONNECTIONMANAGER_H__ + +#include +#include "ConditionalPrinter.h" +#include "EspnowDatabase.h" +#include "EspnowProtocolInterpreter.h" +#include "EncryptedConnectionLog.h" + +class EspnowMeshBackend; + +enum class ConnectionType +{ + NO_CONNECTION = 0, + TEMPORARY_CONNECTION = 1, + PERMANENT_CONNECTION = 2 +}; + +// A value greater than 0 means that an encrypted connection has been established. +enum class EncryptedConnectionStatus +{ + MAX_CONNECTIONS_REACHED_SELF = -3, + REQUEST_TRANSMISSION_FAILED = -2, + MAX_CONNECTIONS_REACHED_PEER = -1, + API_CALL_FAILED = 0, + CONNECTION_ESTABLISHED = 1, + SOFT_LIMIT_CONNECTION_ESTABLISHED = 2 // Only used if _encryptedConnectionsSoftLimit is less than 6. See the setEncryptedConnectionsSoftLimit method documentation for details. +}; + +enum class EncryptedConnectionRemovalOutcome +{ + REMOVAL_REQUEST_FAILED = -1, + REMOVAL_FAILED = 0, + REMOVAL_SUCCEEDED = 1, + REMOVAL_SCHEDULED = 2 +}; + +class EspnowConnectionManager +{ + +public: + + using connectionLogIterator = std::vector::iterator; + + EspnowConnectionManager(ConditionalPrinter &conditionalPrinterInstance, EspnowDatabase &databaseInstance); + + static std::vector & encryptedConnections(); + + static uint8_t numberOfEncryptedConnections(); + + static ConnectionType getConnectionInfo(uint8_t *peerMac, uint32_t *remainingDuration = nullptr); + static ConnectionType getConnectionInfo(const uint32_t connectionIndex, uint32_t *remainingDuration = nullptr, uint8_t *peerMac = nullptr); + + static connectionLogIterator connectionLogEndIterator(); + + void setEspnowEncryptedConnectionKey(const uint8_t espnowEncryptedConnectionKey[EspnowProtocolInterpreter::encryptedConnectionKeyLength]); + void setEspnowEncryptedConnectionKey(const String &espnowEncryptedConnectionKeySeed); + const uint8_t *getEspnowEncryptedConnectionKey() const; + uint8_t *getEspnowEncryptedConnectionKey(uint8_t resultArray[EspnowProtocolInterpreter::encryptedConnectionKeyLength]) const; + // Returns false if failed to apply the current KoK (default KoK is used if no KoK provided) + static bool initializeEncryptionKok(); + static bool setEspnowEncryptionKok(uint8_t espnowEncryptionKok[EspnowProtocolInterpreter::encryptedConnectionKeyLength]); + static bool setEspnowEncryptionKok(const String &espnowEncryptionKokSeed); + static const uint8_t *getEspnowEncryptionKok(); + void setEspnowHashKey(const uint8_t espnowHashKey[EspnowProtocolInterpreter::hashKeyLength]); + void setEspnowHashKey(const String &espnowHashKeySeed); + const uint8_t *getEspnowHashKey() const; + + static uint8_t *getEncryptedMac(const uint8_t *peerMac, uint8_t *resultArray); + + static EncryptedConnectionLog *getEncryptedConnection(const uint8_t *peerMac); + static EncryptedConnectionLog *getEncryptedConnection(const uint32_t connectionIndex); + static EncryptedConnectionLog *getTemporaryEncryptedConnection(const uint8_t *peerMac); + + //@return iterator to connection in connectionContainer, or connectionContainer.end() if element not found + template + static typename T::iterator getEncryptedConnectionIterator(const uint8_t *peerMac, T &connectionContainer); + static bool getEncryptedConnectionIterator(const uint8_t *peerMac, connectionLogIterator &iterator); + // @return 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); + + void setEncryptedConnectionsSoftLimit(const uint8_t softLimit); + uint8_t encryptedConnectionsSoftLimit() const; + + static bool addUnencryptedConnection(const String &serializedConnectionState); + EncryptedConnectionStatus addEncryptedConnection(uint8_t *peerStaMac, uint8_t *peerApMac, const uint64_t peerSessionKey, const uint64_t ownSessionKey); + EncryptedConnectionStatus addEncryptedConnection(const String &serializedConnectionState, const bool ignoreDuration = false); + EncryptedConnectionStatus addTemporaryEncryptedConnection(uint8_t *peerStaMac, uint8_t *peerApMac, const uint64_t peerSessionKey, const uint64_t ownSessionKey, const uint32_t duration); + EncryptedConnectionStatus addTemporaryEncryptedConnection(const String &serializedConnectionState, const uint32_t duration); + + static EncryptedConnectionRemovalOutcome removeEncryptedConnection(const uint8_t *peerMac); + + // 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 EncryptedConnectionRemovalOutcome removeEncryptedConnectionUnprotected(const uint8_t *peerMac, std::vector::iterator *resultingIterator = nullptr); + static EncryptedConnectionRemovalOutcome removeEncryptedConnectionUnprotected(connectionLogIterator &connectionIterator, std::vector::iterator *resultingIterator); + + static bool temporaryEncryptedConnectionToPermanent(const uint8_t *peerMac); + + static String serializeUnencryptedConnection(); + static String serializeEncryptedConnection(const uint8_t *peerMac); + static String serializeEncryptedConnection(const uint32_t connectionIndex); + + static void handlePostponedRemovals(); + + // 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(const bool scheduledRemovalOnly = false); + + /** + * 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. + * @return The generated message ID. + */ + static uint64_t generateMessageID(const EncryptedConnectionLog *encryptedConnection); + +private: + + ConditionalPrinter & _conditionalPrinter; + EspnowDatabase & _database; + + static ConnectionType getConnectionInfoHelper(const EncryptedConnectionLog *encryptedConnection, uint32_t *remainingDuration, uint8_t *peerMac = nullptr); + + uint8_t _espnowEncryptedConnectionKey[EspnowProtocolInterpreter::encryptedConnectionKeyLength] {0}; + uint8_t _espnowHashKey[EspnowProtocolInterpreter::hashKeyLength] {0}; + + uint8_t _encryptedConnectionsSoftLimit = 6; +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/EspnowDatabase.cpp b/libraries/ESP8266WiFiMesh/src/EspnowDatabase.cpp new file mode 100644 index 0000000000..d718726787 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/EspnowDatabase.cpp @@ -0,0 +1,383 @@ +/* + Copyright (C) 2020 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 "EspnowDatabase.h" +#include "EspnowMeshBackend.h" +#include "UtilityFunctions.h" + +namespace +{ + namespace TypeCast = MeshTypeConversionFunctions; + + // _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 _logEntryLifetimeMs = 2500; + uint32_t _broadcastResponseTimeoutMs = 1000; // This is shorter than _logEntryLifetimeMs to preserve RAM since broadcasts are not deleted from sentRequests until they expire. + ExpiringTimeTracker _logClearingCooldown(500); + + uint32_t _encryptionRequestTimeoutMs = 300; + + uint32_t _criticalHeapLevel = 6000; // In bytes + uint32_t _criticalHeapLevelBuffer = 6000; // In bytes + + using EspnowProtocolInterpreter::macAndType_td; + using EspnowProtocolInterpreter::messageID_td; + using EspnowProtocolInterpreter::peerMac_td; + + std::list _responsesToSend = {}; + std::list _peerRequestConfirmationsToSend = {}; + + std::map, MessageData> _receivedEspnowTransmissions = {}; + std::map, RequestData> _sentRequests = {}; + std::map, TimeTracker> _receivedRequests = {}; + + std::shared_ptr _espnowConnectionQueueMutex = std::make_shared(false); + std::shared_ptr _responsesToSendMutex = std::make_shared(false); +} + +std::vector EspnowDatabase::_connectionQueue = {}; +std::vector EspnowDatabase::_latestTransmissionOutcomes = {}; + +EspnowDatabase::EspnowDatabase(ConditionalPrinter &conditionalPrinterInstance, const uint8 espnowWiFiChannel) : _conditionalPrinter(conditionalPrinterInstance), _espnowWiFiChannel(espnowWiFiChannel) +{ +} + +std::vector & EspnowDatabase::connectionQueue() +{ + MutexTracker connectionQueueMutexTracker(_espnowConnectionQueueMutex); + if(!connectionQueueMutexTracker.mutexCaptured()) + { + assert(false && String(F("ERROR! connectionQueue locked. Don't call connectionQueue() from callbacks other than NetworkFilter as this may corrupt program state!"))); + } + + return _connectionQueue; +} + +const std::vector & EspnowDatabase::constConnectionQueue() +{ + return _connectionQueue; +} + +std::vector & EspnowDatabase::latestTransmissionOutcomes() +{ + return _latestTransmissionOutcomes; +} + +void EspnowDatabase::setCriticalHeapLevelBuffer(const uint32_t bufferInBytes) +{ + _criticalHeapLevelBuffer = bufferInBytes; +} + +uint32_t EspnowDatabase::criticalHeapLevelBuffer() +{ + return _criticalHeapLevelBuffer; +} + +uint32_t EspnowDatabase::criticalHeapLevel() +{ + return _criticalHeapLevel; +} + +template +void EspnowDatabase::deleteExpiredLogEntries(std::map, T> &logEntries, const uint32_t maxEntryLifetimeMs) +{ + for(typename std::map, T>::iterator entryIterator = logEntries.begin(); + entryIterator != logEntries.end(); ) + { + if(entryIterator->second.getTimeTracker().timeSinceCreation() > maxEntryLifetimeMs) + { + entryIterator = logEntries.erase(entryIterator); + } + else + ++entryIterator; + } +} + +template +void EspnowDatabase::deleteExpiredLogEntries(std::map, TimeTracker> &logEntries, const uint32_t maxEntryLifetimeMs) +{ + for(typename std::map, TimeTracker>::iterator entryIterator = logEntries.begin(); + entryIterator != logEntries.end(); ) + { + if(entryIterator->second.timeSinceCreation() > maxEntryLifetimeMs) + { + entryIterator = logEntries.erase(entryIterator); + } + else + ++entryIterator; + } +} + +void EspnowDatabase::deleteExpiredLogEntries(std::map, RequestData> &logEntries, const uint32_t requestLifetimeMs, const uint32_t broadcastLifetimeMs) +{ + for(typename std::map, RequestData>::iterator entryIterator = logEntries.begin(); + entryIterator != logEntries.end(); ) + { + bool broadcast = entryIterator->first.first == EspnowProtocolInterpreter::uint64BroadcastMac; + uint32_t timeSinceCreation = entryIterator->second.getTimeTracker().timeSinceCreation(); + + if((!broadcast && timeSinceCreation > requestLifetimeMs) + || (broadcast && timeSinceCreation > broadcastLifetimeMs)) + { + entryIterator = logEntries.erase(entryIterator); + } + else + ++entryIterator; + } +} + +template +void EspnowDatabase::deleteExpiredLogEntries(std::list &logEntries, const uint32_t maxEntryLifetimeMs) +{ + for(typename std::list::iterator entryIterator = logEntries.begin(); + entryIterator != logEntries.end(); ) + { + if(entryIterator->getTimeTracker().timeSinceCreation() > maxEntryLifetimeMs) + { + entryIterator = logEntries.erase(entryIterator); + } + else + ++entryIterator; + } +} + +template <> +void EspnowDatabase::deleteExpiredLogEntries(std::list &logEntries, const uint32_t maxEntryLifetimeMs) +{ + for(typename std::list::iterator entryIterator = logEntries.begin(); + entryIterator != logEntries.end(); ) + { + auto timeTrackerPointer = entryIterator->temporary(); + if(timeTrackerPointer && timeTrackerPointer->elapsedTime() > maxEntryLifetimeMs) + { + entryIterator = logEntries.erase(entryIterator); + } + else + ++entryIterator; + } +} + +void EspnowDatabase::setLogEntryLifetimeMs(const uint32_t logEntryLifetimeMs) +{ + _logEntryLifetimeMs = logEntryLifetimeMs; +} +uint32_t EspnowDatabase::logEntryLifetimeMs() { return _logEntryLifetimeMs; } + +void EspnowDatabase::setBroadcastResponseTimeoutMs(const uint32_t broadcastResponseTimeoutMs) +{ + _broadcastResponseTimeoutMs = broadcastResponseTimeoutMs; +} +uint32_t EspnowDatabase::broadcastResponseTimeoutMs() { return _broadcastResponseTimeoutMs; } + +String EspnowDatabase::getScheduledResponseMessage(const uint32_t responseIndex) +{ + return getScheduledResponse(responseIndex)->getMessage(); +} + +const uint8_t *EspnowDatabase::getScheduledResponseRecipient(const uint32_t responseIndex) +{ + return getScheduledResponse(responseIndex)->getRecipientMac(); +} + +uint32_t EspnowDatabase::numberOfScheduledResponses() {return responsesToSend().size();} + +void EspnowDatabase::clearAllScheduledResponses() +{ + MutexTracker responsesToSendMutexTracker(_responsesToSendMutex); + if(!responsesToSendMutexTracker.mutexCaptured()) + { + assert(false && String(F("ERROR! responsesToSend locked. Don't call clearAllScheduledResponses from callbacks as this may corrupt program state! Aborting."))); + } + + responsesToSend().clear(); +} + +void EspnowDatabase::deleteScheduledResponsesByRecipient(const uint8_t *recipientMac, const bool encryptedOnly) +{ + MutexTracker responsesToSendMutexTracker(_responsesToSendMutex); + if(!responsesToSendMutexTracker.mutexCaptured()) + { + assert(false && String(F("ERROR! responsesToSend locked. Don't call deleteScheduledResponsesByRecipient from callbacks as this may corrupt program state! Aborting."))); + } + + for(auto responseIterator = responsesToSend().begin(); responseIterator != responsesToSend().end(); ) + { + if(MeshUtilityFunctions::macEqual(responseIterator->getRecipientMac(), recipientMac) && + (!encryptedOnly || EspnowProtocolInterpreter::usesEncryption(responseIterator->getRequestID()))) + { + responseIterator = responsesToSend().erase(responseIterator); + } + else + ++responseIterator; + } +} + +void EspnowDatabase::setEncryptionRequestTimeout(const uint32_t timeoutMs) +{ + _encryptionRequestTimeoutMs = timeoutMs; +} +uint32_t EspnowDatabase::getEncryptionRequestTimeout() {return _encryptionRequestTimeoutMs;} + +void EspnowDatabase::setAutoEncryptionDuration(const uint32_t duration) +{ + _autoEncryptionDuration = duration; +} +uint32_t EspnowDatabase::getAutoEncryptionDuration() const {return _autoEncryptionDuration;} + +String EspnowDatabase::getSenderMac() const {return TypeCast::macToString(_senderMac);} +uint8_t *EspnowDatabase::getSenderMac(uint8_t *macArray) const +{ + std::copy_n(_senderMac, 6, macArray); + return macArray; +} + +String EspnowDatabase::getSenderAPMac() const {return TypeCast::macToString(_senderAPMac);} +uint8_t *EspnowDatabase::getSenderAPMac(uint8_t *macArray) const +{ + std::copy_n(_senderAPMac, 6, macArray); + return macArray; +} + +void EspnowDatabase::clearOldLogEntries(bool forced) +{ + // Clearing all old log entries at the same time should help minimize heap fragmentation. + + // uint32_t startTime = millis(); + + if(!forced && !_logClearingCooldown) // Clearing too frequently will cause a lot of unnecessary container iterations. + { + return; + } + + _logClearingCooldown.reset(); + + deleteExpiredLogEntries(receivedEspnowTransmissions(), logEntryLifetimeMs()); + deleteExpiredLogEntries(receivedRequests(), logEntryLifetimeMs()); // Just needs to be long enough to not accept repeated transmissions by mistake. + deleteExpiredLogEntries(sentRequests(), logEntryLifetimeMs(), broadcastResponseTimeoutMs()); + deleteExpiredLogEntries(responsesToSend(), logEntryLifetimeMs()); + deleteExpiredLogEntries(peerRequestConfirmationsToSend(), getEncryptionRequestTimeout()); +} + +std::list::const_iterator EspnowDatabase::getScheduledResponse(const 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; +} + +void EspnowDatabase::setSenderMac(const uint8_t *macArray) +{ + std::copy_n(macArray, 6, _senderMac); +} + +void EspnowDatabase::setSenderAPMac(const uint8_t *macArray) +{ + std::copy_n(macArray, 6, _senderAPMac); +} + +void EspnowDatabase::setWiFiChannel(const uint8 newWiFiChannel) +{ + wifi_country_t wifiCountry; + wifi_get_country(&wifiCountry); // Note: Should return 0 on success and -1 on failure, but always seems to return 1. Possibly broken API. Channels 1 to 13 are the default limits. + assert(wifiCountry.schan <= newWiFiChannel && newWiFiChannel <= wifiCountry.schan + wifiCountry.nchan - 1); + + _espnowWiFiChannel = newWiFiChannel; +} + +uint8 EspnowDatabase::getWiFiChannel() const +{ + return _espnowWiFiChannel; +} + +bool EspnowDatabase::requestReceived(const uint64_t requestMac, const uint64_t requestID) +{ + return receivedRequests().count(std::make_pair(requestMac, requestID)); +} + +MutexTracker EspnowDatabase::captureEspnowConnectionQueueMutex() +{ + // Syntax like this will move the resulting value into its new position (similar to NRVO): https://stackoverflow.com/a/11540204 + return MutexTracker(_espnowConnectionQueueMutex); +} + +MutexTracker EspnowDatabase::captureEspnowConnectionQueueMutex(const std::function destructorHook) { return MutexTracker(_espnowConnectionQueueMutex, destructorHook); } + +MutexTracker EspnowDatabase::captureResponsesToSendMutex(){ return MutexTracker(_responsesToSendMutex); } + +MutexTracker EspnowDatabase::captureResponsesToSendMutex(const std::function destructorHook) { return MutexTracker(_responsesToSendMutex, destructorHook); } + +void EspnowDatabase::storeSentRequest(const uint64_t targetBSSID, const uint64_t messageID, const RequestData &requestData) +{ + sentRequests().insert(std::make_pair(std::make_pair(targetBSSID, messageID), requestData)); +} + +void EspnowDatabase::storeReceivedRequest(const uint64_t senderBSSID, const uint64_t messageID, const TimeTracker &timeTracker) +{ + receivedRequests().insert(std::make_pair(std::make_pair(senderBSSID, messageID), timeTracker)); +} + +EspnowMeshBackend *EspnowDatabase::getOwnerOfSentRequest(const uint64_t requestMac, const 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 EspnowDatabase::deleteSentRequest(const uint64_t requestMac, const uint64_t requestID) +{ + return sentRequests().erase(std::make_pair(requestMac, requestID)); +} + +size_t EspnowDatabase::deleteSentRequestsByOwner(const 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; +} + +std::list & EspnowDatabase::responsesToSend() { return _responsesToSend; } +std::list & EspnowDatabase::peerRequestConfirmationsToSend() { return _peerRequestConfirmationsToSend; } +std::map, MessageData> & EspnowDatabase::receivedEspnowTransmissions() { return _receivedEspnowTransmissions; } +std::map, RequestData> & EspnowDatabase::sentRequests() { return _sentRequests; } +std::map, TimeTracker> & EspnowDatabase::receivedRequests() { return _receivedRequests; } diff --git a/libraries/ESP8266WiFiMesh/src/EspnowDatabase.h b/libraries/ESP8266WiFiMesh/src/EspnowDatabase.h new file mode 100644 index 0000000000..34ec34f924 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/EspnowDatabase.h @@ -0,0 +1,223 @@ +/* + Copyright (C) 2020 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 +*/ + +#ifndef __ESPNOWDATABASE_H__ +#define __ESPNOWDATABASE_H__ + +#include +#include "EspnowNetworkInfo.h" +#include "TransmissionOutcome.h" +#include "ResponseData.h" +#include "RequestData.h" +#include "EspnowProtocolInterpreter.h" +#include +#include +#include "MessageData.h" +#include "MutexTracker.h" +#include "PeerRequestLog.h" +#include "ConditionalPrinter.h" +#include "TypeConversionFunctions.h" + +class EspnowMeshBackend; + +class EspnowDatabase +{ + +public: + + EspnowDatabase(ConditionalPrinter &conditionalPrinterInstance, const uint8 espnowWiFiChannel); + + static std::vector & connectionQueue(); + static const std::vector & constConnectionQueue(); + static std::vector & latestTransmissionOutcomes(); + static uint32_t criticalHeapLevel(); + static void setCriticalHeapLevelBuffer(const uint32_t bufferInBytes); + static uint32_t criticalHeapLevelBuffer(); + static void setLogEntryLifetimeMs(const uint32_t logEntryLifetimeMs); + static uint32_t logEntryLifetimeMs(); + static void setBroadcastResponseTimeoutMs(const uint32_t broadcastResponseTimeoutMs); + static uint32_t broadcastResponseTimeoutMs(); + static String getScheduledResponseMessage(const uint32_t responseIndex); + static const uint8_t *getScheduledResponseRecipient(const uint32_t responseIndex); + static uint32_t numberOfScheduledResponses(); + static void clearAllScheduledResponses(); + static void deleteScheduledResponsesByRecipient(const uint8_t *recipientMac, const bool encryptedOnly); + static void setEncryptionRequestTimeout(const uint32_t timeoutMs); + static uint32_t getEncryptionRequestTimeout(); + + void setAutoEncryptionDuration(const uint32_t duration); + uint32_t getAutoEncryptionDuration() const; + String getSenderMac() const; + uint8_t *getSenderMac(uint8_t *macArray) const; + String getSenderAPMac() const; + uint8_t *getSenderAPMac(uint8_t *macArray) const; + + using macAndType_td = EspnowProtocolInterpreter::macAndType_td; + using messageID_td = EspnowProtocolInterpreter::messageID_td; + using peerMac_td = EspnowProtocolInterpreter::peerMac_td; + + static size_t deleteSentRequestsByOwner(const EspnowMeshBackend *instancePointer); + static std::list & responsesToSend(); + static std::list & peerRequestConfirmationsToSend(); + static std::map, MessageData> & receivedEspnowTransmissions(); + static std::map, RequestData> & sentRequests(); + static std::map, TimeTracker> & receivedRequests(); + + static bool requestReceived(const uint64_t requestMac, const uint64_t requestID); + + /** + * Will be captured when the connectionQueue should not be modified. + */ + static MutexTracker captureEspnowConnectionQueueMutex(); + static MutexTracker captureEspnowConnectionQueueMutex(const std::function destructorHook); + + /** + * Will be captured when no responsesToSend element should be removed. + */ + static MutexTracker captureResponsesToSendMutex(); + static MutexTracker captureResponsesToSendMutex(const std::function destructorHook); + + static void clearOldLogEntries(bool forced); + + 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. + * + * @return A valid EspnowMeshBackend pointer if a matching entry is found in the EspnowMeshBackend sentRequests container. nullptr otherwise. + */ + static EspnowMeshBackend *getOwnerOfSentRequest(const uint64_t requestMac, const uint64_t requestID); + + /** + * Delete all entries in the sentRequests container where requestMac is noted as having received requestID. + * + * @return The number of entries deleted. + */ + static size_t deleteSentRequest(const uint64_t requestMac, const uint64_t requestID); + + /** + * Set the MAC address considered to be the sender of the most recently received ESP-NOW request, response or broadcast. + * + * @param macArray An uint8_t array which contains the MAC address to store. The method will store the first 6 bytes of the array. + */ + void setSenderMac(const uint8_t *macArray); + + /** + * Set the MAC address considered to be the AP MAC of the sender of the most recently received ESP-NOW request, response or broadcast. + * + * @param macArray An uint8_t array which contains the MAC address to store. The method will store the first 6 bytes of the array. + */ + void setSenderAPMac(const uint8_t *macArray); + + void setWiFiChannel(const uint8 newWiFiChannel); + uint8 getWiFiChannel() const; + + /** + * 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, const bool encryptedOnly) + { + bool macFound = false; + + for(typename std::map, T>::iterator entryIterator = logEntries.begin(); + entryIterator != logEntries.end(); ) + { + if(macAndTypeToUint64Mac(entryIterator->first.first) == MeshTypeConversionFunctions::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 + static void deleteEntriesByMac(std::map, T> &logEntries, const uint8_t *peerMac, const bool encryptedOnly) + { + bool macFound = false; + + for(typename std::map, T>::iterator entryIterator = logEntries.begin(); + entryIterator != logEntries.end(); ) + { + if(entryIterator->first.first == MeshTypeConversionFunctions::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; + } + } + +protected: + + static std::vector _connectionQueue; + static std::vector _latestTransmissionOutcomes; + + static std::list::const_iterator getScheduledResponse(const uint32_t responseIndex); + +private: + + ConditionalPrinter & _conditionalPrinter; + + uint32_t _autoEncryptionDuration = 50; + + template + static void deleteExpiredLogEntries(std::map, T> &logEntries, const uint32_t maxEntryLifetimeMs); + + template + static void deleteExpiredLogEntries(std::map, TimeTracker> &logEntries, const uint32_t maxEntryLifetimeMs); + + static void deleteExpiredLogEntries(std::map, RequestData> &logEntries, const uint32_t requestLifetimeMs, const uint32_t broadcastLifetimeMs); + + template + static void deleteExpiredLogEntries(std::list &logEntries, const uint32_t maxEntryLifetimeMs); + + uint8_t _senderMac[6] = {0}; + uint8_t _senderAPMac[6] = {0}; + + uint8 _espnowWiFiChannel; +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/EspnowEncryptionBroker.cpp b/libraries/ESP8266WiFiMesh/src/EspnowEncryptionBroker.cpp new file mode 100644 index 0000000000..f0d11070b6 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/EspnowEncryptionBroker.cpp @@ -0,0 +1,721 @@ +/* + Copyright (C) 2020 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 "EspnowEncryptionBroker.h" +#include "EspnowMeshBackend.h" +#include "JsonTranslator.h" +#include "UtilityFunctions.h" +#include "Serializer.h" +#include "MeshCryptoInterface.h" + +namespace +{ + using EspnowProtocolInterpreter::encryptedConnectionKeyLength; + using EspnowProtocolInterpreter::hashKeyLength; + using EspnowProtocolInterpreter::maxEncryptedConnections; + + using connectionLogIterator = EspnowConnectionManager::connectionLogIterator; + + namespace TypeCast = MeshTypeConversionFunctions; + + String _ongoingPeerRequestNonce; + EspnowMeshBackend *_ongoingPeerRequester = nullptr; + EncryptedConnectionStatus _ongoingPeerRequestResult = EncryptedConnectionStatus::MAX_CONNECTIONS_REACHED_SELF; + ExpiringTimeTracker _ongoingPeerRequestEncryptionTimeout([](){ return EspnowDatabase::getEncryptionRequestTimeout(); }); + uint8_t _ongoingPeerRequestMac[6] = {0}; + bool _reciprocalPeerRequestConfirmation = false; +} + +EspnowEncryptionBroker::EspnowEncryptionBroker(ConditionalPrinter &conditionalPrinterInstance, EspnowDatabase &databaseInstance, EspnowConnectionManager &connectionManagerInstance, EspnowTransmitter &transmitterInstance) + : _conditionalPrinter(conditionalPrinterInstance), _database(databaseInstance), _connectionManager(connectionManagerInstance), _transmitter(transmitterInstance) +{ +} + +void EspnowEncryptionBroker::handlePeerRequest(const uint8_t *macaddr, uint8_t *dataArray, const uint8_t len, const uint64_t uint64StationMac, const 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 softLimitEncryptedConnectionInfoHeader or maxConnectionsReachedHeader. + // Receive: encryptedConnectionVerificationHeader. + + using namespace EspnowProtocolInterpreter; + + if(!EspnowDatabase::requestReceived(uint64StationMac, receivedMessageID)) + { + EspnowDatabase::storeReceivedRequest(uint64StationMac, receivedMessageID, TimeTracker(millis())); + + bool encryptedCorrectly = synchronizePeerSessionKey(receivedMessageID, macaddr); + String message = getHashKeyLength(dataArray, len); + int32_t messageHeaderEndIndex = message.indexOf(':'); + String messageHeader = message.substring(0, messageHeaderEndIndex + 1); + + if(messageHeader == FPSTR(encryptedConnectionVerificationHeader)) + { + if(encryptedCorrectly) + { + int32_t connectionRequestTypeEndIndex = message.indexOf(':', messageHeaderEndIndex + 1); + String connectionRequestType = message.substring(messageHeaderEndIndex + 1, connectionRequestTypeEndIndex + 1); + connectionLogIterator encryptedConnection = EspnowConnectionManager::connectionLogEndIterator(); + if(!EspnowConnectionManager::getEncryptedConnectionIterator(macaddr, encryptedConnection)) + assert(false && String(F("We must have an encrypted connection if we received an encryptedConnectionVerificationHeader which was encryptedCorrectly."))); + + if(connectionRequestType == FPSTR(encryptionRequestHeader)) + { + EspnowConnectionManager::temporaryEncryptedConnectionToPermanent(macaddr); + } + else if(connectionRequestType == FPSTR(temporaryEncryptionRequestHeader)) + { + 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 && String(F("Unknown P-type verification message received!"))); + } + } + } + else if(messageHeader == FPSTR(encryptionRequestHeader) || messageHeader == FPSTR(temporaryEncryptionRequestHeader)) + { + // If there is a espnowRequestManager, get it + if(EspnowMeshBackend *currentEspnowRequestManager = EspnowMeshBackend::getEspnowRequestManager()) + { + String requestNonce; + + if(JsonTranslator::getNonce(message, requestNonce) && requestNonce.length() >= 12) // The destination MAC address requires 12 characters. + { + uint8_t destinationMac[6] = {0}; + TypeCast::stringToMac(requestNonce, destinationMac); + + uint8_t apMac[6] {0}; + WiFi.softAPmacAddress(apMac); + + bool correctDestination = false; + if(MeshUtilityFunctions::macEqual(destinationMac, apMac)) + { + correctDestination = true; + } + else + { + uint8_t staMac[6] {0}; + WiFi.macAddress(staMac); + + if(MeshUtilityFunctions::macEqual(destinationMac, staMac)) + { + correctDestination = true; + } + } + + uint8_t apMacArray[6] = { 0 }; + if(correctDestination && verifyEncryptionRequestHmac(message, macaddr, getTransmissionMac(dataArray, apMacArray), currentEspnowRequestManager->getEspnowHashKey(), hashKeyLength)) + EspnowDatabase::peerRequestConfirmationsToSend().emplace_back(receivedMessageID, encryptedCorrectly, currentEspnowRequestManager->getMeshPassword(), currentEspnowRequestManager->encryptedConnectionsSoftLimit(), + requestNonce, macaddr, apMacArray, currentEspnowRequestManager->getEspnowHashKey()); + } + } + } + else if(messageHeader == FPSTR(encryptedConnectionRemovalRequestHeader)) + { + if(encryptedCorrectly) + EspnowConnectionManager::removeEncryptedConnection(macaddr); + } + else + { + assert(false && String(F("Unknown P-type message received!"))); + } + } +} + +void EspnowEncryptionBroker::handlePeerRequestConfirmation(uint8_t *macaddr, uint8_t *dataArray, const 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 softLimitEncryptedConnectionInfoHeader or maxConnectionsReachedHeader. + // Send: encryptedConnectionVerificationHeader. + + using namespace EspnowProtocolInterpreter; + + if(!_ongoingPeerRequestNonce.isEmpty()) + { + String message = getHashKeyLength(dataArray, len); + String requestNonce; + + 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); + uint8_t apMacArray[6] = { 0 }; + getTransmissionMac(dataArray, apMacArray); + + if(messageHeader == FPSTR(basicConnectionInfoHeader)) + { + // encryptedConnectionEstablished(_ongoingPeerRequestResult) means we have already received a basicConnectionInfoHeader + if(!encryptedConnectionEstablished(_ongoingPeerRequestResult) && + verifyEncryptionRequestHmac(message, macaddr, apMacArray, _ongoingPeerRequester->getEspnowHashKey(), hashKeyLength)) + { + _ongoingPeerRequestEncryptionTimeout.reset(); + + connectionLogIterator existingEncryptedConnection = EspnowConnectionManager::connectionLogEndIterator(); + + if(!EspnowConnectionManager::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, apMacArray, createSessionKey(), createSessionKey(), EspnowDatabase::getEncryptionRequestTimeout()); + } + else + { + // Encrypted connection already exists + _ongoingPeerRequestResult = EncryptedConnectionStatus::CONNECTION_ESTABLISHED; + + if(auto timeTrackerPointer = existingEncryptedConnection->temporary()) + { + if(timeTrackerPointer->remainingDuration() < EspnowDatabase::getEncryptionRequestTimeout()) // Should only extend duration for existing connections. + { + existingEncryptedConnection->setRemainingDuration(EspnowDatabase::getEncryptionRequestTimeout()); + } + } + } + + if(!encryptedConnectionEstablished(_ongoingPeerRequestResult)) + { + // Adding connection failed, abort ongoing peer request. + _ongoingPeerRequestNonce.clear(); + } + } + } + else if(messageHeader == FPSTR(encryptedConnectionInfoHeader) || messageHeader == FPSTR(softLimitEncryptedConnectionInfoHeader)) + { + 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 = EspnowConnectionManager::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); + + if(messageHeader == FPSTR(encryptedConnectionInfoHeader)) + _ongoingPeerRequestResult = EncryptedConnectionStatus::CONNECTION_ESTABLISHED; + else if(messageHeader == FPSTR(softLimitEncryptedConnectionInfoHeader)) + _ongoingPeerRequestResult = EncryptedConnectionStatus::SOFT_LIMIT_CONNECTION_ESTABLISHED; + else + assert(false && String(F("Unknown _ongoingPeerRequestResult!"))); + } + else + { + _ongoingPeerRequestResult = EncryptedConnectionStatus::REQUEST_TRANSMISSION_FAILED; + } + + _ongoingPeerRequestNonce.clear(); + } + } + else if(messageHeader == FPSTR(maxConnectionsReachedHeader)) + { + if(verifyEncryptionRequestHmac(message, macaddr, apMacArray, _ongoingPeerRequester->getEspnowHashKey(), hashKeyLength)) + { + _ongoingPeerRequestResult = EncryptedConnectionStatus::MAX_CONNECTIONS_REACHED_PEER; + _ongoingPeerRequestNonce.clear(); + } + } + else + { + assert(messageHeader == FPSTR(basicConnectionInfoHeader) || messageHeader == FPSTR(encryptedConnectionInfoHeader) || + messageHeader == FPSTR(softLimitEncryptedConnectionInfoHeader) || messageHeader == FPSTR(maxConnectionsReachedHeader)); + } + } + } +} + +void EspnowEncryptionBroker::sendPeerRequestConfirmations(const ExpiringTimeTracker *estimatedMaxDurationTracker) +{ + uint32_t bufferedCriticalHeapLevel = EspnowDatabase::criticalHeapLevel() + EspnowDatabase::criticalHeapLevelBuffer(); // We preferably want to start clearing the logs a bit before things get critical. + // _ongoingPeerRequestNonce can change during every delay(), but we need to remember the initial value to know from where sendPeerRequestConfirmations was called. + String initialOngoingPeerRequestNonce = _ongoingPeerRequestNonce; + + for(std::list::iterator confirmationsIterator = EspnowDatabase::peerRequestConfirmationsToSend().begin(); confirmationsIterator != EspnowDatabase::peerRequestConfirmationsToSend().end(); ) + { + using namespace EspnowProtocolInterpreter; + + // True if confirmationsIterator contains a peer request received from the same node we are currently sending a peer request to. + bool reciprocalPeerRequest = !initialOngoingPeerRequestNonce.isEmpty() && confirmationsIterator->connectedTo(_ongoingPeerRequestMac); + + auto timeTrackerPointer = confirmationsIterator->temporary(); + assert(timeTrackerPointer); // peerRequestConfirmations should always expire and so should always have a timeTracker + if(timeTrackerPointer->elapsedTime() > EspnowDatabase::getEncryptionRequestTimeout() + || (reciprocalPeerRequest && confirmationsIterator->getPeerRequestNonce() <= initialOngoingPeerRequestNonce)) + { + // The peer request has expired, + // or the peer request comes from the node we are currently making a peer request to ourselves and we are supposed to wait in this event to avoid simultaneous session key transfer. + ++confirmationsIterator; + continue; + } + + uint8_t defaultBSSID[6] {0}; + confirmationsIterator->getEncryptedPeerMac(defaultBSSID); + uint8_t unencryptedBSSID[6] {0}; + confirmationsIterator->getUnencryptedPeerMac(unencryptedBSSID); + uint8_t hashKey[hashKeyLength] {0}; + confirmationsIterator->getHashKey(hashKey); + + EncryptedConnectionLog *existingEncryptedConnection = EspnowConnectionManager::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). + + if(!existingEncryptedConnection && + ((reciprocalPeerRequest && EspnowConnectionManager::encryptedConnections().size() >= maxEncryptedConnections) || (!reciprocalPeerRequest && reservedEncryptedConnections() >= maxEncryptedConnections))) + { + EspnowTransmitter::espnowSendPeerRequestConfirmationsUnsynchronized(Serializer::createEncryptionRequestHmacMessage(FPSTR(maxConnectionsReachedHeader), + confirmationsIterator->getPeerRequestNonce(), hashKey, hashKeyLength), + defaultBSSID, 'C'); // Generates a new message ID to avoid sending encrypted sessionKeys over unencrypted connections. + + confirmationsIterator = EspnowDatabase::peerRequestConfirmationsToSend().erase(confirmationsIterator); + } + else if(EspnowTransmitter::espnowSendPeerRequestConfirmationsUnsynchronized(Serializer::createEncryptionRequestHmacMessage(FPSTR(basicConnectionInfoHeader), + confirmationsIterator->getPeerRequestNonce(), hashKey, hashKeyLength), + sendToDefaultBSSID ? defaultBSSID : unencryptedBSSID, 'C') // Generates a new message ID to avoid sending encrypted sessionKeys over unencrypted connections. + == TransmissionStatusType::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(EspnowDatabase::getEncryptionRequestTimeout() > timeTrackerPointer->remainingDuration()) + { + existingEncryptedConnection->setRemainingDuration(EspnowDatabase::getEncryptionRequestTimeout()); + } + } + } + else if(EspnowMeshBackend *currentEspnowRequestManager = EspnowMeshBackend::getEspnowRequestManager()) + { + uint8_t staMacArray[6] = { 0 }; + uint8_t apMacArray[6] = { 0 }; + currentEspnowRequestManager->addTemporaryEncryptedConnection(confirmationsIterator->getPeerStaMac(staMacArray), confirmationsIterator->getPeerApMac(apMacArray), + createSessionKey(), createSessionKey(), EspnowDatabase::getEncryptionRequestTimeout()); + existingEncryptedConnection = EspnowConnectionManager::getEncryptedConnection(defaultBSSID); + } + else + { + ConditionalPrinter::warningPrint(String(F("WARNING! Ignoring received encrypted connection request since no EspnowRequestManager is assigned."))); + } + + if(!existingEncryptedConnection) + { + // Send "node full" message + EspnowTransmitter::espnowSendPeerRequestConfirmationsUnsynchronized(Serializer::createEncryptionRequestHmacMessage(FPSTR(maxConnectionsReachedHeader), + confirmationsIterator->getPeerRequestNonce(), hashKey, hashKeyLength), + defaultBSSID, 'C'); // Generates a new message ID to avoid sending encrypted sessionKeys over unencrypted connections. + } + else + { + if(reciprocalPeerRequest) + _reciprocalPeerRequestConfirmation = true; + + delay(5); // Give some time for the peer to add an encrypted connection + + assert(esp_now_is_peer_exist(defaultBSSID) > 0 && String(F("ERROR! Attempting to send content marked as encrypted via unencrypted connection!"))); + + String messageHeader; + + if(existingEncryptedConnection->temporary() && // Should never change permanent connections + ((reciprocalPeerRequest && EspnowConnectionManager::encryptedConnections().size() > confirmationsIterator->getEncryptedConnectionsSoftLimit()) + || (!reciprocalPeerRequest && reservedEncryptedConnections() > confirmationsIterator->getEncryptedConnectionsSoftLimit()))) + { + messageHeader = FPSTR(softLimitEncryptedConnectionInfoHeader); + } + else + { + messageHeader = FPSTR(encryptedConnectionInfoHeader); + } + + // 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. + EspnowTransmitter::espnowSendPeerRequestConfirmationsUnsynchronized(Serializer::createEncryptedConnectionInfo(messageHeader, + confirmationsIterator->getPeerRequestNonce(), confirmationsIterator->getAuthenticationPassword(), + existingEncryptedConnection->getOwnSessionKey(), existingEncryptedConnection->getPeerSessionKey()), + defaultBSSID, 'C'); // Generates a new message ID to avoid sending encrypted sessionKeys over unencrypted connections. + } + + confirmationsIterator = EspnowDatabase::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. + ConditionalPrinter::warningPrint("WARNING! Free heap below chosen minimum. Performing emergency log clearing."); + EspnowDatabase::clearOldLogEntries(true); + return; // confirmationsIterator may be invalid now. Also, we should give the main loop a chance to respond to the situation. + } + + if(estimatedMaxDurationTracker && estimatedMaxDurationTracker->expired()) + return; + } +} + +EncryptedConnectionStatus EspnowEncryptionBroker::requestEncryptedConnection(const uint8_t *peerMac, EspnowMeshBackend &espnowInstance) +{ + using namespace std::placeholders; + return requestEncryptedConnectionKernel(peerMac, std::bind(defaultEncryptionRequestBuilder, FPSTR(EspnowProtocolInterpreter::encryptionRequestHeader), 0, _connectionManager.getEspnowHashKey(), _1, _2), espnowInstance); +} + +EncryptedConnectionStatus EspnowEncryptionBroker::requestTemporaryEncryptedConnection(const uint8_t *peerMac, const uint32_t durationMs, EspnowMeshBackend &espnowInstance) +{ + using namespace std::placeholders; + return requestEncryptedConnectionKernel(peerMac, std::bind(defaultEncryptionRequestBuilder, FPSTR(EspnowProtocolInterpreter::temporaryEncryptionRequestHeader), + durationMs, _connectionManager.getEspnowHashKey(), _1, _2), espnowInstance); +} + +EncryptedConnectionStatus EspnowEncryptionBroker::requestFlexibleTemporaryEncryptedConnection(const uint8_t *peerMac, const uint32_t minDurationMs, EspnowMeshBackend &espnowInstance) +{ + using namespace std::placeholders; + return requestEncryptedConnectionKernel(peerMac, std::bind(flexibleEncryptionRequestBuilder, minDurationMs, _connectionManager.getEspnowHashKey(), _1, _2), espnowInstance); +} + +EncryptedConnectionRemovalOutcome EspnowEncryptionBroker::requestEncryptedConnectionRemoval(const uint8_t *peerMac) +{ + using EspnowProtocolInterpreter::encryptedConnectionRemovalRequestHeader; + + assert(EspnowConnectionManager::encryptedConnections().size() <= maxEncryptedConnections); // If this is not the case, ESP-NOW is no longer in sync with the library + + MutexTracker mutexTracker(EspnowTransmitter::captureEspnowTransmissionMutex(EspnowConnectionManager::handlePostponedRemovals)); + if(!mutexTracker.mutexCaptured()) + { + assert(false && String(F("ERROR! Transmission in progress. Don't call requestEncryptedConnectionRemoval from callbacks as this may corrupt program state! Aborting."))); + return EncryptedConnectionRemovalOutcome::REMOVAL_REQUEST_FAILED; + } + + if(EncryptedConnectionLog *encryptedConnection = EspnowConnectionManager::getEncryptedConnection(peerMac)) + { + if(EspnowTransmitter::espnowSendToNode(FPSTR(encryptedConnectionRemovalRequestHeader), peerMac, 'P') == TransmissionStatusType::TRANSMISSION_COMPLETE) + { + return EspnowConnectionManager::removeEncryptedConnectionUnprotected(peerMac); + } + else + { + if(encryptedConnection->removalScheduled()) + return EncryptedConnectionRemovalOutcome::REMOVAL_SUCCEEDED; // Removal will be completed by mutex destructorHook. + else + return EncryptedConnectionRemovalOutcome::REMOVAL_REQUEST_FAILED; + } + } + + // peerMac is already removed + return EncryptedConnectionRemovalOutcome::REMOVAL_SUCCEEDED; +} + +bool EspnowEncryptionBroker::encryptedConnectionEstablished(const EncryptedConnectionStatus connectionStatus) +{ + return static_cast(connectionStatus) > 0; +} + +void EspnowEncryptionBroker::setReceivedEncryptedTransmission(const bool receivedEncryptedTransmission) { _receivedEncryptedTransmission = receivedEncryptedTransmission; } +bool EspnowEncryptionBroker::receivedEncryptedTransmission() const {return _receivedEncryptedTransmission;} + +bool EspnowEncryptionBroker::verifyEncryptionRequestHmac(const String &encryptionRequestHmacMessage, const uint8_t *requesterStaMac, const uint8_t *requesterApMac, + const uint8_t *hashKey, const uint8_t hashKeyLength) +{ + using MeshCryptoInterface::verifyMeshHmac; + using namespace JsonTranslator; + + String hmac; + if(getHmac(encryptionRequestHmacMessage, hmac)) + { + int32_t hmacStartIndex = encryptionRequestHmacMessage.indexOf(String('"') + FPSTR(jsonHmac) + F("\":")); + if(hmacStartIndex < 0) + return false; + + if(hmac.length() == 2*experimental::crypto::SHA256::NATURAL_LENGTH // We know that each HMAC byte should become 2 String characters due to uint8ArrayToHexString. + && verifyMeshHmac(TypeCast::macToString(requesterStaMac) + TypeCast::macToString(requesterApMac) + encryptionRequestHmacMessage.substring(0, hmacStartIndex), hmac, hashKey, hashKeyLength)) + { + return true; + } + } + + return false; +} + +bool EspnowEncryptionBroker::verifyPeerSessionKey(const uint64_t sessionKey, const uint8_t *peerMac, const char messageType) +{ + if(EncryptedConnectionLog *encryptedConnection = EspnowConnectionManager::getEncryptedConnection(peerMac)) + { + return verifyPeerSessionKey(sessionKey, *encryptedConnection, TypeCast::macToUint64(peerMac), messageType); + } + + return false; +} + +bool EspnowEncryptionBroker::verifyPeerSessionKey(const uint64_t sessionKey, const EncryptedConnectionLog &encryptedConnection, const uint64_t uint64PeerMac, const char messageType) +{ + using namespace EspnowProtocolInterpreter; + + if(usesEncryption(sessionKey)) + { + if(sessionKey == encryptedConnection.getPeerSessionKey() + || EspnowDatabase::receivedEspnowTransmissions().find(std::make_pair(createMacAndTypeValue(uint64PeerMac, messageType), sessionKey)) + != EspnowDatabase::receivedEspnowTransmissions().end()) + { + // If sessionKey is correct or sessionKey is one part of a multi-part transmission. + return true; + } + } + + return false; +} + +bool EspnowEncryptionBroker::synchronizePeerSessionKey(const uint64_t sessionKey, const uint8_t *peerMac) +{ + if(EncryptedConnectionLog *encryptedConnection = EspnowConnectionManager::getEncryptedConnection(peerMac)) + { + return synchronizePeerSessionKey(sessionKey, *encryptedConnection); + } + + return false; +} + +bool EspnowEncryptionBroker::synchronizePeerSessionKey(const uint64_t sessionKey, EncryptedConnectionLog &encryptedConnection) +{ + if(EspnowProtocolInterpreter::usesEncryption(sessionKey)) + { + if(sessionKey == encryptedConnection.getPeerSessionKey()) + { + uint8_t hashKey[hashKeyLength] {0}; + encryptedConnection.setPeerSessionKey(EncryptedConnectionLog::incrementSessionKey(sessionKey, encryptedConnection.getHashKey(hashKey), hashKeyLength)); + return true; + } + } + + return false; +} + +uint8_t EspnowEncryptionBroker::reservedEncryptedConnections() +{ + if(!_ongoingPeerRequestNonce.isEmpty()) + if(!EspnowConnectionManager::getEncryptedConnection(_ongoingPeerRequestMac)) + return EspnowConnectionManager::encryptedConnections().size() + 1; // Reserve one connection spot if we are currently making a peer request to a new node. + + return EspnowConnectionManager::encryptedConnections().size(); +} + +EncryptedConnectionStatus EspnowEncryptionBroker::requestEncryptedConnectionKernel(const uint8_t *peerMac, const encryptionRequestBuilderType &encryptionRequestBuilder, EspnowMeshBackend &espnowInstance) +{ + using namespace EspnowProtocolInterpreter; + + assert(EspnowConnectionManager::encryptedConnections().size() <= maxEncryptedConnections); // If this is not the case, ESP-NOW is no longer in sync with the library + + MutexTracker mutexTracker(EspnowTransmitter::captureEspnowTransmissionMutex(EspnowConnectionManager::handlePostponedRemovals)); + if(!mutexTracker.mutexCaptured()) + { + assert(false && String(F("ERROR! Transmission in progress. Don't call requestEncryptedConnection from callbacks as this may corrupt program state! Aborting."))); + return EncryptedConnectionStatus::REQUEST_TRANSMISSION_FAILED; + } + + EncryptedConnectionLog *existingEncryptedConnection = EspnowConnectionManager::getEncryptedConnection(peerMac); + ExpiringTimeTracker existingTimeTracker = existingEncryptedConnection && existingEncryptedConnection->temporary() ? + *existingEncryptedConnection->temporary() : ExpiringTimeTracker(0); + + if(!existingEncryptedConnection && EspnowConnectionManager::encryptedConnections().size() >= maxEncryptedConnections) + { + assert(EspnowConnectionManager::encryptedConnections().size() == maxEncryptedConnections); + + // No capacity for more encrypted connections. + return EncryptedConnectionStatus::MAX_CONNECTIONS_REACHED_SELF; + } + + String requestNonce = TypeCast::macToString(peerMac) + TypeCast::uint64ToString(MeshUtilityFunctions::randomUint64()) + + TypeCast::uint64ToString(MeshUtilityFunctions::randomUint64()); + _ongoingPeerRequestResult = EncryptedConnectionStatus::REQUEST_TRANSMISSION_FAILED; + _ongoingPeerRequestNonce = requestNonce; + _ongoingPeerRequester = &espnowInstance; + _reciprocalPeerRequestConfirmation = false; + std::copy_n(peerMac, 6, _ongoingPeerRequestMac); + String requestMessage = encryptionRequestBuilder(requestNonce, existingTimeTracker); + + _conditionalPrinter.verboseModePrint(String(F("Sending encrypted connection request to: ")) + TypeCast::macToString(peerMac)); + + if(EspnowTransmitter::espnowSendToNode(requestMessage, peerMac, 'P') == TransmissionStatusType::TRANSMISSION_COMPLETE) + { + ExpiringTimeTracker requestTimeout([](){ return EspnowDatabase::getEncryptionRequestTimeout(); }); + // _ongoingPeerRequestNonce is set to "" when a peer confirmation response from the mac is received + while(!requestTimeout && !_ongoingPeerRequestNonce.isEmpty()) + { + // For obvious reasons dividing by exactly 10 is a good choice. + ExpiringTimeTracker maxDurationTracker = ExpiringTimeTracker(EspnowDatabase::getEncryptionRequestTimeout()/10); + sendPeerRequestConfirmations(&maxDurationTracker); // Must be called before delay() to ensure !_ongoingPeerRequestNonce.isEmpty() is still true, so reciprocal peer request order is preserved. + delay(1); + } + } + + if(!_ongoingPeerRequestNonce.isEmpty()) + { + // If nonce != "" we only received the basic connection info, so the pairing process is incomplete + _ongoingPeerRequestResult = EncryptedConnectionStatus::REQUEST_TRANSMISSION_FAILED; + _ongoingPeerRequestNonce.clear(); + } + else if(encryptedConnectionEstablished(_ongoingPeerRequestResult)) + { + if(_ongoingPeerRequestResult == EncryptedConnectionStatus::CONNECTION_ESTABLISHED) + // Give the builder a chance to update the message + requestMessage = encryptionRequestBuilder(requestNonce, existingTimeTracker); + else if(_ongoingPeerRequestResult == EncryptedConnectionStatus::SOFT_LIMIT_CONNECTION_ESTABLISHED) + // We will only get a soft limit connection. Adjust future actions based on this. + requestMessage = Serializer::createEncryptionRequestHmacMessage(FPSTR(temporaryEncryptionRequestHeader), requestNonce, _connectionManager.getEspnowHashKey(), + hashKeyLength, _database.getAutoEncryptionDuration()); + else + assert(false && String(F("Unknown _ongoingPeerRequestResult during encrypted connection finalization!"))); + + 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(EspnowTransmitter::espnowSendToNode(String(FPSTR(encryptedConnectionVerificationHeader)) + requestMessage, peerMac, 'P') == TransmissionStatusType::TRANSMISSION_COMPLETE + && !_ongoingPeerRequestEncryptionTimeout) + { + EncryptedConnectionLog *encryptedConnection = EspnowConnectionManager::getEncryptedConnection(peerMac); + if(!encryptedConnection) + { + assert(encryptedConnection && String(F("requestEncryptedConnectionKernel cannot find an encrypted connection!"))); + // requestEncryptedConnectionRemoval received. + _ongoingPeerRequestResult = EncryptedConnectionStatus::REQUEST_TRANSMISSION_FAILED; + } + else if(encryptedConnection->removalScheduled() || (encryptedConnection->temporary() && encryptedConnection->temporary()->expired())) + { + // Could possibly be caused by a simultaneous temporary peer request from the peer. + _ongoingPeerRequestResult = EncryptedConnectionStatus::REQUEST_TRANSMISSION_FAILED; + } + else + { + // Finalize connection + if(messageHeader == FPSTR(encryptionRequestHeader)) + { + EspnowConnectionManager::temporaryEncryptedConnectionToPermanent(peerMac); + } + else if(messageHeader == FPSTR(temporaryEncryptionRequestHeader)) + { + if(encryptedConnection->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 && String(F("Unknown messageHeader during encrypted connection finalization!"))); + _ongoingPeerRequestResult = EncryptedConnectionStatus::API_CALL_FAILED; + } + } + } + else + { + _ongoingPeerRequestResult = EncryptedConnectionStatus::REQUEST_TRANSMISSION_FAILED; + } + } + + if(!encryptedConnectionEstablished(_ongoingPeerRequestResult)) + { + if(!existingEncryptedConnection && !_reciprocalPeerRequestConfirmation) + { + // Remove any connection that was added during the request attempt and is no longer in use. + EspnowConnectionManager::removeEncryptedConnectionUnprotected(peerMac); + } + } + + _ongoingPeerRequester = nullptr; + + return _ongoingPeerRequestResult; +} + +EncryptedConnectionStatus EspnowEncryptionBroker::initiateAutoEncryptingConnection(const EspnowNetworkInfo &recipientInfo, const bool requestPermanentConnection, uint8_t *targetBSSID, EncryptedConnectionLog **existingEncryptedConnection, EspnowMeshBackend &espnowInstance) +{ + assert(recipientInfo.BSSID() != nullptr); // We need at least the BSSID to connect + recipientInfo.getBSSID(targetBSSID); + + if(_conditionalPrinter.verboseMode()) // Avoid string generation if not required + { + espnowInstance.printAPInfo(recipientInfo); + _conditionalPrinter.verboseModePrint(emptyString); + } + + *existingEncryptedConnection = EspnowConnectionManager::getEncryptedConnection(targetBSSID); + EncryptedConnectionStatus connectionStatus = EncryptedConnectionStatus::MAX_CONNECTIONS_REACHED_SELF; + + if(requestPermanentConnection) + connectionStatus = requestEncryptedConnection(targetBSSID, espnowInstance); + else + connectionStatus = requestFlexibleTemporaryEncryptedConnection(targetBSSID, _database.getAutoEncryptionDuration(), espnowInstance); + + return connectionStatus; +} + +void EspnowEncryptionBroker::finalizeAutoEncryptingConnection(const uint8_t *targetBSSID, const EncryptedConnectionLog *existingEncryptedConnection, const bool requestPermanentConnection) +{ + if(!existingEncryptedConnection && !requestPermanentConnection && !_reciprocalPeerRequestConfirmation) + { + // Remove any connection that was added during the transmission attempt and is no longer in use. + EspnowConnectionManager::removeEncryptedConnectionUnprotected(targetBSSID); + } +} + +String EspnowEncryptionBroker::defaultEncryptionRequestBuilder(const String &requestHeader, const uint32_t durationMs, const uint8_t *hashKey, + const String &requestNonce, const ExpiringTimeTracker &existingTimeTracker) +{ + (void)existingTimeTracker; // This removes a "unused parameter" compiler warning. Does nothing else. + + return Serializer::createEncryptionRequestHmacMessage(requestHeader, requestNonce, hashKey, hashKeyLength, durationMs); +} + +String EspnowEncryptionBroker::flexibleEncryptionRequestBuilder(const uint32_t minDurationMs, const uint8_t *hashKey, + const String &requestNonce, const ExpiringTimeTracker &existingTimeTracker) +{ + using namespace JsonTranslator; + using EspnowProtocolInterpreter::temporaryEncryptionRequestHeader; + + uint32_t connectionDuration = minDurationMs >= existingTimeTracker.remainingDuration() ? + minDurationMs : existingTimeTracker.remainingDuration(); + + return Serializer::createEncryptionRequestHmacMessage(FPSTR(temporaryEncryptionRequestHeader), requestNonce, hashKey, hashKeyLength, connectionDuration); +} diff --git a/libraries/ESP8266WiFiMesh/src/EspnowEncryptionBroker.h b/libraries/ESP8266WiFiMesh/src/EspnowEncryptionBroker.h new file mode 100644 index 0000000000..25f4720979 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/EspnowEncryptionBroker.h @@ -0,0 +1,105 @@ +/* + Copyright (C) 2020 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 +*/ + +#ifndef __ESPNOWENCRYPTIONBROKER_H__ +#define __ESPNOWENCRYPTIONBROKER_H__ + +#include +#include "ConditionalPrinter.h" +#include "EspnowDatabase.h" +#include "EspnowConnectionManager.h" +#include "EspnowTransmitter.h" + +class EspnowMeshBackend; + +class EspnowEncryptionBroker +{ + +public: + + EspnowEncryptionBroker(ConditionalPrinter &conditionalPrinterInstance, EspnowDatabase &databaseInstance, EspnowConnectionManager &connectionManagerInstance, EspnowTransmitter &transmitterInstance); + + static void handlePeerRequest(const uint8_t *macaddr, uint8_t *dataArray, const uint8_t len, const uint64_t uint64StationMac, const uint64_t receivedMessageID); + static void handlePeerRequestConfirmation(uint8_t *macaddr, uint8_t *dataArray, const uint8_t len); + + /* + * @param estimatedMaxDurationTracker A pointer to an ExpiringTimeTracker initialized with the desired max duration for the method. If set to nullptr there is no duration limit. + * Note that setting the estimatedMaxDuration too low may result in missed ESP-NOW transmissions because of too little time for maintenance. + * Also note that although the method will try to respect the max duration limit, there is no guarantee. Overshoots by tens of milliseconds are possible. + */ + static void sendPeerRequestConfirmations(const ExpiringTimeTracker *estimatedMaxDurationTracker = nullptr); + + EncryptedConnectionStatus requestEncryptedConnection(const uint8_t *peerMac, EspnowMeshBackend &espnowInstance); + EncryptedConnectionStatus requestTemporaryEncryptedConnection(const uint8_t *peerMac, const uint32_t durationMs, EspnowMeshBackend &espnowInstance); + EncryptedConnectionStatus requestFlexibleTemporaryEncryptedConnection(const uint8_t *peerMac, const uint32_t minDurationMs, EspnowMeshBackend &espnowInstance); + EncryptedConnectionRemovalOutcome requestEncryptedConnectionRemoval(const uint8_t *peerMac); + + static bool encryptedConnectionEstablished(const EncryptedConnectionStatus connectionStatus); + + static bool verifyPeerSessionKey(const uint64_t sessionKey, const uint8_t *peerMac, const char messageType); + static bool verifyPeerSessionKey(const uint64_t sessionKey, const EncryptedConnectionLog &encryptedConnection, const uint64_t uint64PeerMac, const char messageType); + + static bool synchronizePeerSessionKey(const uint64_t sessionKey, const uint8_t *peerMac); + static bool synchronizePeerSessionKey(const uint64_t sessionKey, EncryptedConnectionLog &encryptedConnection); + + /** + * Set whether the most recently received ESP-NOW request, response or broadcast is presented as having been sent over an encrypted connection or not + * + * @param receivedEncryptedTransmission If true, the request, response or broadcast is presented as having been sent over an encrypted connection. + */ + void setReceivedEncryptedTransmission(const bool receivedEncryptedTransmission); + bool receivedEncryptedTransmission() const; + + /** + * reservedEncryptedConnections never underestimates but sometimes temporarily overestimates. + * numberOfEncryptedConnections sometimes temporarily underestimates but never overestimates. + * + * @return The current number of encrypted ESP-NOW connections, but with an encrypted connection immediately reserved if required while making a peer request. + */ + static uint8_t reservedEncryptedConnections(); + + EncryptedConnectionStatus initiateAutoEncryptingConnection(const EspnowNetworkInfo &recipientInfo, const bool requestPermanentConnection, uint8_t *targetBSSID, EncryptedConnectionLog **existingEncryptedConnection, EspnowMeshBackend &espnowInstance); + void finalizeAutoEncryptingConnection(const uint8_t *targetBSSID, const EncryptedConnectionLog *existingEncryptedConnection, const bool requestPermanentConnection); + +private: + + ConditionalPrinter & _conditionalPrinter; + EspnowDatabase & _database; + EspnowConnectionManager & _connectionManager; + EspnowTransmitter & _transmitter; + + using encryptionRequestBuilderType = std::function; + static String defaultEncryptionRequestBuilder(const String &requestHeader, const uint32_t durationMs, const uint8_t *hashKey, const String &requestNonce, const ExpiringTimeTracker &existingTimeTracker); + static String flexibleEncryptionRequestBuilder(const uint32_t minDurationMs, const uint8_t *hashKey, const String &requestNonce, const ExpiringTimeTracker &existingTimeTracker); + + /** + * 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 found in Serializer::createEncryptionRequestHmacMessage. + * @param espnowInstance The EspnowMeshBackend instance that is requesting the encrypted connection. + * @return The ultimate status of the requested encrypted connection, as EncryptedConnectionStatus. + */ + EncryptedConnectionStatus requestEncryptedConnectionKernel(const uint8_t *peerMac, const encryptionRequestBuilderType &encryptionRequestBuilder, EspnowMeshBackend &espnowInstance); + + static bool verifyEncryptionRequestHmac(const String &encryptionRequestHmacMessage, const uint8_t *requesterStaMac, const uint8_t *requesterApMac, const uint8_t *hashKey, const uint8_t hashKeyLength); + + bool _receivedEncryptedTransmission = false; +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.cpp b/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.cpp new file mode 100644 index 0000000000..f89f300813 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.cpp @@ -0,0 +1,1044 @@ +/* + 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 "MeshCryptoInterface.h" +#include "Serializer.h" + +namespace +{ + using EspnowProtocolInterpreter::encryptedConnectionKeyLength; + using EspnowProtocolInterpreter::hashKeyLength; + + namespace TypeCast = MeshTypeConversionFunctions; + + EspnowMeshBackend *_espnowRequestManager = nullptr; +} + +void espnowDelay(uint32_t durationMs) +{ + ExpiringTimeTracker timeout(durationMs); + + do + { + // We want to delay before performEspnowMaintenance() so background tasks can be managed first. + // Initial while combined with YieldAndDelayMs polledTimeout::YieldPolicy is not suitable since the delay then occurs before evaluating the condition (meaning durationMs = 1 never executes the loop interior). + delay(1); + EspnowMeshBackend::performEspnowMaintenance(); + } + while(!timeout); +} + +EspnowMeshBackend::EspnowMeshBackend(const requestHandlerType requestHandler, const responseHandlerType responseHandler, const networkFilterType networkFilter, + const broadcastFilterType broadcastFilter, const String &meshPassword, const String &ssidPrefix, const String &ssidSuffix, const bool verboseMode, + const uint8 meshWiFiChannel) + : MeshBackendBase(requestHandler, responseHandler, networkFilter, MeshBackendType::ESP_NOW), + _database(*getConditionalPrinter(), meshWiFiChannel), _connectionManager(*getConditionalPrinter(), *getDatabase()), + _transmitter(*getConditionalPrinter(), *getDatabase(), *getConnectionManager()), + _encryptionBroker(*getConditionalPrinter(), *getDatabase(), *getConnectionManager(), *getTransmitter()) +{ + setBroadcastFilter(broadcastFilter); + setSSID(ssidPrefix, emptyString, ssidSuffix); + setMeshPassword(meshPassword); + setVerboseModeState(verboseMode); + EspnowMeshBackend::setWiFiChannel(meshWiFiChannel); +} + +EspnowMeshBackend::EspnowMeshBackend(const requestHandlerType requestHandler, const responseHandlerType responseHandler, const networkFilterType networkFilter, + const broadcastFilterType broadcastFilter, const String &meshPassword, const uint8_t espnowEncryptedConnectionKey[encryptedConnectionKeyLength], + const uint8_t espnowHashKey[hashKeyLength], const String &ssidPrefix, const String &ssidSuffix, const bool verboseMode, + const uint8 meshWiFiChannel) + : EspnowMeshBackend(requestHandler, responseHandler, networkFilter, broadcastFilter, meshPassword, ssidPrefix, ssidSuffix, verboseMode, meshWiFiChannel) +{ + setEspnowEncryptedConnectionKey(espnowEncryptedConnectionKey); + setEspnowHashKey(espnowHashKey); +} + +EspnowMeshBackend::EspnowMeshBackend(const requestHandlerType requestHandler, const responseHandlerType responseHandler, const networkFilterType networkFilter, + const broadcastFilterType broadcastFilter, const String &meshPassword, const String &espnowEncryptedConnectionKeySeed, + const String &espnowHashKeySeed, const String &ssidPrefix, const String &ssidSuffix, const bool verboseMode, + const uint8 meshWiFiChannel) + : EspnowMeshBackend(requestHandler, responseHandler, networkFilter, broadcastFilter, meshPassword, ssidPrefix, ssidSuffix, verboseMode, meshWiFiChannel) +{ + setEspnowEncryptedConnectionKey(espnowEncryptedConnectionKeySeed); + setEspnowHashKey(espnowHashKeySeed); +} + +EspnowMeshBackend::~EspnowMeshBackend() +{ + if(isEspnowRequestManager()) + { + setEspnowRequestManager(nullptr); + } + + _database.deleteSentRequestsByOwner(this); +} + +std::vector & EspnowMeshBackend::connectionQueue() +{ + return EspnowDatabase::connectionQueue(); +} + +const std::vector & EspnowMeshBackend::constConnectionQueue() +{ + return EspnowDatabase::constConnectionQueue(); +} + +std::vector & EspnowMeshBackend::latestTransmissionOutcomes() +{ + return EspnowDatabase::latestTransmissionOutcomes(); +} + +bool EspnowMeshBackend::latestTransmissionSuccessful() +{ + return latestTransmissionSuccessfulBase(latestTransmissionOutcomes()); +} + +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(); +} + +bool EspnowMeshBackend::activateEspnow() +{ + if (esp_now_init()==0) + { + if(!EspnowConnectionManager::initializeEncryptionKok()) + warningPrint(String(F("Failed to set ESP-NOW KoK!"))); + + if(getEspnowRequestManager() == nullptr) + { + setEspnowRequestManager(this); + } + + esp_now_register_recv_cb(espnowReceiveCallbackWrapper); + esp_now_register_send_cb(EspnowTransmitter::espnowSendCallback); + + // 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(String(F("Failed to set ESP-NOW role! Maybe ESP-NOW peers are already added?"))); + + verboseModePrint(String(F("ESP-NOW activated."))); + verboseModePrint(String(F("My ESP-NOW STA MAC: ")) + WiFi.macAddress() + '\n'); // Get the station MAC address. The softAP MAC is different. + + return true; + } + else + { + warningPrint(String(F("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) + { + EspnowDatabase::responsesToSend().clear(); + EspnowDatabase::peerRequestConfirmationsToSend().clear(); + EspnowDatabase::receivedEspnowTransmissions().clear(); + EspnowDatabase::sentRequests().clear(); + EspnowDatabase::receivedRequests().clear(); + EspnowConnectionManager::encryptedConnections().clear(); + EncryptedConnectionLog::setNewRemovalsScheduled(false); + + return true; + } + else + { + return false; + } +} + +void EspnowMeshBackend::performEspnowMaintenance(const uint32_t estimatedMaxDuration) +{ + ExpiringTimeTracker estimatedMaxDurationTracker = ExpiringTimeTracker(estimatedMaxDuration); + + // Doing this during an ESP-NOW transmission could invalidate iterators + MutexTracker mutexTracker(EspnowTransmitter::captureEspnowTransmissionMutex(EspnowConnectionManager::handlePostponedRemovals)); + if(!mutexTracker.mutexCaptured()) + { + assert(false && String(F("ERROR! Transmission in progress. Don't call performEspnowMaintenance from callbacks as this may corrupt program state! Aborting."))); + return; + } + + EspnowDatabase::clearOldLogEntries(false); + + if(EncryptedConnectionLog::getSoonestExpiringConnectionTracker() && EncryptedConnectionLog::getSoonestExpiringConnectionTracker()->expired()) + { + EspnowConnectionManager::updateTemporaryEncryptedConnections(); + } + + if(estimatedMaxDuration > 0) + { + if(estimatedMaxDurationTracker.expired()) + return; + else + sendStoredEspnowMessages(&estimatedMaxDurationTracker); + } + else + { + sendStoredEspnowMessages(); + } +} + +void EspnowMeshBackend::espnowReceiveCallbackWrapper(uint8_t *macaddr, uint8_t *dataArray, const uint8_t len) +{ + using namespace EspnowProtocolInterpreter; + + // Since this callback can be called during any delay(), we should always consider all mutexes captured. + // This provides a consistent mutex environment, which facilitates development and debugging. + // Otherwise we get issues such as EspnowTransmitter::_espnowTransmissionMutex will usually be free, but occasionally taken (when callback occurs in a delay() during attemptTransmission). + MutexTracker captureBanTracker(MutexTracker::captureBan()); + + if(len >= metadataSize()) // If we do not receive at least the metadata bytes, the transmission is invalid. + { + //uint32_t callbackStart = millis(); + + // If there is a espnowRequestManager, get it + EspnowMeshBackend *currentEspnowRequestManager = getEspnowRequestManager(); + + char messageType = getMessageType(dataArray); + uint64_t receivedMessageID = getMessageID(dataArray); + + if(currentEspnowRequestManager && !currentEspnowRequestManager->acceptsUnverifiedRequests() + && !usesConstantSessionKey(messageType) && !EspnowEncryptionBroker::verifyPeerSessionKey(receivedMessageID, macaddr, messageType)) + { + return; + } + + if(EspnowTransmitter::useEncryptedMessages()) + { + // chacha20Poly1305Decrypt decrypts dataArray in place. + // We are using the protocol bytes as a key salt. + if(!experimental::crypto::ChaCha20Poly1305::decrypt(dataArray + metadataSize(), len - metadataSize(), getEspnowMessageEncryptionKey(), dataArray, + protocolBytesSize, dataArray + protocolBytesSize, dataArray + protocolBytesSize + 12)) + { + return; // Decryption of message failed. + } + } + + uint64_t uint64StationMac = TypeCast::macToUint64(macaddr); + bool transmissionEncrypted = usesEncryption(receivedMessageID); + + // Useful when debugging the protocol + //Serial.print("Received from Mac: " + TypeCast::macToString(macaddr) + " ID: " + TypeCast::uint64ToString(receivedMessageID)); + //Serial.println(transmissionEncrypted ? " Encrypted" : " Unencrypted"); + + if(messageType == 'Q' || messageType == 'B') // Question (request) or Broadcast + { + if(ESP.getFreeHeap() <= criticalHeapLevel()) + { + warningPrint("WARNING! Free heap below critical level. Suspending ESP-NOW request processing until the situation improves."); + return; + } + + if(currentEspnowRequestManager) + { + if(!EspnowDatabase::requestReceived(uint64StationMac, receivedMessageID)) // If the request has not already been received + { + if(transmissionEncrypted) + { + EncryptedConnectionLog *encryptedConnection = EspnowConnectionManager::getEncryptedConnection(macaddr); + + if(!encryptedConnection || (!EspnowEncryptionBroker::synchronizePeerSessionKey(receivedMessageID, *encryptedConnection) && + !EspnowEncryptionBroker::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 = EspnowDatabase::getOwnerOfSentRequest(requestMac, receivedMessageID); + } + else + { + // An unencrypted transmission was probably sent to the AP interface as a result of a scan. + requestMac = getTransmissionMac(dataArray); + requestSender = EspnowDatabase::getOwnerOfSentRequest(requestMac, receivedMessageID); + + // But if not, also check if it was sent to the station interface. + if(!requestSender) + { + requestMac = uint64StationMac; + requestSender = EspnowDatabase::getOwnerOfSentRequest(requestMac, receivedMessageID); + } + + // Or if it was sent as a broadcast. (A broadcast can never be encrypted) + if(!requestSender) + { + requestSender = EspnowDatabase::getOwnerOfSentRequest(uint64BroadcastMac, receivedMessageID); + } + } + + // If this node sent the request and it has not already been answered. + if(requestSender) + { + uint8_t macArray[6] = { 0 }; + + requestSender->espnowReceiveCallback(TypeCast::uint64ToMac(requestMac, macArray), dataArray, len); + } + } + else if(messageType == 'S') // Synchronization request + { + EspnowEncryptionBroker::synchronizePeerSessionKey(receivedMessageID, macaddr); + } + else if(messageType == 'P') // Peer request + { + EspnowEncryptionBroker::handlePeerRequest(macaddr, dataArray, len, uint64StationMac, receivedMessageID); + } + else if(messageType == 'C') // peer request Confirmation + { + EspnowEncryptionBroker::handlePeerRequestConfirmation(macaddr, dataArray, len); + } + else + { + assert(messageType == 'Q' || messageType == 'A' || messageType == 'B' || messageType == 'S' || messageType == 'P' || messageType == 'C'); + } + + //Serial.println("espnowReceiveCallbackWrapper duration " + String(millis() - callbackStart)); + } +} + +void EspnowMeshBackend::espnowReceiveCallback(const uint8_t *macaddr, uint8_t *dataArray, const 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 = getMessageType(dataArray); + uint8_t transmissionsRemaining = getTransmissionsRemaining(dataArray); + uint64_t uint64Mac = TypeCast::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 = getMessageID(dataArray); + + //uint32_t methodStart = millis(); + + if(isMessageStart(dataArray)) + { + if(messageType == 'B') + { + auto key = std::make_pair(macAndType, messageID); + if(EspnowDatabase::receivedEspnowTransmissions().find(key) != EspnowDatabase::receivedEspnowTransmissions().end()) + return; // Should not call BroadcastFilter more than once for an accepted message + + String message = getHashKeyLength(dataArray, len); + _database.setSenderMac(macaddr); + uint8_t senderAPMac[6] {0}; + _database.setSenderAPMac(getTransmissionMac(dataArray, senderAPMac)); + _encryptionBroker.setReceivedEncryptedTransmission(usesEncryption(messageID)); + bool acceptBroadcast = getBroadcastFilter()(message, *this); + if(acceptBroadcast) + { + // Does nothing if key already in receivedEspnowTransmissions + EspnowDatabase::receivedEspnowTransmissions().insert(std::make_pair(key, MessageData(message, getTransmissionsRemaining(dataArray)))); + } + else + { + return; + } + } + else + { + // Does nothing if key already in receivedEspnowTransmissions + EspnowDatabase::receivedEspnowTransmissions().insert(std::make_pair(std::make_pair(macAndType, messageID), MessageData(dataArray, len))); + } + } + else + { + std::map, MessageData>::iterator storedMessageIterator = EspnowDatabase::receivedEspnowTransmissions().find(std::make_pair(macAndType, messageID)); + + if(storedMessageIterator == EspnowDatabase::receivedEspnowTransmissions().end()) // If we have not stored the key already, we missed the first message part. + { + return; + } + + 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) + { + EspnowDatabase::receivedEspnowTransmissions().erase(storedMessageIterator); + return; + } + } + } + + //Serial.println("methodStart storage done " + String(millis() - methodStart)); + + if(transmissionsRemaining != 0) + { + return; + } + + std::map, MessageData>::iterator storedMessageIterator = EspnowDatabase::receivedEspnowTransmissions().find(std::make_pair(macAndType, messageID)); + assert(storedMessageIterator != EspnowDatabase::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 + + EspnowDatabase::receivedEspnowTransmissions().erase(storedMessageIterator); // Erase the extra copy of the totalMessage, to save RAM. + + //Serial.println("methodStart erase done " + String(millis() - methodStart)); + + if(messageType == 'Q' || messageType == 'B') // Question (request) or Broadcast + { + EspnowDatabase::storeReceivedRequest(uint64Mac, messageID, TimeTracker(millis())); + //Serial.println("methodStart request stored " + String(millis() - methodStart)); + + _database.setSenderMac(macaddr); + uint8_t senderAPMac[6] {0}; + _database.setSenderAPMac(getTransmissionMac(dataArray, senderAPMac)); + _encryptionBroker.setReceivedEncryptedTransmission(usesEncryption(messageID)); + String response = getRequestHandler()(totalMessage, *this); + //Serial.println("methodStart response acquired " + String(millis() - methodStart)); + + if(response.length() > 0) + { + EspnowDatabase::responsesToSend().emplace_back(response, macaddr, messageID); + + //Serial.println("methodStart Q done " + String(millis() - methodStart)); + } + } + else if(messageType == 'A') // Answer (response) + { + EspnowDatabase::deleteSentRequest(uint64Mac, messageID); // Request has been answered, so stop accepting new answers about it. + + if(EncryptedConnectionLog *encryptedConnection = EspnowConnectionManager::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(); + } + } + + _database.setSenderMac(macaddr); + uint8_t senderAPMac[6] {0}; + _database.setSenderAPMac(getTransmissionMac(dataArray, senderAPMac)); + _encryptionBroker.setReceivedEncryptedTransmission(usesEncryption(messageID)); + getResponseHandler()(totalMessage, *this); + } + else + { + assert(messageType == 'Q' || messageType == 'A' || messageType == 'B'); + } + + ESP.wdtFeed(); // Prevents WDT reset in case we receive a lot of transmissions without break. + + //Serial.println("methodStart wdtFeed done " + String(millis() - methodStart)); +} + +void EspnowMeshBackend::setEspnowRequestManager(EspnowMeshBackend *espnowMeshInstance) +{ + _espnowRequestManager = espnowMeshInstance; +} + +EspnowMeshBackend *EspnowMeshBackend::getEspnowRequestManager() {return _espnowRequestManager;} + +bool EspnowMeshBackend::isEspnowRequestManager() const +{ + return (this == getEspnowRequestManager()); +} + +void EspnowMeshBackend::setLogEntryLifetimeMs(const uint32_t logEntryLifetimeMs) +{ + EspnowDatabase::setLogEntryLifetimeMs(logEntryLifetimeMs); +} +uint32_t EspnowMeshBackend::logEntryLifetimeMs() { return EspnowDatabase::logEntryLifetimeMs(); } + +void EspnowMeshBackend::setBroadcastResponseTimeoutMs(const uint32_t broadcastResponseTimeoutMs) +{ + EspnowDatabase::setBroadcastResponseTimeoutMs(broadcastResponseTimeoutMs); +} +uint32_t EspnowMeshBackend::broadcastResponseTimeoutMs() { return EspnowDatabase::broadcastResponseTimeoutMs(); } + +void EspnowMeshBackend::setCriticalHeapLevelBuffer(const uint32_t bufferInBytes) +{ + EspnowDatabase::setCriticalHeapLevelBuffer(bufferInBytes); +} + +uint32_t EspnowMeshBackend::criticalHeapLevelBuffer() +{ + return EspnowDatabase::criticalHeapLevelBuffer(); +} + +uint32_t EspnowMeshBackend::criticalHeapLevel() +{ + return EspnowDatabase::criticalHeapLevel(); +} + +void EspnowMeshBackend::setEspnowTransmissionTimeout(const uint32_t timeoutMs) +{ + EspnowTransmitter::setEspnowTransmissionTimeout(timeoutMs); +} +uint32_t EspnowMeshBackend::getEspnowTransmissionTimeout() {return EspnowTransmitter::getEspnowTransmissionTimeout();} + +void EspnowMeshBackend::setEspnowRetransmissionInterval(const uint32_t intervalMs) +{ + EspnowTransmitter::setEspnowRetransmissionInterval(intervalMs); +} +uint32_t EspnowMeshBackend::getEspnowRetransmissionInterval() {return EspnowTransmitter::getEspnowRetransmissionInterval();} + +void EspnowMeshBackend::setEncryptionRequestTimeout(const uint32_t timeoutMs) +{ + EspnowDatabase::setEncryptionRequestTimeout(timeoutMs); +} +uint32_t EspnowMeshBackend::getEncryptionRequestTimeout() {return EspnowDatabase::getEncryptionRequestTimeout();} + +void EspnowMeshBackend::setAutoEncryptionDuration(const uint32_t duration) +{ + _database.setAutoEncryptionDuration(duration); +} +uint32_t EspnowMeshBackend::getAutoEncryptionDuration() const {return _database.getAutoEncryptionDuration();} + +void EspnowMeshBackend::setBroadcastFilter(const broadcastFilterType broadcastFilter) {_broadcastFilter = broadcastFilter;} +EspnowMeshBackend::broadcastFilterType EspnowMeshBackend::getBroadcastFilter() const {return _broadcastFilter;} + +void EspnowMeshBackend::setEspnowEncryptedConnectionKey(const uint8_t espnowEncryptedConnectionKey[encryptedConnectionKeyLength]) +{ + _connectionManager.setEspnowEncryptedConnectionKey(espnowEncryptedConnectionKey); +} + +void EspnowMeshBackend::setEspnowEncryptedConnectionKey(const String &espnowEncryptedConnectionKeySeed) +{ + _connectionManager.setEspnowEncryptedConnectionKey(espnowEncryptedConnectionKeySeed); +} + +const uint8_t *EspnowMeshBackend::getEspnowEncryptedConnectionKey() const +{ + return _connectionManager.getEspnowEncryptedConnectionKey(); +} + +uint8_t *EspnowMeshBackend::getEspnowEncryptedConnectionKey(uint8_t resultArray[encryptedConnectionKeyLength]) const +{ + return _connectionManager.getEspnowEncryptedConnectionKey(resultArray); +} + +bool EspnowMeshBackend::setEspnowEncryptionKok(uint8_t espnowEncryptionKok[encryptedConnectionKeyLength]) +{ + return EspnowConnectionManager::setEspnowEncryptionKok(espnowEncryptionKok); +} + +bool EspnowMeshBackend::setEspnowEncryptionKok(const String &espnowEncryptionKokSeed) +{ + return EspnowConnectionManager::setEspnowEncryptionKok(espnowEncryptionKokSeed); +} + +const uint8_t *EspnowMeshBackend::getEspnowEncryptionKok() +{ + return EspnowConnectionManager::getEspnowEncryptionKok(); +} + +void EspnowMeshBackend::setEspnowHashKey(const uint8_t espnowHashKey[hashKeyLength]) +{ + _connectionManager.setEspnowHashKey(espnowHashKey); +} + +void EspnowMeshBackend::setEspnowHashKey(const String &espnowHashKeySeed) +{ + _connectionManager.setEspnowHashKey(espnowHashKeySeed); +} + +const uint8_t *EspnowMeshBackend::getEspnowHashKey() const +{ + return _connectionManager.getEspnowHashKey(); +} + +void EspnowMeshBackend::setUseEncryptedMessages(const bool useEncryptedMessages) +{ + EspnowTransmitter::setUseEncryptedMessages(useEncryptedMessages); +} +bool EspnowMeshBackend::useEncryptedMessages() { return EspnowTransmitter::useEncryptedMessages(); } + +void EspnowMeshBackend::setEspnowMessageEncryptionKey(const uint8_t espnowMessageEncryptionKey[experimental::crypto::ENCRYPTION_KEY_LENGTH]) +{ + EspnowTransmitter::setEspnowMessageEncryptionKey(espnowMessageEncryptionKey); +} + +void EspnowMeshBackend::setEspnowMessageEncryptionKey(const String &espnowMessageEncryptionKeySeed) +{ + EspnowTransmitter::setEspnowMessageEncryptionKey(espnowMessageEncryptionKeySeed); +} + +const uint8_t *EspnowMeshBackend::getEspnowMessageEncryptionKey() +{ + return EspnowTransmitter::getEspnowMessageEncryptionKey(); +} + +String EspnowMeshBackend::getScheduledResponseMessage(const uint32_t responseIndex) +{ + return EspnowDatabase::getScheduledResponseMessage(responseIndex); +} + +const uint8_t *EspnowMeshBackend::getScheduledResponseRecipient(const uint32_t responseIndex) +{ + return EspnowDatabase::getScheduledResponseRecipient(responseIndex); +} + +uint32_t EspnowMeshBackend::numberOfScheduledResponses() {return EspnowDatabase::numberOfScheduledResponses();} + +void EspnowMeshBackend::clearAllScheduledResponses() +{ + EspnowDatabase::clearAllScheduledResponses(); +} + +void EspnowMeshBackend::deleteScheduledResponsesByRecipient(const uint8_t *recipientMac, const bool encryptedOnly) +{ + EspnowDatabase::deleteScheduledResponsesByRecipient(recipientMac, encryptedOnly); +} + +String EspnowMeshBackend::getSenderMac() const {return _database.getSenderMac();} +uint8_t *EspnowMeshBackend::getSenderMac(uint8_t *macArray) const +{ + return _database.getSenderMac(macArray); +} + +String EspnowMeshBackend::getSenderAPMac() const {return _database.getSenderAPMac();} +uint8_t *EspnowMeshBackend::getSenderAPMac(uint8_t *macArray) const +{ + return _database.getSenderAPMac(macArray); +} + +bool EspnowMeshBackend::receivedEncryptedTransmission() const {return _encryptionBroker.receivedEncryptedTransmission();} + +bool EspnowMeshBackend::addUnencryptedConnection(const String &serializedConnectionState) +{ + return EspnowConnectionManager::addUnencryptedConnection(serializedConnectionState); +} + +EncryptedConnectionStatus EspnowMeshBackend::addEncryptedConnection(uint8_t *peerStaMac, uint8_t *peerApMac, const uint64_t peerSessionKey, const uint64_t ownSessionKey) +{ + return _connectionManager.addEncryptedConnection(peerStaMac, peerApMac, peerSessionKey, ownSessionKey); +} + +EncryptedConnectionStatus EspnowMeshBackend::addEncryptedConnection(const String &serializedConnectionState, const bool ignoreDuration) +{ + return _connectionManager.addEncryptedConnection(serializedConnectionState, ignoreDuration); +} + +EncryptedConnectionStatus EspnowMeshBackend::addTemporaryEncryptedConnection(uint8_t *peerStaMac, uint8_t *peerApMac, const uint64_t peerSessionKey, const uint64_t ownSessionKey, const uint32_t duration) +{ + return _connectionManager.addTemporaryEncryptedConnection(peerStaMac, peerApMac, peerSessionKey, ownSessionKey, duration); +} + +EncryptedConnectionStatus EspnowMeshBackend::addTemporaryEncryptedConnection(const String &serializedConnectionState, const uint32_t duration) +{ + return _connectionManager.addTemporaryEncryptedConnection(serializedConnectionState, duration); +} + +EncryptedConnectionStatus EspnowMeshBackend::requestEncryptedConnection(const uint8_t *peerMac) +{ + return _encryptionBroker.requestEncryptedConnection(peerMac, *this); +} + +EncryptedConnectionStatus EspnowMeshBackend::requestTemporaryEncryptedConnection(const uint8_t *peerMac, const uint32_t durationMs) +{ + return _encryptionBroker.requestTemporaryEncryptedConnection(peerMac, durationMs, *this); +} + +EncryptedConnectionStatus EspnowMeshBackend::requestFlexibleTemporaryEncryptedConnection(const uint8_t *peerMac, const uint32_t minDurationMs) +{ + return _encryptionBroker.requestFlexibleTemporaryEncryptedConnection(peerMac, minDurationMs, *this); +} + +EncryptedConnectionRemovalOutcome EspnowMeshBackend::removeEncryptedConnection(const uint8_t *peerMac) +{ + return EspnowConnectionManager::removeEncryptedConnection(peerMac); +} + +EncryptedConnectionRemovalOutcome EspnowMeshBackend::requestEncryptedConnectionRemoval(const uint8_t *peerMac) +{ + return _encryptionBroker.requestEncryptedConnectionRemoval(peerMac); +} + +void EspnowMeshBackend::setAcceptsUnverifiedRequests(const bool acceptsUnverifiedRequests) { _acceptsUnverifiedRequests = acceptsUnverifiedRequests; } +bool EspnowMeshBackend::acceptsUnverifiedRequests() const { return _acceptsUnverifiedRequests; } + +void EspnowMeshBackend::setEncryptedConnectionsSoftLimit(const uint8_t softLimit) +{ + _connectionManager.setEncryptedConnectionsSoftLimit(softLimit); +} + +uint8_t EspnowMeshBackend::encryptedConnectionsSoftLimit() const { return _connectionManager.encryptedConnectionsSoftLimit(); } + +uint8_t *EspnowMeshBackend::getEncryptedMac(const uint8_t *peerMac, uint8_t *resultArray) +{ + return EspnowConnectionManager::getEncryptedMac(peerMac, resultArray); +} + +void EspnowMeshBackend::prepareForTransmission(const String &message, const bool scan, const bool scanAllWiFiChannels) +{ + setMessage(message); + + latestTransmissionOutcomes().clear(); + + if(scan) + { + connectionQueue().clear(); + scanForNetworks(scanAllWiFiChannels); + } +} + +TransmissionStatusType EspnowMeshBackend::initiateTransmission(const String &message, const EspnowNetworkInfo &recipientInfo) +{ + uint8_t targetBSSID[6] {0}; + + assert(recipientInfo.BSSID() != nullptr); // We need at least the BSSID to connect + recipientInfo.getBSSID(targetBSSID); + + if(verboseMode()) // Avoid string generation if not required + { + printAPInfo(recipientInfo); + verboseModePrint(emptyString); + } + + return initiateTransmissionKernel(message, targetBSSID); +} + +TransmissionStatusType EspnowMeshBackend::initiateTransmissionKernel(const String &message, const uint8_t *targetBSSID) +{ + uint32_t transmissionStartTime = millis(); + TransmissionStatusType transmissionResult = _transmitter.sendRequest(message, targetBSSID, this); + + uint32_t transmissionDuration = millis() - transmissionStartTime; + + if(verboseMode() && transmissionResult == TransmissionStatusType::TRANSMISSION_COMPLETE) // Avoid calculations if not required + { + totalDurationWhenSuccessful_AT += transmissionDuration; + ++successfulTransmissions_AT; + if(transmissionDuration > maxTransmissionDuration_AT) + { + maxTransmissionDuration_AT = transmissionDuration; + } + } + + return transmissionResult; +} + +void EspnowMeshBackend::printTransmissionStatistics() const +{ + if(verboseMode() && successfulTransmissions_AT > 0) // Avoid calculations if not required + { + verboseModePrint(String(F("Average duration of successful transmissions: ")) + String(totalDurationWhenSuccessful_AT/successfulTransmissions_AT) + String(F(" ms."))); + verboseModePrint(String(F("Maximum duration of successful transmissions: ")) + String(maxTransmissionDuration_AT) + String(F(" ms."))); + } + else + { + verboseModePrint(String(F("No successful transmission."))); + } +} + +void EspnowMeshBackend::attemptTransmission(const String &message, const bool scan, const bool scanAllWiFiChannels) +{ + MutexTracker mutexTracker(EspnowTransmitter::captureEspnowTransmissionMutex(EspnowConnectionManager::handlePostponedRemovals)); + if(!mutexTracker.mutexCaptured()) + { + assert(false && String(F("ERROR! Transmission in progress. Don't call attemptTransmission from callbacks as this may corrupt program state! Aborting."))); + return; + } + + prepareForTransmission(message, scan, scanAllWiFiChannels); + + MutexTracker connectionQueueMutexTracker(EspnowDatabase::captureEspnowConnectionQueueMutex()); + if(!connectionQueueMutexTracker.mutexCaptured()) + { + assert(false && String(F("ERROR! connectionQueue locked. Don't call attemptTransmission from callbacks as this may corrupt program state! Aborting."))); + } + else + { + for(const EspnowNetworkInfo ¤tNetwork : constConnectionQueue()) + { + TransmissionStatusType transmissionResult = initiateTransmission(getMessage(), currentNetwork); + + latestTransmissionOutcomes().push_back(TransmissionOutcome{.origin = currentNetwork, .transmissionStatus = transmissionResult}); + + if(!getTransmissionOutcomesUpdateHook()(*this)) + break; + } + } + + printTransmissionStatistics(); +} + +TransmissionStatusType EspnowMeshBackend::attemptTransmission(const String &message, const EspnowNetworkInfo &recipientInfo) +{ + MutexTracker mutexTracker(EspnowTransmitter::captureEspnowTransmissionMutex(EspnowConnectionManager::handlePostponedRemovals)); + if(!mutexTracker.mutexCaptured()) + { + assert(false && String(F("ERROR! Transmission in progress. Don't call attemptTransmission from callbacks as this may corrupt program state! Aborting."))); + return TransmissionStatusType::CONNECTION_FAILED; + } + + return initiateTransmission(message, recipientInfo); +} + +TransmissionStatusType EspnowMeshBackend::initiateAutoEncryptingTransmission(const String &message, uint8_t *targetBSSID, EncryptedConnectionStatus connectionStatus) +{ + TransmissionStatusType transmissionResult = TransmissionStatusType::CONNECTION_FAILED; + + if(EspnowEncryptionBroker::encryptedConnectionEstablished(connectionStatus)) + { + uint8_t encryptedMac[6] {0}; + assert(getEncryptedMac(targetBSSID, encryptedMac) && esp_now_is_peer_exist(encryptedMac) > 0 && String(F("ERROR! Attempting to send content marked as encrypted via unencrypted connection!"))); + transmissionResult = initiateTransmissionKernel(message, targetBSSID); + } + + return transmissionResult; +} + +void EspnowMeshBackend::attemptAutoEncryptingTransmission(const String &message, const bool requestPermanentConnections, const bool scan, const bool scanAllWiFiChannels) +{ + MutexTracker outerMutexTracker(EspnowTransmitter::captureEspnowTransmissionMutex(EspnowConnectionManager::handlePostponedRemovals)); + if(!outerMutexTracker.mutexCaptured()) + { + assert(false && String(F("ERROR! Transmission in progress. Don't call attemptAutoEncryptingTransmission from callbacks as this may corrupt program state! Aborting."))); + return; + } + + prepareForTransmission(message, scan, scanAllWiFiChannels); + + outerMutexTracker.releaseMutex(); + + MutexTracker connectionQueueMutexTracker(EspnowDatabase::captureEspnowConnectionQueueMutex()); + if(!connectionQueueMutexTracker.mutexCaptured()) + { + assert(false && String(F("ERROR! connectionQueue locked. Don't call attemptAutoEncryptingTransmission from callbacks as this may corrupt program state! Aborting."))); + } + else + { + for(const EspnowNetworkInfo ¤tNetwork : constConnectionQueue()) + { + uint8_t currentBSSID[6] {0}; + EncryptedConnectionLog *existingEncryptedConnection = nullptr; + EncryptedConnectionStatus connectionStatus = _encryptionBroker.initiateAutoEncryptingConnection(currentNetwork, requestPermanentConnections, currentBSSID, &existingEncryptedConnection, *this); + + MutexTracker innerMutexTracker(EspnowTransmitter::captureEspnowTransmissionMutex()); + if(!innerMutexTracker.mutexCaptured()) + { + assert(false && String(F("ERROR! Unable to recapture Mutex in attemptAutoEncryptingTransmission. Aborting."))); + return; + } + + TransmissionStatusType transmissionResult = initiateAutoEncryptingTransmission(getMessage(), currentBSSID, connectionStatus); + + latestTransmissionOutcomes().push_back(TransmissionOutcome{.origin = currentNetwork, .transmissionStatus = transmissionResult}); + + _encryptionBroker.finalizeAutoEncryptingConnection(currentBSSID, existingEncryptedConnection, requestPermanentConnections); + + if(!getTransmissionOutcomesUpdateHook()(*this)) + break; + } + } + + printTransmissionStatistics(); +} + +TransmissionStatusType EspnowMeshBackend::attemptAutoEncryptingTransmission(const String &message, const EspnowNetworkInfo &recipientInfo, const bool requestPermanentConnection) +{ + uint8_t targetBSSID[6] {0}; + EncryptedConnectionLog *existingEncryptedConnection = nullptr; + EncryptedConnectionStatus connectionStatus = _encryptionBroker.initiateAutoEncryptingConnection(recipientInfo, requestPermanentConnection, targetBSSID, &existingEncryptedConnection, *this); + + MutexTracker mutexTracker(EspnowTransmitter::captureEspnowTransmissionMutex(EspnowConnectionManager::handlePostponedRemovals)); + if(!mutexTracker.mutexCaptured()) + { + assert(false && String(F("ERROR! Transmission in progress. Don't call attemptTransmission from callbacks as this may corrupt program state! Aborting."))); + return TransmissionStatusType::CONNECTION_FAILED; + } + + TransmissionStatusType transmissionResult = initiateAutoEncryptingTransmission(message, targetBSSID, connectionStatus); + + _encryptionBroker.finalizeAutoEncryptingConnection(targetBSSID, existingEncryptedConnection, requestPermanentConnection); + + return transmissionResult; +} + +void EspnowMeshBackend::broadcast(const String &message) +{ + MutexTracker mutexTracker(EspnowTransmitter::captureEspnowTransmissionMutex(EspnowConnectionManager::handlePostponedRemovals)); + if(!mutexTracker.mutexCaptured()) + { + assert(false && String(F("ERROR! Transmission in progress. Don't call broadcast from callbacks as this may corrupt program state! Aborting."))); + return; + } + + EspnowTransmitter::espnowSendToNode(message, EspnowProtocolInterpreter::broadcastMac, 'B', this); +} + +void EspnowMeshBackend::setBroadcastTransmissionRedundancy(const uint8_t redundancy) { _transmitter.setBroadcastTransmissionRedundancy(redundancy); } +uint8_t EspnowMeshBackend::getBroadcastTransmissionRedundancy() const { return _transmitter.getBroadcastTransmissionRedundancy(); } + +void EspnowMeshBackend::setResponseTransmittedHook(const EspnowTransmitter::responseTransmittedHookType responseTransmittedHook) { _transmitter.setResponseTransmittedHook(responseTransmittedHook); } +EspnowTransmitter::responseTransmittedHookType EspnowMeshBackend::getResponseTransmittedHook() const { return _transmitter.getResponseTransmittedHook(); } + +EspnowDatabase *EspnowMeshBackend::getDatabase() { return &_database; } +const EspnowDatabase *EspnowMeshBackend::getDatabaseConst() const { return &_database; } +EspnowConnectionManager *EspnowMeshBackend::getConnectionManager() { return &_connectionManager; } +const EspnowConnectionManager *EspnowMeshBackend::getConnectionManagerConst() const { return &_connectionManager; } +EspnowTransmitter *EspnowMeshBackend::getTransmitter() { return &_transmitter; } +const EspnowTransmitter *EspnowMeshBackend::getTransmitterConst() const { return &_transmitter; } +EspnowEncryptionBroker *EspnowMeshBackend::getEncryptionBroker() { return &_encryptionBroker; } +const EspnowEncryptionBroker *EspnowMeshBackend::getEncryptionBrokerConst() const { return &_encryptionBroker; } + +void EspnowMeshBackend::sendStoredEspnowMessages(const ExpiringTimeTracker *estimatedMaxDurationTracker) +{ + EspnowEncryptionBroker::sendPeerRequestConfirmations(estimatedMaxDurationTracker); + + if(estimatedMaxDurationTracker && estimatedMaxDurationTracker->expired()) + return; + + EspnowTransmitter::sendEspnowResponses(estimatedMaxDurationTracker); +} + +uint32_t EspnowMeshBackend::getMaxMessageBytesPerTransmission() +{ + return EspnowProtocolInterpreter::getMaxMessageBytesPerTransmission(); +} + +void EspnowMeshBackend::setMaxTransmissionsPerMessage(const uint8_t maxTransmissionsPerMessage) +{ + EspnowTransmitter::setMaxTransmissionsPerMessage(maxTransmissionsPerMessage); +} + +uint8_t EspnowMeshBackend::getMaxTransmissionsPerMessage() {return EspnowTransmitter::getMaxTransmissionsPerMessage();} + +uint32_t EspnowMeshBackend::getMaxMessageLength() +{ + return EspnowTransmitter::getMaxMessageLength(); +} + +void EspnowMeshBackend::setVerboseModeState(const bool enabled) {(*getConditionalPrinter()).setVerboseModeState(enabled); ConditionalPrinter::setStaticVerboseModeState(enabled);} +bool EspnowMeshBackend::verboseMode() const {return ConditionalPrinter::staticVerboseMode();} + +void EspnowMeshBackend::verboseModePrint(const String &stringToPrint, const bool newline) const +{ + (*getConditionalPrinterConst()).verboseModePrint(stringToPrint, newline); +} + +bool EspnowMeshBackend::staticVerboseMode() {return ConditionalPrinter::staticVerboseMode();} +void EspnowMeshBackend::staticVerboseModePrint(const String &stringToPrint, const bool newline) +{ + ConditionalPrinter::staticVerboseModePrint(stringToPrint, newline); +} + +uint8_t EspnowMeshBackend::numberOfEncryptedConnections() +{ + return EspnowConnectionManager::numberOfEncryptedConnections(); +} + +ConnectionType EspnowMeshBackend::getConnectionInfo(uint8_t *peerMac, uint32_t *remainingDuration) +{ + return EspnowConnectionManager::getConnectionInfo(peerMac, remainingDuration); +} + +ConnectionType EspnowMeshBackend::getConnectionInfo(const uint32_t connectionIndex, uint32_t *remainingDuration, uint8_t *peerMac) +{ + return EspnowConnectionManager::getConnectionInfo(connectionIndex, remainingDuration, peerMac); +} + +double EspnowMeshBackend::getTransmissionFailRate() +{ + return EspnowTransmitter::getTransmissionFailRate(); +} + +void EspnowMeshBackend::resetTransmissionFailRate() +{ + EspnowTransmitter::resetTransmissionFailRate(); +} + +String EspnowMeshBackend::serializeUnencryptedConnection() +{ + return EspnowConnectionManager::serializeUnencryptedConnection(); +} + +String EspnowMeshBackend::serializeEncryptedConnection(const uint8_t *peerMac) +{ + return EspnowConnectionManager::serializeEncryptedConnection(peerMac); +} + +String EspnowMeshBackend::serializeEncryptedConnection(const uint32_t connectionIndex) +{ + return EspnowConnectionManager::serializeEncryptedConnection(connectionIndex); +} + +void EspnowMeshBackend::setWiFiChannel(const uint8 newWiFiChannel) +{ + MeshBackendBase::setWiFiChannel(newWiFiChannel); + _database.setWiFiChannel(newWiFiChannel); +} diff --git a/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.h b/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.h new file mode 100644 index 0000000000..4519372f3d --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/EspnowMeshBackend.h @@ -0,0 +1,1015 @@ +/* + 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). + +/** + * This ESP-NOW framework uses a few different message types to enable easier interpretation of transmissions. + * The message type is stored in the first transmission byte, see EspnowProtocolInterpreter.h for more detailed information on the protocol. + * Available message types are 'Q' for question (request), 'A' for answer (response), + * 'B' for broadcast, 'S' for synchronization request, 'P' for peer request and 'C' for peer request confirmation. + * + * 'B', 'Q' and 'A' are the message types that are assigned to data transmitted by the user. + * 'S', 'P' and 'C' are used only for internal framework transmissions. + * + * Messages with type 'B' are only used for broadcasts. They cannot be encrypted. + * + * Messages with type 'Q' are used for requests sent by the user. They can be encrypted. + * + * Messages with type 'A' are used for responses given by the user when 'B' or 'Q' messages have been received. They can be encrypted. + * + * Messages with type 'P' and 'C' are used exclusively for automatically pairing two ESP-NOW nodes to each other. + * This enables flexible easy-to-use encrypted ESP-NOW communication. 'P' and 'C' messages can be encrypted. + * The encryption pairing process works as follows (from top to bottom): + * + * Encrypted connection pairing process, schematic overview: + * + * Connection | Peer sends ('C'): | Peer requester sends ('P'): | Connection + * encrypted: | | | encrypted: + * | | Peer request + Nonce + HMAC | + * | 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. + * + * Messages with type 'S' are used exclusively when we try to send an encrypted 'R' or 'P' transmission and the last such transmission we tried failed to receive an ack. + * Since we then do not know if the receiving node has incremented its corresponding session key or not, we first send an 'S' request to make sure the key is incremented. + * Once we get an ack for our 'S' request we send the new encrypted 'R' or 'P' transmission. 'S' messages are always encrypted. + * + * Messages of type 'A' and 'C' are response types, and thus use the same session key as the corresponding 'R' and 'P' message they are responding to. + * This means they can never cause a desynchronization to occur, and therefore they do not trigger 'S' messages. + * + * In addition to using encrypted ESP-NOW connections the framework can also send automatically encrypted messages (AEAD) over both encrypted and unencrypted connections. + * Using AEAD will only encrypt the message content, not the transmission metadata. + * The AEAD encryption does not require any pairing, and is thus faster for single messages than establishing a new encrypted connection before transfer. + * AEAD encryption also works with ESP-NOW broadcasts and supports an unlimited number of nodes, which is not true for encrypted connections. + * Encrypted ESP-NOW connections do however come with built in replay attack protection, which is not provided by the framework when using AEAD encryption, + * and allow EspnowProtocolInterpreter::aeadMetadataSize extra message bytes per transmission. + * Transmissions via encrypted connections are also slightly faster than via AEAD once a connection has been established. + */ + +#ifndef __ESPNOWMESHBACKEND_H__ +#define __ESPNOWMESHBACKEND_H__ + +#include "EspnowDatabase.h" +#include "EspnowConnectionManager.h" +#include "EspnowTransmitter.h" +#include "EspnowEncryptionBroker.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 "EspnowNetworkInfo.h" + +/** + * An alternative to standard delay(). Will continuously call performEspnowMaintenance() 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, networkFilter or broadcastFilter callbacks since performEspnowMaintenance() can alter the ESP-NOW state. + * + * @param durationMs The shortest allowed delay duration, in milliseconds. + */ +void espnowDelay(const uint32_t durationMs); + +class EspnowMeshBackend : public MeshBackendBase { + +public: + + using broadcastFilterType = std::function; + + /** + * ESP-NOW constructor method. Creates an ESP-NOW node, ready to be initialised. + * + * @param requestHandler The callback handler for dealing with received requests. Takes a string as an argument which + * is the request string received from another node and returns the string to send back. + * @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 TransmissionStatusType. + * @param networkFilter The callback handler for deciding which WiFi networks to connect to. + * @param broadcastFilter The callback handler for deciding which ESP-NOW broadcasts to accept. + * @param meshPassword The WiFi password for the mesh network. + * @param espnowEncryptedConnectionKey An uint8_t array containing the secret key used by this EspnowMeshBackend instance for creating encrypted ESP-NOW connections. + * @param espnowHashKey An uint8_t array containing the secret key used by this EspnowMeshBackend to generate HMACs for encrypted ESP-NOW connections. + * @param ssidPrefix The prefix (first part) of the node SSID. + * @param ssidSuffix The suffix (last part) of the node SSID. + * @param verboseMode Determines if we should print the events occurring in the library to Serial. Off by default. This setting is shared by all EspnowMeshBackend instances. + * @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 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(const requestHandlerType requestHandler, const responseHandlerType responseHandler, const networkFilterType networkFilter, const broadcastFilterType broadcastFilter, + const String &meshPassword, const uint8_t espnowEncryptedConnectionKey[EspnowProtocolInterpreter::encryptedConnectionKeyLength], + const uint8_t espnowHashKey[EspnowProtocolInterpreter::hashKeyLength], const String &ssidPrefix, + const String &ssidSuffix, const bool verboseMode = false, const uint8 meshWiFiChannel = 1); + + /** + * ESP-NOW constructor method. Creates an ESP-NOW node, ready to be initialised. + * + * @param requestHandler The callback handler for dealing with received requests. Takes a string as an argument which + * is the request string received from another node and returns the string to send back. + * @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 TransmissionStatusType. + * @param networkFilter The callback handler for deciding which WiFi networks to connect to. + * @param broadcastFilter The callback handler for deciding which ESP-NOW broadcasts to accept. + * @param meshPassword The WiFi password for the mesh network. + * @param espnowEncryptedConnectionKeySeed A string containing the seed that will generate the secret key used by this EspnowMeshBackend instance for creating encrypted ESP-NOW connections. + * @param espnowHashKeySeed A string containing the seed that will generate the secret key used by this EspnowMeshBackend to generate HMACs for encrypted ESP-NOW connections. + * @param ssidPrefix The prefix (first part) of the node SSID. + * @param ssidSuffix The suffix (last part) of the node SSID. + * @param verboseMode Determines if we should print the events occurring in the library to Serial. Off by default. This setting is shared by all EspnowMeshBackend instances. + * @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 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(const requestHandlerType requestHandler, const responseHandlerType responseHandler, const networkFilterType networkFilter, const broadcastFilterType broadcastFilter, + const String &meshPassword, const String &espnowEncryptedConnectionKeySeed, const String &espnowHashKeySeed, const String &ssidPrefix, + const String &ssidSuffix, const bool verboseMode = false, const uint8 meshWiFiChannel = 1); + + ~EspnowMeshBackend() override; + + /** + * Returns a vector that contains the NetworkInfo for each WiFi network to connect to. + * This vector is unique for each mesh backend, but NetworkInfo elements can be directly transferred between the vectors as long as both SSID and BSSID are present. + * 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. + * + * Since the connectionQueue() is iterated over during transmissions, always use constConnectionQueue() from callbacks other than NetworkFilter. + */ + static std::vector & connectionQueue(); + + /** + * Same as connectionQueue(), but can be called from all callbacks since the returned reference is const. + */ + static const std::vector & constConnectionQueue(); + + /** + * Returns a vector with the TransmissionOutcome for each AP to which a transmission was attempted during the latest attemptTransmission call. + * This vector is unique for each mesh backend. + * 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(); + + /** + * @return True if latest transmission was successful (i.e. latestTransmissionOutcomes is not empty and all entries have transmissionStatus TransmissionStatusType::TRANSMISSION_COMPLETE). False otherwise. + * The result is unique for each mesh backend. + */ + static bool latestTransmissionSuccessful(); + + /** + * 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 performEspnowMaintenance will likely cause the method to take longer to complete, so plan accordingly. + * + * Should not be used inside responseHandler, requestHandler, networkFilter or broadcastFilter callbacks since performEspnowMaintenance() can alter the ESP-NOW state. + * + * @param estimatedMaxDuration The desired max duration for the method. If set to 0 there is no duration limit. + * Note that setting the estimatedMaxDuration too low may result in missed ESP-NOW transmissions because of too little time for maintenance. + * Also note that although the method will try to respect the max duration limit, there is no guarantee. Overshoots by tens of milliseconds are possible. + */ + static void performEspnowMaintenance(const uint32_t estimatedMaxDuration = 0); + + /** + * 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(const uint32_t bufferInBytes); + static uint32_t criticalHeapLevelBuffer(); + + /** + * Deactivates Espnow for this node. Call begin() on a EspnowMeshBackend instance to reactivate Espnow. + * + * @return True if deactivation was successful. False otherwise. + */ + static bool deactivateEspnow(); + + void attemptTransmission(const String &message, const bool scan = true, const bool scanAllWiFiChannels = false) override; + + /** + * Transmit message to a single recipient without changing the local transmission state. + * Will not change connectionQueue, latestTransmissionOutcomes or stored message. + * + * @param recipientInfo The recipient information. + */ + TransmissionStatusType attemptTransmission(const String &message, const EspnowNetworkInfo &recipientInfo); + + /* + * 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 (unless the requestPermanentConnections argument is set to true). + * 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. + * + * @param message The message to send to other nodes. It will be stored in the class instance until replaced via attemptTransmission or setMessage. + * @param requestPermanentConnections If true, the method will request that encrypted connections used for this transmission become permanent so they are not removed once the transmission is complete. + * This means that encrypted responses to the transmission are received, as long as the encrypted connection is not removed by other means. + * The receiving node has no obligation to obey the request, although it normally will. + * If encryptedConnectionsSoftLimit() is set to less than 6 for the transmission receiver, + * it is possible that a short lived autoEncryptionConnection is created instead of a permanent encrypted connection. + * Note that a maximum of 6 encrypted ESP-NOW connections can be maintained at the same time by the node. + * 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 attemptAutoEncryptingTransmission(const String &message, const bool requestPermanentConnections = false, const bool scan = true, const bool scanAllWiFiChannels = false); + + /** + * Transmit message to a single recipient without changing the local transmission state (apart from encrypted connections). + * Will not change connectionQueue, latestTransmissionOutcomes or stored message. + */ + TransmissionStatusType attemptAutoEncryptingTransmission(const String &message, const EspnowNetworkInfo &recipientInfo, const bool requestPermanentConnection = false); + + /** + * Send a message simultaneously to all nearby nodes which have ESP-NOW activated. + * A broadcast is always treated as a request by the receiving node. + * There is no limit to the number of responses a node can get when sending a broadcast, it will always accept new responses until the broadcastResponseTimeout is reached. + * This also means that the broadcaster can receive duplicate responses from the same node if transmission conditions are poor and an ack is lost. + * A broadcast can never be sent encrypted. + * + * Note that the node needs to have its AP active to be able to receive broadcasts. Nodes can send broadcasts even if their AP is off. + * + * @param message The message to send to the other nodes. Unlike the attemptTransmission method, the message will not be stored in the class instance, since there is no certain way to change the message during an ongoing broadcast. + */ + void broadcast(const String &message); + + /** + * Set the number of redundant transmissions that will be made for every broadcast. + * A greater number increases the likelihood that the broadcast is received, but also means it takes longer time to send. + * + * @param redundancy The number of extra transmissions to make of each broadcast. Defaults to 1. + */ + void setBroadcastTransmissionRedundancy(const uint8_t redundancy); + uint8_t getBroadcastTransmissionRedundancy() const; + + /** + * 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. + * + * @return True if this EspnowMeshBackend is the espnowRequestManager. False otherwise. + */ + bool isEspnowRequestManager() const; + + /** + * Set the duration of most ESP-NOW log entries. Used for all ESP-NOW communication except for broadcasts and encrypted connection requests. + * Setting the duration too long may cause the node to run out of RAM, especially if there is intense transmission activity. + * Setting the duration too short may cause ESP-NOW transmissions to stop working, or make the node receive the same transmission multiple times. + * + * Set to 2500 ms by default. + * + * @param logEntryLifetimeMs The duration to use for most ESP-NOW log entries, in milliseconds. + */ + static void setLogEntryLifetimeMs(const uint32_t logEntryLifetimeMs); + static uint32_t logEntryLifetimeMs(); + + /** + * Set the duration during which sent ESP-NOW broadcast are stored in the log and can receive responses. + * This is shorter by default than logEntryLifetimeMs() in order to preserve RAM since broadcasts are always kept in the log until they expire, + * whereas normal transmissions are only kept till they receive a response. + * Setting the duration too long may cause the node to run out of RAM, especially if there is intense broadcast activity. + * Setting the duration too short may cause ESP-NOW broadcasts to stop working, or make the node never receive responses to broadcasts. + * + * Set to 1000 ms by default. + * + * @param broadcastResponseTimeoutMs The duration sent ESP-NOW broadcasts will be stored in the log, in milliseconds. + */ + static void setBroadcastResponseTimeoutMs(const uint32_t broadcastResponseTimeoutMs); + static uint32_t broadcastResponseTimeoutMs(); + + /** + * 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 encrypted connection 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 espnowEncryptedConnectionKey An array containing the encryptedConnectionKeyLength bytes that will be used as the encryption key. + */ + void setEspnowEncryptedConnectionKey(const uint8_t espnowEncryptedConnectionKey[EspnowProtocolInterpreter::encryptedConnectionKeyLength]); + + /** + * 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 encrypted connection 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 espnowHashKeySeed A string that will be used to generate the encryption key. The same string will always generate the same key. + * A minimum of 8 random characters are recommended to ensure sufficient key variation. + */ + void setEspnowEncryptedConnectionKey(const String &espnowEncryptedConnectionKeySeed); + + /** + * Get the encryption key used by this EspnowMeshBackend instance for creating encrypted ESP-NOW connections. + * + * @return The current espnowEncryptedConnectionKey for this EspnowMeshBackend instance. + */ + const uint8_t *getEspnowEncryptedConnectionKey() const; + uint8_t *getEspnowEncryptedConnectionKey(uint8_t resultArray[EspnowProtocolInterpreter::encryptedConnectionKeyLength]) const; + + /** + * Change the key used to encrypt/decrypt the encrypted connection 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 encrypted connection 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 encryptedConnectionKeyLength 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::encryptedConnectionKeyLength]); + + /** + * 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 encrypted connection 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 espnowEncryptionKokSeed A string that will be used to generate the KoK. The same string will always generate the same KoK. + * A minimum of 8 random characters are recommended to ensure sufficient KoK variation. + * @return True if Kok was changed successfully. False if Kok was not changed. + */ + static bool setEspnowEncryptionKok(const String &espnowEncryptionKokSeed); + + /** + * 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 hashKeyLength bytes that will be used as the HMAC key. + */ + void setEspnowHashKey(const uint8_t espnowHashKey[EspnowProtocolInterpreter::hashKeyLength]); + + /** + * 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 espnowHashKeySeed A string that will be used to generate the HMAC key. The same string will always generate the same key. + * A minimum of 8 random characters are recommended to ensure sufficient key variation. + */ + void setEspnowHashKey(const String &espnowHashKeySeed); + + const uint8_t *getEspnowHashKey() const; + + /** + * If true, AEAD will be used to encrypt/decrypt all messages sent/received by this node via ESP-NOW, regardless of whether the connection is encrypted or not. + * All nodes this node wishes to communicate with must then also use encrypted messages with the same getEspnowMessageEncryptionKey(), or messages will not be accepted. + * Note that using encrypted messages will reduce the number of message bytes that can be transmitted. + * + * Using AEAD will only encrypt the message content, not the transmission metadata. + * The AEAD encryption does not require any pairing, and is thus faster for single messages than establishing a new encrypted connection before transfer. + * AEAD encryption also works with ESP-NOW broadcasts and supports an unlimited number of nodes, which is not true for encrypted connections. + * Encrypted ESP-NOW connections do however come with built in replay attack protection, which is not provided by the framework when using AEAD encryption, + * and allow EspnowProtocolInterpreter::aeadMetadataSize extra message bytes per transmission. + * Transmissions via encrypted connections are also slightly faster than via AEAD once a connection has been established. + * + * useEncryptedMessages() is false by default. + * + * @param useEncryptedMessages If true, AEAD encryption/decryption is enabled. If false, AEAD encryption/decryption is disabled. + */ + static void setUseEncryptedMessages(const bool useEncryptedMessages); + static bool useEncryptedMessages(); + + /** + * Change the key used to encrypt/decrypt messages when using AEAD encryption. + * If no message encryption key is provided by the user, a default key consisting of all zeroes is used. + * + * This changes the message encryption key for all EspnowMeshBackend instances on this ESP8266. + * + * @param espnowMessageEncryptionKey An array containing the experimental::crypto::ENCRYPTION_KEY_LENGTH bytes that will be used as the message encryption key. + */ + static void setEspnowMessageEncryptionKey(const uint8_t espnowMessageEncryptionKey[experimental::crypto::ENCRYPTION_KEY_LENGTH]); + + /** + * Change the key used to encrypt/decrypt messages when using AEAD encryption. + * If no message encryption key is provided by the user, a default key consisting of all zeroes is used. + * + * This changes the message encryption key for all EspnowMeshBackend instances on this ESP8266. + * + * @param espnowMessageEncryptionKeySeed A string that will be used to generate the message encryption key. The same string will always generate the same key. + * A minimum of 8 random characters are recommended to ensure sufficient key variation. + */ + static void setEspnowMessageEncryptionKey(const String &espnowMessageEncryptionKeySeed); + + /** + * Get the key used to encrypt/decrypt messages when using AEAD encryption. + * + * @return An uint8_t array with size experimental::crypto::ENCRYPTION_KEY_LENGTH containing the currently used message encryption key. + */ + static const uint8_t *getEspnowMessageEncryptionKey(); + + /** + * Hint: Use String.length() to get the ASCII length of a String. + * + * @return 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. + * Also note that this value will be reduced by EspnowProtocolInterpreter::aeadMetadataSize if useEncryptedMessages() is true. + */ + 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(const uint8_t maxTransmissionsPerMessage); + static uint8_t getMaxTransmissionsPerMessage(); + + /** + * Hint: Use String.length() to get the ASCII length of a String. + * + * @return The maximum length in bytes an ASCII message is allowed to be when transmitted/broadcasted by this node. + * Note that non-ASCII characters usually require at least two bytes each. + * Also note that this value will be reduced if useEncryptedMessages() is true. + */ + 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(const bool enabled) override; + bool verboseMode() const 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, const bool newline = true) const override; + + /** + * Same as verboseMode(), but used for printing from static functions. + * + * @return 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, const 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(). + * @return A String containing the message of the response at responseIndex. + */ + static String getScheduledResponseMessage(const 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(). + * @return An array with six bytes containing the MAC address for the recipient of the response at responseIndex. + */ + static const uint8_t *getScheduledResponseRecipient(const uint32_t responseIndex); + + /** + * Get the number of ESP-NOW responses that are scheduled for transmission from this node. + * + * @return 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, const 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(const 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(const 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(const uint32_t timeoutMs); + static uint32_t getEncryptionRequestTimeout(); + + void setAutoEncryptionDuration(const uint32_t duration); + uint32_t getAutoEncryptionDuration() const; + + void setBroadcastFilter(const broadcastFilterType broadcastFilter); + broadcastFilterType getBroadcastFilter() const; + + /** + * Set a function that should be called after each attempted ESP-NOW response transmission. + * In case of a successful response transmission, the call happens just before the response is removed from the waiting list. + * Only the hook of the EspnowMeshBackend instance that is getEspnowRequestManager() will be called. + * + * The hook should return a bool. + * If this return value is true, the response transmission process will continue with the next response in the waiting list. + * If it is false, the response transmission process will stop once processing of the just sent response is complete. + * The default responseTransmittedHook always returns true. + */ + void setResponseTransmittedHook(const EspnowTransmitter::responseTransmittedHookType responseTransmittedHook); + EspnowTransmitter::responseTransmittedHookType getResponseTransmittedHook() const; + + /** + * Get the MAC address of the sender of the most recently received ESP-NOW request, response or broadcast 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. + * + * @return A String filled with a hexadecimal representation of the MAC, without delimiters. + */ + String getSenderMac() const; + + /** + * Get the MAC address of the sender of the most recently received ESP-NOW request, response or broadcast to this EspnowMeshBackend instance. + * Returns a uint8_t array. + * 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. + * @return macArray filled with the sender MAC. + */ + uint8_t *getSenderMac(uint8_t *macArray) const; + + /** + * Get the AP MAC address of the sender of the most recently received ESP-NOW request, response or broadcast to this EspnowMeshBackend instance. + * Returns a String. + * + * @return A String filled with a hexadecimal representation of the AP MAC, without delimiters. + */ + String getSenderAPMac() const; + + /** + * Get the AP MAC address of the sender of the most recently received ESP-NOW request, response or broadcast to this EspnowMeshBackend instance. + * Returns a uint8_t array. + * + * @param macArray The array that should store the MAC address. Must be at least 6 bytes. + * @return macArray filled with the sender AP MAC. + */ + uint8_t *getSenderAPMac(uint8_t *macArray) const; + + /** + * Get whether the ESP-NOW request, response or broadcast which was most recently received by this EspnowMeshBackend instance was sent over an encrypted connection or not. + * + * @return If true, the request, response or broadcast was sent over an encrypted connection. If false, the connection was unencrypted. + */ + bool receivedEncryptedTransmission() const; + + /** + * Should be used together with serializeUnencryptedConnection() if the node sends unencrypted transmissions + * and will go to sleep for less than logEntryLifetimeMs() while other nodes stay awake. + * Otherwise the message ID will be reset after sleep, which means that the nodes that stayed awake may ignore new unencrypted transmissions until logEntryLifetimeMs() ms has passed. + * + * @param serializedConnectionState A serialized state of an unencrypted ESP-NOW connection. + * + * @return True if connection was added. False otherwise (e.g. if there is faulty input). + */ + static bool addUnencryptedConnection(const String &serializedConnectionState); + + /** + * Adds a new permanent encrypted ESP-NOW connection, or makes the duration of an existing temporary connection permanent. + * Note that this will not add an encrypted ESP-NOW connection automatically to the other node. Thus the same method will need to be called on the other node as well to establish an encrypted connection. + * Methods such as requestEncryptedConnection creates an encrypted connection automatically in both nodes, but requires information exchange between the nodes before the connection is established (and is thus much slower). + * + * When called, the method will update an existing encrypted ESP-NOW connection with the current stored encrypted connection key. (in case it has changed since the connection was established) + * + * The maximum number of simultaneous encrypted connections is restricted by the ESP-NOW API and is EspnowProtocolInterpreter::maxEncryptedConnections (6 by default). + * + * @param peerStaMac The station MAC of the other node. + * @param peerApMac The AP MAC of the other node. + * @param peerSessionKey The session key of the other node. At least one of the leftmost 32 bits should be 1, since the key otherwise indicates the connection is unencrypted. + * @param peerSessionKey The session key of this node. At least one of the leftmost 32 bits should be 1, since the key otherwise indicates the connection is unencrypted. + * + * @return EncryptedConnectionStatus::CONNECTION_ESTABLISHED if the connection was created. Otherwise another status code based on the outcome. + */ + EncryptedConnectionStatus addEncryptedConnection(uint8_t *peerStaMac, uint8_t *peerApMac, const uint64_t peerSessionKey, const uint64_t ownSessionKey); + + /** + * Create an encrypted ESP-NOW connection on this node based on the information stored in serializedConnectionState. + * Note that this will not add an encrypted ESP-NOW connection automatically to the other node. Thus the same method will need to be called on the other node as well to establish an encrypted connection. + * Methods such as requestEncryptedConnection creates an encrypted connection automatically in both nodes, but requires information exchange between the nodes before the connection is established (and is thus much slower). + * + * When called, the method will update an existing encrypted ESP-NOW connection with the current stored encrypted connection key. (in case it has changed since the connection was established) + * + * The maximum number of simultaneous encrypted connections is restricted by the ESP-NOW API and is EspnowProtocolInterpreter::maxEncryptedConnections (6 by default). + * + * Note that the espnowEncryptedConnectionKey, espnowEncryptionKok, espnowHashKey and espnowMessageEncryptionKey are not serialized. + * These will be set to the values of the EspnowMeshBackend instance that is adding the serialized encrypted connection. + * + * @param serializedConnectionState A String containing the serialized connection state. + * @param ignoreDuration Ignores any stored duration in serializedConnectionState, guaranteeing that the created connection will be permanent. + * + * @return EncryptedConnectionStatus::CONNECTION_ESTABLISHED if the connection was created. Otherwise another status code based on the outcome. EncryptedConnectionStatus::REQUEST_TRANSMISSION_FAILED indicates a malformed serializedConnectionState. + */ + EncryptedConnectionStatus addEncryptedConnection(const String &serializedConnectionState, const bool ignoreDuration = false); + + /** + * Adds a new temporary encrypted ESP-NOW connection, or changes the duration of an existing temporary connection (only updates keys, not duration, for existing permanent connections). + * Note that this will not add an encrypted ESP-NOW connection automatically to the other node. Thus the same method will need to be called on the other node as well to establish an encrypted connection. + * Methods such as requestEncryptedConnection creates an encrypted connection automatically in both nodes, but requires information exchange between the nodes before the connection is established (and is thus much slower). + * + * When called, the method will update an existing encrypted ESP-NOW connection with the current stored encrypted connection key. (in case it has changed since the connection was established) + * + * The maximum number of simultaneous encrypted connections is restricted by the ESP-NOW API and is EspnowProtocolInterpreter::maxEncryptedConnections (6 by default). + * + * As with all these methods, changes will only take effect once the requester proves it has the ability to decrypt the session key. + * + * @param peerStaMac The station MAC of the other node. + * @param peerApMac The AP MAC of the other node. + * @param peerSessionKey The session key of the other node. At least one of the leftmost 32 bits should be 1, since the key otherwise indicates the connection is unencrypted. + * @param peerSessionKey The session key of this node. At least one of the leftmost 32 bits should be 1, since the key otherwise indicates the connection is unencrypted. + * @param duration The desired duration of the connection. + * + * @return EncryptedConnectionStatus::CONNECTION_ESTABLISHED if the connection was created. Otherwise another status code based on the outcome. + */ + EncryptedConnectionStatus addTemporaryEncryptedConnection(uint8_t *peerStaMac, uint8_t *peerApMac, const uint64_t peerSessionKey, const uint64_t ownSessionKey, const uint32_t duration); + + /** + * Create a temporary encrypted ESP-NOW connection on this node based on the information stored in serializedConnectionState. + * Note that this will not add an encrypted ESP-NOW connection automatically to the other node. Thus the same method will need to be called on the other node as well to establish an encrypted connection. + * Methods such as requestEncryptedConnection creates an encrypted connection automatically in both nodes, but requires information exchange between the nodes before the connection is established (and is thus much slower). + * + * When called, the method will update an existing encrypted ESP-NOW connection with the current stored encrypted connection key. (in case it has changed since the connection was established) + * + * The maximum number of simultaneous encrypted connections is restricted by the ESP-NOW API and is EspnowProtocolInterpreter::maxEncryptedConnections (6 by default). + * + * Note that the espnowEncryptedConnectionKey, espnowEncryptionKok, espnowHashKey and espnowMessageEncryptionKey are not serialized. + * These will be set to the values of the EspnowMeshBackend instance that is adding the serialized encrypted connection. + * + * @param serializedConnectionState A String containing the serialized connection state. + * @param ignoreDuration Ignores any stored duration in serializedConnectionState, guaranteeing that the created connection will be permanent. + * @param duration The desired duration of the connection. Overrides any stored duration in the serializedConnectionState. + * + * @return EncryptedConnectionStatus::CONNECTION_ESTABLISHED if the connection was created. Otherwise another status code based on the outcome. EncryptedConnectionStatus::REQUEST_TRANSMISSION_FAILED indicates a malformed serializedConnectionState. + */ + EncryptedConnectionStatus addTemporaryEncryptedConnection(const String &serializedConnectionState, const uint32_t duration); + + /** + * Request a permanent encrypted ESP-NOW connection with the node that uses peerMac. + * 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. + * The method makes sure both nodes have an encrypted connection to each other that's permanent. + * + * The maximum number of simultaneous encrypted connections is restricted by the ESP-NOW API and is EspnowProtocolInterpreter::maxEncryptedConnections (6 by default). + * + * @param peerMac The MAC of the other node to which the request should be sent. + * + * @return EncryptedConnectionStatus::CONNECTION_ESTABLISHED if the permanent connection was created. EncryptedConnectionStatus::SOFT_LIMIT_CONNECTION_ESTABLISHED if only a temporary soft limit connection could be established (see the setEncryptedConnectionsSoftLimit method documentation for details). Otherwise another status code based on the outcome. + */ + EncryptedConnectionStatus requestEncryptedConnection(const uint8_t *peerMac); + + /** + * Request a temporary encrypted ESP-NOW connection with the node that uses peerMac. + * If a temporary encrypted connection to peerMac already exists, only connection duration is updated. All other settings are kept as is. Permanent connections are not modified. Use removeEncryptedConnection/requestEncryptedConnectionRemoval first if encryption keys should be updated. + * The method makes sure both nodes have an encrypted connection to each other that's either permanent or has exactly the duration specified. + * + * The maximum number of simultaneous encrypted connections is restricted by the ESP-NOW API and is EspnowProtocolInterpreter::maxEncryptedConnections (6 by default). + * + * @param peerMac The MAC of the other node to which the request should be sent. + * @param durationMs The desired duration of the connection. + * + * @return EncryptedConnectionStatus::CONNECTION_ESTABLISHED if the request was succesful. EncryptedConnectionStatus::SOFT_LIMIT_CONNECTION_ESTABLISHED if only a temporary soft limit connection could be established (see the setEncryptedConnectionsSoftLimit method documentation for details). Otherwise another status code based on the outcome. + */ + EncryptedConnectionStatus requestTemporaryEncryptedConnection(const uint8_t *peerMac, const uint32_t durationMs); + + /** + * Request a flexible temporary encrypted ESP-NOW connection with the node that uses peerMac. + * If a temporary encrypted connection to peerMac with a shorter duration already exists, connection duration is updated. All other settings are kept as is. Permanent connections are not modified. Use removeEncryptedConnection/requestEncryptedConnectionRemoval first if encryption keys should be updated. + * The method makes sure both nodes have an encrypted connection to each other that's either permanent or has at least the duration specified. + * + * The maximum number of simultaneous encrypted connections is restricted by the ESP-NOW API and is EspnowProtocolInterpreter::maxEncryptedConnections (6 by default). + * + * 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. + * + * @param peerMac The MAC of the other node to which the request should be sent. + * @param minDurationMs The desired minimum duration of the connection. + * + * @return EncryptedConnectionStatus::CONNECTION_ESTABLISHED if the request was succesful. EncryptedConnectionStatus::SOFT_LIMIT_CONNECTION_ESTABLISHED if only a temporary soft limit connection could be established (see the setEncryptedConnectionsSoftLimit method documentation for details). Otherwise another status code based on the outcome. + */ + EncryptedConnectionStatus requestFlexibleTemporaryEncryptedConnection(const uint8_t *peerMac, const uint32_t minDurationMs); + + /** + * Remove the encrypted ESP-NOW connection to peerMac from this node. + * Note that this will not remove the encrypted ESP-NOW connection automatically from the other node. Thus the same method will need to be called on the other node as well to complete the encrypted connection removal. + * The method requestEncryptedConnectionRemoval removes the encrypted connection automatically in both nodes, but requires extra information exchange between the nodes (and is thus much slower). + * + * @param peerMac The MAC of the other node. + * + * @return EncryptedConnectionRemovalOutcome::REMOVAL_SUCCEEDED if the removal succeeded. EncryptedConnectionRemovalOutcome::REMOVAL_SCHEDULED if the removal is scheduled to occur as soon as it is safe to do so (generally as soon as an ongoing transmission is complete, or at the latest during the next performEspnowMaintenance call). Otherwise another status code based on the outcome. + */ + static EncryptedConnectionRemovalOutcome removeEncryptedConnection(const uint8_t *peerMac); + + /** + * Request the removal of the encrypted ESP-NOW connection between this node and the node that uses peerMac. + * The method makes sure both nodes remove the encrypted connection to each other. + * + * @param peerMac The MAC of the other node to which the request should be sent. + * + * @return EncryptedConnectionRemovalOutcome::REMOVAL_SUCCEEDED if the removal succeeded. Otherwise another status code based on the outcome (never REMOVAL_SCHEDULED). + */ + EncryptedConnectionRemovalOutcome requestEncryptedConnectionRemoval(const uint8_t *peerMac); + + /** + * Set whether this EspnowMeshBackend instance will accept ESP-NOW requests from unencrypted connections 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 requests for encrypted connection to the node over an unencrypted connection, + * which can be useful if too many such requests could otherwise be expected. + * + * True by default. + * + * @param acceptsUnverifiedRequests If and only if true, requests from unencrypted connections will be processed when this EspnowMeshBackend instance is acting as EspnowRequestManager. + */ + void setAcceptsUnverifiedRequests(const bool acceptsUnverifiedRequests); + bool acceptsUnverifiedRequests() const; + + /** + * Set a soft upper limit on the number of encrypted connections this node can have when receiving encrypted connection requests. + * The soft limit can be used to ensure there is normally a pool of free encrypted connection slots that can be used if required. + * Each EspnowMeshBackend instance can have a separate value. The value used is that of the current EspnowRequestManager. + * The hard upper limit is 6 encrypted connections, mandated by the ESP-NOW API. + * + * Default is 6. + * + * When a request for encrypted connection is received from a node to which there is no existing permanent encrypted connection, + * and the number of encrypted connections exceeds the soft limit, + * this request will automatically be converted to an autoEncryptionRequest. + * This means it will be a temporary connection with very short duration (with default framework settings). + * + * @param softLimit The new soft limit. Valid values are 0 to 6. + */ + void setEncryptedConnectionsSoftLimit(const uint8_t softLimit); + uint8_t encryptedConnectionsSoftLimit() const; + + /** + * @return The current number of encrypted ESP-NOW connections. + */ + static uint8_t numberOfEncryptedConnections(); + + /** + * @return 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); + + /** + * Should be used together with addUnencryptedConnection if the node sends unencrypted transmissions + * and will go to sleep for less than logEntryLifetimeMs() while other nodes stay awake. + * Otherwise the message ID will be reset after sleep, which means that the nodes that stayed awake may ignore new unencrypted transmissions until logEntryLifetimeMs() ms has passed. + * + * @return The serialized state of the unencrypted ESP-NOW connection. + */ + static String serializeUnencryptedConnection(); + + /** + * 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. + * Also note that this saves the current state only, so if encrypted communication between the nodes happen after this, the stored state is invalid. + * @return 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(const 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 ConnectionType::TEMPORARY_CONNECTION the variable will be set to the remaining duration of the connection. + * Otherwise the variable value is not modified. + * @return The ConnectionType of the connection with peerMac. + */ + static ConnectionType 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 ConnectionType::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. + * @return The ConnectionType of the connection given by connectionIndex. + */ + static ConnectionType getConnectionInfo(const uint32_t connectionIndex, uint32_t *remainingDuration = nullptr, uint8_t *peerMac = nullptr); + + /** + * @return 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(); + + void setWiFiChannel(const uint8 newWiFiChannel) override; + +protected: + + EspnowDatabase *getDatabase(); + const EspnowDatabase *getDatabaseConst() const; + EspnowConnectionManager *getConnectionManager(); + const EspnowConnectionManager *getConnectionManagerConst() const; + EspnowTransmitter *getTransmitter(); + const EspnowTransmitter *getTransmitterConst() const; + EspnowEncryptionBroker *getEncryptionBroker(); + const EspnowEncryptionBroker *getEncryptionBrokerConst() const; + + 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. + * + * @param estimatedMaxDurationTracker A pointer to an ExpiringTimeTracker initialized with the desired max duration for the method. If set to nullptr there is no duration limit. + * Note that setting the estimatedMaxDuration too low may result in missed ESP-NOW transmissions because of too little time for maintenance. + * Also note that although the method will try to respect the max duration limit, there is no guarantee. Overshoots by tens of milliseconds are possible. + */ + static void sendStoredEspnowMessages(const ExpiringTimeTracker *estimatedMaxDurationTracker = nullptr); + + using macAndType_td = EspnowProtocolInterpreter::macAndType_td; + using messageID_td = EspnowProtocolInterpreter::messageID_td; + using peerMac_td = EspnowProtocolInterpreter::peerMac_td; + +private: + + EspnowMeshBackend(const requestHandlerType requestHandler, const responseHandlerType responseHandler, const networkFilterType networkFilter, const broadcastFilterType broadcastFilter, + const String &meshPassword, const String &ssidPrefix, const String &ssidSuffix, const bool verboseMode, const uint8 meshWiFiChannel); + + EspnowDatabase _database; + EspnowConnectionManager _connectionManager; + EspnowTransmitter _transmitter; + EspnowEncryptionBroker _encryptionBroker; + + void prepareForTransmission(const String &message, const bool scan, const bool scanAllWiFiChannels); + TransmissionStatusType initiateTransmission(const String &message, const EspnowNetworkInfo &recipientInfo); + TransmissionStatusType initiateTransmissionKernel(const String &message, const uint8_t *targetBSSID); + TransmissionStatusType initiateAutoEncryptingTransmission(const String &message, uint8_t *targetBSSID, const EncryptedConnectionStatus connectionStatus); + void printTransmissionStatistics() const; + + // 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; + + /** + * 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, const uint8_t len); + void espnowReceiveCallback(const uint8_t *macaddr, uint8_t *data, const uint8_t len); + + broadcastFilterType _broadcastFilter; + + bool _acceptsUnverifiedRequests = true; +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/EspnowNetworkInfo.cpp b/libraries/ESP8266WiFiMesh/src/EspnowNetworkInfo.cpp new file mode 100644 index 0000000000..6e52925e39 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/EspnowNetworkInfo.cpp @@ -0,0 +1,38 @@ +/* + * 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 "EspnowNetworkInfo.h" +#include + +EspnowNetworkInfo::EspnowNetworkInfo(const int networkIndex) : NetworkInfoBase(networkIndex) { }; + +EspnowNetworkInfo::EspnowNetworkInfo(const NetworkInfoBase &originalNetworkInfo) : NetworkInfoBase(originalNetworkInfo) +{ + assert(BSSID() != defaultBSSID); // We need at least BSSID to be able to connect. +}; + +EspnowNetworkInfo::EspnowNetworkInfo(const uint8_t BSSID[6], const String &SSID, const int32_t wifiChannel, const uint8_t encryptionType, const int32_t RSSI , const bool isHidden) + : NetworkInfoBase(SSID, wifiChannel, BSSID, encryptionType, RSSI, isHidden) +{ } + diff --git a/libraries/ESP8266WiFiMesh/src/EspnowNetworkInfo.h b/libraries/ESP8266WiFiMesh/src/EspnowNetworkInfo.h new file mode 100644 index 0000000000..06b39b987c --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/EspnowNetworkInfo.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 __ESPNOWNETWORKINFO_H__ +#define __ESPNOWNETWORKINFO_H__ + +#include "NetworkInfoBase.h" + +class EspnowNetworkInfo : public NetworkInfoBase { + +public: + + /** + * Automatically fill in the rest of the network info using networkIndex and the WiFi scan results. + */ + EspnowNetworkInfo(const int networkIndex); + + EspnowNetworkInfo(const NetworkInfoBase &originalNetworkInfo); + + EspnowNetworkInfo(const uint8_t BSSID[6], const String &SSID = defaultSSID, const int32_t wifiChannel = defaultWifiChannel, const uint8_t encryptionType = defaultEncryptionType, + const int32_t RSSI = defaultRSSI, const bool isHidden = defaultIsHidden); +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/EspnowProtocolInterpreter.cpp b/libraries/ESP8266WiFiMesh/src/EspnowProtocolInterpreter.cpp new file mode 100644 index 0000000000..6bf1568ea1 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/EspnowProtocolInterpreter.cpp @@ -0,0 +1,129 @@ +/* + * 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 +#include "EspnowTransmitter.h" +#include "UtilityFunctions.h" + +namespace +{ + namespace TypeCast = MeshTypeConversionFunctions; +} + +namespace EspnowProtocolInterpreter +{ + uint8_t metadataSize() + { + return protocolBytesSize + (EspnowTransmitter::useEncryptedMessages() ? aeadMetadataSize : 0); + } + + uint32_t getMaxBytesPerTransmission() + { + return 250; + } + + uint32_t getMaxMessageBytesPerTransmission() + { + return getMaxBytesPerTransmission() - metadataSize(); + } + + String getHashKeyLength(uint8_t *transmissionDataArray, const uint8_t transmissionLength) + { + String messageContent = emptyString; + + if(transmissionLength >= metadataSize()) + { + uint8_t messageSize = transmissionLength - metadataSize(); + + messageContent = TypeCast::uint8ArrayToMultiString(transmissionDataArray + metadataSize(), messageSize); + } + + return messageContent; + } + + char getMessageType(const uint8_t *transmissionDataArray) + { + return char(transmissionDataArray[messageTypeIndex]); + } + + uint8_t getTransmissionsRemaining(const uint8_t *transmissionDataArray) + { + return (transmissionDataArray[transmissionsRemainingIndex] & 0x7F); + } + + bool isMessageStart(const uint8_t *transmissionDataArray) + { + return (transmissionDataArray[transmissionsRemainingIndex] & 0x80); // If MSB is one we have messageStart + } + + uint64_t getTransmissionMac(const uint8_t *transmissionDataArray) + { + return TypeCast::macToUint64(transmissionDataArray + transmissionMacIndex); + } + + uint8_t *getTransmissionMac(const uint8_t *transmissionDataArray, uint8_t *resultArray) + { + std::copy_n((transmissionDataArray + transmissionMacIndex), 6, resultArray); + return resultArray; + } + + uint64_t getMessageID(const uint8_t *transmissionDataArray) + { + return TypeCast::uint8ArrayToUint64(transmissionDataArray + messageIDIndex); + } + + uint8_t *setMessageID(uint8_t *transmissionDataArray, const uint64_t messageID) + { + return TypeCast::uint64ToUint8Array(messageID, transmissionDataArray + messageIDIndex); + } + + bool usesEncryption(const uint64_t messageID) + { + // At least one of the leftmost half of bits in messageID is 1 if the transmission is encrypted. + return messageID & uint64LeftmostBits; + } + + bool usesConstantSessionKey(const char messageType) + { + return messageType == 'A' || messageType == 'C'; + } + + uint64_t createSessionKey() + { + uint64_t newSessionKey = MeshUtilityFunctions::randomUint64(); + return usesEncryption(newSessionKey) ? newSessionKey : (newSessionKey | ((uint64_t)ESP.random()) << 32 | uint64MSB); + } + + macAndType_td createMacAndTypeValue(const uint64_t uint64Mac, const char messageType) + { + return static_cast(uint64Mac << 8 | (uint64_t)messageType); + } + + uint64_t macAndTypeToUint64Mac(const macAndType_td &macAndTypeValue) + { + return static_cast(macAndTypeValue) >> 8; + } +} diff --git a/libraries/ESP8266WiFiMesh/src/EspnowProtocolInterpreter.h b/libraries/ESP8266WiFiMesh/src/EspnowProtocolInterpreter.h new file mode 100644 index 0000000000..705637868a --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/EspnowProtocolInterpreter.h @@ -0,0 +1,104 @@ +/* + * 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 +{ + constexpr char synchronizationRequestHeader[] PROGMEM = "Synchronization request."; + constexpr char encryptionRequestHeader[] PROGMEM = "AddEC:"; // Add encrypted connection + constexpr char temporaryEncryptionRequestHeader[] PROGMEM = "AddTEC:"; // Add temporary encrypted connection + constexpr char basicConnectionInfoHeader[] PROGMEM = "BasicCI:"; // Basic connection info + constexpr char encryptedConnectionInfoHeader[] PROGMEM = "EncryptedCI:"; // Encrypted connection info + constexpr char softLimitEncryptedConnectionInfoHeader[] PROGMEM = "SLEncryptedCI:"; // Soft limit encrypted connection info + constexpr char maxConnectionsReachedHeader[] PROGMEM = "MAX_CONNECTIONS_REACHED_PEER:"; + constexpr char encryptedConnectionVerificationHeader[] PROGMEM = "ECVerified:"; // Encrypted connection verified + constexpr char encryptedConnectionRemovalRequestHeader[] PROGMEM = "RemoveEC:"; // Remove encrypted connection + + constexpr uint8_t messageTypeIndex = 0; + constexpr uint8_t transmissionsRemainingIndex = 1; + constexpr uint8_t transmissionMacIndex = 2; + constexpr uint8_t messageIDIndex = 8; + + constexpr 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. + + constexpr uint8_t protocolBytesSize = 16; + constexpr uint8_t aeadMetadataSize = 28; + uint8_t metadataSize(); + uint32_t getMaxBytesPerTransmission(); + uint32_t getMaxMessageBytesPerTransmission(); + + constexpr uint64_t uint64BroadcastMac = 0xFFFFFFFFFFFF; + constexpr uint8_t broadcastMac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + + constexpr uint8_t encryptedConnectionKeyLength = 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. + constexpr uint8_t hashKeyLength = 16; // This can be changed to any value up to 255. Common values are 16 and 32. + + constexpr uint64_t uint64LeftmostBits = 0xFFFFFFFF00000000; + constexpr uint64_t uint64MSB = 0x8000000000000000; + + String getHashKeyLength(uint8_t *transmissionDataArray, const uint8_t transmissionLength); + char getMessageType(const uint8_t *transmissionDataArray); + uint8_t getTransmissionsRemaining(const uint8_t *transmissionDataArray); + bool isMessageStart(const uint8_t *transmissionDataArray); + uint64_t getTransmissionMac(const uint8_t *transmissionDataArray); + uint8_t *getTransmissionMac(const uint8_t *transmissionDataArray, uint8_t *resultArray); + uint64_t getMessageID(const uint8_t *transmissionDataArray); + // @return a pointer to transmissionDataArray + uint8_t *setMessageID(uint8_t *transmissionDataArray, const uint64_t messageID); + + bool usesEncryption(const uint64_t messageID); + bool usesConstantSessionKey(const char messageType); + + /** + * Create a new session key for an encrypted connection using the built in RANDOM_REG32/ESP.random() 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. + * + * @return A uint64_t containing a new session key for an encrypted connection. + */ + uint64_t createSessionKey(); + + enum class macAndType_td : uint64_t {}; + using messageID_td = uint64_t; + using peerMac_td = uint64_t; + + macAndType_td createMacAndTypeValue(const uint64_t uint64Mac, const char messageType); + uint64_t macAndTypeToUint64Mac(const macAndType_td &macAndTypeValue); +} + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/EspnowTransmitter.cpp b/libraries/ESP8266WiFiMesh/src/EspnowTransmitter.cpp new file mode 100644 index 0000000000..3670844c86 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/EspnowTransmitter.cpp @@ -0,0 +1,428 @@ +/* + Copyright (C) 2020 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 "EspnowTransmitter.h" +#include "EspnowMeshBackend.h" +#include "TypeConversionFunctions.h" +#include "UtilityFunctions.h" +#include "MeshCryptoInterface.h" +#include "JsonTranslator.h" + +namespace +{ + namespace TypeCast = MeshTypeConversionFunctions; + + double _transmissionsTotal = 0; + double _transmissionsFailed = 0; + + std::shared_ptr _espnowTransmissionMutex = std::make_shared(false); + std::shared_ptr _espnowSendToNodeMutex = std::make_shared(false); + + uint32_t _espnowTransmissionTimeoutMs = 40; + uint32_t _espnowRetransmissionIntervalMs = 15; + + uint8_t _espnowMessageEncryptionKey[experimental::crypto::ENCRYPTION_KEY_LENGTH] = { 0 }; + bool _useEncryptedMessages = false; + + uint8_t _transmissionTargetBSSID[6] = {0}; + + bool _espnowSendConfirmed = false; + + uint8_t _maxTransmissionsPerMessage = 3; +} + +EspnowTransmitter::EspnowTransmitter(ConditionalPrinter &conditionalPrinterInstance, EspnowDatabase &databaseInstance, EspnowConnectionManager &connectionManagerInstance) + : _conditionalPrinter(conditionalPrinterInstance), _database(databaseInstance), _connectionManager(connectionManagerInstance) +{ +} + +void EspnowTransmitter::espnowSendCallback(uint8_t* mac, uint8_t sendStatus) +{ + if(_espnowSendConfirmed) + return; + else if(!sendStatus && MeshUtilityFunctions::macEqual(mac, _transmissionTargetBSSID)) // sendStatus == 0 when send was OK. + _espnowSendConfirmed = true; // We do not want to reset this to false. That only happens before transmissions. Otherwise subsequent failed send attempts may obscure an initial successful one. +} + +void EspnowTransmitter::setUseEncryptedMessages(const bool useEncryptedMessages) +{ + MutexTracker mutexTracker(_espnowSendToNodeMutex); + if(!mutexTracker.mutexCaptured()) + { + assert(false && String(F("ERROR! espnowSendToNode in progress. Don't call setUseEncryptedMessages from non-hook callbacks since this may modify the ESP-NOW transmission parameters during ongoing transmissions! Aborting."))); + } + + _useEncryptedMessages = useEncryptedMessages; +} +bool EspnowTransmitter::useEncryptedMessages() { return _useEncryptedMessages; } + +void EspnowTransmitter::setEspnowMessageEncryptionKey(const uint8_t espnowMessageEncryptionKey[experimental::crypto::ENCRYPTION_KEY_LENGTH]) +{ + assert(espnowMessageEncryptionKey != nullptr); + + for(int i = 0; i < experimental::crypto::ENCRYPTION_KEY_LENGTH; ++i) + { + _espnowMessageEncryptionKey[i] = espnowMessageEncryptionKey[i]; + } +} + +void EspnowTransmitter::setEspnowMessageEncryptionKey(const String &espnowMessageEncryptionKeySeed) +{ + MeshCryptoInterface::initializeKey(_espnowMessageEncryptionKey, experimental::crypto::ENCRYPTION_KEY_LENGTH, espnowMessageEncryptionKeySeed); +} + +const uint8_t *EspnowTransmitter::getEspnowMessageEncryptionKey() +{ + return _espnowMessageEncryptionKey; +} + +void EspnowTransmitter::setBroadcastTransmissionRedundancy(const uint8_t redundancy) { _broadcastTransmissionRedundancy = redundancy; } +uint8_t EspnowTransmitter::getBroadcastTransmissionRedundancy() const { return _broadcastTransmissionRedundancy; } + +void EspnowTransmitter::setResponseTransmittedHook(const responseTransmittedHookType responseTransmittedHook) { _responseTransmittedHook = responseTransmittedHook; } +EspnowTransmitter::responseTransmittedHookType EspnowTransmitter::getResponseTransmittedHook() const { return _responseTransmittedHook; } + +void EspnowTransmitter::setMaxTransmissionsPerMessage(const uint8_t maxTransmissionsPerMessage) +{ + assert(1 <= maxTransmissionsPerMessage && maxTransmissionsPerMessage <= 128); + + _maxTransmissionsPerMessage = maxTransmissionsPerMessage; +} + +uint8_t EspnowTransmitter::getMaxTransmissionsPerMessage() {return _maxTransmissionsPerMessage;} + +uint32_t EspnowTransmitter::getMaxMessageLength() +{ + return getMaxTransmissionsPerMessage() * EspnowProtocolInterpreter::getMaxMessageBytesPerTransmission(); +} + +void EspnowTransmitter::setEspnowTransmissionTimeout(const uint32_t timeoutMs) +{ + _espnowTransmissionTimeoutMs = timeoutMs; +} +uint32_t EspnowTransmitter::getEspnowTransmissionTimeout() {return _espnowTransmissionTimeoutMs;} + +void EspnowTransmitter::setEspnowRetransmissionInterval(const uint32_t intervalMs) +{ + _espnowRetransmissionIntervalMs = intervalMs; +} +uint32_t EspnowTransmitter::getEspnowRetransmissionInterval() {return _espnowRetransmissionIntervalMs;} + +double EspnowTransmitter::getTransmissionFailRate() +{ + if(_transmissionsTotal == 0) + return 0; + + return _transmissionsFailed/_transmissionsTotal; +} + +void EspnowTransmitter::resetTransmissionFailRate() +{ + _transmissionsFailed = 0; + _transmissionsTotal = 0; +} + +void EspnowTransmitter::sendEspnowResponses(const ExpiringTimeTracker *estimatedMaxDurationTracker) +{ + uint32_t bufferedCriticalHeapLevel = EspnowDatabase::criticalHeapLevel() + EspnowDatabase::criticalHeapLevelBuffer(); // We preferably want to start clearing the logs a bit before things get critical. + + MutexTracker responsesToSendMutexTracker(EspnowDatabase::captureResponsesToSendMutex()); + if(!responsesToSendMutexTracker.mutexCaptured()) + { + assert(false && String(F("ERROR! responsesToSend locked. Don't call sendEspnowResponses from callbacks as this may corrupt program state! Aborting."))); + } + + uint32_t responseIndex = 0; + for(std::list::iterator responseIterator = EspnowDatabase::responsesToSend().begin(); responseIterator != EspnowDatabase::responsesToSend().end(); ++responseIndex) + { + if(responseIterator->getTimeTracker().timeSinceCreation() > EspnowDatabase::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). + bool transmissionSuccessful = espnowSendToNodeUnsynchronized(responseIterator->getMessage(), responseIterator->getRecipientMac(), 'A', responseIterator->getRequestID()) + == TransmissionStatusType::TRANSMISSION_COMPLETE; + + bool hookOutcome = true; + if(EspnowMeshBackend *currentEspnowRequestManager = EspnowMeshBackend::getEspnowRequestManager()) + hookOutcome = currentEspnowRequestManager->getResponseTransmittedHook()(transmissionSuccessful, responseIterator->getMessage(), responseIterator->getRecipientMac(), responseIndex, *currentEspnowRequestManager); + + if(transmissionSuccessful) + { + responseIterator = EspnowDatabase::responsesToSend().erase(responseIterator); + --responseIndex; + } + 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. + ConditionalPrinter::warningPrint("WARNING! Free heap below chosen minimum. Performing emergency log clearing."); + EspnowDatabase::clearOldLogEntries(true); + return; // responseIterator may be invalid now. Also, we should give the main loop a chance to respond to the situation. + } + + if(!hookOutcome || (estimatedMaxDurationTracker && estimatedMaxDurationTracker->expired())) + return; + } +} + +MutexTracker EspnowTransmitter::captureEspnowTransmissionMutex() +{ + // Syntax like this will move the resulting value into its new position (similar to NRVO): https://stackoverflow.com/a/11540204 + return MutexTracker(_espnowTransmissionMutex); +} + +MutexTracker EspnowTransmitter::captureEspnowTransmissionMutex(const std::function destructorHook) { return MutexTracker(_espnowTransmissionMutex, destructorHook); } + +bool EspnowTransmitter::transmissionInProgress(){return *_espnowTransmissionMutex;} + +TransmissionStatusType EspnowTransmitter::espnowSendToNode(const String &message, const uint8_t *targetBSSID, const char messageType, EspnowMeshBackend *espnowInstance) +{ + using EspnowProtocolInterpreter::synchronizationRequestHeader; + + EncryptedConnectionLog *encryptedConnection = EspnowConnectionManager::getEncryptedConnection(targetBSSID); + + if(encryptedConnection) + { + uint8_t encryptedMac[6] {0}; + encryptedConnection->getEncryptedPeerMac(encryptedMac); + + assert(esp_now_is_peer_exist(encryptedMac) > 0 && String(F("ERROR! Attempting to send content marked as encrypted via unencrypted connection!"))); + + if(encryptedConnection->desync()) + { + espnowSendToNodeUnsynchronized(FPSTR(synchronizationRequestHeader), encryptedMac, 'S', EspnowConnectionManager::generateMessageID(encryptedConnection), espnowInstance); + + if(encryptedConnection->desync()) + { + return TransmissionStatusType::TRANSMISSION_FAILED; + } + } + + return espnowSendToNodeUnsynchronized(message, encryptedMac, messageType, EspnowConnectionManager::generateMessageID(encryptedConnection), espnowInstance); + } + + return espnowSendToNodeUnsynchronized(message, targetBSSID, messageType, EspnowConnectionManager::generateMessageID(encryptedConnection), espnowInstance); +} + +TransmissionStatusType EspnowTransmitter::espnowSendToNodeUnsynchronized(const String message, const uint8_t *targetBSSID, const char messageType, const uint64_t messageID, EspnowMeshBackend *espnowInstance) +{ + using namespace EspnowProtocolInterpreter; + + MutexTracker mutexTracker(_espnowSendToNodeMutex); + if(!mutexTracker.mutexCaptured()) + { + assert(false && String(F("ERROR! espnowSendToNode already in progress. Don't call espnowSendToNode from callbacks as this will make it impossible to know which transmissions succeed! Aborting."))); + return TransmissionStatusType::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 = EspnowConnectionManager::getEncryptedConnection(_transmissionTargetBSSID); + + int32_t transmissionsRequired = ceil((double)message.length() / getMaxMessageBytesPerTransmission()); + int32_t transmissionsRemaining = transmissionsRequired > 1 ? transmissionsRequired - 1 : 0; + + _transmissionsTotal++; + + // Though it is possible to handle messages requiring more than 3 transmissions with the current design, transmission fail rates would increase dramatically. + // Messages composed of up to 128 transmissions can be handled without modification, but RAM limitations on the ESP8266 would make this hard in practice. + // We thus prefer to keep the code simple and performant instead. + // Very large messages can always be split by the user as required. + assert(transmissionsRequired <= getMaxTransmissionsPerMessage()); + assert(messageType == 'Q' || messageType == 'A' || messageType == 'B' || messageType == 'S' || messageType == 'P' || messageType == 'C'); + if(messageType == 'P' || messageType == 'C') + { + assert(transmissionsRequired == 1); // These messages are assumed to be contained in one message by the receive callbacks. + } + + uint8_t transmissionSize = 0; + bool messageStart = true; + uint8_t espnowMetadataSize = metadataSize(); + + do + { + ////// Manage logs ////// + + if(transmissionsRemaining == 0 && (messageType == 'Q' || messageType == 'B')) + { + assert(espnowInstance); // espnowInstance required when transmitting 'Q' and 'B' type messages. + // If we are sending the last transmission of a request we should store the sent request in the log no matter if we receive an ack for the final transmission or not. + // That way we will always be ready to receive the response to the request when there is a chance the request message was transmitted successfully, + // even if the final ack for the request message was lost. + EspnowDatabase::storeSentRequest(TypeCast::macToUint64(_transmissionTargetBSSID), messageID, RequestData(*espnowInstance)); + } + + ////// Create transmission array ////// + + if(transmissionsRemaining > 0) + { + transmissionSize = getMaxBytesPerTransmission(); + } + else + { + transmissionSize = espnowMetadataSize; + + if(message.length() > 0) + { + uint32_t remainingLength = message.length() % getMaxMessageBytesPerTransmission(); + transmissionSize += (remainingLength == 0 ? getMaxMessageBytesPerTransmission() : remainingLength); + } + } + + uint8_t transmission[transmissionSize]; + + ////// Fill protocol bytes ////// + + transmission[messageTypeIndex] = messageType; + + if(messageStart) + { + transmission[transmissionsRemainingIndex] = (char)(transmissionsRemaining | 0x80); + } + else + { + transmission[transmissionsRemainingIndex] = (char)transmissionsRemaining; + } + + // Fills indicies in range [transmissionMacIndex, transmissionMacIndex + 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 + transmissionMacIndex); + + setMessageID(transmission, messageID); + + ////// Fill message bytes ////// + + int32_t transmissionStartIndex = (transmissionsRequired - transmissionsRemaining - 1) * getMaxMessageBytesPerTransmission(); + + std::copy_n(message.begin() + transmissionStartIndex, transmissionSize - espnowMetadataSize, transmission + espnowMetadataSize); + + if(useEncryptedMessages()) + { + // chacha20Poly1305Encrypt encrypts transmission in place. + // We are using the protocol bytes as a key salt. + experimental::crypto::ChaCha20Poly1305::encrypt(transmission + espnowMetadataSize, transmissionSize - espnowMetadataSize, getEspnowMessageEncryptionKey(), transmission, + protocolBytesSize, transmission + protocolBytesSize, transmission + protocolBytesSize + 12); + } + + ////// Transmit ////// + + uint32_t retransmissions = 0; + if(messageType == 'B') + retransmissions = espnowInstance->getBroadcastTransmissionRedundancy(); + + for(uint32_t i = 0; i <= retransmissions; ++i) + { + _espnowSendConfirmed = false; + ExpiringTimeTracker transmissionTimeout([](){ return getEspnowTransmissionTimeout(); }); + + while(!_espnowSendConfirmed && !transmissionTimeout) + { + if(esp_now_send(_transmissionTargetBSSID, transmission, transmissionSize) == 0) // == 0 => Success + { + ExpiringTimeTracker retransmissionTime([](){ return getEspnowRetransmissionInterval(); }); + while(!_espnowSendConfirmed && !retransmissionTime && !transmissionTimeout) + { + 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; + + ConditionalPrinter::staticVerboseModePrint(String(F("espnowSendToNode failed!"))); + ConditionalPrinter::staticVerboseModePrint(String(F("Transmission #: ")) + String(transmissionsRequired - transmissionsRemaining) + String('/') + String(transmissionsRequired)); + ConditionalPrinter::staticVerboseModePrint(String(F("Transmission fail rate (up) ")) + String(getTransmissionFailRate())); + + if(messageStart && encryptedConnection && !usesConstantSessionKey(messageType) && encryptedConnection->getOwnSessionKey() == messageID) + encryptedConnection->setDesync(true); + + return TransmissionStatusType::TRANSMISSION_FAILED; + } + + --transmissionsRemaining; // This is used when transfering multi-transmission messages. + + } while(transmissionsRemaining >= 0); + + // Useful when debugging the protocol + //_conditionalPrinter.staticVerboseModePrint("Sent to Mac: " + TypeCast::macToString(_transmissionTargetBSSID) + " ID: " + TypeCast::uint64ToString(messageID)); + + return TransmissionStatusType::TRANSMISSION_COMPLETE; +} + +TransmissionStatusType EspnowTransmitter::espnowSendPeerRequestConfirmationsUnsynchronized(const String message, const uint8_t *targetBSSID, const char messageType, EspnowMeshBackend *espnowInstance) +{ + return espnowSendToNodeUnsynchronized(message, targetBSSID, messageType, EspnowConnectionManager::generateMessageID(nullptr), espnowInstance); +} + +TransmissionStatusType EspnowTransmitter::sendRequest(const String &message, const uint8_t *targetBSSID, EspnowMeshBackend *espnowInstance) +{ + TransmissionStatusType transmissionStatus = espnowSendToNode(message, targetBSSID, 'Q', espnowInstance); + + return transmissionStatus; +} + +TransmissionStatusType EspnowTransmitter::sendResponse(const String &message, const uint64_t requestID, const uint8_t *targetBSSID, EspnowMeshBackend *espnowInstance) +{ + EncryptedConnectionLog *encryptedConnection = EspnowConnectionManager::getEncryptedConnection(targetBSSID); + uint8_t encryptedMac[6] {0}; + + if(encryptedConnection) + { + encryptedConnection->getEncryptedPeerMac(encryptedMac); + assert(esp_now_is_peer_exist(encryptedMac) > 0 && String(F("ERROR! Attempting to send content marked as encrypted via unencrypted connection!"))); + } + + return espnowSendToNodeUnsynchronized(message, encryptedConnection ? encryptedMac : targetBSSID, 'A', requestID, espnowInstance); +} diff --git a/libraries/ESP8266WiFiMesh/src/EspnowTransmitter.h b/libraries/ESP8266WiFiMesh/src/EspnowTransmitter.h new file mode 100644 index 0000000000..403bea3027 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/EspnowTransmitter.h @@ -0,0 +1,109 @@ +/* + Copyright (C) 2020 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 +*/ + +#ifndef __ESPNOWTRANSMITTER_H__ +#define __ESPNOWTRANSMITTER_H__ + +#include +#include +#include "ExpiringTimeTracker.h" +#include "EspnowDatabase.h" +#include "EspnowConnectionManager.h" +#include "ConditionalPrinter.h" + +class EspnowMeshBackend; + +class EspnowTransmitter +{ + +public: + + using responseTransmittedHookType = std::function; + + EspnowTransmitter(ConditionalPrinter &conditionalPrinterInstance, EspnowDatabase &databaseInstance, EspnowConnectionManager &connectionManagerInstance); + + static void espnowSendCallback(uint8_t* mac, uint8_t sendStatus); + + /** + * 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), 'B' for broadcast, 'S' for synchronization request, 'P' for peer request and 'C' for peer request confirmation. + * @return 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 TransmissionStatusType espnowSendToNode(const String &message, const uint8_t *targetBSSID, const char messageType, EspnowMeshBackend *espnowInstance = nullptr); + // Send a message using exactly the arguments given, without consideration for any encrypted connections. + static TransmissionStatusType espnowSendToNodeUnsynchronized(const String message, const uint8_t *targetBSSID, const char messageType, const uint64_t messageID, EspnowMeshBackend *espnowInstance = nullptr); + + // Send a PeerRequestConfirmation using exactly the arguments given, without consideration for any encrypted connections. + static TransmissionStatusType espnowSendPeerRequestConfirmationsUnsynchronized(const String message, const uint8_t *targetBSSID, const char messageType, EspnowMeshBackend *espnowInstance = nullptr); + + TransmissionStatusType sendRequest(const String &message, const uint8_t *targetBSSID, EspnowMeshBackend *espnowInstance); + TransmissionStatusType sendResponse(const String &message, const uint64_t requestID, const uint8_t *targetBSSID, EspnowMeshBackend *espnowInstance); + + static void setUseEncryptedMessages(const bool useEncryptedMessages); + static bool useEncryptedMessages(); + static void setEspnowMessageEncryptionKey(const uint8_t espnowMessageEncryptionKey[experimental::crypto::ENCRYPTION_KEY_LENGTH]); + static void setEspnowMessageEncryptionKey(const String &espnowMessageEncryptionKeySeed); + static const uint8_t *getEspnowMessageEncryptionKey(); + + void setBroadcastTransmissionRedundancy(const uint8_t redundancy); + uint8_t getBroadcastTransmissionRedundancy() const; + void setResponseTransmittedHook(const responseTransmittedHookType responseTransmittedHook); + responseTransmittedHookType getResponseTransmittedHook() const; + static void setMaxTransmissionsPerMessage(const uint8_t maxTransmissionsPerMessage); + static uint8_t getMaxTransmissionsPerMessage(); + static uint32_t getMaxMessageLength(); + static void setEspnowTransmissionTimeout(const uint32_t timeoutMs); + static uint32_t getEspnowTransmissionTimeout(); + static void setEspnowRetransmissionInterval(const uint32_t intervalMs); + static uint32_t getEspnowRetransmissionInterval(); + static double getTransmissionFailRate(); + static void resetTransmissionFailRate(); + + /* + * @param estimatedMaxDurationTracker A pointer to an ExpiringTimeTracker initialized with the desired max duration for the method. If set to nullptr there is no duration limit. + * Note that setting the estimatedMaxDuration too low may result in missed ESP-NOW transmissions because of too little time for maintenance. + * Also note that although the method will try to respect the max duration limit, there is no guarantee. Overshoots by tens of milliseconds are possible. + */ + static void sendEspnowResponses(const ExpiringTimeTracker *estimatedMaxDurationTracker = nullptr); + + /** + * Will be captured if a transmission initiated by a public method is in progress. + */ + static MutexTracker captureEspnowTransmissionMutex(); + static MutexTracker captureEspnowTransmissionMutex(const std::function destructorHook); + + /** + * Check if there is an ongoing ESP-NOW transmission in the library. Used to avoid interrupting transmissions. + * + * @return True if a transmission initiated by a public method is in progress. + */ + static bool transmissionInProgress(); + +private: + + ConditionalPrinter & _conditionalPrinter; + EspnowDatabase & _database; + EspnowConnectionManager & _connectionManager; + + responseTransmittedHookType _responseTransmittedHook = [](bool, const String &, const uint8_t *, uint32_t, EspnowMeshBackend &){ return true; }; + + uint8_t _broadcastTransmissionRedundancy = 1; +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/ExpiringTimeTracker.cpp b/libraries/ESP8266WiFiMesh/src/ExpiringTimeTracker.cpp new file mode 100644 index 0000000000..5151c0bc5a --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/ExpiringTimeTracker.cpp @@ -0,0 +1,124 @@ +/* + * 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(const uint32_t duration, const uint32_t creationTimeMs) : + timeoutTemplate(0) +{ + setDuration(duration); + _start = creationTimeMs; +} + +ExpiringTimeTracker::ExpiringTimeTracker(const calculatorType durationCalculator, const uint32_t creationTimeMs) : + timeoutTemplate(0) +{ + setDuration(durationCalculator); + _start = creationTimeMs; +} + +uint32_t ExpiringTimeTracker::duration() const +{ + if(useCalculator) + return _durationCalculator(); + + return getTimeout(); +} + +IRAM_ATTR // called from ISR +void ExpiringTimeTracker::setTimeout(const uint32_t newUserTimeout) +{ + _timeout = newUserTimeout; + _neverExpires = (newUserTimeout > timeMax()); // newUserTimeout < 0 is always false for uint32_t +} + +void ExpiringTimeTracker::setDuration(const uint32_t duration) +{ + setTimeout(duration); + useCalculator = false; +} + +void ExpiringTimeTracker::setDuration(const calculatorType durationCalculator) +{ + _durationCalculator = durationCalculator; + useCalculator = true; +} + +void ExpiringTimeTracker::setRemainingDuration(const uint32_t remainingDuration) +{ + setDuration(elapsedTime() + remainingDuration); +} + +void ExpiringTimeTracker::setRemainingDuration(const calculatorType remainingDurationCalculator) +{ + uint32_t currentElapsedTime = elapsedTime(); + setDuration([remainingDurationCalculator, currentElapsedTime](){ return currentElapsedTime + remainingDurationCalculator(); }); +} + +uint32_t ExpiringTimeTracker::remainingDuration() const +{ + uint32_t remainingDuration = 0; + + if(!expired()) // If expired, overflow will probably occur for remainingDuration calculation. + { + remainingDuration = duration() - elapsedTime(); + } + + return remainingDuration; +} + +uint32_t ExpiringTimeTracker::elapsedTime() const +{ + return millis() - _start; +} + +bool ExpiringTimeTracker::expired() const +{ + if(useCalculator) + return elapsedTime() >= duration(); + + return expiredOneShot(); +} + +void ExpiringTimeTracker::reset() +{ + timeoutTemplate::reset(); +} + +void ExpiringTimeTracker::reset(const uint32_t newDuration) +{ + setDuration(newDuration); + ExpiringTimeTracker::reset(); +} + +void ExpiringTimeTracker::reset(const calculatorType newDurationCalculator) +{ + setDuration(newDurationCalculator); + ExpiringTimeTracker::reset(); +} + +ExpiringTimeTracker::operator bool() const +{ + return ExpiringTimeTracker::expired(); +} diff --git a/libraries/ESP8266WiFiMesh/src/ExpiringTimeTracker.h b/libraries/ESP8266WiFiMesh/src/ExpiringTimeTracker.h new file mode 100644 index 0000000000..4a83f979ae --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/ExpiringTimeTracker.h @@ -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. + */ + +#ifndef __EXPIRINGTIMETRACKER_H__ +#define __EXPIRINGTIMETRACKER_H__ + +#include +#include + +class ExpiringTimeTracker : private esp8266::polledTimeout::oneShotMs { + +public: + + using calculatorType = std::function; + + virtual ~ExpiringTimeTracker() = default; + + ExpiringTimeTracker(const uint32_t duration, const uint32_t creationTimeMs = millis()); + ExpiringTimeTracker(const calculatorType durationCalculator, const uint32_t creationTimeMs = millis()); + + uint32_t duration() const; + void setDuration(const uint32_t duration); + void setDuration(const calculatorType durationCalculator); + + uint32_t remainingDuration() const; + + /** + * Sets a new duration which includes the current elapsedTime(). This means elapsedTime() is not reset. + * Note that reset() will use this new duration, including the saved elapsedTime(). + */ + void setRemainingDuration(const uint32_t remainingDuration); + + /** + * Sets a new duration which includes the current elapsedTime(). This means elapsedTime() is not reset. + * Note that reset() will use this new duration, including the saved elapsedTime(). + */ + void setRemainingDuration(const calculatorType remainingDurationCalculator); + + /** + * Get the time since the ExpiringTimeTracker instance creation or the last reset(), whichever is more recent. + */ + uint32_t elapsedTime() const; + bool expired() const; + void reset(); + void reset(const uint32_t newDuration); + void reset(const calculatorType newDurationCalculator); + explicit operator bool() const; + +private: + + calculatorType _durationCalculator; + + void setTimeout(const uint32_t newUserTimeout); + + bool useCalculator = false; +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/FloodingMesh.cpp b/libraries/ESP8266WiFiMesh/src/FloodingMesh.cpp new file mode 100644 index 0000000000..434df2fca7 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/FloodingMesh.cpp @@ -0,0 +1,575 @@ +/* + * Copyright (C) 2019 Anders Löfgren + * + * License (MIT license): + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "FloodingMesh.h" +#include "TypeConversionFunctions.h" +#include "JsonTranslator.h" +#include "Serializer.h" + +namespace +{ + namespace TypeCast = MeshTypeConversionFunctions; + + constexpr uint8_t MESSAGE_ID_LENGTH = 17; // 16 characters and one delimiter + constexpr uint8_t MESSAGE_COMPLETE = 255; + + char _metadataDelimiter = 23; // Defaults to 23 = End-of-Transmission-Block (ETB) control character in ASCII +} + +std::set FloodingMesh::availableFloodingMeshes = {}; + +void floodingMeshDelay(const uint32_t durationMs) +{ + ExpiringTimeTracker timeout(durationMs); + + do + { + // We want to delay before performMeshMaintenance() so background tasks can be managed first. + // Initial while combined with YieldAndDelayMs polledTimeout::YieldPolicy is not suitable since the delay then occurs before evaluating the condition (meaning durationMs = 1 never executes the loop interior). + delay(1); + FloodingMesh::performMeshMaintenance(); + } + while(!timeout); +} + +FloodingMesh::FloodingMesh(messageHandlerType messageHandler, const String &meshPassword, const uint8_t espnowEncryptedConnectionKey[EspnowProtocolInterpreter::encryptedConnectionKeyLength], + const uint8_t espnowHashKey[EspnowProtocolInterpreter::hashKeyLength], const String &ssidPrefix, + const String &ssidSuffix, const bool verboseMode, const uint8 meshWiFiChannel) + : _espnowBackend( + [this](const String &request, MeshBackendBase &meshInstance){ return _defaultRequestHandler(request, meshInstance); }, + [this](const String &response, MeshBackendBase &meshInstance){ return _defaultResponseHandler(response, meshInstance); }, + [this](int numberOfNetworks, MeshBackendBase &meshInstance){ return _defaultNetworkFilter(numberOfNetworks, meshInstance); }, + [this](String &firstTransmission, EspnowMeshBackend &meshInstance){ return _defaultBroadcastFilter(firstTransmission, meshInstance); }, + meshPassword, espnowEncryptedConnectionKey, espnowHashKey, ssidPrefix, ssidSuffix, verboseMode, meshWiFiChannel) +{ + setMessageHandler(messageHandler); + restoreDefaultTransmissionOutcomesUpdateHook(); + restoreDefaultResponseTransmittedHook(); +} + +FloodingMesh::FloodingMesh(messageHandlerType messageHandler, const String &meshPassword, const String &espnowEncryptedConnectionKeySeed, const String &espnowHashKeySeed, + const String &ssidPrefix, const String &ssidSuffix, const bool verboseMode, const uint8 meshWiFiChannel) + : FloodingMesh(messageHandler, meshPassword, (const uint8_t[EspnowProtocolInterpreter::encryptedConnectionKeyLength]){0}, + (const uint8_t[EspnowProtocolInterpreter::hashKeyLength]){0}, ssidPrefix, ssidSuffix, verboseMode, meshWiFiChannel) +{ + getEspnowMeshBackend().setEspnowEncryptedConnectionKey(espnowEncryptedConnectionKeySeed); + getEspnowMeshBackend().setEspnowHashKey(espnowHashKeySeed); +} + +FloodingMesh::FloodingMesh(const String &serializedMeshState, messageHandlerType messageHandler, const String &meshPassword, + const uint8_t espnowEncryptedConnectionKey[EspnowProtocolInterpreter::encryptedConnectionKeyLength], + const uint8_t espnowHashKey[EspnowProtocolInterpreter::hashKeyLength], const String &ssidPrefix, + const String &ssidSuffix, const bool verboseMode, const uint8 meshWiFiChannel) + : FloodingMesh(messageHandler, meshPassword, espnowEncryptedConnectionKey, espnowHashKey, ssidPrefix, ssidSuffix, verboseMode, meshWiFiChannel) +{ + loadMeshState(serializedMeshState); +} + +FloodingMesh::FloodingMesh(const String &serializedMeshState, messageHandlerType messageHandler, const String &meshPassword, + const String &espnowEncryptedConnectionKeySeed, const String &espnowHashKeySeed, const String &ssidPrefix, + const String &ssidSuffix, const bool verboseMode, const uint8 meshWiFiChannel) + : FloodingMesh(messageHandler, meshPassword, espnowEncryptedConnectionKeySeed, espnowHashKeySeed, ssidPrefix, ssidSuffix, verboseMode, meshWiFiChannel) +{ + loadMeshState(serializedMeshState); +} + +FloodingMesh::~FloodingMesh() +{ + availableFloodingMeshes.erase(this); +} + +void FloodingMesh::begin() +{ + // Initialise the mesh node + getEspnowMeshBackend().begin(); + + // Used for encrypted broadcasts + getEspnowMeshBackend().setEncryptedConnectionsSoftLimit(3); + + availableFloodingMeshes.insert(this); // Returns std::pair +} + +void FloodingMesh::activateAP() +{ + getEspnowMeshBackend().activateAP(); +} + +void FloodingMesh::deactivateAP() +{ + MeshBackendBase::deactivateAP(); +} + +void FloodingMesh::performMeshMaintenance() +{ + for(FloodingMesh *meshInstance : availableFloodingMeshes) + { + meshInstance->performMeshInstanceMaintenance(); + } +} + +void FloodingMesh::performMeshInstanceMaintenance() +{ + EspnowMeshBackend::performEspnowMaintenance(); + + for(std::list>::iterator backlogIterator = getForwardingBacklog().begin(); backlogIterator != getForwardingBacklog().end(); ) + { + std::pair &messageData = *backlogIterator; + if(messageData.second) // message encrypted + { + getMacIgnoreList() = messageData.first.substring(0, 12) + ','; // The message should contain the messageID first + encryptedBroadcastKernel(messageData.first); + getMacIgnoreList() = emptyString; + } + else + { + broadcastKernel(messageData.first); + } + + backlogIterator = getForwardingBacklog().erase(backlogIterator); + + EspnowMeshBackend::performEspnowMaintenance(); // It is best to performEspnowMaintenance frequently to keep the Espnow backend responsive. Especially if each encryptedBroadcast takes a lot of time. + } +} + +String FloodingMesh::serializeMeshState() const +{ + String connectionState = getEspnowMeshBackendConst().serializeUnencryptedConnection(); + uint32_t unsyncMsgID = 0; + JsonTranslator::getUnsynchronizedMessageID(connectionState, unsyncMsgID); + + return Serializer::serializeMeshState(String(unsyncMsgID), String(_messageCount)); +} + +void FloodingMesh::loadMeshState(const String &serializedMeshState) +{ + using namespace JsonTranslator; + + if(!getMeshMessageCount(serializedMeshState, _messageCount)) + getEspnowMeshBackend().warningPrint(String(F("WARNING! serializedMeshState did not contain MeshMessageCount. Using default instead."))); + + String connectionState; + if(!getConnectionState(serializedMeshState, connectionState) || !getEspnowMeshBackend().addUnencryptedConnection(connectionState)) + { + getEspnowMeshBackend().warningPrint(String(F("WARNING! serializedMeshState did not contain unsynchronizedMessageID. Using default instead."))); + } +} + +String FloodingMesh::generateMessageID() +{ + char messageCountArray[5] = { 0 }; + snprintf(messageCountArray, 5, "%04X", _messageCount++); + uint8_t apMac[6] {0}; + return TypeCast::macToString(WiFi.softAPmacAddress(apMac)) + String(messageCountArray); // We use the AP MAC address as ID since it is what shows up during WiFi scans +} + +void FloodingMesh::broadcast(const String &message) +{ + assert(message.length() <= maxUnencryptedMessageLength()); + + String messageID = generateMessageID(); + + // Remove getEspnowMeshBackend().getMeshName() from the metadata below to broadcast to all ESP-NOW nodes regardless of MeshName. + String targetMeshName = getEspnowMeshBackend().getMeshName(); + + broadcastKernel(targetMeshName + String(metadataDelimiter()) + messageID + String(metadataDelimiter()) + message); +} + +void FloodingMesh::broadcastKernel(const String &message) +{ + getEspnowMeshBackend().broadcast(message); +} + +void FloodingMesh::setBroadcastReceptionRedundancy(const uint8_t redundancy) +{ + assert(redundancy < 255); + _broadcastReceptionRedundancy = redundancy; +} +uint8_t FloodingMesh::getBroadcastReceptionRedundancy() const { return _broadcastReceptionRedundancy; } + +void FloodingMesh::encryptedBroadcast(const String &message) +{ + assert(message.length() <= maxEncryptedMessageLength()); + + String messageID = generateMessageID(); + + encryptedBroadcastKernel(messageID + String(metadataDelimiter()) + message); +} + +void FloodingMesh::encryptedBroadcastKernel(const String &message) +{ + getEspnowMeshBackend().attemptAutoEncryptingTransmission(message, true); +} + +void FloodingMesh::clearMessageLogs() +{ + _messageIDs.clear(); + std::queue().swap(_messageIdOrder); +} + +void FloodingMesh::clearForwardingBacklog() +{ + getForwardingBacklog().clear(); +} + +void FloodingMesh::setMessageHandler(const messageHandlerType messageHandler) { _messageHandler = messageHandler; } +FloodingMesh::messageHandlerType FloodingMesh::getMessageHandler() const { return _messageHandler; } + +void FloodingMesh::setOriginMac(const uint8_t *macArray) +{ + std::copy_n(macArray, 6, _originMac); +} + +String FloodingMesh::getOriginMac() const { return TypeCast::macToString(_originMac); } +uint8_t *FloodingMesh::getOriginMac(uint8_t *macArray) const +{ + std::copy_n(_originMac, 6, macArray); + return macArray; +} + +std::list> & FloodingMesh::getForwardingBacklog() { return _forwardingBacklog; } + +String & FloodingMesh::getMacIgnoreList() { return _macIgnoreList; } + +uint32_t FloodingMesh::maxUnencryptedMessageLength() const +{ + return getEspnowMeshBackendConst().getMaxMessageLength() - MESSAGE_ID_LENGTH - (getEspnowMeshBackendConst().getMeshName().length() + 1); // Need room for mesh name + delimiter +} + +uint32_t FloodingMesh::maxEncryptedMessageLength() const +{ + // Need 1 extra delimiter character for maximum metadata efficiency (makes it possible to store exactly 18 MACs in metadata by adding an extra transmission) + return getEspnowMeshBackendConst().getMaxMessageLength() - MESSAGE_ID_LENGTH - 1; +} + +void FloodingMesh::setMessageLogSize(const uint16_t messageLogSize) +{ + assert(messageLogSize >= 1); + _messageLogSize = messageLogSize; +} +uint16_t FloodingMesh::messageLogSize() const { return _messageLogSize; } + +void FloodingMesh::setMetadataDelimiter(const char metadataDelimiter) +{ + // Using HEX number characters as a delimiter is a bad idea regardless of broadcast type, since they are always in the broadcast metadata. + // We therefore check for those characters below. + assert(metadataDelimiter < '0' || '9' < metadataDelimiter); + assert(metadataDelimiter < 'A' || 'F' < metadataDelimiter); + assert(metadataDelimiter < 'a' || 'f' < metadataDelimiter); + + // Reserved for encryptedBroadcast for now + assert(metadataDelimiter != ','); + + _metadataDelimiter = metadataDelimiter; +} +char FloodingMesh::metadataDelimiter() { return _metadataDelimiter; } + +EspnowMeshBackend &FloodingMesh::getEspnowMeshBackend() +{ + return _espnowBackend; +} + +const EspnowMeshBackend &FloodingMesh::getEspnowMeshBackendConst() const +{ + return _espnowBackend; +} + +bool FloodingMesh::insertPreliminaryMessageID(const uint64_t messageID) +{ + uint8_t apMacArray[6] = { 0 }; + if(messageID >> 16 == TypeCast::macToUint64(WiFi.softAPmacAddress(apMacArray))) + return false; // The node should not receive its own messages. + + auto insertionResult = _messageIDs.emplace(messageID, 0); // Returns std::pair + + if(insertionResult.second) // Insertion succeeded. + updateMessageQueue(insertionResult.first); + else if(insertionResult.first->second < getBroadcastReceptionRedundancy()) // messageID exists but not with desired redundancy + insertionResult.first->second++; + else + return false; // messageID already existed in _messageIDs with desired redundancy + + return true; +} + +bool FloodingMesh::insertCompletedMessageID(const uint64_t messageID) +{ + uint8_t apMacArray[6] = { 0 }; + if(messageID >> 16 == TypeCast::macToUint64(WiFi.softAPmacAddress(apMacArray))) + return false; // The node should not receive its own messages. + + auto insertionResult = _messageIDs.emplace(messageID, MESSAGE_COMPLETE); // Returns std::pair + + if(insertionResult.second) // Insertion succeeded. + updateMessageQueue(insertionResult.first); + else if(insertionResult.first->second < MESSAGE_COMPLETE) // messageID exists but is not complete + insertionResult.first->second = MESSAGE_COMPLETE; + else + return false; // messageID already existed in _messageIDs and is complete + + return true; +} + +void FloodingMesh::updateMessageQueue(const messageQueueElementType messageIterator) +{ + _messageIdOrder.emplace(messageIterator); + + if(_messageIDs.size() > messageLogSize()) + { + _messageIDs.erase(_messageIdOrder.front()); + _messageIdOrder.pop(); + assert(_messageIDs.size() == messageLogSize()); // If this is false we either have too many elements in messageIDs or we deleted too many elements. + assert(_messageIDs.size() == _messageIdOrder.size()); // The containers should always be in sync + } +} + +void FloodingMesh::restoreDefaultRequestHandler() +{ + getEspnowMeshBackend().setRequestHandler([this](const String &request, MeshBackendBase &meshInstance){ return _defaultRequestHandler(request, meshInstance); }); +} + +void FloodingMesh::restoreDefaultResponseHandler() +{ + getEspnowMeshBackend().setResponseHandler([this](const String &response, MeshBackendBase &meshInstance){ return _defaultResponseHandler(response, meshInstance); }); +} + +void FloodingMesh::restoreDefaultNetworkFilter() +{ + getEspnowMeshBackend().setNetworkFilter([this](int numberOfNetworks, MeshBackendBase &meshInstance){ return _defaultNetworkFilter(numberOfNetworks, meshInstance); }); +} + +void FloodingMesh::restoreDefaultBroadcastFilter() +{ + getEspnowMeshBackend().setBroadcastFilter([this](String &firstTransmission, EspnowMeshBackend &meshInstance){ return _defaultBroadcastFilter(firstTransmission, meshInstance); }); +} + +void FloodingMesh::restoreDefaultTransmissionOutcomesUpdateHook() +{ + /* Optional way of doing things. Lambda is supposedly better https://stackoverflow.com/a/36596295 . + + using namespace std::placeholders; + + getEspnowMeshBackend().setTransmissionOutcomesUpdateHook(std::bind(&FloodingMesh::_defaultTransmissionOutcomesUpdateHook, this, _1)); + */ + + getEspnowMeshBackend().setTransmissionOutcomesUpdateHook([this](MeshBackendBase &meshInstance){ return _defaultTransmissionOutcomesUpdateHook(meshInstance); }); +} + +void FloodingMesh::restoreDefaultResponseTransmittedHook() +{ + getEspnowMeshBackend().setResponseTransmittedHook([this](bool transmissionSuccessful, const String &response, const uint8_t *recipientMac, uint32_t responseIndex, EspnowMeshBackend &meshInstance) + { return _defaultResponseTransmittedHook(transmissionSuccessful, response, recipientMac, responseIndex, meshInstance); }); +} + +/** + * Callback for when other nodes send you a request + * + * @param request The request string received from another node in the mesh + * @param meshInstance The MeshBackendBase instance that called the function. + * @return The string to send back to the other node. For ESP-NOW, return an empy string ("") if no response should be sent. + */ +String FloodingMesh::_defaultRequestHandler(const String &request, MeshBackendBase &meshInstance) +{ + (void)meshInstance; // This is useful to remove a "unused parameter" compiler warning. Does nothing else. + + String broadcastTarget; + String remainingRequest = request; + + if(request.charAt(0) == metadataDelimiter()) + { + int32_t broadcastTargetEndIndex = request.indexOf(metadataDelimiter(), 1); + + if(broadcastTargetEndIndex == -1) + return emptyString; // metadataDelimiter not found + + broadcastTarget = request.substring(1, broadcastTargetEndIndex + 1); // Include delimiter + remainingRequest.remove(0, broadcastTargetEndIndex + 1); + } + + int32_t messageIDEndIndex = remainingRequest.indexOf(metadataDelimiter()); + + if(messageIDEndIndex == -1) + return emptyString; // metadataDelimiter not found + + uint64_t messageID = TypeCast::stringToUint64(remainingRequest.substring(0, messageIDEndIndex)); + + if(insertCompletedMessageID(messageID)) + { + uint8_t originMacArray[6] = { 0 }; + setOriginMac(TypeCast::uint64ToMac(messageID >> 16, originMacArray)); // messageID consists of MAC + 16 bit counter + + String message = remainingRequest; + message.remove(0, messageIDEndIndex + 1); // This approach avoids the null value removal of substring() + + if(getMessageHandler()(message, *this)) + { + message = broadcastTarget + remainingRequest.substring(0, messageIDEndIndex + 1) + message; + assert(message.length() <= _espnowBackend.getMaxMessageLength()); + getForwardingBacklog().emplace_back(message, getEspnowMeshBackend().receivedEncryptedTransmission()); + } + } + + return emptyString; +} + +/** + * Callback for when you get a response from other nodes + * + * @param response The response string received from another node in the mesh + * @param meshInstance The MeshBackendBase instance that called the function. + * @return The status code resulting from the response, as an int + */ +TransmissionStatusType FloodingMesh::_defaultResponseHandler(const String &response, MeshBackendBase &meshInstance) +{ + TransmissionStatusType statusCode = TransmissionStatusType::TRANSMISSION_COMPLETE; + + getEspnowMeshBackend().warningPrint(String(F("WARNING! Response to FloodingMesh broadcast received, but none is expected!"))); + + (void)response; // This is useful to remove a "unused parameter" compiler warning. Does nothing else. + (void)meshInstance; // This is useful to remove a "unused parameter" compiler warning. Does nothing else. + + return statusCode; +} + +/** + * Callback used to decide which networks to connect to once a WiFi scan has been completed. + * + * @param numberOfNetworks The number of networks found in the WiFi scan. + * @param meshInstance The MeshBackendBase instance that called the function. + */ +void FloodingMesh::_defaultNetworkFilter(const int numberOfNetworks, MeshBackendBase &meshInstance) +{ + // Note that the network index of a given node may change whenever a new scan is done. + for (int networkIndex = 0; networkIndex < numberOfNetworks; ++networkIndex) + { + String currentSSID = WiFi.SSID(networkIndex); + int meshNameIndex = currentSSID.indexOf(meshInstance.getMeshName()); + + // Connect to any APs which contain meshInstance.getMeshName() + if(meshNameIndex >= 0) + { + if(getMacIgnoreList().indexOf(TypeCast::macToString(WiFi.BSSID(networkIndex))) == -1) // If the BSSID is not in the ignore list + { + if(EspnowMeshBackend *espnowInstance = TypeCast::meshBackendCast(&meshInstance)) + { + espnowInstance->connectionQueue().emplace_back(networkIndex); + } + else + { + Serial.println(String(F("Invalid mesh backend!"))); + } + } + } + } +} + +/** + * Callback used to decide which broadcast messages to accept. Only called for the first transmission in each broadcast. + * If true is returned from this callback, the first broadcast transmission is saved until the entire broadcast message has been received. + * The complete broadcast message will then be sent to the requestHandler. + * If false is returned from this callback, the broadcast message is discarded. + * Note that the BroadcastFilter may be called multiple times for messages that are discarded in this way, but is only called once for accepted messages. + * + * @param firstTransmission The first transmission of the broadcast. + * @param meshInstance The EspnowMeshBackend instance that called the function. + * + * @return True if the broadcast should be accepted. False otherwise. + */ +bool FloodingMesh::_defaultBroadcastFilter(String &firstTransmission, EspnowMeshBackend &meshInstance) +{ + // This broadcastFilter will accept a transmission if it contains the metadataDelimiter + // and as metaData either no targetMeshName or a targetMeshName that matches the MeshName of meshInstance + // and insertPreliminaryMessageID(messageID) returns true. + + // Broadcast firstTransmission String structure: targetMeshName+messageID+message. + + int32_t metadataEndIndex = firstTransmission.indexOf(metadataDelimiter()); + + if(metadataEndIndex == -1) + return false; // metadataDelimiter not found + + String targetMeshName = firstTransmission.substring(0, metadataEndIndex); + + if(!targetMeshName.isEmpty() && meshInstance.getMeshName() != targetMeshName) + { + return false; // Broadcast is for another mesh network + } + + int32_t messageIDEndIndex = firstTransmission.indexOf(metadataDelimiter(), metadataEndIndex + 1); + + if(messageIDEndIndex == -1) + return false; // metadataDelimiter not found + + uint64_t messageID = TypeCast::stringToUint64(firstTransmission.substring(metadataEndIndex + 1, messageIDEndIndex)); + + if(insertPreliminaryMessageID(messageID)) + { + // Add broadcast identifier to stored message and mark as accepted broadcast. + firstTransmission = String(metadataDelimiter()) + firstTransmission; + return true; + } + + return false; // Broadcast has already been received the maximum number of times +} + +/** + * Once passed to the setTransmissionOutcomesUpdateHook method of the ESP-NOW backend, + * this function will be called after each update of the latestTransmissionOutcomes vector during attemptTransmission. + * (which happens after each individual transmission has finished) + * + * @param meshInstance The MeshBackendBase instance that called the function. + * + * @return True if attemptTransmission should continue with the next entry in the connectionQueue. False if attemptTransmission should stop. + */ +bool FloodingMesh::_defaultTransmissionOutcomesUpdateHook(MeshBackendBase &meshInstance) +{ + (void)meshInstance; // This is useful to remove a "unused parameter" compiler warning. Does nothing else. + + return true; +} + +/** + * Once passed to the setResponseTransmittedHook method of the ESP-NOW backend, + * this function will be called after each attempted ESP-NOW response transmission. + * In case of a successful response transmission, this happens just before the response is removed from the waiting list. + * Only the hook of the EspnowMeshBackend instance that is getEspnowRequestManager() will be called. + * + * @param transmissionSuccessful True if the response was transmitted successfully. False otherwise. + * @param response The sent response. + * @param recipientMac The MAC address the response was sent to. + * @param responseIndex The index of the response in the waiting list. + * @param meshInstance The EspnowMeshBackend instance that called the function. + * + * @return True if the response transmission process should continue with the next response in the waiting list. + * False if the response transmission process should stop after processing of the just sent response is complete. + */ +bool FloodingMesh::_defaultResponseTransmittedHook(bool transmissionSuccessful, const String &response, const uint8_t *recipientMac, const uint32_t responseIndex, EspnowMeshBackend &meshInstance) +{ + (void)transmissionSuccessful; // This is useful to remove a "unused parameter" compiler warning. Does nothing else. + (void)response; + (void)recipientMac; + (void)responseIndex; + (void)meshInstance; + + return true; +} diff --git a/libraries/ESP8266WiFiMesh/src/FloodingMesh.h b/libraries/ESP8266WiFiMesh/src/FloodingMesh.h new file mode 100644 index 0000000000..eccbb8e96d --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/FloodingMesh.h @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2019 Anders Löfgren + * + * License (MIT license): + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef __FLOODINGMESH_H__ +#define __FLOODINGMESH_H__ + +#include "EspnowMeshBackend.h" +#include +#include + +/** + * An alternative to standard delay(). Will continuously call performMeshMaintenance() during the waiting time, so that the FloodingMesh node remains responsive. + * Note that if there is a lot of FloodingMesh transmission activity to the node during the floodingMeshDelay, the desired duration may be overshot by several ms. + * Thus, if precise timing is required, use standard delay() instead. + * + * Should not be used inside callbacks since performMeshMaintenance() can alter the ESP-NOW state. + * + * @param durationMs The shortest allowed delay duration, in milliseconds. + */ +void floodingMeshDelay(const uint32_t durationMs); + +class FloodingMesh { + +public: + + using messageHandlerType = std::function; + + /** + * FloodingMesh constructor method. Creates a FloodingMesh node, ready to be initialised. + * + * @param messageHandler The callback handler responsible for dealing with messages received from the mesh. + * @param meshPassword The WiFi password for the mesh network. + * @param espnowEncryptedConnectionKey An uint8_t array containing the secret key used by the EspnowMeshBackend instance for creating encrypted ESP-NOW connections. + * @param espnowHashKey An uint8_t array containing the secret key used by the EspnowMeshBackend instance to generate HMACs for encrypted ESP-NOW connections. + * @param ssidPrefix The prefix (first part) of the node SSID. + * @param ssidSuffix The suffix (last part) of the node SSID. + * @param verboseMode Determines if we should print the events occurring in the library to Serial. Off by default. This setting is shared by all EspnowMeshBackend instances. + * @param meshWiFiChannel The WiFi channel used by the mesh network. Valid values are integers from 1 to 13. Defaults to 1. + * WARNING: The ESP8266 has only one WiFi channel, and the station/client mode is always prioritized for channel selection. + * This can cause problems if several mesh instances exist on the same ESP8266 and use different WiFi channels. + * In such a case, whenever the station of one mesh instance connects to an AP, it will silently force the + * WiFi channel of any active AP on the ESP8266 to match that of the station. This will cause disconnects and possibly + * make it impossible for other stations to detect the APs whose WiFi channels have changed. + * + */ + FloodingMesh(messageHandlerType messageHandler, const String &meshPassword, const uint8_t espnowEncryptedConnectionKey[EspnowProtocolInterpreter::encryptedConnectionKeyLength], + const uint8_t espnowHashKey[EspnowProtocolInterpreter::hashKeyLength], const String &ssidPrefix, + const String &ssidSuffix, const bool verboseMode = false, const uint8 meshWiFiChannel = 1); + + /** + * FloodingMesh constructor method. Creates a FloodingMesh node, ready to be initialised. + * + * @param messageHandler The callback handler responsible for dealing with messages received from the mesh. + * @param meshPassword The WiFi password for the mesh network. + * @param espnowEncryptedConnectionKeySeed A string containing the seed that will generate the secret key used by the EspnowMeshBackend instance for creating encrypted ESP-NOW connections. + * @param espnowHashKeySeed A string containing the seed that will generate the secret key used by the EspnowMeshBackend to generate HMACs for encrypted ESP-NOW connections. + * @param ssidPrefix The prefix (first part) of the node SSID. + * @param ssidSuffix The suffix (last part) of the node SSID. + * @param verboseMode Determines if we should print the events occurring in the library to Serial. Off by default. This setting is shared by all EspnowMeshBackend instances. + * @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 station/client mode is always prioritized for channel selection. + * This can cause problems if several mesh instances exist on the same ESP8266 and use different WiFi channels. + * In such a case, whenever the station of one mesh instance connects to an AP, it will silently force the + * WiFi channel of any active AP on the ESP8266 to match that of the station. This will cause disconnects and possibly + * make it impossible for other stations to detect the APs whose WiFi channels have changed. + * + */ + FloodingMesh(messageHandlerType messageHandler, const String &meshPassword, const String &espnowEncryptedConnectionKeySeed, const String &espnowHashKeySeed, + const String &ssidPrefix, const String &ssidSuffix, const bool verboseMode = false, const uint8 meshWiFiChannel = 1); + + /** + * This constructor should be used in combination with serializeMeshState() when the node has gone to sleep while other nodes stayed awake. + * Otherwise the message ID will be reset after sleep, which means that the nodes that stayed awake may ignore new broadcasts for a while. + * + * @param serializedMeshState A String with a serialized mesh node state that the node should use. + */ + FloodingMesh(const String &serializedMeshState, messageHandlerType messageHandler, const String &meshPassword, + const uint8_t espnowEncryptedConnectionKey[EspnowProtocolInterpreter::encryptedConnectionKeyLength], + const uint8_t espnowHashKey[EspnowProtocolInterpreter::hashKeyLength], const String &ssidPrefix, + const String &ssidSuffix, const bool verboseMode = false, const uint8 meshWiFiChannel = 1); + + /** + * This constructor should be used in combination with serializeMeshState() when the node has gone to sleep while other nodes stayed awake. + * Otherwise the message ID will be reset after sleep, which means that the nodes that stayed awake may ignore new broadcasts for a while. + * + * @param serializedMeshState A String with a serialized mesh node state that the node should use. + */ + FloodingMesh(const String &serializedMeshState, messageHandlerType messageHandler, const String &meshPassword, const String &espnowEncryptedConnectionKeySeed, + const String &espnowHashKeySeed, const String &ssidPrefix, const String &ssidSuffix, const bool verboseMode = false, const uint8 meshWiFiChannel = 1); + + virtual ~FloodingMesh(); + + /** + * The method responsible for initialising this FloodingMesh instance. + */ + void begin(); + + /** + * Activate the WiFi access point of this ESP8266. + * This makes it possible to find the node through scans, and also makes it possible to recover from an encrypted ESP-NOW connection where only the other node is encrypted. + * Required for encryptedBroadcast() usage, but also slows down the start-up of the node. + * + * Note that only one AP can be active at a time in total (there is only one WiFi radio on the ESP8266), and this will always be the one which was last activated. + * Thus the AP is shared by all backends. + * All FloodingMesh instances can still broadcast messages though, even if their AP is not visible. + */ + void activateAP(); + + /** + * Deactivate the WiFi access point of this ESP8266. + * + * Note that only one AP can be active at a time in total (there is only one WiFi radio on the ESP8266), and this will always be the one which was last activated. + * Thus the AP is shared by all backends. + * All FloodingMesh instances can still broadcast messages though, even if their AP is not visible. + */ + static void deactivateAP(); + + /** + * Performs maintenance for all available Flooding Mesh instances + */ + static void performMeshMaintenance(); + + /** + * Performs maintenance for this particular Flooding Mesh instance + */ + void performMeshInstanceMaintenance(); + + /** + * Serialize the current mesh node state. Useful to save a state before the node goes to sleep. + * Note that this saves the current state only, so if a broadcast is made after this, the stored state is invalid. + * + * @return A string with the serialized current mesh node state. + */ + String serializeMeshState() const; + + /** + * Make an unencrypted broadcast to the entire mesh network. + * + * activateAP() must have been called for nodes to be able to receive broadcasts. Nodes can however send broadcasts even if their AP is off. + * + * It is recommended that there is at most one new message transmitted in the mesh every 10, 20, 30 ms for messages up to length maxUnencryptedMessageLength()*n, + * where n is (roughly, depending on mesh name length) 1/4, 3/5 and 1 respectively. If transmissions are more frequent than this, message loss will increase. + * + * @param message The message to broadcast. Maximum message length is given by maxUnencryptedMessageLength(). The longer the message, the longer the transmission time. + */ + void broadcast(const String &message); + + /** + * Set the maximum number of redundant copies that will be received of every broadcast. (from different senders) + * A greater number increases the likelihood that at least one of the copies is received successfully, but will also use more RAM. + * + * @param redundancy The maximum number of extra copies that will be accepted. Defaults to 2. Valid values are 0 to 254. + */ + void setBroadcastReceptionRedundancy(const uint8_t redundancy); + uint8_t getBroadcastReceptionRedundancy() const; + + /** + * Make an encrypted broadcast to the entire mesh network. + * + * activateAP() must have been called for encryptedBroadcast to work. + * + * ########## WARNING! This an experimental feature. API may change at any time. Only use if you like it when things break. ########## + * Will be very slow compared to unencrypted broadcasts. Probably works OK in a small mesh with a maximum of 2-3 new messages transmitted in the mesh every second. + * Because of the throughput difference, mixing encypted and unencrypted broadcasts is not recommended if there are frequent mesh broadcasts (multiple per second), + * since a lot of unencrypted broadcasts can build up while a single encrypted broadcast is sent. + * + * It is recommended that verboseMode is turned off if using this, to avoid slowdowns due to excessive Serial printing. + * + * @param message The message to broadcast. Maximum message length is given by maxEncryptedMessageLength(). The longer the message, the longer the transmission time. + */ + void encryptedBroadcast(const String &message); + + /** + * Clear the logs used for remembering which messages this node has received from the mesh network. + */ + void clearMessageLogs(); + + /** + * Remove all messages received from the mesh network which are stored waiting to be forwarded by this node. + */ + void clearForwardingBacklog(); + + /** + * Set the callback handler responsible for dealing with messages received from the mesh. + * + * @param messageHandler The message handler callback function to use. + */ + void setMessageHandler(const messageHandlerType messageHandler); + messageHandlerType getMessageHandler() const; + + /** + * Get the origin AP MAC address of the most recently received mesh message. + * Returns a String. + * + * @return A String filled with a hexadecimal representation of the MAC, without delimiters. + */ + String getOriginMac() const; + + /** + * Get the origin AP MAC address of the most recently received mesh message. + * Returns a uint8_t array. + * + * @param macArray The array that should store the MAC address. Must be at least 6 bytes. + * @return macArray filled with the origin MAC. + */ + uint8_t *getOriginMac(uint8_t *macArray) const; + + /** + * The number of received messageID:s that will be stored by the node. Used to remember which messages have been received. + * Setting this too low will cause the same message to be received many times. + * Setting this too high will cause the node to run out of RAM. + * In practice, setting this value to more than 1337 is probably a bad idea since the node will run out of RAM quickly and crash as a result. + * + * Defaults to 100. + * + * @param messageLogSize The size of the message log for this FloodingMesh instance. Valid values are 1 to 65535 (uint16_t_max). + * If a value close to the maximum is chosen, there is a high risk the node will ignore transmissions on messageID rollover if they are sent only by one node + * (especially if some transmissions are missed), since the messageID also uses uint16_t. + */ + void setMessageLogSize(const uint16_t messageLogSize); + uint16_t messageLogSize() const; + + /** + * Hint: Use String.length() to get the ASCII length of a String. + * + * @return The maximum length in bytes an unencrypted ASCII message is allowed to be when broadcasted by this node. + * Note that non-ASCII characters usually require at least two bytes each. + * Also note that for unencrypted messages the maximum size will depend on getEspnowMeshBackend().getMeshName().length() + */ + uint32_t maxUnencryptedMessageLength() const; + + /** + * Hint: Use String.length() to get the ASCII length of a String. + * + * @return The maximum length in bytes an encrypted ASCII message is allowed to be when broadcasted by this node. + * Note that non-ASCII characters usually require at least two bytes each. + */ + uint32_t maxEncryptedMessageLength() const; + + /** + * Set the delimiter character used for metadata by every FloodingMesh instance. + * Using characters found in the mesh name or in HEX numbers is unwise, as is using ','. + * + * @param metadataDelimiter The metadata delimiter character to use. + * Defaults to 23 = End-of-Transmission-Block (ETB) control character in ASCII + */ + static void setMetadataDelimiter(const char metadataDelimiter); + static char metadataDelimiter(); + + /* + * Gives you access to the EspnowMeshBackend used by the mesh node. + * The backend handles all mesh communication, and modifying it allows you to change every aspect of the mesh behaviour. + * Random interactions with the backend have a high chance of breaking the mesh network, + * and so are discouraged for those who prefer it when things just work. + */ + EspnowMeshBackend &getEspnowMeshBackend(); + const EspnowMeshBackend &getEspnowMeshBackendConst() const; + + void restoreDefaultRequestHandler(); + void restoreDefaultResponseHandler(); + void restoreDefaultNetworkFilter(); + void restoreDefaultBroadcastFilter(); + void restoreDefaultTransmissionOutcomesUpdateHook(); + void restoreDefaultResponseTransmittedHook(); + +protected: + + using messageQueueElementType = std::map::iterator; + + static std::set availableFloodingMeshes; + + String generateMessageID(); + + void broadcastKernel(const String &message); + + void encryptedBroadcastKernel(const String &message); + + bool insertPreliminaryMessageID(const uint64_t messageID); + bool insertCompletedMessageID(const uint64_t messageID); + void updateMessageQueue(const messageQueueElementType messageIterator); + + void loadMeshState(const String &serializedMeshState); + + /** + * Set the MAC address considered to be the origin AP MAC address of the most recently received mesh message. + * + * @param macArray An uint8_t array which contains the MAC address to store. The method will store the first 6 bytes of the array. + */ + void setOriginMac(const uint8_t *macArray); + + std::list> & getForwardingBacklog(); + + String & getMacIgnoreList(); // Experimental, may break in the future. + +private: + + EspnowMeshBackend _espnowBackend; + + messageHandlerType _messageHandler; + + std::map _messageIDs = {}; + std::queue _messageIdOrder = {}; + std::list> _forwardingBacklog = {}; + + String _macIgnoreList; + + String _defaultRequestHandler(const String &request, MeshBackendBase &meshInstance); + TransmissionStatusType _defaultResponseHandler(const String &response, MeshBackendBase &meshInstance); + void _defaultNetworkFilter(const int numberOfNetworks, MeshBackendBase &meshInstance); + bool _defaultBroadcastFilter(String &firstTransmission, EspnowMeshBackend &meshInstance); + bool _defaultTransmissionOutcomesUpdateHook(MeshBackendBase &meshInstance); + bool _defaultResponseTransmittedHook(bool transmissionSuccessful, const String &response, const uint8_t *recipientMac, const uint32_t responseIndex, EspnowMeshBackend &meshInstance); + + uint8_t _originMac[6] = {0}; + + uint16_t _messageCount = 0; + uint16_t _messageLogSize = 100; + + uint8_t _broadcastReceptionRedundancy = 2; +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/HeapMonitor.cpp b/libraries/ESP8266WiFiMesh/src/HeapMonitor.cpp new file mode 100644 index 0000000000..8bda148355 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/HeapMonitor.cpp @@ -0,0 +1,63 @@ +/* + * 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 "HeapMonitor.h" + +HeapMonitor::HeapMonitor(const uint32_t criticalHeapLevel, const uint32_t criticalHeapLevelBuffer) : + _criticalHeapLevel(criticalHeapLevel), _criticalHeapLevelBuffer(criticalHeapLevelBuffer) +{ } + +void HeapMonitor::setCriticalHeapLevel(const uint32_t freeHeapInBytes) +{ + _criticalHeapLevel = freeHeapInBytes; +} + +uint32_t HeapMonitor::getCriticalHeapLevel() const +{ + return _criticalHeapLevel; +} + +void HeapMonitor::setCriticalHeapLevelBuffer(const uint32_t bufferInBytes) +{ + _criticalHeapLevelBuffer = bufferInBytes; +} + +uint32_t HeapMonitor::getCriticalHeapLevelBuffer() const +{ + return _criticalHeapLevelBuffer; +} + +HeapMonitor::HeapStatus HeapMonitor::getHeapStatus() const +{ + HeapStatus heapStatus = HeapStatus::NOMINAL; + + uint32_t freeHeap = ESP.getFreeHeap(); + + if(freeHeap <= getCriticalHeapLevel()) + heapStatus = HeapStatus::CRITICAL; + else if(freeHeap <= getCriticalHeapLevel() + getCriticalHeapLevelBuffer()) + heapStatus = HeapStatus::LIMITED; + + return heapStatus; +} diff --git a/libraries/ESP8266WiFiMesh/src/HeapMonitor.h b/libraries/ESP8266WiFiMesh/src/HeapMonitor.h new file mode 100644 index 0000000000..44550e30d7 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/HeapMonitor.h @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2020 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 __ESPHEAPMONITOR_H__ +#define __ESPHEAPMONITOR_H__ + +#include + +class HeapMonitor { + +public: + + enum class HeapStatus + { + NOMINAL = 0, + LIMITED = 1, + CRITICAL = 2 + }; + + HeapMonitor(const uint32_t criticalHeapLevel, const uint32_t criticalHeapLevelBuffer); + + virtual ~HeapMonitor() = default; + + /** + * Set the maximum free heap level in bytes within which free heap size is considered critical. + */ + void setCriticalHeapLevel(const uint32_t freeHeapInBytes); + uint32_t getCriticalHeapLevel() const; + + /** + * Set the buffer of the critical heap level, within which free heap size is considered limited. + */ + void setCriticalHeapLevelBuffer(const uint32_t bufferInBytes); + uint32_t getCriticalHeapLevelBuffer() const; + + HeapStatus getHeapStatus() const; + +private: + + uint32_t _criticalHeapLevel; + uint32_t _criticalHeapLevelBuffer; + +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/JsonTranslator.cpp b/libraries/ESP8266WiFiMesh/src/JsonTranslator.cpp new file mode 100644 index 0000000000..fa9cf2b1ae --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/JsonTranslator.cpp @@ -0,0 +1,263 @@ +/* + * 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 "EspnowProtocolInterpreter.h" +#include "TypeConversionFunctions.h" + +namespace +{ + namespace TypeCast = MeshTypeConversionFunctions; + + bool getMac(const String &jsonString, const String &valueIdentifier, uint8_t *resultArray) + { + String jsonValue; + bool decoded = JsonTranslator::decode(jsonString, valueIdentifier, jsonValue); + + if(jsonValue.length() != 12) + decoded = false; // Mac String is always 12 characters long + + if(decoded) + TypeCast::stringToMac(jsonValue, resultArray); + + return decoded; + } +} + +namespace JsonTranslator +{ + int32_t getStartIndex(const String &jsonString, const String &valueIdentifier, const int32_t searchStartIndex) + { + int32_t startIndex = jsonString.indexOf(String('"') + valueIdentifier + F("\":"), searchStartIndex); + if(startIndex < 0) + return startIndex; + + startIndex += valueIdentifier.length() + 3; // Do not include valueIdentifier and associated characters + return startIndex; + } + + int32_t getEndIndex(const String &jsonString, const int32_t searchStartIndex) + { + int32_t endIndex = -1; + + if(jsonString[searchStartIndex] == '"') + { + endIndex = jsonString.indexOf('"', searchStartIndex + 1); + } + else if(jsonString[searchStartIndex] == '{') + { + uint32_t depth = 1; + bool withinString = false; + + for(uint32_t index = searchStartIndex + 1; depth != 0 && index < jsonString.length(); ++index) + { + if(jsonString[index] == '"') + withinString = !withinString; + else if(!withinString) + { + if(jsonString[index] == '{') + ++depth; + else if(jsonString[index] == '}') + --depth; + } + + if(depth == 0) + { + assert(index < 0x80000000); // Must avoid int32_t overflow + endIndex = index; + } + } + } + + return endIndex; + } + + String encode(std::initializer_list identifiersAndValues) + { + assert(identifiersAndValues.size() % 2 == 0); // List must consist of identifer-value pairs. + + String result = String('{'); + + bool isIdentifier = true; + for(String element : identifiersAndValues) + { + bool isObject = !isIdentifier && element[0] == '{'; + if(isObject) + result += element; + else + result += String('"') + element + String('"'); + + if(isIdentifier) + result += ':'; + else + result += ','; + + isIdentifier = !isIdentifier; + } + + result[result.length() - 1] = '}'; + + return result; + } + + String encodeLiterally(std::initializer_list identifiersAndValues) + { + assert(identifiersAndValues.size() % 2 == 0); // List must consist of identifer-value pairs. + + String result = String('{'); + + bool isIdentifier = true; + for(String element : identifiersAndValues) + { + if(isIdentifier) + result += String('"') + element + String('"') + ':'; + else + result += element + ','; + + isIdentifier = !isIdentifier; + } + + result[result.length() - 1] = '}'; + + return result; + } + + bool decode(const String &jsonString, const String &valueIdentifier, String &value) + { + int32_t startIndex = getStartIndex(jsonString, valueIdentifier); + if(startIndex < 0) + return false; + + int32_t endIndex = getEndIndex(jsonString, startIndex); + if(endIndex < 0) + return false; + + if(jsonString[startIndex] == '"') + ++startIndex; // Should not include starting " + else if(jsonString[startIndex] == '{') + ++endIndex; // Should include ending } + else + assert(false && F("Illegal JSON starting character!")); + + value = jsonString.substring(startIndex, endIndex); + return true; + } + + bool decode(const String &jsonString, const String &valueIdentifier, uint32_t &value) + { + String jsonValue; + bool decoded = decode(jsonString, valueIdentifier, jsonValue); + + if(decoded) + value = strtoul(jsonValue.c_str(), nullptr, 0); // strtoul stops reading input when an invalid character is discovered. + + return decoded; + } + + bool decodeRadix(const String &jsonString, const String &valueIdentifier, uint64_t &value, const uint8_t radix) + { + String jsonValue; + bool decoded = decode(jsonString, valueIdentifier, jsonValue); + + if(decoded) + value = TypeCast::stringToUint64(jsonValue, radix); + + return decoded; + } + + bool getConnectionState(const String &jsonString, String &result) + { + return decode(jsonString, FPSTR(jsonConnectionState), result); + } + + bool getPassword(const String &jsonString, String &result) + { + return decode(jsonString, FPSTR(jsonPassword), result); + } + + bool getOwnSessionKey(const String &jsonString, uint64_t &result) + { + return decodeRadix(jsonString, FPSTR(jsonOwnSessionKey), result); + } + + bool getPeerSessionKey(const String &jsonString, uint64_t &result) + { + return decodeRadix(jsonString, FPSTR(jsonPeerSessionKey), result); + } + + bool getPeerStaMac(const String &jsonString, uint8_t *resultArray) + { + return getMac(jsonString, FPSTR(jsonPeerStaMac), resultArray); + } + + bool getPeerApMac(const String &jsonString, uint8_t *resultArray) + { + return getMac(jsonString, FPSTR(jsonPeerApMac), resultArray); + } + + bool getDuration(const String &jsonString, uint32_t &result) + { + return decode(jsonString, FPSTR(jsonDuration), result); + } + + bool getNonce(const String &jsonString, String &result) + { + return decode(jsonString, FPSTR(jsonNonce), result); + } + + bool getHmac(const String &jsonString, String &result) + { + return decode(jsonString, FPSTR(jsonHmac), result); + } + + bool getDesync(const String &jsonString, bool &result) + { + String jsonValue; + bool decoded = decode(jsonString, FPSTR(jsonDesync), jsonValue); + + if(decoded) + result = bool(strtoul(jsonValue.c_str(), nullptr, 0)); // strtoul stops reading input when an invalid character is discovered. + + return decoded; + } + + bool getUnsynchronizedMessageID(const String &jsonString, uint32_t &result) + { + return decode(jsonString, FPSTR(jsonUnsynchronizedMessageID), result); + } + + bool getMeshMessageCount(const String &jsonString, uint16_t &result) + { + uint32_t longResult = 0; + bool decoded = decode(jsonString, FPSTR(jsonMeshMessageCount), longResult); + + if(longResult > 65535) // Must fit within uint16_t + decoded = false; + + if(decoded) + result = longResult; + + return decoded; + } +} diff --git a/libraries/ESP8266WiFiMesh/src/JsonTranslator.h b/libraries/ESP8266WiFiMesh/src/JsonTranslator.h new file mode 100644 index 0000000000..337d6c03df --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/JsonTranslator.h @@ -0,0 +1,174 @@ +/* + * 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 + +namespace JsonTranslator +{ + constexpr char jsonConnectionState[] PROGMEM = "connectionState"; + constexpr char jsonMeshState[] PROGMEM = "meshState"; + constexpr char jsonPassword[] PROGMEM = "password"; + constexpr char jsonOwnSessionKey[] PROGMEM = "ownSK"; + constexpr char jsonPeerSessionKey[] PROGMEM = "peerSK"; + constexpr char jsonPeerStaMac[] PROGMEM = "peerStaMac"; + constexpr char jsonPeerApMac[] PROGMEM = "peerApMac"; + constexpr char jsonDuration[] PROGMEM = "duration"; + constexpr char jsonNonce[] PROGMEM = "nonce"; + constexpr char jsonHmac[] PROGMEM = "hmac"; + constexpr char jsonDesync[] PROGMEM = "desync"; + constexpr char jsonUnsynchronizedMessageID[] PROGMEM = "unsyncMsgID"; + constexpr char jsonMeshMessageCount[] PROGMEM = "meshMsgCount"; + constexpr char jsonArguments[] PROGMEM = "arguments"; + + + /** + * Provides the index within jsonString where the value of valueIdentifier starts. + * Note that including " within a JSON string value will result in errors. + * + * @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. + * + * @return 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, const int32_t searchStartIndex = 0); + + /** + * Provides the index within jsonString where the JSON object or JSON string value ends, starting the search from searchStartIndex. + * Note that including " within a JSON string value will result in errors. + * + * The character at searchStartIndex must be either " (for a string) or { (for an object), otherwise the search fails. + * + * @param jsonString The String to search within. + * @param searchStartIndex The index of jsonString where the search will start. The index position should contain either " or {. + * + * @return An int32_t containing the index within jsonString where the JSON string/object ends, or a negative value if no such character was found. + */ + int32_t getEndIndex(const String &jsonString, const int32_t searchStartIndex); + + /* + * Create a JSON String based on the identifiers and values given. + * + * Assumes all values are either strings or JSON objects. A value is interpreted as a JSON object if it starts with { + * Assumes all identifiers are strings. + * + * @param identifiersAndValues Any even number of String arguments. It is assumed that the identifiers and values are given in an alternating manner, as in encode({Identifier1, Value1, Identifier2, Value2, ...}) + */ + String encode(std::initializer_list identifiersAndValues); + + /* + * Create a JSON String based on the identifiers and values given. + * + * Does not make any assumptions regarding value types. " must be added manually around string values. + * Useful for example if your JSON values can contain starting { characters, since the regular encode() will then interpret them as JSON objects. + * Assumes all identifiers are strings. + * + * @param identifiersAndValues Any even number of String arguments. It is assumed that the identifiers and values are given in an alternating manner, as in encodeLiterally({Identifier1, Value1, Identifier2, Value2, ...}) + */ + String encodeLiterally(std::initializer_list identifiersAndValues); + + /* + * Get a value from a JSON String. + * Assumes all values are either JSON strings ( starting with " ) or JSON objects ( starting with { ). + * + * Note that including " within a JSON string value will result in errors. + * Escape characters are not supported at this moment, since we do not want string length modification to occur during ESP-NOW protocol transmissions. + * + * @param jsonString The String to search within. + * @param valueIdentifier The identifier to search for. + * @param value The String variable to put the result in. + * + * @return True if a value was found. False otherwise. The value argument is not modified if false is returned. + */ + bool decode(const String &jsonString, const String &valueIdentifier, String &value); + + /* + * Get a value from a JSON String. + * Assumes all values are stored as strings in standard C-format (i.e. decimal by default). + * + * Note that including " within a JSON string value will result in errors. + * Escape characters are not supported at this moment, since we do not want string length modification to occur during ESP-NOW protocol transmissions. + * + * @param jsonString The String to search within. + * @param valueIdentifier The identifier to search for. + * @param value The uint32_t variable to put the result in. + * + * @return True if a value was found. False otherwise. The value argument is not modified if false is returned. + */ + bool decode(const String &jsonString, const String &valueIdentifier, uint32_t &value); + + /* + * Get a value from a JSON String. + * Assumes all values are stored as strings encoded in the specified radix. Hexadecimal encoding is the default. + * + * Note that including " within a JSON string value will result in errors. + * Escape characters are not supported at this moment, since we do not want string length modification to occur during ESP-NOW protocol transmissions. + * + * @param jsonString The String to search within. + * @param valueIdentifier The identifier to search for. + * @param value The uint64_t variable to put the result in. + * @param radix The base to use when converting the string value to uint64_t. Must be between 2 and 36. + * + * @return True if a value was found. False otherwise. The value argument is not modified if false is returned. + */ + bool decodeRadix(const String &jsonString, const String &valueIdentifier, uint64_t &value, const uint8_t radix = 16); + + bool getConnectionState(const String &jsonString, String &result); + /** + * 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. + * + * @return 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. + * + * @return 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); + bool getUnsynchronizedMessageID(const String &jsonString, uint32_t &result); + bool getMeshMessageCount(const String &jsonString, uint16_t &result); +} + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/MeshBackendBase.cpp b/libraries/ESP8266WiFiMesh/src/MeshBackendBase.cpp new file mode 100644 index 0000000000..046c25fa37 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/MeshBackendBase.cpp @@ -0,0 +1,328 @@ +/* + 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 "TypeConversionFunctions.h" +#include "MutexTracker.h" + +#include + +namespace +{ + namespace TypeCast = MeshTypeConversionFunctions; + + MeshBackendBase *apController = nullptr; +} + +std::shared_ptr MeshBackendBase::_scanMutex = std::make_shared(false); + +MeshBackendBase::MeshBackendBase(const requestHandlerType requestHandler, const responseHandlerType responseHandler, const networkFilterType networkFilter, const MeshBackendType classType) +{ + setRequestHandler(requestHandler); + setResponseHandler(responseHandler); + setNetworkFilter(networkFilter); + setClassType(classType); +} + +MeshBackendBase::~MeshBackendBase() +{ + deactivateControlledAP(); +} + +void MeshBackendBase::setClassType(const MeshBackendType classType) +{ + _classType = classType; +} + +MeshBackendType MeshBackendBase::getClassType() const {return _classType;} + +void MeshBackendBase::setVerboseModeState(const bool enabled) { _conditionalPrinter.setVerboseModeState(enabled); } +bool MeshBackendBase::verboseMode() const { return _conditionalPrinter.verboseMode(); } + +void MeshBackendBase::verboseModePrint(const String &stringToPrint, const bool newline) const +{ + _conditionalPrinter.verboseModePrint(stringToPrint, newline); +} + +void MeshBackendBase::setPrintWarnings(const bool printEnabled) { ConditionalPrinter::setPrintWarnings(printEnabled); } +bool MeshBackendBase::printWarnings() {return ConditionalPrinter::printWarnings();} + +void MeshBackendBase::warningPrint(const String &stringToPrint, const bool newline) +{ + ConditionalPrinter::warningPrint(stringToPrint, newline); +} + +void MeshBackendBase::activateAP() +{ + // Deactivate active AP to avoid two servers using the same port, which can lead to crashes. + 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(MeshBackendBase *currentAPController = MeshBackendBase::getAPController()) + currentAPController->deactivateControlledAP(); +} + +bool MeshBackendBase::deactivateControlledAP() +{ + 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; + + return true; + } + + return false; +} + +void MeshBackendBase::deactivateAPHook() +{ +} + +void MeshBackendBase::restartAP() +{ + deactivateAP(); + yield(); + activateAP(); + yield(); +} + +MeshBackendBase *MeshBackendBase::getAPController() +{ + return apController; +} + +bool MeshBackendBase::isAPController() const +{ + return (this == getAPController()); +} + +void MeshBackendBase::setWiFiChannel(const uint8 newWiFiChannel) +{ + wifi_country_t wifiCountry; + wifi_get_country(&wifiCountry); // Note: Should return 0 on success and -1 on failure, but always seems to return 1. Possibly broken API. Channels 1 to 13 are the default limits. + assert(wifiCountry.schan <= newWiFiChannel && newWiFiChannel <= wifiCountry.schan + wifiCountry.nchan - 1); + + _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() const +{ + return _meshWiFiChannel; +} + +void MeshBackendBase::setSSID(const String &newSSIDPrefix, const String &newSSIDRoot, const String &newSSIDSuffix) +{ + if(!newSSIDPrefix.isEmpty()) + _SSIDPrefix = newSSIDPrefix; + if(!newSSIDRoot.isEmpty()) + _SSIDRoot = newSSIDRoot; + if(!newSSIDSuffix.isEmpty()) + _SSIDSuffix = newSSIDSuffix; + + String newSSID = _SSIDPrefix + _SSIDRoot + _SSIDSuffix; + + assert(newSSID.length() <= 31); + + if(getSSID() != newSSID) + { + _SSID = newSSID; + + // Apply SSID changes to active AP. + if(isAPController()) + restartAP(); + } +} + +String MeshBackendBase::getSSID() const {return _SSID;} + +void MeshBackendBase::setSSIDPrefix(const String &newSSIDPrefix) +{ + setSSID(newSSIDPrefix); +} + +String MeshBackendBase::getSSIDPrefix() const {return _SSIDPrefix;} + +void MeshBackendBase::setSSIDRoot(const String &newSSIDRoot) +{ + setSSID(emptyString, newSSIDRoot); +} + +String MeshBackendBase::getSSIDRoot() const {return _SSIDRoot;} + +void MeshBackendBase::setSSIDSuffix(const String &newSSIDSuffix) +{ + setSSID(emptyString, emptyString, newSSIDSuffix); +} + +String MeshBackendBase::getSSIDSuffix() const {return _SSIDSuffix;} + +void MeshBackendBase::setMeshName(const String &newMeshName) +{ + setSSIDPrefix(newMeshName); +} + +String MeshBackendBase::getMeshName() const {return getSSIDPrefix();} + +void MeshBackendBase::setNodeID(const String &newNodeID) +{ + setSSIDSuffix(newNodeID); +} + +String MeshBackendBase::getNodeID() const {return getSSIDSuffix();} + +void MeshBackendBase::setMeshPassword(const String &newMeshPassword) +{ + assert(8 <= newMeshPassword.length() && newMeshPassword.length() <= 64); // Limited by the ESP8266 API. + assert(newMeshPassword.indexOf('"') == -1); // " is not allowed in passwords to allow for easier JSON parsing and predictable password length (no need for extra escape characters). + + _meshPassword = newMeshPassword; + + // Apply changes to active AP. + if(isAPController()) + restartAP(); +} + +String MeshBackendBase::getMeshPassword() const {return _meshPassword;} + +void MeshBackendBase::setMessage(const String &newMessage) {_message = newMessage;} +String MeshBackendBase::getMessage() const {return _message;} + +void MeshBackendBase::setRequestHandler(const MeshBackendBase::requestHandlerType requestHandler) {_requestHandler = requestHandler;} +MeshBackendBase::requestHandlerType MeshBackendBase::getRequestHandler() const {return _requestHandler;} + +void MeshBackendBase::setResponseHandler(const MeshBackendBase::responseHandlerType responseHandler) {_responseHandler = responseHandler;} +MeshBackendBase::responseHandlerType MeshBackendBase::getResponseHandler() const {return _responseHandler;} + +void MeshBackendBase::setNetworkFilter(const MeshBackendBase::networkFilterType networkFilter) {_networkFilter = networkFilter;} +MeshBackendBase::networkFilterType MeshBackendBase::getNetworkFilter() const {return _networkFilter;} + +void MeshBackendBase::setTransmissionOutcomesUpdateHook(const MeshBackendBase::transmissionOutcomesUpdateHookType transmissionOutcomesUpdateHook) {_transmissionOutcomesUpdateHook = transmissionOutcomesUpdateHook;} +MeshBackendBase::transmissionOutcomesUpdateHookType MeshBackendBase::getTransmissionOutcomesUpdateHook() const {return _transmissionOutcomesUpdateHook;} + +void MeshBackendBase::setScanHidden(const bool scanHidden) +{ + _scanHidden = scanHidden; +} + +bool MeshBackendBase::getScanHidden() const {return _scanHidden;} + +void MeshBackendBase::setAPHidden(const bool apHidden) +{ + if(getAPHidden() != apHidden) + { + _apHidden = apHidden; + + // Apply changes to active AP. + if(isAPController()) + restartAP(); + } +} + +bool MeshBackendBase::getAPHidden() const {return _apHidden;} + +ConditionalPrinter *MeshBackendBase::getConditionalPrinter() {return &_conditionalPrinter;} +const ConditionalPrinter *MeshBackendBase::getConditionalPrinterConst() const {return &_conditionalPrinter;} + +bool MeshBackendBase::latestTransmissionSuccessfulBase(const std::vector &latestTransmissionOutcomes) +{ + if(latestTransmissionOutcomes.empty()) + return false; + + for(const TransmissionOutcome &transmissionOutcome : latestTransmissionOutcomes) + if(transmissionOutcome.transmissionStatus() != TransmissionStatusType::TRANSMISSION_COMPLETE) + return false; + + return true; +} + +void MeshBackendBase::scanForNetworks(const bool scanAllWiFiChannels) +{ + MutexTracker mutexTracker(_scanMutex); + if(!mutexTracker.mutexCaptured()) + { + assert(false && String(F("ERROR! Scan already in progress. Don't call scanForNetworks from callbacks as this may corrupt program state! Aborting."))); + return; + } + + verboseModePrint(F("Scanning... "), false); + + /* Scan for APs */ + + // 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 NetworkInfoBase &apNetworkInfo) +{ + String mainNetworkIdentifier = apNetworkInfo.SSID(); + if(mainNetworkIdentifier == NetworkInfoBase::defaultSSID) // If SSID not provided, use BSSID instead + { + mainNetworkIdentifier = TypeCast::macToString(apNetworkInfo.BSSID()); + } + + verboseModePrint(String(F("AP acquired: ")) + mainNetworkIdentifier + String(F(", Ch:")) + String(apNetworkInfo.wifiChannel()) + ' ', false); + + if(apNetworkInfo.RSSI() != NetworkInfoBase::defaultRSSI) + { + verboseModePrint(String('(') + String(apNetworkInfo.RSSI()) + String(F("dBm) ")) + + (apNetworkInfo.encryptionType() == ENC_TYPE_NONE ? String(F("open")) : emptyString), false); + } + + verboseModePrint(F("... "), false); +} diff --git a/libraries/ESP8266WiFiMesh/src/MeshBackendBase.h b/libraries/ESP8266WiFiMesh/src/MeshBackendBase.h new file mode 100644 index 0000000000..483deaf319 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/MeshBackendBase.h @@ -0,0 +1,343 @@ +/* + 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 "TransmissionOutcome.h" +#include "NetworkInfoBase.h" +#include "ConditionalPrinter.h" + +enum class MeshBackendType +{ + TCP_IP = 0, + ESP_NOW = 1 +}; + +class MeshBackendBase { + +public: + + using requestHandlerType = std::function ; + using responseHandlerType = std::function; + using networkFilterType = std::function; + using transmissionOutcomesUpdateHookType = std::function; + + MeshBackendBase(const requestHandlerType requestHandler, const responseHandlerType responseHandler, const networkFilterType networkFilter, const MeshBackendType classType); + + virtual ~MeshBackendBase(); + + /** + * Initialises the node. + */ + virtual void begin() = 0; + + /** + * Activate the WiFi access point of this ESP8266. + * + * For TCP/IP, 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. + * + * Note that only one AP can be active at a time in total (there is only one WiFi radio on the ESP8266), and this will always be the one which was last activated. + * Thus the AP is shared by all backends. + */ + void activateAP(); + + /** + * Deactivate the WiFi access point of this ESP8266, regardless of which MeshBackendBase instance is in control of the AP. + * + * Note that only one AP can be active at a time in total (there is only one WiFi radio on the ESP8266), and this will always be the one which was last activated. + * Thus the AP is shared by all backends. + */ + static void deactivateAP(); + + /** + * Deactivate the WiFi access point of this ESP8266, provided that this MeshBackendBase instance is in control of the AP (which normally is the case for the MeshBackendBase instance that did the last activateAP() call). + * + * Note that only one AP can be active at a time in total (there is only one WiFi radio on the ESP8266), and this will always be the one which was last activated. + * Thus the AP is shared by all backends. + * + * @return True if the AP was deactivated. False otherwise. + */ + bool deactivateControlledAP(); + + /** + * Restart the WiFi access point of this ESP8266. + * + * Note that only one AP can be active at a time in total (there is only one WiFi radio on the ESP8266), and this will always be the one which was last activated. + * Thus the AP is shared by all backends. + */ + 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. + * + * @return 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. + * + * @return True if this MeshBackendBase instance is in control of the ESP8266 AP. False otherwise. + */ + bool isAPController() const; + + /** + * Change the WiFi channel used by this MeshBackendBase instance. + * Will also change the WiFi channel for the active AP (via an AP restart) + * 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 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 determined by wifi_get_country, usually integers from 1 to 11 or 1 to 13. + * + */ + virtual void setWiFiChannel(const uint8 newWiFiChannel); + virtual uint8 getWiFiChannel() const; + + /** + * Change the SSID used by this MeshBackendBase instance. + * The SSID can be at most 31 characters long. + * Will also change the SSID for the active AP (via an AP restart) + * 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 = emptyString, const String &newSSIDRoot = emptyString, + const String &newSSIDSuffix = emptyString); + String getSSID() const; + + /** + * 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 (via an AP restart) + * 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() const; + + /** + * 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 (via an AP restart) + * 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() const; + + /** + * 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 (via an AP restart) + * 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() const; + + /** + * Change the mesh name used by this MeshBackendBase instance. + * Will also change the mesh name for the active AP (via an AP restart) + * 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() const; + + /** + * Change the node id used by this MeshBackendBase instance. + * Will also change the node id for the active AP (via an AP restart) + * 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() const; + + /** + * 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 (via an AP restart) + * if this MeshBackendBase instance is the current AP controller. + * + * @param newMeshPassword The password to use. Must be between 8 and 64 characters long. " is an illegal character because of JSON parsing requirements. + */ + void setMeshPassword(const String &newMeshPassword); + String getMeshPassword() const; + + /** + * 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() const; + + virtual void attemptTransmission(const String &message, const bool scan = true, const bool scanAllWiFiChannels = false) = 0; + + void setRequestHandler(const requestHandlerType requestHandler); + requestHandlerType getRequestHandler() const; + + void setResponseHandler(const responseHandlerType responseHandler); + responseHandlerType getResponseHandler() const; + + void setNetworkFilter(const networkFilterType networkFilter); + networkFilterType getNetworkFilter() const; + + /** + * Set a function that should be called after each update of the latestTransmissionOutcomes vector during attemptTransmission. (which happens after each individual transmission has finished) + * The hook should return a bool. If this return value is true, attemptTransmission will continue with the next entry in the connectionQueue. If it is false, attemptTransmission will stop. + * The default transmissionOutcomesUpdateHook always returns true. + * + * Example use cases is modifying getMessage() between transmissions, or aborting attemptTransmission before all nodes in the connectionQueue have been contacted. + */ + void setTransmissionOutcomesUpdateHook(const transmissionOutcomesUpdateHookType transmissionOutcomesUpdateHook); + transmissionOutcomesUpdateHookType getTransmissionOutcomesUpdateHook() const; + + /** + * 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(const bool scanHidden); + bool getScanHidden() const; + + /** + * 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 (via an AP restart) + * if this MeshBackendBase instance is the current AP controller. + * + * @param apHidden If true, the WiFi network created will have a hidden SSID. + */ + void setAPHidden(const bool apHidden); + bool getAPHidden() const; + + /** + * 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(const bool enabled); + virtual bool verboseMode() const; + + /** + * 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, const bool newline = true) const; + + /** + * 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(const 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, const bool newline = true); + + MeshBackendType getClassType() const; + + virtual void printAPInfo(const NetworkInfoBase &apNetworkInfo); + +protected: + + ConditionalPrinter *getConditionalPrinter(); + const ConditionalPrinter *getConditionalPrinterConst() const; + + /** + * @param latestTransmissionOutcomes The transmission outcomes vector to check. + * + * @return True if latest transmission was successful (i.e. latestTransmissionOutcomes is not empty and all entries have transmissionStatus TransmissionStatusType::TRANSMISSION_COMPLETE). False otherwise. + */ + static bool latestTransmissionSuccessfulBase(const std::vector &latestTransmissionOutcomes); + + virtual void scanForNetworks(const bool scanAllWiFiChannels); + + /** + * 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(const MeshBackendType classType); + + static std::shared_ptr _scanMutex; + +private: + + MeshBackendType _classType; + + ConditionalPrinter _conditionalPrinter; + + String _SSID; + String _SSIDPrefix; + String _SSIDRoot; + String _SSIDSuffix; + String _meshPassword; + String _message; + + requestHandlerType _requestHandler; + responseHandlerType _responseHandler; + networkFilterType _networkFilter; + transmissionOutcomesUpdateHookType _transmissionOutcomesUpdateHook = [](MeshBackendBase &){return true;}; + + uint8 _meshWiFiChannel; + bool _scanHidden = false; + bool _apHidden = false; +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/MeshCryptoInterface.cpp b/libraries/ESP8266WiFiMesh/src/MeshCryptoInterface.cpp new file mode 100644 index 0000000000..68e9b29065 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/MeshCryptoInterface.cpp @@ -0,0 +1,52 @@ +/* + * 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 "MeshCryptoInterface.h" +#include + +namespace MeshCryptoInterface +{ + String createMeshHmac(const String &message, const void *hashKey, const size_t hashKeyLength, const size_t hmacLength) + { + return experimental::crypto::SHA256::hmac(message, hashKey, hashKeyLength, hmacLength); + } + + bool verifyMeshHmac(const String &message, const String &messageHmac, const uint8_t *hashKey, const uint8_t hashKeyLength) + { + String generatedHmac = createMeshHmac(message, hashKey, hashKeyLength, messageHmac.length()/2); // We know that each HMAC byte should become 2 String characters due to uint8ArrayToHexString. + if(generatedHmac == messageHmac) + return true; + else + return false; + } + + uint8_t *initializeKey(uint8_t *key, const uint8_t keyLength, const String &keySeed) + { + assert(keyLength <= experimental::crypto::SHA256::NATURAL_LENGTH); + uint8_t hashArray[experimental::crypto::SHA256::NATURAL_LENGTH] {}; + experimental::crypto::SHA256::hash(keySeed.c_str(), keySeed.length(), hashArray); + memcpy(key, hashArray, keyLength); + return key; + } +} diff --git a/libraries/ESP8266WiFiMesh/src/MeshCryptoInterface.h b/libraries/ESP8266WiFiMesh/src/MeshCryptoInterface.h new file mode 100644 index 0000000000..6fa3fe7e6e --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/MeshCryptoInterface.h @@ -0,0 +1,83 @@ +/* + * 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 __MESHCRYPTOINTERFACE_H__ +#define __MESHCRYPTOINTERFACE_H__ + +#include +#include + +namespace MeshCryptoInterface +{ + /** + * There is a constant-time HMAC version available. More constant-time info here: https://www.bearssl.org/constanttime.html + * For small messages, it takes substantially longer time to complete than a normal HMAC (5 ms vs 2 ms in a quick benchmark, + * determined by the difference between min and max allowed message length), and it also sets a maximum length that messages can be (1024 bytes by default). + * Making the fixed max length variable would defeat the whole purpose of using constant-time, and not making it variable would create the wrong HMAC if message size exceeds the maximum. + * + * Also, HMAC is already partially constant-time. Quoting the link above: + * "Hash functions implemented by BearSSL (MD5, SHA-1, SHA-224, SHA-256, SHA-384 and SHA-512) consist in bitwise logical operations and additions on 32-bit or 64-bit words, + * naturally yielding constant-time operations. HMAC is naturally as constant-time as the underlying hash function. The size of the MACed data, and the size of the key, + * may leak, though; only the contents are protected." + * + * Thus the non constant-time version is used within the mesh framework instead. + */ + + /** + * Create a SHA256 HMAC from the message, using the provided hashKey. The result will be hmacLength bytes long and returned as a String in HEX format. + * + * @param message The string from which to create the HMAC. + * @param hashKey The hash key to use when creating the HMAC. + * @param hashKeyLength The length of the hash key in bytes. + * @param hmacLength The desired length of the generated HMAC, in bytes. Valid values are 1 to 32. Defaults to experimental::crypto::SHA256::NATURAL_LENGTH. + * + * @return A String with the generated HMAC in HEX format. + */ + String createMeshHmac(const String &message, const void *hashKey, const size_t hashKeyLength, const size_t hmacLength = experimental::crypto::SHA256::NATURAL_LENGTH); + + /** + * Verify a SHA256 HMAC which was created from the message using the provided hashKey. + * + * @param message The string from which the HMAC was created. + * @param messageHmac A string with the generated HMAC in HEX format. Valid messageHmac.length() is 2 to 64. + * @param hashKey The hash key to use when creating the HMAC. + * @param hashKeyLength The length of the hash key in bytes. + * + * @return True if the HMAC is correct. False otherwise. + */ + bool verifyMeshHmac(const String &message, const String &messageHmac, const uint8_t *hashKey, const uint8_t hashKeyLength); + + /** + * Initialize key with a SHA-256 hash of keySeed. + * + * @param key A uint8_t array containing the key to be initialized. + * @param keyLength The length of the key array in bytes. Maximum value is experimental::crypto::SHA256::NATURAL_LENGTH. + * @param keySeed The key seed. + * + * @return A pointer to the initialized key array. + */ + uint8_t *initializeKey(uint8_t *key, const uint8_t keyLength, const String &keySeed); +} + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/MessageData.cpp b/libraries/ESP8266WiFiMesh/src/MessageData.cpp new file mode 100644 index 0000000000..72d087aed6 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/MessageData.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 "MessageData.h" +#include "EspnowProtocolInterpreter.h" +#include "EspnowMeshBackend.h" +#include + +MessageData::MessageData(const String &message, const uint8_t transmissionsRemaining, const uint32_t creationTimeMs) : + _timeTracker(creationTimeMs) +{ + _transmissionsExpected = transmissionsRemaining + 1; + _totalMessage += message; + ++_transmissionsReceived; +} + +MessageData::MessageData(uint8_t *initialTransmission, const uint8_t transmissionLength, const uint32_t creationTimeMs) : + _timeTracker(creationTimeMs) +{ + _transmissionsExpected = EspnowProtocolInterpreter::getTransmissionsRemaining(initialTransmission) + 1; + addToMessage(initialTransmission, transmissionLength); +} + +bool MessageData::addToMessage(uint8_t *transmission, const uint8_t transmissionLength) +{ + if(EspnowProtocolInterpreter::getTransmissionsRemaining(transmission) == getTransmissionsRemaining() - 1) + { + String message = EspnowProtocolInterpreter::getHashKeyLength(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() const +{ + return _transmissionsReceived; +} + +uint8_t MessageData::getTransmissionsExpected() const +{ + return _transmissionsExpected; +} + +uint8_t MessageData::getTransmissionsRemaining() const +{ + return getTransmissionsExpected() - getTransmissionsReceived(); +} + +String MessageData::getTotalMessage() const +{ + return _totalMessage; +} + +const TimeTracker &MessageData::getTimeTracker() const { return _timeTracker; } diff --git a/libraries/ESP8266WiFiMesh/src/MessageData.h b/libraries/ESP8266WiFiMesh/src/MessageData.h new file mode 100644 index 0000000000..825ec13f57 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/MessageData.h @@ -0,0 +1,56 @@ +/* + * 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: + + MessageData(const String &message, const uint8_t transmissionsRemaining, const uint32_t creationTimeMs = millis()); + MessageData(uint8_t *initialTransmission, const uint8_t transmissionLength, const uint32_t creationTimeMs = millis()); + /** + * @transmission A string of characters, including initial protocol bytes. Not const since that would increase heap consumption during processing. + * @transmissionLength Length of transmission. + */ + bool addToMessage(uint8_t *transmission, const uint8_t transmissionLength); + uint8_t getTransmissionsReceived() const; + uint8_t getTransmissionsExpected() const; + uint8_t getTransmissionsRemaining() const; + String getTotalMessage() const; + const TimeTracker &getTimeTracker() const; + +private: + + TimeTracker _timeTracker; + String _totalMessage; + uint8_t _transmissionsReceived = 0; + uint8_t _transmissionsExpected; +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/MutexTracker.cpp b/libraries/ESP8266WiFiMesh/src/MutexTracker.cpp new file mode 100644 index 0000000000..b4fdad89a8 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/MutexTracker.cpp @@ -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. + */ + +#include "MutexTracker.h" + +namespace +{ + std::shared_ptr _captureBan = std::make_shared(false); +} + +MutexTracker::MutexTracker(const std::shared_ptr &mutexToCapture) +{ + attemptMutexCapture(mutexToCapture); +} + +MutexTracker::MutexTracker(const std::shared_ptr &mutexToCapture, const std::function destructorHook) : MutexTracker(mutexToCapture) +{ + _destructorHook = destructorHook; +} + +MutexTracker::~MutexTracker() +{ + releaseMutex(); + _destructorHook(); +} + +MutexTracker MutexTracker::captureBan() +{ + // Syntax like this will move the resulting value into its new position (similar to NRVO): https://stackoverflow.com/a/11540204 + return MutexTracker(_captureBan); +} + +MutexTracker MutexTracker::captureBan(const std::function destructorHook) { return MutexTracker(_captureBan, destructorHook); } + +bool MutexTracker::mutexFree(const std::shared_ptr &mutex) +{ + if(mutex != nullptr && !(*mutex)) + return true; + + return false; +} + +bool MutexTracker::mutexCaptured(const std::shared_ptr &mutex) +{ + if(mutex != nullptr && (*mutex)) + return true; + + return false; +} + +bool MutexTracker::mutexCaptured() const +{ + return mutexCaptured(_capturedMutex); +} + +void MutexTracker::releaseMutex() +{ + if(mutexCaptured()) + { + *_capturedMutex = false; + _capturedMutex.reset(); + } +} + +bool MutexTracker::attemptMutexCapture(const std::shared_ptr &mutexToCapture) +{ + if(mutexFree(_captureBan) && mutexFree(mutexToCapture)) + { + _capturedMutex = mutexToCapture; + *_capturedMutex = true; + return true; + } + + return false; +} diff --git a/libraries/ESP8266WiFiMesh/src/MutexTracker.h b/libraries/ESP8266WiFiMesh/src/MutexTracker.h new file mode 100644 index 0000000000..ac911fda81 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/MutexTracker.h @@ -0,0 +1,83 @@ +/* + * 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 +#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(const std::shared_ptr &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(const std::shared_ptr &mutexToCapture, const std::function destructorHook); + + ~MutexTracker(); + + /* + * If captureBan is active, trying to capture a mutex will always fail. + * Inactive by default. + * captureBan can be managed by MutexTracker like any other mutex. + */ + static MutexTracker captureBan(); + static MutexTracker captureBan(const std::function destructorHook); + + bool mutexCaptured() const; + + /** + * Set the mutex free to roam the binary plains, giving new MutexTrackers a chance to capture it. + */ + void releaseMutex(); + + private: + + std::shared_ptr _capturedMutex; + std::function _destructorHook = [](){ }; + + static bool mutexFree(const std::shared_ptr &mutex); + static bool mutexCaptured(const std::shared_ptr &mutex); + + /** + * Attempt to capture the mutex. + * + * @return True if mutex was caught (meaning it exists and no other instance is holding the mutex). False otherwise. + */ + bool attemptMutexCapture(const std::shared_ptr &mutexToCapture); +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/NetworkInfo.cpp b/libraries/ESP8266WiFiMesh/src/NetworkInfo.cpp index 10300dd7bf..7e983ce867 100644 --- a/libraries/ESP8266WiFiMesh/src/NetworkInfo.cpp +++ b/libraries/ESP8266WiFiMesh/src/NetworkInfo.cpp @@ -22,6 +22,29 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ + + + + + + + +/******************************************************************************************** +* 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 EspnowNetworkInfo.h or TcpIpNetworkInfo.h source files. +* +* TODO: delete this file. +********************************************************************************************/ + + + + + + + #include "NetworkInfo.h" @@ -74,4 +97,3 @@ NetworkInfo & NetworkInfo::operator=(const NetworkInfo &other) networkIndex = other.networkIndex; return *this; } - diff --git a/libraries/ESP8266WiFiMesh/src/NetworkInfo.h b/libraries/ESP8266WiFiMesh/src/NetworkInfo.h index 82fcd90c04..4462759f94 100644 --- a/libraries/ESP8266WiFiMesh/src/NetworkInfo.h +++ b/libraries/ESP8266WiFiMesh/src/NetworkInfo.h @@ -23,12 +23,34 @@ * THE SOFTWARE. */ + + + + + + +/******************************************************************************************** +* 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 EspnowNetworkInfo.h or TcpIpNetworkInfo.h source files. +* +* TODO: delete this file. +********************************************************************************************/ + + + + + + + + #ifndef __NETWORKINFO_H__ #define __NETWORKINFO_H__ #include - -const int NETWORK_INFO_DEFAULT_INT = -1; +#include "NetworkInfoBase.h" class NetworkInfo { @@ -38,7 +60,7 @@ class NetworkInfo { public: - String SSID = ""; + String SSID; int wifiChannel = NETWORK_INFO_DEFAULT_INT; uint8_t *BSSID = NULL; int networkIndex = NETWORK_INFO_DEFAULT_INT; diff --git a/libraries/ESP8266WiFiMesh/src/NetworkInfoBase.cpp b/libraries/ESP8266WiFiMesh/src/NetworkInfoBase.cpp new file mode 100644 index 0000000000..7549c5ebc0 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/NetworkInfoBase.cpp @@ -0,0 +1,120 @@ +/* + * 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 "NetworkInfoBase.h" + +uint8_t * const NetworkInfoBase::defaultBSSID = nullptr; +const String NetworkInfoBase::defaultSSID; +const int32_t NetworkInfoBase::defaultWifiChannel = NETWORK_INFO_DEFAULT_INT; +const uint8_t NetworkInfoBase::defaultEncryptionType = 0; +const int32_t NetworkInfoBase::defaultRSSI = ~0; +const bool NetworkInfoBase::defaultIsHidden = false; + +void NetworkInfoBase::storeBSSID(const uint8_t newBSSID[6]) +{ + if(newBSSID != nullptr) + { + if(_BSSID == nullptr) + { + _BSSID = _bssidArray; + } + + for(int i = 0; i < 6; ++i) + { + _BSSID[i] = newBSSID[i]; + } + } + else + { + _BSSID = nullptr; + } +} + +NetworkInfoBase::NetworkInfoBase() {}; + +NetworkInfoBase::NetworkInfoBase(const uint8_t networkIndex) +{ + uint8_t *bssidPtr = nullptr; + WiFi.getNetworkInfo(networkIndex, _SSID, _encryptionType, _RSSI, bssidPtr, _wifiChannel, _isHidden); + storeBSSID(bssidPtr); +} + +NetworkInfoBase::NetworkInfoBase(const String &SSID, const int32_t wifiChannel, const uint8_t BSSID[6], const uint8_t encryptionType, const int32_t RSSI, const bool isHidden) : + _SSID(SSID), _wifiChannel(wifiChannel), _RSSI(RSSI), _encryptionType(encryptionType), _isHidden(isHidden) +{ + storeBSSID(BSSID); +} + +NetworkInfoBase::NetworkInfoBase(const NetworkInfoBase &other) : _SSID(other.SSID()), _wifiChannel(other.wifiChannel()), _RSSI(other.RSSI()), + _encryptionType(other.encryptionType()), _isHidden(other.isHidden()) +{ + storeBSSID(other.BSSID()); +} + +NetworkInfoBase & NetworkInfoBase::operator=(const NetworkInfoBase &other) +{ + if(this != &other) + { + storeBSSID(other.BSSID()); + _SSID = other.SSID(); + _wifiChannel = other.wifiChannel(); + _encryptionType = other.encryptionType(); + _RSSI = other.RSSI(); + _isHidden = other.isHidden(); + } + + return *this; +} + +NetworkInfoBase::~NetworkInfoBase() { }; + +void NetworkInfoBase::setBSSID(const uint8_t BSSID[6]) { storeBSSID(BSSID); } +const uint8_t *NetworkInfoBase::BSSID() const { return _BSSID; } +uint8_t *NetworkInfoBase::getBSSID(uint8_t resultArray[6]) const +{ + if(BSSID()) + { + std::copy_n(_bssidArray, 6, resultArray); + return resultArray; + } + else + { + return nullptr; + } +} + +void NetworkInfoBase::setSSID(const String &SSID) { _SSID = SSID; } +String NetworkInfoBase::SSID() const { return _SSID; } + +void NetworkInfoBase::setWifiChannel(const int32_t wifiChannel) { _wifiChannel = wifiChannel; } +int32_t NetworkInfoBase::wifiChannel() const { return _wifiChannel; } + +void NetworkInfoBase::setEncryptionType(const uint8_t encryptionType) { _encryptionType = encryptionType; } +uint8_t NetworkInfoBase::encryptionType() const { return _encryptionType; } + +void NetworkInfoBase::setRSSI(const int32_t RSSI) { _RSSI = RSSI; } +int32_t NetworkInfoBase::RSSI() const { return _RSSI; } + +void NetworkInfoBase::setIsHidden(const bool isHidden) { _isHidden = isHidden; } +bool NetworkInfoBase::isHidden() const { return _isHidden; } diff --git a/libraries/ESP8266WiFiMesh/src/NetworkInfoBase.h b/libraries/ESP8266WiFiMesh/src/NetworkInfoBase.h new file mode 100644 index 0000000000..7122a3f9e4 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/NetworkInfoBase.h @@ -0,0 +1,102 @@ +/* + * 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 __NETWORKINFOBASE_H__ +#define __NETWORKINFOBASE_H__ + +#include + +const int NETWORK_INFO_DEFAULT_INT = -1; + +class NetworkInfoBase { + +public: + + /** + * Automatically fill in the rest of the network info using networkIndex and the WiFi scan results. + */ + NetworkInfoBase(const uint8_t networkIndex); + + /** + * Without giving channel and BSSID, connection time is longer. + */ + NetworkInfoBase(const String &SSID, const int32_t wifiChannel, const uint8_t BSSID[6], const uint8_t encryptionType, const int32_t RSSI, const bool isHidden); + + NetworkInfoBase(const NetworkInfoBase &other); + + NetworkInfoBase & operator=(const NetworkInfoBase &other); + + void setBSSID(const uint8_t BSSID[6]); + const uint8_t *BSSID() const; + /** + * @return If BSSID is set, a pointer to resultArray which will contain a copy of BSSID. nullptr otherwise. + */ + uint8_t *getBSSID(uint8_t resultArray[6]) const; + + void setSSID(const String &SSID); + String SSID() const; + + void setWifiChannel(const int32_t wifiChannel); + int32_t wifiChannel() const; + + void setEncryptionType(const uint8_t encryptionType); + uint8_t encryptionType() const; + + void setRSSI(const int32_t RSSI); + int32_t RSSI() const; + + void setIsHidden(const bool isHidden); + bool isHidden() const; + + static uint8_t * const defaultBSSID; + static const uint8_t defaultEncryptionType; + static const bool defaultIsHidden; + static const String defaultSSID; + static const int32_t defaultWifiChannel; + static const int32_t defaultRSSI; + +protected: + + ~NetworkInfoBase(); + + NetworkInfoBase(); + + /** + * Copy newBSSID into _BSSID. + * Prefer this method for changing NetworkInfo BSSID, unless you actually want to change the _BSSID pointer. + */ + void storeBSSID(const uint8_t newBSSID[6]); + +private: + + uint8_t *_BSSID = defaultBSSID; + String _SSID = defaultSSID; + int32_t _wifiChannel = defaultWifiChannel; + int32_t _RSSI = defaultRSSI; + uint8_t _bssidArray[6] {0}; + uint8_t _encryptionType = defaultEncryptionType; // see enum wl_enc_type for values + bool _isHidden = defaultIsHidden; +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/PeerRequestLog.cpp b/libraries/ESP8266WiFiMesh/src/PeerRequestLog.cpp new file mode 100644 index 0000000000..db87fb5d8c --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/PeerRequestLog.cpp @@ -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. + */ + +#include "PeerRequestLog.h" +#include "EspnowMeshBackend.h" + +namespace +{ + using EspnowProtocolInterpreter::hashKeyLength; +} + +PeerRequestLog::PeerRequestLog(const uint64_t requestID, const bool requestEncrypted, const String &authenticationPassword, const uint8_t encryptedConnectionsSoftLimit, + const String &peerRequestNonce, const uint8_t peerStaMac[6], const uint8_t peerApMac[6], const uint8_t hashKey[hashKeyLength]) + : EncryptedConnectionData(peerStaMac, peerApMac, 0, 0, EspnowMeshBackend::getEncryptionRequestTimeout(), hashKey), + _requestID(requestID), _authenticationPassword(authenticationPassword), _peerRequestNonce(peerRequestNonce) + , _requestEncrypted(requestEncrypted), _encryptedConnectionsSoftLimit(encryptedConnectionsSoftLimit) +{ } + +PeerRequestLog::PeerRequestLog(const uint64_t requestID, const bool requestEncrypted, const String &authenticationPassword, const uint8_t encryptedConnectionsSoftLimit, const String &peerRequestNonce, + const uint8_t peerStaMac[6], const uint8_t peerApMac[6], const uint64_t peerSessionKey, const uint64_t ownSessionKey, const uint8_t hashKey[hashKeyLength]) + : EncryptedConnectionData(peerStaMac, peerApMac, peerSessionKey, ownSessionKey, EspnowMeshBackend::getEncryptionRequestTimeout(), hashKey), + _requestID(requestID), _authenticationPassword(authenticationPassword), _peerRequestNonce(peerRequestNonce) + , _requestEncrypted(requestEncrypted), _encryptedConnectionsSoftLimit(encryptedConnectionsSoftLimit) +{ } + +void PeerRequestLog::setRequestID(const uint64_t requestID) { _requestID = requestID; } +uint64_t PeerRequestLog::getRequestID() const { return _requestID; } + +void PeerRequestLog::setRequestEncrypted(const bool requestEncrypted) { _requestEncrypted = requestEncrypted; } +bool PeerRequestLog::requestEncrypted() const { return _requestEncrypted; } + +void PeerRequestLog::setAuthenticationPassword(const String &password) { _authenticationPassword = password; } +String PeerRequestLog::getAuthenticationPassword() const { return _authenticationPassword; } + +void PeerRequestLog::setEncryptedConnectionsSoftLimit(const uint8_t softLimit) { _encryptedConnectionsSoftLimit = softLimit; } +uint8_t PeerRequestLog::getEncryptedConnectionsSoftLimit() const { return _encryptedConnectionsSoftLimit; } + +void PeerRequestLog::setPeerRequestNonce(const String &nonce) { _peerRequestNonce = nonce; } +String PeerRequestLog::getPeerRequestNonce() const { return _peerRequestNonce; } diff --git a/libraries/ESP8266WiFiMesh/src/PeerRequestLog.h b/libraries/ESP8266WiFiMesh/src/PeerRequestLog.h new file mode 100644 index 0000000000..e541dab2fe --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/PeerRequestLog.h @@ -0,0 +1,65 @@ +/* + * 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(const uint64_t requestID, const bool requestEncrypted, const String &authenticationPassword, const uint8_t encryptedConnectionsSoftLimit, const String &peerRequestNonce, + const uint8_t peerStaMac[6], const uint8_t peerApMac[6], const uint8_t hashKey[EspnowProtocolInterpreter::hashKeyLength]); + PeerRequestLog(const uint64_t requestID, const bool requestEncrypted, const String &authenticationPassword, const uint8_t encryptedConnectionsSoftLimit, const String &peerRequestNonce, + const uint8_t peerStaMac[6], const uint8_t peerApMac[6], const uint64_t peerSessionKey, const uint64_t ownSessionKey, + const uint8_t hashKey[EspnowProtocolInterpreter::hashKeyLength]); + + void setRequestID(const uint64_t requestID); + uint64_t getRequestID() const; + + void setRequestEncrypted(const bool requestEncrypted); + bool requestEncrypted() const; + + void setAuthenticationPassword(const String &password); + String getAuthenticationPassword() const; + + void setEncryptedConnectionsSoftLimit(const uint8_t softLimit); + uint8_t getEncryptedConnectionsSoftLimit() const; + + void setPeerRequestNonce(const String &nonce); + String getPeerRequestNonce() const; + +private: + + uint64_t _requestID; + String _authenticationPassword; + String _peerRequestNonce; + bool _requestEncrypted; + uint8_t _encryptedConnectionsSoftLimit; +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/RequestData.cpp b/libraries/ESP8266WiFiMesh/src/RequestData.cpp new file mode 100644 index 0000000000..a3946215cf --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/RequestData.cpp @@ -0,0 +1,33 @@ +/* + * 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" +#include "EspnowMeshBackend.h" + +RequestData::RequestData(EspnowMeshBackend &meshInstance, const uint32_t creationTimeMs) : + _timeTracker(creationTimeMs), _meshInstance(meshInstance) +{ } + +EspnowMeshBackend &RequestData::getMeshInstance() const { return _meshInstance; } +const TimeTracker &RequestData::getTimeTracker() const { return _timeTracker; } diff --git a/libraries/ESP8266WiFiMesh/src/RequestData.h b/libraries/ESP8266WiFiMesh/src/RequestData.h new file mode 100644 index 0000000000..c2f5a55965 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/RequestData.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 __ESPNOWREQUESTDATA_H__ +#define __ESPNOWREQUESTDATA_H__ + +#include +#include "TimeTracker.h" + +class EspnowMeshBackend; + +class RequestData { + +public: + + RequestData(EspnowMeshBackend &meshInstance, const uint32_t creationTimeMs = millis()); + + EspnowMeshBackend &getMeshInstance() const; + const TimeTracker &getTimeTracker() const; + +private: + + TimeTracker _timeTracker; + EspnowMeshBackend &_meshInstance; +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/ResponseData.cpp b/libraries/ESP8266WiFiMesh/src/ResponseData.cpp new file mode 100644 index 0000000000..5c7cb93eb8 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/ResponseData.cpp @@ -0,0 +1,80 @@ +/* + * 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], const uint64_t requestID, const uint32_t creationTimeMs) : + _timeTracker(creationTimeMs), _message(message), _requestID(requestID) +{ + storeRecipientMac(recipientMac); +} + +ResponseData::ResponseData(const ResponseData &other) + : _timeTracker(other.getTimeTracker()), _message(other.getMessage()), _requestID(other.getRequestID()) +{ + storeRecipientMac(other.getRecipientMac()); +} + +ResponseData & ResponseData::operator=(const ResponseData &other) +{ + if(this != &other) + { + _timeTracker = other.getTimeTracker(); + _message = other.getMessage(); + _requestID = other.getRequestID(); + storeRecipientMac(other.getRecipientMac()); + } + + return *this; +} + +void ResponseData::storeRecipientMac(const uint8_t newRecipientMac[6]) +{ + if(newRecipientMac == nullptr) + { + _recipientMac = nullptr; + return; + } + + if(_recipientMac == nullptr) + { + _recipientMac = _recipientMacArray; + } + + for(int i = 0; i < 6; ++i) + { + _recipientMac[i] = newRecipientMac[i]; + } +} + +void ResponseData::setRecipientMac(const uint8_t recipientMac[6]) { storeRecipientMac(recipientMac); } +const uint8_t *ResponseData::getRecipientMac() const { return _recipientMac; } + +void ResponseData::setMessage(const String &message) { _message = message; } +String ResponseData::getMessage() const { return _message; } + +void ResponseData::setRequestID(const uint64_t requestID) { _requestID = requestID; } +uint64_t ResponseData::getRequestID() const { return _requestID; } + +const TimeTracker &ResponseData::getTimeTracker() const { return _timeTracker; } diff --git a/libraries/ESP8266WiFiMesh/src/ResponseData.h b/libraries/ESP8266WiFiMesh/src/ResponseData.h new file mode 100644 index 0000000000..32273d497f --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/ResponseData.h @@ -0,0 +1,63 @@ +/* + * 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: + + ResponseData(const String &message, const uint8_t recipientMac[6], const uint64_t requestID, const 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(const String &message); + String getMessage() const; + + void setRequestID(const uint64_t requestID); + uint64_t getRequestID() const; + + const TimeTracker &getTimeTracker() const; + +private: + + void storeRecipientMac(const uint8_t newRecipientMac[6]); + + TimeTracker _timeTracker; + + uint8_t *_recipientMac = nullptr; + String _message; + uint64_t _requestID = 0; + uint8_t _recipientMacArray[6] {0}; +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/Serializer.cpp b/libraries/ESP8266WiFiMesh/src/Serializer.cpp new file mode 100644 index 0000000000..2b4454fb03 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/Serializer.cpp @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2020 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 "Serializer.h" +#include "JsonTranslator.h" +#include "TypeConversionFunctions.h" +#include "MeshCryptoInterface.h" +#include "EspnowProtocolInterpreter.h" +#include + +namespace +{ + namespace TypeCast = MeshTypeConversionFunctions; + + String createJsonEndPair(const String &valueIdentifier, const String &value) + { + const String q = String('"'); + return q + valueIdentifier + q + ':' + q + value + F("\"}}"); + } +} + +namespace Serializer +{ + /* + * NOTE: The internal states may be changed in future updates, so the function signatures here are not guaranteed to be stable. + */ + + String serializeMeshState(const String &unsyncMsgID, const String &meshMsgCount) + { + using namespace JsonTranslator; + + // Returns: {"meshState":{"connectionState":{"unsyncMsgID":"123"},"meshMsgCount":"123"}} + return encode({FPSTR(jsonMeshState), encode({FPSTR(jsonConnectionState), encode({FPSTR(jsonUnsynchronizedMessageID), unsyncMsgID}), FPSTR(jsonMeshMessageCount), meshMsgCount})}); + } + + String serializeUnencryptedConnection(const String &unsyncMsgID) + { + using namespace JsonTranslator; + + // Returns: {"connectionState":{"unsyncMsgID":"123"}} + return encode({FPSTR(jsonConnectionState), encode({FPSTR(jsonUnsynchronizedMessageID), unsyncMsgID})}); + } + + String serializeEncryptedConnection(const String &duration, const String &desync, const String &ownSK, const String &peerSK, const String &peerStaMac, const String &peerApMac) + { + using namespace JsonTranslator; + + if(duration.isEmpty()) + { + // Returns: {"connectionState":{"desync":"0","ownSK":"1A2","peerSK":"3B4","peerStaMac":"F2","peerApMac":"E3"}} + return encode({FPSTR(jsonConnectionState), encode({FPSTR(jsonDesync), desync, FPSTR(jsonOwnSessionKey), ownSK, FPSTR(jsonPeerSessionKey), peerSK, + FPSTR(jsonPeerStaMac), peerStaMac, FPSTR(jsonPeerApMac), peerApMac})}); + } + + // Returns: {"connectionState":{"duration":"123","desync":"0","ownSK":"1A2","peerSK":"3B4","peerStaMac":"F2","peerApMac":"E3"}} + return encode({FPSTR(jsonConnectionState), encode({FPSTR(jsonDuration), duration, FPSTR(jsonDesync), desync, FPSTR(jsonOwnSessionKey), ownSK, FPSTR(jsonPeerSessionKey), peerSK, + FPSTR(jsonPeerStaMac), peerStaMac, FPSTR(jsonPeerApMac), peerApMac})}); + } + + String createEncryptedConnectionInfo(const String &infoHeader, const String &requestNonce, const String &authenticationPassword, const uint64_t ownSessionKey, const uint64_t peerSessionKey) + { + using namespace JsonTranslator; + + const String q = String('"'); + + // Returns: infoHeader{"arguments":{"nonce":"1F2","password":"abc","ownSK":"3B4","peerSK":"1A2"}} + return + infoHeader + + encode({FPSTR(jsonArguments), + encodeLiterally({FPSTR(jsonNonce), q + requestNonce + q, + FPSTR(jsonPassword), q + authenticationPassword + q, + FPSTR(jsonOwnSessionKey), q + TypeCast::uint64ToString(peerSessionKey) + q, // Exchanges session keys since it should be valid for the receiver. + FPSTR(jsonPeerSessionKey), q + TypeCast::uint64ToString(ownSessionKey) + q})}); + } + + String createEncryptionRequestHmacMessage(const String &requestHeader, const String &requestNonce, const uint8_t *hashKey, const uint8_t hashKeyLength, const uint32_t duration) + { + using namespace JsonTranslator; + + String mainMessage = requestHeader; + + if(requestHeader == FPSTR(EspnowProtocolInterpreter::temporaryEncryptionRequestHeader)) + { + mainMessage += encode({FPSTR(jsonArguments), encode({FPSTR(jsonDuration), String(duration), FPSTR(jsonNonce), requestNonce})}); + } + else + { + mainMessage += encode({FPSTR(jsonArguments), encode({FPSTR(jsonNonce), requestNonce})}); + } + + // We need to have an open JSON object so we can add the HMAC later. + mainMessage.remove(mainMessage.length() - 2); + mainMessage += ','; + + uint8_t staMac[6] {0}; + uint8_t apMac[6] {0}; + String requesterStaApMac = TypeCast::macToString(WiFi.macAddress(staMac)) + TypeCast::macToString(WiFi.softAPmacAddress(apMac)); + String hmac = MeshCryptoInterface::createMeshHmac(requesterStaApMac + mainMessage, hashKey, hashKeyLength); + + // Returns: requestHeader{"arguments":{"duration":"123","nonce":"1F2","hmac":"3B4"}} + return mainMessage + createJsonEndPair(FPSTR(jsonHmac), hmac); + } +} diff --git a/libraries/ESP8266WiFiMesh/src/Serializer.h b/libraries/ESP8266WiFiMesh/src/Serializer.h new file mode 100644 index 0000000000..d5d32dbaa2 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/Serializer.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2020 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 __ESP8266MESHSERIALIZER_H__ +#define __ESP8266MESHSERIALIZER_H__ + +#include + +namespace Serializer +{ + /* + * NOTE: The internal states may be changed in future updates, so the function signatures here are not guaranteed to be stable. + */ + + String serializeMeshState(const String &unsyncMsgID, const String &meshMsgCount); + String serializeUnencryptedConnection(const String &unsyncMsgID); + String serializeEncryptedConnection(const String &duration, const String &desync, const String &ownSK, const String &peerSK, const String &peerStaMac, const String &peerApMac); + + String createEncryptedConnectionInfo(const String &infoHeader, const String &requestNonce, const String &authenticationPassword, const uint64_t ownSessionKey, const uint64_t peerSessionKey); + String createEncryptionRequestHmacMessage(const String &requestHeader, const String &requestNonce, const uint8_t *hashKey, const uint8_t hashKeyLength, const uint32_t duration = 0); +} + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/TcpIpMeshBackend.cpp b/libraries/ESP8266WiFiMesh/src/TcpIpMeshBackend.cpp new file mode 100644 index 0000000000..6aebe46ba0 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/TcpIpMeshBackend.cpp @@ -0,0 +1,561 @@ +/* + 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" +#include "ExpiringTimeTracker.h" + +namespace +{ + constexpr char SERVER_IP_ADDR[] PROGMEM = "192.168.4.1"; + + String _temporaryMessage; + String lastSSID; + bool 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 staticIP; + IPAddress gateway(192,168,4,1); + IPAddress subnetMask(255,255,255,0); +} + +const IPAddress TcpIpMeshBackend::emptyIP; + +std::shared_ptr TcpIpMeshBackend::_tcpIpTransmissionMutex = std::make_shared(false); +std::shared_ptr TcpIpMeshBackend::_tcpIpConnectionQueueMutex = std::make_shared(false); + +std::vector TcpIpMeshBackend::_connectionQueue = {}; +std::vector TcpIpMeshBackend::_latestTransmissionOutcomes = {}; + +TcpIpMeshBackend::TcpIpMeshBackend(const requestHandlerType requestHandler, const responseHandlerType responseHandler, + const networkFilterType networkFilter, const String &meshPassword, const String &ssidPrefix, + const String &ssidSuffix, const bool verboseMode, const uint8 meshWiFiChannel, const uint16_t serverPort) + : MeshBackendBase(requestHandler, responseHandler, networkFilter, MeshBackendType::TCP_IP), _server(serverPort) +{ + setSSID(ssidPrefix, emptyString, ssidSuffix); + setMeshPassword(meshPassword); + setVerboseModeState(verboseMode); + setWiFiChannel(meshWiFiChannel); + setServerPort(serverPort); +} + +std::vector & TcpIpMeshBackend::connectionQueue() +{ + MutexTracker connectionQueueMutexTracker(_tcpIpConnectionQueueMutex); + if(!connectionQueueMutexTracker.mutexCaptured()) + { + assert(false && String(F("ERROR! connectionQueue locked. Don't call connectionQueue() from callbacks other than NetworkFilter as this may corrupt program state!"))); + } + + return _connectionQueue; +} + +const std::vector & TcpIpMeshBackend::constConnectionQueue() +{ + return _connectionQueue; +} + +std::vector & TcpIpMeshBackend::latestTransmissionOutcomes() +{ + return _latestTransmissionOutcomes; +} + +bool TcpIpMeshBackend::latestTransmissionSuccessful() +{ + return latestTransmissionSuccessfulBase(latestTransmissionOutcomes()); +} + +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::setTemporaryMessage(const String &newTemporaryMessage) {_temporaryMessage = newTemporaryMessage;} +String TcpIpMeshBackend::getTemporaryMessage() const {return _temporaryMessage;} +void TcpIpMeshBackend::clearTemporaryMessage() {_temporaryMessage.clear();} + +String TcpIpMeshBackend::getCurrentMessage() const +{ + String message = getTemporaryMessage(); + + if(message.isEmpty()) // If no temporary message stored + message = getMessage(); + + return message; +} + +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() const +{ + if(staticIPActivated) + return staticIP; + + return emptyIP; +} + +void TcpIpMeshBackend::disableStaticIP() +{ + WiFi.config(0u,0u,0u); + yield(); + staticIPActivated = false; +} + +void TcpIpMeshBackend::setServerPort(const uint16_t serverPort) +{ + _serverPort = serverPort; + + // Apply changes to active AP. + if(isAPController()) + restartAP(); +} + +uint16_t TcpIpMeshBackend::getServerPort() const {return _serverPort;} + +void TcpIpMeshBackend::setMaxAPStations(const 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() const {return _maxAPStations;} + +void TcpIpMeshBackend::setConnectionAttemptTimeout(const uint32_t connectionAttemptTimeoutMs) +{ + _connectionAttemptTimeoutMs = connectionAttemptTimeoutMs; +} + +uint32_t TcpIpMeshBackend::getConnectionAttemptTimeout() const {return _connectionAttemptTimeoutMs;} + +void TcpIpMeshBackend::setStationModeTimeout(const int stationModeTimeoutMs) +{ + _stationModeTimeoutMs = stationModeTimeoutMs; +} + +int TcpIpMeshBackend::getStationModeTimeout() const {return _stationModeTimeoutMs;} + +void TcpIpMeshBackend::setAPModeTimeout(const uint32_t apModeTimeoutMs) +{ + _apModeTimeoutMs = apModeTimeoutMs; +} + +uint32_t TcpIpMeshBackend::getAPModeTimeout() const {return _apModeTimeoutMs;} + +/** + * Disconnect completely from a network. + */ +void TcpIpMeshBackend::fullStop(WiFiClient &currClient) +{ + currClient.stop(); + yield(); + WiFi.disconnect(); + yield(); +} + +/** + * Wait for a WiFiClient to transmit + * + * @return True if the client is ready, false otherwise. + * + */ +bool TcpIpMeshBackend::waitForClientTransmission(WiFiClient &currClient, const uint32_t maxWait) +{ + ExpiringTimeTracker timeout(maxWait); + + while(currClient.connected() && !currClient.available() && !timeout) + { + delay(1); + } + + /* 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. + * @return A status code based on the outcome of the exchange. + * + */ +TransmissionStatusType TcpIpMeshBackend::exchangeInfo(WiFiClient &currClient) +{ + verboseModePrint(String(F("Transmitting"))); + + currClient.print(getCurrentMessage() + '\r'); + yield(); + + if (!waitForClientTransmission(currClient, _stationModeTimeoutMs)) + { + fullStop(currClient); + return TransmissionStatusType::CONNECTION_FAILED; + } + + if (!currClient.available()) + { + verboseModePrint(F("No response!")); + return TransmissionStatusType::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. + * + * @return A status code based on the outcome of the data transfer attempt. + */ +TransmissionStatusType 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 AP_STA 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); + TransmissionStatusType 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. + * + * @return A status code based on the outcome of the data transfer attempt. + */ +TransmissionStatusType TcpIpMeshBackend::attemptDataTransferKernel() +{ + WiFiClient currClient; + currClient.setTimeout(_stationModeTimeoutMs); + + /* Connect to the node's server */ + if (!currClient.connect(FPSTR(SERVER_IP_ADDR), getServerPort())) + { + fullStop(currClient); + verboseModePrint(F("Server unavailable")); + return TransmissionStatusType::CONNECTION_FAILED; + } + + TransmissionStatusType transmissionOutcome = exchangeInfo(currClient); + if (static_cast(transmissionOutcome) <= 0) + { + verboseModePrint(F("Transmission failed during exchangeInfo.")); + return transmissionOutcome; + } + + currClient.stop(); + yield(); + + return transmissionOutcome; +} + +void TcpIpMeshBackend::initiateConnectionToAP(const String &targetSSID, const int targetChannel, const 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. + * @return A status code based on the outcome of the connection and data transfer process. + * + */ +TransmissionStatusType TcpIpMeshBackend::connectToNode(const String &targetSSID, const int targetChannel, const uint8_t *targetBSSID) +{ + if(staticIPActivated && !lastSSID.isEmpty() && 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 attemptNumber = 1; + ExpiringTimeTracker connectionAttemptTimeout([this](){ return _connectionAttemptTimeoutMs; }); + + while((WiFi.status() == WL_DISCONNECTED) && !connectionAttemptTimeout) + { + if(connectionAttemptTimeout.elapsedTime() > 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); + } + + verboseModePrint(String(connectionAttemptTimeout.elapsedTime())); + + /* If the connection timed out */ + if (WiFi.status() != WL_CONNECTED) + { + verboseModePrint(F("Timeout")); + return TransmissionStatusType::CONNECTION_FAILED; + } + + return attemptDataTransfer(); +} + +TransmissionStatusType TcpIpMeshBackend::initiateTransmission(const TcpIpNetworkInfo &recipientInfo) +{ + WiFi.disconnect(); + yield(); + + assert(!recipientInfo.SSID().isEmpty()); // We need at least SSID to connect + String targetSSID = recipientInfo.SSID(); + int32_t targetWiFiChannel = recipientInfo.wifiChannel(); + uint8_t targetBSSID[6] {0}; + recipientInfo.getBSSID(targetBSSID); + + if(verboseMode()) // Avoid string generation if not required + { + printAPInfo(recipientInfo); + } + + return connectToNode(targetSSID, targetWiFiChannel, targetBSSID); +} + +void TcpIpMeshBackend::enterPostTransmissionState(const bool concludingDisconnect) +{ + 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, const bool scan, const bool scanAllWiFiChannels, const bool concludingDisconnect, const bool initialDisconnect) +{ + MutexTracker mutexTracker(_tcpIpTransmissionMutex); + if(!mutexTracker.mutexCaptured()) + { + assert(false && String(F("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) + { + TransmissionStatusType transmissionResult = attemptDataTransfer(); + latestTransmissionOutcomes().push_back(TransmissionOutcome(constConnectionQueue().back(), transmissionResult)); + + getTransmissionOutcomesUpdateHook()(*this); + } + else + { + if(scan) + { + connectionQueue().clear(); + scanForNetworks(scanAllWiFiChannels); + } + + MutexTracker connectionQueueMutexTracker(_tcpIpConnectionQueueMutex); + if(!connectionQueueMutexTracker.mutexCaptured()) + { + assert(false && String(F("ERROR! connectionQueue locked. Don't call attemptTransmission from callbacks as this may corrupt program state! Aborting."))); + } + else + { + for(const TcpIpNetworkInfo ¤tNetwork : constConnectionQueue()) + { + TransmissionStatusType transmissionResult = initiateTransmission(currentNetwork); + + latestTransmissionOutcomes().push_back(TransmissionOutcome{.origin = currentNetwork, .transmissionStatus = transmissionResult}); + + if(!getTransmissionOutcomesUpdateHook()(*this)) + break; + } + } + } + + enterPostTransmissionState(concludingDisconnect); +} + +void TcpIpMeshBackend::attemptTransmission(const String &message, const bool scan, const bool scanAllWiFiChannels) +{ + attemptTransmission(message, scan, scanAllWiFiChannels, true, false); +} + +TransmissionStatusType TcpIpMeshBackend::attemptTransmission(const String &message, const TcpIpNetworkInfo &recipientInfo, const bool concludingDisconnect, const bool initialDisconnect) +{ + MutexTracker mutexTracker(_tcpIpTransmissionMutex); + if(!mutexTracker.mutexCaptured()) + { + assert(false && String(F("ERROR! TCP/IP transmission in progress. Don't call attemptTransmission from callbacks as this may corrupt program state! Aborting."))); + return TransmissionStatusType::CONNECTION_FAILED; + } + + TransmissionStatusType transmissionResult = TransmissionStatusType::CONNECTION_FAILED; + setTemporaryMessage(message); + + if(initialDisconnect) + { + WiFi.disconnect(); + yield(); + } + + if(WiFi.status() == WL_CONNECTED && WiFi.SSID() == recipientInfo.SSID()) + { + transmissionResult = attemptDataTransfer(); + } + else + { + transmissionResult = initiateTransmission(recipientInfo); + } + + enterPostTransmissionState(concludingDisconnect); + clearTemporaryMessage(); + + return transmissionResult; +} + +void TcpIpMeshBackend::acceptRequests() +{ + MutexTracker mutexTracker(_tcpIpTransmissionMutex); + if(!mutexTracker.mutexCaptured()) + { + assert(false && String(F("ERROR! TCP/IP transmission in progress. Don't call acceptRequests from 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(String(F("Responding"))); + _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 0000000000..9b3de94eb4 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/TcpIpMeshBackend.h @@ -0,0 +1,278 @@ +/* + 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 "TcpIpNetworkInfo.h" + +class TcpIpMeshBackend : public MeshBackendBase { + +public: + + /** + * TCP/IP constructor method. Creates a TCP/IP node, ready to be initialised. + * + * @param requestHandler The callback handler for dealing with received requests. Takes a string as an argument which + * is the request string received from another node and returns the string to send back. + * @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 TransmissionStatusType. + * @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 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(const requestHandlerType requestHandler, const responseHandlerType responseHandler, const networkFilterType networkFilter, + const String &meshPassword, const String &ssidPrefix, const String &ssidSuffix, const bool verboseMode = false, + const uint8 meshWiFiChannel = 1, const uint16_t serverPort = 4011); + + /** + * Returns a vector that contains the NetworkInfo for each WiFi network to connect to. + * This vector is unique for each mesh backend, but NetworkInfo elements can be directly transferred between the vectors as long as both SSID and BSSID are present. + * 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. + * + * Since the connectionQueue() is iterated over during transmissions, always use constConnectionQueue() from callbacks other than NetworkFilter. + */ + static std::vector & connectionQueue(); + + /** + * Same as connectionQueue(), but can be called from all callbacks since the returned reference is const. + */ + static const std::vector & constConnectionQueue(); + + /** + * Returns a vector with the TransmissionOutcome for each AP to which a transmission was attempted during the latest attemptTransmission call. + * This vector is unique for each mesh backend. + * 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(); + + /** + * @return True if latest transmission was successful (i.e. latestTransmissionOutcomes is not empty and all entries have transmissionStatus TransmissionStatusType::TRANSMISSION_COMPLETE). False otherwise. + * The result is unique for each mesh backend. + */ + static bool latestTransmissionSuccessful(); + + /** + * 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, const bool scan, const bool scanAllWiFiChannels, const bool concludingDisconnect, const bool initialDisconnect = false); + + void attemptTransmission(const String &message, const bool scan = true, const bool scanAllWiFiChannels = false) override; + + /** + * Transmit message to a single recipient without changing the local transmission state (apart from connecting to the recipient if required). + * Will not change connectionQueue, latestTransmissionOutcomes or stored message. + * + * Note that if wifiChannel and BSSID are missing from recipientInfo, connection time will be longer. + */ + TransmissionStatusType attemptTransmission(const String &message, const TcpIpNetworkInfo &recipientInfo, const bool concludingDisconnect = true, const bool initialDisconnect = false); + + /** + * If any clients are connected, accept their requests and call the requestHandler function for each one. + */ + void acceptRequests(); + + /** + * Get the TCP/IP message that is currently scheduled for transmission. + * Unlike the getMessage() method, this will be correct even when the single recipient attemptTransmission method is used. + */ + String getCurrentMessage() const; + + /** + * 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() const; + 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 (via an AP restart) + * if this TcpIpMeshBackend instance is the current AP controller. + * + * @param serverPort The server port to use. + * + */ + void setServerPort(const uint16_t serverPort); + uint16_t getServerPort() const; + + /** + * 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 (via an AP restart) + * 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(const uint8_t maxAPStations); + bool getMaxAPStations() const; + + /** + * 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(const uint32_t connectionAttemptTimeoutMs); + uint32_t getConnectionAttemptTimeout() const; + + /** + * 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(const int stationModeTimeoutMs); + int getStationModeTimeout() const; + + /** + * 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 acceptRequests method. + * The timeout is 4 500 ms by default. + * Will also change the setting for the active AP (without an AP restart) + * if this TcpIpMeshBackend instance is the current AP controller. + * + * @param apModeTimeoutMs The timeout to use, in milliseconds. + */ + void setAPModeTimeout(const uint32_t apModeTimeoutMs); + uint32_t getAPModeTimeout() const; + +protected: + + static std::vector _connectionQueue; + static std::vector _latestTransmissionOutcomes; + + /** + * 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 std::shared_ptr _tcpIpTransmissionMutex; + + /** + * Will be true when the connectionQueue should not be modified. + */ + static std::shared_ptr _tcpIpConnectionQueueMutex; + + /** + * Check if there is an ongoing TCP/IP transmission in the library. Used to avoid interrupting transmissions. + * + * @return True if a transmission initiated by a public method is in progress. + */ + static bool transmissionInProgress(); + + /** + * Set a message that will be sent to other nodes when calling attemptTransmission, instead of the regular getMessage(). + * This message is used until clearTemporaryMessage() is called. + * + * @param newMessage The message to send. + */ + void setTemporaryMessage(const String &newMessage); + String getTemporaryMessage() const; + void clearTemporaryMessage(); + +private: + + void fullStop(WiFiClient &currClient); + void initiateConnectionToAP(const String &targetSSID, const int targetChannel = NETWORK_INFO_DEFAULT_INT, const uint8_t *targetBSSID = NULL); + TransmissionStatusType connectToNode(const String &targetSSID, const int targetChannel = NETWORK_INFO_DEFAULT_INT, const uint8_t *targetBSSID = NULL); + TransmissionStatusType exchangeInfo(WiFiClient &currClient); + bool waitForClientTransmission(WiFiClient &currClient, const uint32_t maxWait); + TransmissionStatusType attemptDataTransfer(); + TransmissionStatusType attemptDataTransferKernel(); + TransmissionStatusType initiateTransmission(const TcpIpNetworkInfo &recipientInfo); + void enterPostTransmissionState(const bool concludingDisconnect); + + uint32_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; + + WiFiServer _server; + uint16_t _serverPort; + uint8_t _maxAPStations = 4; // Only affects TCP/IP connections, not ESP-NOW connections + + bool useStaticIP; +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/TcpIpNetworkInfo.cpp b/libraries/ESP8266WiFiMesh/src/TcpIpNetworkInfo.cpp new file mode 100644 index 0000000000..874b750211 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/TcpIpNetworkInfo.cpp @@ -0,0 +1,38 @@ +/* + * 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 "TcpIpNetworkInfo.h" +#include + +TcpIpNetworkInfo::TcpIpNetworkInfo(const int networkIndex) : NetworkInfoBase(networkIndex) { }; + + +TcpIpNetworkInfo::TcpIpNetworkInfo(const NetworkInfoBase &originalNetworkInfo) : NetworkInfoBase(originalNetworkInfo) +{ + assert(SSID() != defaultSSID); // We need at least SSID to be able to connect. +}; + +TcpIpNetworkInfo::TcpIpNetworkInfo(const String &SSID, const int32_t wifiChannel, const uint8_t BSSID[6], const uint8_t encryptionType, const int32_t RSSI , const bool isHidden) + : NetworkInfoBase(SSID, wifiChannel, BSSID, encryptionType, RSSI, isHidden) +{ } diff --git a/libraries/ESP8266WiFiMesh/src/TcpIpNetworkInfo.h b/libraries/ESP8266WiFiMesh/src/TcpIpNetworkInfo.h new file mode 100644 index 0000000000..c3eb63d03a --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/TcpIpNetworkInfo.h @@ -0,0 +1,49 @@ +/* + * 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 __TCPIPNETWORKINFO_H__ +#define __TCPIPNETWORKINFO_H__ + +#include "NetworkInfoBase.h" + +class TcpIpNetworkInfo : public NetworkInfoBase { + +public: + + /** + * Automatically fill in the rest of the network info using networkIndex and the WiFi scan results. + */ + TcpIpNetworkInfo(const int networkIndex); + + + TcpIpNetworkInfo(const NetworkInfoBase &originalNetworkInfo); + + /** + * Without giving wifiChannel and BSSID, connection time is longer. + */ + TcpIpNetworkInfo(const String &SSID, const int32_t wifiChannel = defaultWifiChannel, const uint8_t BSSID[6] = defaultBSSID, const uint8_t encryptionType = defaultEncryptionType, + const int32_t RSSI = defaultRSSI, const bool isHidden = defaultIsHidden); +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/TimeTracker.cpp b/libraries/ESP8266WiFiMesh/src/TimeTracker.cpp new file mode 100644 index 0000000000..c98e73fe04 --- /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(const 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 0000000000..970566065e --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/TimeTracker.h @@ -0,0 +1,46 @@ +/* + * 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 + +// Minimal time tracking class. Used instead of other classes like ExpiringTimeTracker when small memory footprint is important and other functionality not required. +class TimeTracker { + +public: + + virtual ~TimeTracker() = default; + + TimeTracker(const uint32_t creationTimeMs); + uint32_t timeSinceCreation() const; + uint32_t creationTimeMs() const; + +private: + + uint32_t _creationTimeMs; +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/TransmissionOutcome.cpp b/libraries/ESP8266WiFiMesh/src/TransmissionOutcome.cpp new file mode 100644 index 0000000000..7fb6b567b4 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/TransmissionOutcome.cpp @@ -0,0 +1,37 @@ +/* + * 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 "TransmissionOutcome.h" + +TransmissionOutcome::TransmissionOutcome(const NetworkInfoBase &origin, const TransmissionStatusType transmissionStatus) + : NetworkInfoBase(origin), _transmissionStatus(transmissionStatus) +{ } + +TransmissionOutcome::TransmissionOutcome(const String &SSID, const int32_t wifiChannel, const uint8_t BSSID[6], const uint8_t encryptionType, + const int32_t RSSI, const bool isHidden, const TransmissionStatusType transmissionStatus) + : NetworkInfoBase(SSID, wifiChannel, BSSID, encryptionType, RSSI, isHidden), _transmissionStatus(transmissionStatus) +{ } + +void TransmissionOutcome::setTransmissionStatus(const TransmissionStatusType transmissionStatus) { _transmissionStatus = transmissionStatus; } +TransmissionStatusType TransmissionOutcome::transmissionStatus() const { return _transmissionStatus; } diff --git a/libraries/ESP8266WiFiMesh/src/TransmissionOutcome.h b/libraries/ESP8266WiFiMesh/src/TransmissionOutcome.h new file mode 100644 index 0000000000..cdc9437b6f --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/TransmissionOutcome.h @@ -0,0 +1,55 @@ +/* + * 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 __TRANSMISSIONOUTCOME_H__ +#define __TRANSMISSIONOUTCOME_H__ + +#include +#include "NetworkInfoBase.h" + +enum class TransmissionStatusType +{ + CONNECTION_FAILED = -1, + TRANSMISSION_FAILED = 0, + TRANSMISSION_COMPLETE = 1 +}; + +class TransmissionOutcome : public NetworkInfoBase { + +public: + + TransmissionOutcome(const NetworkInfoBase &origin, const TransmissionStatusType transmissionStatus); + + TransmissionOutcome(const String &SSID, const int32_t wifiChannel, const uint8_t BSSID[6], const uint8_t encryptionType, + const int32_t RSSI, const bool isHidden, const TransmissionStatusType transmissionStatus); + + void setTransmissionStatus(const TransmissionStatusType transmissionStatus); + TransmissionStatusType transmissionStatus() const; + +private: + + TransmissionStatusType _transmissionStatus; +}; + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/TransmissionResult.cpp b/libraries/ESP8266WiFiMesh/src/TransmissionResult.cpp index 81f9312461..29b37675c6 100644 --- a/libraries/ESP8266WiFiMesh/src/TransmissionResult.cpp +++ b/libraries/ESP8266WiFiMesh/src/TransmissionResult.cpp @@ -23,6 +23,29 @@ * THE SOFTWARE. */ + + + + + + +/******************************************************************************************** +* 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 EspnowNetworkInfo.h or TcpIpNetworkInfo.h source files. +* +* TODO: delete this file. +********************************************************************************************/ + + + + + + + + #include "TransmissionResult.h" TransmissionResult::TransmissionResult(int newNetworkIndex, transmission_status_t newTransmissionStatus, bool autofill) : diff --git a/libraries/ESP8266WiFiMesh/src/TransmissionResult.h b/libraries/ESP8266WiFiMesh/src/TransmissionResult.h index 8cc4cc020b..709e258f97 100644 --- a/libraries/ESP8266WiFiMesh/src/TransmissionResult.h +++ b/libraries/ESP8266WiFiMesh/src/TransmissionResult.h @@ -23,11 +23,36 @@ * THE SOFTWARE. */ + + + + + + +/******************************************************************************************** +* 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 EspnowNetworkInfo.h or TcpIpNetworkInfo.h source files. +* +* TODO: delete this file. +********************************************************************************************/ + + + + + + + + + #ifndef __TRANSMISSIONRESULT_H__ #define __TRANSMISSIONRESULT_H__ #include #include "NetworkInfo.h" +#include "TransmissionOutcome.h" typedef enum { @@ -45,11 +70,11 @@ class TransmissionResult : public NetworkInfo { /** * @param autofill Automatically fill in the rest of the network info using newNetworkIndex and the WiFi scan results. */ - TransmissionResult(int newNetworkIndex, transmission_status_t newTransmissionStatus, bool autofill = true); + TransmissionResult(int newNetworkIndex, transmission_status_t newTransmissionStatus, bool autofill = true) __attribute__((deprecated)); - TransmissionResult(const String &newSSID, int newWiFiChannel, uint8_t newBSSID[6], transmission_status_t newTransmissionStatus); + TransmissionResult(const String &newSSID, int newWiFiChannel, uint8_t newBSSID[6], transmission_status_t newTransmissionStatus) __attribute__((deprecated)); - TransmissionResult(const String &newSSID, int newWiFiChannel, uint8_t newBSSID[6], int newNetworkIndex, transmission_status_t newTransmissionStatus); + TransmissionResult(const String &newSSID, int newWiFiChannel, uint8_t newBSSID[6], int newNetworkIndex, transmission_status_t newTransmissionStatus) __attribute__((deprecated)); TransmissionResult(const NetworkInfo& origin, transmission_status_t newTransmissionStatus); }; diff --git a/libraries/ESP8266WiFiMesh/src/TypeConversionFunctions.cpp b/libraries/ESP8266WiFiMesh/src/TypeConversionFunctions.cpp index be908b556e..cfcd8f6aed 100644 --- a/libraries/ESP8266WiFiMesh/src/TypeConversionFunctions.cpp +++ b/libraries/ESP8266WiFiMesh/src/TypeConversionFunctions.cpp @@ -1,6 +1,6 @@ /* * TypeConversionFunctions - * Copyright (C) 2018 Anders Löfgren + * Copyright (C) 2018-2019 Anders Löfgren * * License (MIT license): * @@ -24,35 +24,184 @@ */ #include "TypeConversionFunctions.h" +#include "MeshBackendBase.h" +#include "TcpIpMeshBackend.h" +#include "EspnowMeshBackend.h" +#include "TypeConversion.h" -String uint64ToString(uint64_t number, byte base) +using namespace experimental::TypeConversion; + +namespace MeshTypeConversionFunctions { - assert(2 <= base && base <= 36); + String uint64ToString(uint64_t number, const byte base) + { + assert(2 <= base && base <= 36); + + String result; + + if(base == 16) + { + do { + result += (char)pgm_read_byte(base36Chars + number % base); + number >>= 4; // We could write number /= 16; and the compiler would optimize it to a shift, but the explicit shift notation makes it clearer where the speed-up comes from. + } while ( number ); + } + else + { + do { + result += (char)pgm_read_byte(base36Chars + number % base); + number /= base; + } while ( number ); + } + + std::reverse( result.begin(), result.end() ); + + return result; + } - String result = ""; - - while(number > 0) + uint64_t stringToUint64(const String &string, const byte base) { - result = String((uint32_t)(number % base), base) + result; - number /= base; + assert(2 <= base && base <= 36); + + uint64_t result = 0; + + if(base == 16) + { + for(uint32_t i = 0; i < string.length(); ++i) + { + result <<= 4; // We could write result *= 16; and the compiler would optimize it to a shift, but the explicit shift notation makes it clearer where the speed-up comes from. + result += pgm_read_byte(base36CharValues + string.charAt(i) - '0'); + } + } + else + { + for(uint32_t i = 0; i < string.length(); ++i) + { + result *= base; + result += pgm_read_byte(base36CharValues + string.charAt(i) - '0'); + } + } + + return result; } - return (result == "" ? "0" : result); -} - -uint64_t stringToUint64(const String &string, byte base) -{ - assert(2 <= base && base <= 36); + String uint8ArrayToHexString(const uint8_t *uint8Array, const uint32_t arrayLength) + { + return experimental::TypeConversion::uint8ArrayToHexString(uint8Array, arrayLength); + } - uint64_t result = 0; - - char currentCharacter[1]; - for(uint32_t i = 0; i < string.length(); i++) + uint8_t *hexStringToUint8Array(const String &hexString, uint8_t *uint8Array, const uint32_t arrayLength) + { + return experimental::TypeConversion::hexStringToUint8Array(hexString, uint8Array, arrayLength); + } + + String uint8ArrayToMultiString(uint8_t *uint8Array, const uint32_t arrayLength) + { + String multiString; + if(!multiString.reserve(arrayLength)) + return emptyString; + + // Ensure we have a NULL terminated character array so the String() constructor knows where to stop. + char finalChar = uint8Array[arrayLength - 1]; + uint8Array[arrayLength - 1] = 0; + + multiString += (char *)(uint8Array); + while(multiString.length() < arrayLength - 1) + { + multiString += (char)0; // String construction only stops for null values, so we need to add those manually. + multiString += (char *)(uint8Array + multiString.length()); + } + + multiString += finalChar; + uint8Array[arrayLength - 1] = finalChar; + + return multiString; + } + + String bufferedUint8ArrayToMultiString(const uint8_t *uint8Array, const uint32_t arrayLength) + { + String multiString; + if(!multiString.reserve(arrayLength)) + return emptyString; + + // Ensure we have a NULL terminated character array so the String() constructor knows where to stop. + uint8_t bufferedData[arrayLength + 1]; + std::copy_n(uint8Array, arrayLength, bufferedData); + bufferedData[arrayLength] = 0; + + multiString += (char *)(bufferedData); + while(multiString.length() < arrayLength) + { + multiString += (char)0; // String construction only stops for null values, so we need to add those manually. + multiString += (char *)(bufferedData + multiString.length()); + } + + return multiString; + } + + String macToString(const uint8_t *mac) + { + return MeshTypeConversionFunctions::uint8ArrayToHexString(mac, 6); + } + + uint8_t *stringToMac(const String &macString, uint8_t *macArray) + { + return MeshTypeConversionFunctions::hexStringToUint8Array(macString, macArray, 6); + } + + uint64_t macToUint64(const uint8_t *macArray) + { + uint64_t result = (uint64_t)macArray[0] << 40 | (uint64_t)macArray[1] << 32 | (uint64_t)macArray[2] << 24 | (uint64_t)macArray[3] << 16 | (uint64_t)macArray[4] << 8 | (uint64_t)macArray[5]; + return result; + } + + uint8_t *uint64ToMac(const uint64_t macValue, uint8_t *macArray) + { + assert(macValue <= 0xFFFFFFFFFFFF); // Overflow will occur if value can't fit within 6 bytes + + macArray[5] = macValue; + macArray[4] = macValue >> 8; + macArray[3] = macValue >> 16; + macArray[2] = macValue >> 24; + macArray[1] = macValue >> 32; + macArray[0] = macValue >> 40; + + return macArray; + } + + uint8_t *uint64ToUint8Array(const uint64_t value, uint8_t *resultArray) + { + return uint64ToUint8ArrayBE(value, resultArray); + } + + uint64_t uint8ArrayToUint64(const uint8_t *inputArray) + { + return uint8ArrayToUint64BE(inputArray); + } + + /** + * Helper function for meshBackendCast. + */ + template + T attemptPointerCast(MeshBackendBase *meshBackendBaseInstance, MeshBackendType resultClassType) + { + if(meshBackendBaseInstance && meshBackendBaseInstance->getClassType() == resultClassType) + { + return static_cast(meshBackendBaseInstance); + } + + return nullptr; + } + + template <> + EspnowMeshBackend *meshBackendCast(MeshBackendBase *meshBackendBaseInstance) { - result *= base; - currentCharacter[0] = string.charAt(i); - result += strtoul(currentCharacter, NULL, base); + return attemptPointerCast(meshBackendBaseInstance, MeshBackendType::ESP_NOW); } - return result; + template <> + TcpIpMeshBackend *meshBackendCast(MeshBackendBase *meshBackendBaseInstance) + { + return attemptPointerCast(meshBackendBaseInstance, MeshBackendType::TCP_IP); + } } diff --git a/libraries/ESP8266WiFiMesh/src/TypeConversionFunctions.h b/libraries/ESP8266WiFiMesh/src/TypeConversionFunctions.h index 5d42e414cc..c8cc72d1ac 100644 --- a/libraries/ESP8266WiFiMesh/src/TypeConversionFunctions.h +++ b/libraries/ESP8266WiFiMesh/src/TypeConversionFunctions.h @@ -1,6 +1,6 @@ /* * TypeConversionFunctions - * Copyright (C) 2018 Anders Löfgren + * Copyright (C) 2018-2019 Anders Löfgren * * License (MIT license): * @@ -29,22 +29,155 @@ #include #include -/** - * Note that using a base higher than 16 increases likelihood of randomly generating SSID strings containing controversial words. - * - * @param number The number to convert to a string with radix "base". - * @param base The radix to convert "number" into. Must be between 2 and 36. - * @returns A string of "number" encoded in radix "base". - */ -String uint64ToString(uint64_t number, byte base = 16); +class MeshBackendBase; +class TcpIpMeshBackend; +class EspnowMeshBackend; -/** - * Note that using a base higher than 16 increases likelihood of randomly generating SSID strings containing controversial words. - * - * @param string The string to convert to uint64_t. String must use radix "base". - * @param base The radix of "string". Must be between 2 and 36. - * @returns A uint64_t of the string, using radix "base" during decoding. - */ -uint64_t stringToUint64(const String &string, byte base = 16); +namespace MeshTypeConversionFunctions +{ + /** + * Note that using base 10 instead of 16 increases conversion time by roughly a factor of 5, due to unfavourable 64-bit arithmetic. + * Note that using a base higher than 16 increases likelihood of randomly generating SSID strings containing controversial words. + * + * @param number The number to convert to a string with radix "base". + * @param base The radix to convert "number" into. Must be between 2 and 36. + * @return A string of "number" encoded in radix "base". + */ + String uint64ToString(uint64_t number, const uint8_t base = 16); + + /** + * Note that using base 10 instead of 16 increases conversion time by roughly a factor of 2, due to unfavourable 64-bit arithmetic. + * Note that using a base higher than 16 increases likelihood of randomly generating SSID strings containing controversial words. + * + * @param string The string to convert to uint64_t. String must use radix "base". + * @param base The radix of "string". Must be between 2 and 36. + * @return A uint64_t of the string, using radix "base" during decoding. + */ + uint64_t stringToUint64(const String &string, const uint8_t base = 16); + + /** + * Convert the contents of a uint8_t array to a String in HEX format. The resulting String starts from index 0 of the array. + * All array elements will be padded with zeroes to ensure they are converted to 2 String characters each. + * + * @param uint8Array The array to make into a HEX String. + * @param arrayLength The size of uint8Array, in bytes. + * @return Normally a String containing the HEX representation of the uint8Array. An empty String if the memory allocation for the String failed. + */ + String uint8ArrayToHexString(const uint8_t *uint8Array, const uint32_t arrayLength); + + /** + * Convert the contents of a String in HEX format to a uint8_t array. Index 0 of the array will represent the start of the String. + * There must be 2 String characters for each array element. Use padding with zeroes where required. + * + * @param hexString The HEX String to convert to a uint8_t array. Must contain at least 2*arrayLength characters. + * @param uint8Array The array to fill with the contents of the hexString. + * @param arrayLength The number of bytes to fill in uint8Array. + * @return A pointer to the uint8Array. + */ + uint8_t *hexStringToUint8Array(const String &hexString, uint8_t *uint8Array, const uint32_t arrayLength); + + /** + * Stores the exact values of uint8Array in a String, even null values. + * Note that Strings containing null values will look like several separate Strings to methods that rely on null values to find the String end, such as String::substring. + * In these cases, it may be helpful to use String::c_str() or String::begin() to access the String data buffer directly instead. + * + * The unbuffered version temporarily edits uint8Array during execution, but restores the array to its original state when returning in a controlled manner. + * + * @param uint8Array The array to make into a multiString. + * @param arrayLength The size of uint8Array, in bytes. + * @return Normally a String containing the same data as the uint8Array. An empty String if the memory allocation for the String failed. + */ + String uint8ArrayToMultiString(uint8_t *uint8Array, const uint32_t arrayLength); + + /** + * Stores the exact values of uint8Array in a String, even null values. + * Note that Strings containing null values will look like several separate Strings to methods that rely on null values to find the String end, such as String::substring. + * In these cases, it may be helpful to use String::c_str() or String::begin() to access the String data buffer directly instead. + * + * The buffered version is slower and uses more memory than the unbuffered version, but can operate on const arrays. + * + * @param uint8Array The array to make into a multiString. + * @param arrayLength The size of uint8Array, in bytes. + * @return Normally a String containing the same data as the uint8Array. An empty String if the memory allocation for the String failed. + */ + String bufferedUint8ArrayToMultiString(const uint8_t *uint8Array, const 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. + * @return 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. + * @return 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. + * @return 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 (LSB) in a uint8_t array. Assumes index 0 of the array should contain MSB. + * + * @param macValue The uint64_t value to convert to a mac array. Value must fit within 6 bytes. + * @param macArray A uint8_t array that will hold the mac address once the function returns. Should have a size of at least 6 bytes. + * @return The macArray. + */ + uint8_t *uint64ToMac(const uint64_t macValue, uint8_t *macArray); + + /** + * Takes a uint64_t value and stores the bits in a uint8_t array. Assumes index 0 of the array should contain MSB (big endian). + * + * @param value The uint64_t value to convert to a uint8_t array. + * @param resultArray A uint8_t array that will hold the result once the function returns. Should have a size of at least 8 bytes. + * @return The resultArray. + */ + uint8_t *uint64ToUint8Array(const uint64_t value, uint8_t *resultArray); + + /** + * Takes a uint8_t array and converts the first 8 (lowest index) elements to a uint64_t. Assumes index 0 of the array contains MSB (big endian). + * + * @param inputArray A uint8_t array containing the data to convert to a uint64_t. Should have a size of at least 8 bytes. + * @return A uint64_t representation of the first 8 bytes of the array. + */ + uint64_t uint8ArrayToUint64(const uint8_t *inputArray); + + /** + * 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. + * @return 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); +} + +#ifndef ESP8266WIFIMESH_DISABLE_COMPATIBILITY +using namespace MeshTypeConversionFunctions; // Required to retain backwards compatibility. TODO: Remove in core release 3.0.0 +#endif #endif diff --git a/libraries/ESP8266WiFiMesh/src/UtilityFunctions.cpp b/libraries/ESP8266WiFiMesh/src/UtilityFunctions.cpp new file mode 100644 index 0000000000..3ef2f8e6c6 --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/UtilityFunctions.cpp @@ -0,0 +1,62 @@ +/* + * 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 +#include + +namespace MeshUtilityFunctions +{ + 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)ESP.random() << 32) | (uint64_t)ESP.random()); + } + + template + T *getMapValue(std::map &mapIn, const uint64_t keyIn) + { + typename std::map::iterator mapIterator = mapIn.find(keyIn); + + if(mapIterator != mapIn.end()) + { + return &mapIterator->second; + } + + return nullptr; + } +} diff --git a/libraries/ESP8266WiFiMesh/src/UtilityFunctions.h b/libraries/ESP8266WiFiMesh/src/UtilityFunctions.h new file mode 100644 index 0000000000..b68947d3af --- /dev/null +++ b/libraries/ESP8266WiFiMesh/src/UtilityFunctions.h @@ -0,0 +1,42 @@ +/* + * 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 +#include + +namespace MeshUtilityFunctions +{ + bool macEqual(const uint8_t *macOne, const uint8_t *macTwo); + + uint64_t randomUint64(); + + template + T *getMapValue(std::map &mapIn, const uint64_t keyIn); +} + +#endif diff --git a/libraries/ESP8266WiFiMesh/src/UtilityMethods.cpp b/libraries/ESP8266WiFiMesh/src/UtilityMethods.cpp deleted file mode 100644 index 795eacb638..0000000000 --- a/libraries/ESP8266WiFiMesh/src/UtilityMethods.cpp +++ /dev/null @@ -1,81 +0,0 @@ -/* - * TransmissionResult - * Copyright (C) 2018 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 "TypeConversionFunctions.h" -#include "ESP8266WiFiMesh.h" - -void ESP8266WiFiMesh::verboseModePrint(const String &stringToPrint, bool newline) -{ - if(_verboseMode) - { - if(newline) - Serial.println(stringToPrint); - else - Serial.print(stringToPrint); - } -} - -/** - * 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() -{ - // 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++) - { - 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); - } - } -} - -/** - * 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; - } - - return true; -}