|
| 1 | +#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. |
| 2 | + |
| 3 | +#include <ESP8266WiFi.h> |
| 4 | +#include <TcpIpMeshBackend.h> |
| 5 | +#include <EspnowMeshBackend.h> |
| 6 | +#include <TypeConversionFunctions.h> |
| 7 | +#include <assert.h> |
| 8 | + |
| 9 | +namespace TypeCast = MeshTypeConversionFunctions; |
| 10 | + |
| 11 | +/** |
| 12 | + NOTE: Although we could define the strings below as normal String variables, |
| 13 | + here we are using PROGMEM combined with the FPSTR() macro (and also just the F() macro further down in the file). |
| 14 | + The reason is that this approach will place the strings in flash memory which will help save RAM during program execution. |
| 15 | + Reading strings from flash will be slower than reading them from RAM, |
| 16 | + but this will be a negligible difference when printing them to Serial. |
| 17 | +
|
| 18 | + More on F(), FPSTR() and PROGMEM: |
| 19 | + https://github.com/esp8266/Arduino/issues/1143 |
| 20 | + https://arduino-esp8266.readthedocs.io/en/latest/PROGMEM.html |
| 21 | +*/ |
| 22 | +constexpr char exampleMeshName[] PROGMEM = "MeshNode_"; |
| 23 | +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. |
| 24 | + |
| 25 | +unsigned int requestNumber = 0; |
| 26 | +unsigned int responseNumber = 0; |
| 27 | + |
| 28 | +String manageRequest(const String &request, MeshBackendBase &meshInstance); |
| 29 | +TransmissionStatusType manageResponse(const String &response, MeshBackendBase &meshInstance); |
| 30 | +void networkFilter(int numberOfNetworks, MeshBackendBase &meshInstance); |
| 31 | + |
| 32 | +/* Create the mesh node object */ |
| 33 | +TcpIpMeshBackend tcpIpNode = TcpIpMeshBackend(manageRequest, manageResponse, networkFilter, FPSTR(exampleWiFiPassword), FPSTR(exampleMeshName), TypeCast::uint64ToString(ESP.getChipId()), true); |
| 34 | + |
| 35 | +/** |
| 36 | + Callback for when other nodes send you a request |
| 37 | +
|
| 38 | + @param request The request string received from another node in the mesh |
| 39 | + @param meshInstance The MeshBackendBase instance that called the function. |
| 40 | + @return The string to send back to the other node. For ESP-NOW, return an empy string ("") if no response should be sent. |
| 41 | +*/ |
| 42 | +String manageRequest(const String &request, MeshBackendBase &meshInstance) { |
| 43 | + // To get the actual class of the polymorphic meshInstance, do as follows (meshBackendCast replaces dynamic_cast since RTTI is disabled) |
| 44 | + if (EspnowMeshBackend *espnowInstance = TypeCast::meshBackendCast<EspnowMeshBackend *>(&meshInstance)) { |
| 45 | + String transmissionEncrypted = espnowInstance->receivedEncryptedTransmission() ? F(", Encrypted transmission") : F(", Unencrypted transmission"); |
| 46 | + Serial.print(String(F("ESP-NOW (")) + espnowInstance->getSenderMac() + transmissionEncrypted + F("): ")); |
| 47 | + } else if (TcpIpMeshBackend *tcpIpInstance = TypeCast::meshBackendCast<TcpIpMeshBackend *>(&meshInstance)) { |
| 48 | + (void)tcpIpInstance; // This is useful to remove a "unused parameter" compiler warning. Does nothing else. |
| 49 | + Serial.print(F("TCP/IP: ")); |
| 50 | + } else { |
| 51 | + Serial.print(F("UNKNOWN!: ")); |
| 52 | + } |
| 53 | + |
| 54 | + /* Print out received message */ |
| 55 | + // Only show first 100 characters because printing a large String takes a lot of time, which is a bad thing for a callback function. |
| 56 | + // If you need to print the whole String it is better to store it and print it in the loop() later. |
| 57 | + // Note that request.substring will not work as expected if the String contains null values as data. |
| 58 | + Serial.print(F("Request received: ")); |
| 59 | + Serial.println(request.substring(0, 100)); |
| 60 | + |
| 61 | + /* return a string to send back */ |
| 62 | + return (String(F("Hello world response #")) + String(responseNumber++) + F(" from ") + meshInstance.getMeshName() + meshInstance.getNodeID() + F(" with AP MAC ") + WiFi.softAPmacAddress() + String('.')); |
| 63 | +} |
| 64 | + |
| 65 | +/** |
| 66 | + Callback for when you get a response from other nodes |
| 67 | +
|
| 68 | + @param response The response string received from another node in the mesh |
| 69 | + @param meshInstance The MeshBackendBase instance that called the function. |
| 70 | + @return The status code resulting from the response, as an int |
| 71 | +*/ |
| 72 | +TransmissionStatusType manageResponse(const String &response, MeshBackendBase &meshInstance) { |
| 73 | + TransmissionStatusType statusCode = TransmissionStatusType::TRANSMISSION_COMPLETE; |
| 74 | + |
| 75 | + // To get the actual class of the polymorphic meshInstance, do as follows (meshBackendCast replaces dynamic_cast since RTTI is disabled) |
| 76 | + if (EspnowMeshBackend *espnowInstance = TypeCast::meshBackendCast<EspnowMeshBackend *>(&meshInstance)) { |
| 77 | + String transmissionEncrypted = espnowInstance->receivedEncryptedTransmission() ? F(", Encrypted transmission") : F(", Unencrypted transmission"); |
| 78 | + Serial.print(String(F("ESP-NOW (")) + espnowInstance->getSenderMac() + transmissionEncrypted + F("): ")); |
| 79 | + } else if (TcpIpMeshBackend *tcpIpInstance = TypeCast::meshBackendCast<TcpIpMeshBackend *>(&meshInstance)) { |
| 80 | + Serial.print(F("TCP/IP: ")); |
| 81 | + |
| 82 | + // Getting the sent message like this will work as long as ONLY(!) TCP/IP is used. |
| 83 | + // With TCP/IP the response will follow immediately after the request, so the stored message will not have changed. |
| 84 | + // 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. |
| 85 | + // So for ESP-NOW, adding unique identifiers in the response and request is required to associate a response with a request. |
| 86 | + Serial.print(F("Request sent: ")); |
| 87 | + Serial.println(tcpIpInstance->getCurrentMessage().substring(0, 100)); |
| 88 | + } else { |
| 89 | + Serial.print(F("UNKNOWN!: ")); |
| 90 | + } |
| 91 | + |
| 92 | + /* Print out received message */ |
| 93 | + // Only show first 100 characters because printing a large String takes a lot of time, which is a bad thing for a callback function. |
| 94 | + // If you need to print the whole String it is better to store it and print it in the loop() later. |
| 95 | + // Note that response.substring will not work as expected if the String contains null values as data. |
| 96 | + Serial.print(F("Response received: ")); |
| 97 | + Serial.println(response.substring(0, 100)); |
| 98 | + |
| 99 | + return statusCode; |
| 100 | +} |
| 101 | + |
| 102 | +/** |
| 103 | + Callback used to decide which networks to connect to once a WiFi scan has been completed. |
| 104 | +
|
| 105 | + @param numberOfNetworks The number of networks found in the WiFi scan. |
| 106 | + @param meshInstance The MeshBackendBase instance that called the function. |
| 107 | +*/ |
| 108 | +void networkFilter(int numberOfNetworks, MeshBackendBase &meshInstance) { |
| 109 | + // Note that the network index of a given node may change whenever a new scan is done. |
| 110 | + for (int networkIndex = 0; networkIndex < numberOfNetworks; ++networkIndex) { |
| 111 | + String currentSSID = WiFi.SSID(networkIndex); |
| 112 | + int meshNameIndex = currentSSID.indexOf(meshInstance.getMeshName()); |
| 113 | + |
| 114 | + /* Connect to any _suitable_ APs which contain meshInstance.getMeshName() */ |
| 115 | + if (meshNameIndex >= 0) { |
| 116 | + uint64_t targetNodeID = TypeCast::stringToUint64(currentSSID.substring(meshNameIndex + meshInstance.getMeshName().length())); |
| 117 | + |
| 118 | + if (targetNodeID < TypeCast::stringToUint64(meshInstance.getNodeID())) { |
| 119 | + if (EspnowMeshBackend *espnowInstance = TypeCast::meshBackendCast<EspnowMeshBackend *>(&meshInstance)) { |
| 120 | + espnowInstance->connectionQueue().emplace_back(networkIndex); |
| 121 | + } else if (TcpIpMeshBackend *tcpIpInstance = TypeCast::meshBackendCast<TcpIpMeshBackend *>(&meshInstance)) { |
| 122 | + tcpIpInstance->connectionQueue().emplace_back(networkIndex); |
| 123 | + } else { |
| 124 | + Serial.println(F("Invalid mesh backend!")); |
| 125 | + } |
| 126 | + } |
| 127 | + } |
| 128 | + } |
| 129 | +} |
| 130 | + |
| 131 | +/** |
| 132 | + Once passed to the setTransmissionOutcomesUpdateHook method of the TCP/IP backend, |
| 133 | + this function will be called after each update of the latestTransmissionOutcomes vector during attemptTransmission. |
| 134 | + (which happens after each individual transmission has finished) |
| 135 | +
|
| 136 | + Example use cases is modifying getMessage() between transmissions, or aborting attemptTransmission before all nodes in the connectionQueue have been contacted. |
| 137 | +
|
| 138 | + @param meshInstance The MeshBackendBase instance that called the function. |
| 139 | +
|
| 140 | + @return True if attemptTransmission should continue with the next entry in the connectionQueue. False if attemptTransmission should stop. |
| 141 | +*/ |
| 142 | +bool exampleTransmissionOutcomesUpdateHook(MeshBackendBase &meshInstance) { |
| 143 | + // The default hook only returns true and does nothing else. |
| 144 | + |
| 145 | + if (TcpIpMeshBackend *tcpIpInstance = TypeCast::meshBackendCast<TcpIpMeshBackend *>(&meshInstance)) { |
| 146 | + if (tcpIpInstance->latestTransmissionOutcomes().back().transmissionStatus() == TransmissionStatusType::TRANSMISSION_COMPLETE) { |
| 147 | + // Our last request got a response, so time to create a new request. |
| 148 | + meshInstance.setMessage(String(F("Hello world request #")) + String(++requestNumber) + F(" from ") |
| 149 | + + meshInstance.getMeshName() + meshInstance.getNodeID() + String('.')); |
| 150 | + } |
| 151 | + } else { |
| 152 | + Serial.println(F("Invalid mesh backend!")); |
| 153 | + } |
| 154 | + |
| 155 | + return true; |
| 156 | +} |
| 157 | + |
| 158 | +void setup() { |
| 159 | + // Prevents the flash memory from being worn out, see: https://github.com/esp8266/Arduino/issues/1054 . |
| 160 | + // 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. |
| 161 | + WiFi.persistent(false); |
| 162 | + |
| 163 | + Serial.begin(115200); |
| 164 | + |
| 165 | + Serial.println(); |
| 166 | + Serial.println(); |
| 167 | + |
| 168 | + Serial.println(F("Note that this library can use static IP:s for the nodes to speed up connection times.\n" |
| 169 | + "Use the setStaticIP method as shown in this example to enable this.\n" |
| 170 | + "Ensure that nodes connecting to the same AP have distinct static IP:s.\n" |
| 171 | + "Also, remember to change the default mesh network password!\n\n")); |
| 172 | + |
| 173 | + Serial.println(F("Setting up mesh node...")); |
| 174 | + |
| 175 | + /* Initialise the mesh node */ |
| 176 | + tcpIpNode.begin(); |
| 177 | + tcpIpNode.activateAP(); // Each AP requires a separate server port. |
| 178 | + tcpIpNode.setStaticIP(IPAddress(192, 168, 4, 22)); // Activate static IP mode to speed up connection times. |
| 179 | + |
| 180 | + // Storing our message in the TcpIpMeshBackend instance is not required, but can be useful for organizing code, especially when using many TcpIpMeshBackend instances. |
| 181 | + // Note that calling the multi-recipient tcpIpNode.attemptTransmission will replace the stored message with whatever message is transmitted. |
| 182 | + tcpIpNode.setMessage(String(F("Hello world request #")) + String(requestNumber) + F(" from ") + tcpIpNode.getMeshName() + tcpIpNode.getNodeID() + String('.')); |
| 183 | + |
| 184 | + tcpIpNode.setTransmissionOutcomesUpdateHook(exampleTransmissionOutcomesUpdateHook); |
| 185 | +} |
| 186 | + |
| 187 | +int32_t timeOfLastScan = -10000; |
| 188 | +void loop() { |
| 189 | + if (millis() - timeOfLastScan > 3000 // Give other nodes some time to connect between data transfers. |
| 190 | + || (WiFi.status() != WL_CONNECTED && millis() - timeOfLastScan > 2000)) { // Scan for networks with two second intervals when not already connected. |
| 191 | + |
| 192 | + // attemptTransmission(message, scan, scanAllWiFiChannels, concludingDisconnect, initialDisconnect = false) |
| 193 | + tcpIpNode.attemptTransmission(tcpIpNode.getMessage(), true, false, false); |
| 194 | + timeOfLastScan = millis(); |
| 195 | + |
| 196 | + // One way to check how attemptTransmission worked out |
| 197 | + if (tcpIpNode.latestTransmissionSuccessful()) { |
| 198 | + Serial.println(F("Transmission successful.")); |
| 199 | + } |
| 200 | + |
| 201 | + // Another way to check how attemptTransmission worked out |
| 202 | + if (tcpIpNode.latestTransmissionOutcomes().empty()) { |
| 203 | + Serial.println(F("No mesh AP found.")); |
| 204 | + } else { |
| 205 | + for (TransmissionOutcome &transmissionOutcome : tcpIpNode.latestTransmissionOutcomes()) { |
| 206 | + if (transmissionOutcome.transmissionStatus() == TransmissionStatusType::TRANSMISSION_FAILED) { |
| 207 | + Serial.println(String(F("Transmission failed to mesh AP ")) + transmissionOutcome.SSID()); |
| 208 | + } else if (transmissionOutcome.transmissionStatus() == TransmissionStatusType::CONNECTION_FAILED) { |
| 209 | + Serial.println(String(F("Connection failed to mesh AP ")) + transmissionOutcome.SSID()); |
| 210 | + } else if (transmissionOutcome.transmissionStatus() == TransmissionStatusType::TRANSMISSION_COMPLETE) { |
| 211 | + // No need to do anything, transmission was successful. |
| 212 | + } else { |
| 213 | + Serial.println(String(F("Invalid transmission status for ")) + transmissionOutcome.SSID() + String('!')); |
| 214 | + assert(F("Invalid transmission status returned from responseHandler!") && false); |
| 215 | + } |
| 216 | + } |
| 217 | + } |
| 218 | + Serial.println(); |
| 219 | + } else { |
| 220 | + /* Accept any incoming connections */ |
| 221 | + tcpIpNode.acceptRequests(); |
| 222 | + } |
| 223 | +} |
0 commit comments