├── README.md └── wifi_temperature_nodes.ino /README.md: -------------------------------------------------------------------------------- 1 | # esp8266_arduino_temperature_nodes 2 | Allows a NodeMCU ESP8266 module to be used to capture/display/serve temperature information - uses OLED display and DHT or Dallas type temp sensors 3 | 4 | NOTE: I am posting this here for my own as well as for others' benefit. It is my first github public repository. It is also my first foray into the fun and exciting world of ESP8266. Like most of you, I have a job and a family so I may not have a lot of spare time to update this. Having said that, I hope others find some use for this code. Note that I probably snagged bits and pieces of functions from other public places. I have tried, best as I can, to go over and provide references/links in the code to the places where I got the info from. Some similarities to other code might be coincidental, but if it looks too familiar, it's possible I missed a place where I should have provided a link so let me know and I'll fix it. 5 | 6 | This piece of code does potentially interesting things like: 7 | * Accepts and processes HTTP requests. 8 | * Makes HTTP requests to other nodes. 9 | * Makes UDP requests to and parses data from NPT time servers. 10 | * Reads DHT11, DHT22 and Dallas DS temperature sensors. 11 | * Displays data to a 128X64 OLED display (optional) 12 | * Displays data to an ILI9341 TFT touch display optional, and no touch functionality implemented yet) 13 | * Uses ArduinoOTA so that it can be code updated wirelessly. 14 | * Sets up static IP address for ESP8266 node. 15 | 16 | Please read through all the code, especially the comments, for information on how to set up some of the functionality and how to connect the hardware. It is important to set up the proper IP. Afterwards, if you start programming the devices wirelessly, make sure you're set up for the right IP in code define as well as the remote PORT you're programming to. 17 | 18 | This has been written for the ESP8266 Arduino environment. 19 | Note that some variants of the libraries need to be ESP8266 specific. I have tried to add URLs to thos libraries. For sure you also have to set up your Arduino IDE menu -> Preferences-> Additional Board Managers URLs to "http://arduino.esp8266.com/stable/package_esp8266com_index.json" so that you can code for ESP8266. The Board I selected after that is "NodeMCU 1.0 (ESP=12E Module)". Also note that I've had issues with some NODEMCU modules not programming at any baud rate higher than 115200 but others (that looked identical) from another seller worked up to 921600. Once you get OTA working and you can program wirelessly, baud rate is not an issue. Currently I have also started seeing the module stop waiting for SSID connection after an program upload, and even resetting won't get it to move past that point. The only option is to disconnect/reconnect the USB power. 20 | 21 | A little info on hardware: I am using an I2C OLED 128x64 OLED display with an SSD1306 chip. I have also added support for an ILI9341 TFT touch screen display. Look at the comments to enable/disable display options and to find out how to hook up the hardware. Both flavors of display can be powered from the NODEMCU's 3.3V pins. Currently, you can have both types of display connected at the same time and the display functions will just mirror the output to both of them. Note that the code can be used w/o any display since you can see your temperature data through a browser. Just point the browser to the appropriate IP and port 8484. To get a simple data string that's easy to parse, you can add a "/data" to your URL. You can even remote reboot the module by sending a "/reset" to the URL. 22 | 23 | Good luck to those who wish to replicate this project. Let me know if you run into any gotchas, especially if you solve them, so I can add to the documentaion for anybody else that may follow in your footsteps. 24 | -------------------------------------------------------------------------------- /wifi_temperature_nodes.ino: -------------------------------------------------------------------------------- 1 | // This code can be found at: https://github.com/horack/esp8266_arduino_temperature_nodes 2 | // It has been build with snippets of code from many places, I've tried to provide links wherever I remembered the source... 3 | 4 | #include // using version 1.0.2 (1.0.1 does not have esp8266 support, https://github.com/adafruit/Adafruit_ILI9341 ) 5 | #include // using version 1.1.5 6 | #include // needed for TFT display 7 | #include // (for TF, find other fonts in Adafruit_GFX/Fonts folder) 8 | // These font settings have to match to the font you've included above 9 | #define THE_FONT &FreeSerifBold18pt7b 10 | #define FONT_HEIGHT FreeSerifBold18pt7b.yAdvance 11 | #define FONT_OFFSET 10 12 | 13 | // Both of these are included when you pick the Time library in Arduino IDE library manager 14 | #include 15 | #include 16 | // These come from ESP8266 libs I get when I use http://arduino.esp8266.com/stable/package_esp8266com_index.json in Arduino Preferences for Additional Board Manager URLs 17 | // to set up Arduino for ESP8266 development. 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | // This is an ESP8266 specific SSD1306 library (which shows up as "ESP8266 OLED Driver for SSD1306 display" in Library Manager) 24 | #include // this variant comes from https://github.com/adafruit/Adafruit-SSD1331-OLED-Driver-Library-for-Arduino 25 | 26 | // Include appropriate temperature sensor lib here 27 | #include // Adafruit DHT (NOT Unified DHT) library ( https://github.com/adafruit/DHT-sensor-library ) 28 | #include // ( https://github.com/milesburton/Arduino-Temperature-Control-Library ) 29 | #include 30 | #define DS18 18 // pseudo value for Dallas temperature sensors 31 | 32 | // *************************************************************************************************************************************************************** 33 | // *************************************************************************************************************************************************************** 34 | // These are defines you'll want to change depending on which node you're programming, also look for AP_SSID and AP_PASSWORD 35 | // More info on these values and others further down, readl all comments (even if some MIGHT be out of date) 36 | #define IP_LAST_OCTET 40 // This defines this node's static IP, read on for more info (also find array NODEMCU_NODES) 37 | #define TEMP_TYPE DHT22 // set this to DS18 for Dallas 3 pin sensors, or DHT22 or DTH11 for the DHT 4 pin sensors 38 | #define USE_OLED // Uncomment if you're using an SSD1306 type 128x64 OLED display (currently set up for I2C version) 39 | //#define USE_TFT // Uncomment if you're using an ILI9341 type TFT display (you can have both OLED and TFT hooked up at the same time, display functions currently will mirror the data) 40 | // find Graphics section below to get pinout information for OLED and/or TFT module 41 | // *************************************************************************************************************************************************************** 42 | // When this code runs, you can query your module over http at: http://192.168.1.40:8484 (the ip may be different depending on your changes and network) 43 | // you can also get a more machine-readable version of the data by browsing to: http://192.168.1.40:8484/data 44 | // *************************************************************************************************************************************************************** 45 | 46 | // Apparently the Dn NodeMCU pins are no longer defined, so here they are 47 | #ifndef D1 48 | #define D0 16 // cannot use for interrupts 49 | #define D1 5 50 | #define D2 4 51 | #define D3 0 52 | #define D4 2 53 | #define D5 14 // this is also SCLK 54 | #define D6 12 // this is also MISO 55 | #define D7 13 // this is also MOSI 56 | #define D8 15 // this is also CS 57 | #define D9 3 58 | #define D10 1 59 | #endif 60 | 61 | // This program will assign static IPs to your module(s). In my case my internal IPs are configured by the wifi router as 192.168.1.XXX 62 | // Note that you should make sure that the NODEMCU_NODES array (further down in code) does contain an entry for whatever value you set IP_LAST_OCTET to 63 | // ------------------------------------------------------------------------------------------------------------------------------------------------- 64 | // NOTE: Always make sure you set up IP_LAST_OCTET correctly when you burn OTA (wirelessly), because if you're selecting one specific device over-the-air, but will be burning a different IP to it, it can cause you confusion, 65 | // especially if there is already another node online with that same IP. 66 | // -------------------------------------------------------------------------------------------------------------------------------------------------- 67 | 68 | // NOTE: this is a path to my own accesspoint.h file, that contains my router's SSID and password, I am keeping this private. 69 | // You can 70 | // EITHER create your own file and change the #include path below to point to it (due to Arduino IDE it must be an absolute path, and 71 | // copy the accessinfo.h contents listed between the comment lines and change values as needed) 72 | // OR you can just comment out the #include and just plug in your SSID and password values directly into the AP_SSID and AP_PASSWORD defines 73 | // I include this accesspoint.h in multiple projects, so I prefer having this common file. 74 | #include "D:/projects/Arduino/ESP8266/accesspoint.h" 75 | // start of accessinfo.h contents ---------------------------- 76 | #ifndef AP_INFO_H 77 | #define AP_INFO_H 78 | #define AP_SSID "your_wifi_router_SSID_here" 79 | #define AP_PASSWORD "your_accesspoint_password" 80 | #endif // AP_INFO_H 81 | // end of accessinfo.h contents ------------------------------ 82 | #define PORT 8484 // This is the port that the node will listen on 83 | WiFiServer server(PORT); 84 | 85 | // Here's info that needs to match you local WiFi configuration. Make sure the info is correct for your set up. 86 | // You can probably get this info by examining your computer's wifi network configuration (in Windows type "ipconfig" in a command line window) 87 | // Note that the static IP is based on value of IP_LAST_OCTET 88 | const IPAddress myIp(192, 168, 1, IP_LAST_OCTET); 89 | const IPAddress gateway(192, 168, 1, 1); 90 | const IPAddress subnet(255, 255, 255, 0); 91 | const IPAddress dns1(192, 168, 1, 1); 92 | const IPAddress dns2(192, 168, 1, 1); 93 | 94 | // I am defining a namespace here since it appears to be the easiest way to get structs defined in Arduino without needing separate files 95 | namespace codeus { 96 | // A SLAVE_RESULT contains info received from a slave 97 | typedef struct SLAVE_RESULT { 98 | time_t lastUpdateEpoch; 99 | String name; 100 | String temperature; 101 | String humidity; 102 | }; 103 | 104 | // A NODEMCU_NODE has info specific to a node in this master/slave pseudo network 105 | typedef struct NODEMCU_NODE { 106 | bool isMaster; 107 | byte lastIPOctet; 108 | char* defaultName; 109 | SLAVE_RESULT* result; // this will be populated with the slave's results when the master queries it 110 | }; 111 | 112 | } 113 | // This array is a list of possible NodeMCU nodes I MAY have running, not all of them need to be running and available. 114 | // Note that only one should have the isMaster flag set to true, it will run in master mode and query the other nodes for updates. 115 | // In this particular example, if I have set IP_LAST_OCTET to 41, that means I am compiling for a master node (the "Master LR" node in the list has octet 41 and isMaster true). 116 | // The nodes start out with the defined names in this list for each IP. You can change the names in the list and you can also change a name remotely, over HTTP. 117 | // Make sure this array has an entry for whatever you set IP_LAST_OCTET to at compile time !!!! 118 | // You can add to, or remove from this list and name your modules whatever you want, you can even have duplicate names, but do not duplicate the IP octet values. 119 | // TODO: perhaps save node info in a local "file" in the node's flash 120 | // TODO: change code to allow slave nodes to ping a master and add or remove themselves dynamically 121 | codeus::NODEMCU_NODE NODEMCU_NODES[] = { 122 | {true, 40, "Livingroom", NULL}, 123 | {false, 41, "SW Room", NULL}, // this node is flagged as the master, you can move the 'true' flag to another node, if you want 124 | {false, 42, "Mel", NULL}, 125 | {false, 43, "Cold Room", NULL}, 126 | {false, 44, "Garage", NULL}, 127 | {false, 45, "Outside", NULL} 128 | }; 129 | #define NODEMCU_NODE_COUNT sizeof NODEMCU_NODES / sizeof NODEMCU_NODES[0] 130 | 131 | // general purpose function to locate a node by IP octet in the above list 132 | codeus::NODEMCU_NODE* findNode(byte lastIPOctet) { 133 | codeus::NODEMCU_NODE* result = NULL; 134 | for (int i = 0; i < NODEMCU_NODE_COUNT; i++) { 135 | if (NODEMCU_NODES[i].lastIPOctet == lastIPOctet) { 136 | result = &NODEMCU_NODES[i]; 137 | } 138 | } 139 | return result; 140 | } 141 | 142 | const codeus::NODEMCU_NODE* ThisNode = findNode(IP_LAST_OCTET); // In the code, ThisNode will contain a pointer to this specific node's info (determined by IP_LAST_OCTET at build time) 143 | String nodeName = String(ThisNode->defaultName); 144 | 145 | // time stuff ------------------------------------------------------------------------------------- 146 | bool firstTimeGot = false; 147 | String timeStr = "NO-TIME-SET"; 148 | String dateStr = "NO-DATE-SET"; 149 | // the following 2 ints are just for diagnostic purposes to see how many times we tried to reach NPT and succeeded (and how far) 150 | int nptGets = 0; 151 | int nptAttempts = 0; 152 | 153 | // Temperature sensor stuff ------------------------------------------------------------------------------------- 154 | const long tempInterval = 5000; // interval at which to read sensor 155 | unsigned long previousTempMillis = tempInterval; // will store last temp was read 156 | #define TEMP_PIN D1 // data pin for sensor 157 | 158 | // Initialize temperature sensor 159 | #if TEMP_TYPE == DS18 160 | OneWire oneWire(TEMP_PIN); 161 | DallasTemperature DS18B20(&oneWire); 162 | #else 163 | // NOTE: For working with a faster than ATmega328p 16 MHz Arduino chip, like an ESP8266, 164 | // you need to increase the threshold for cycle counts considered a 1 or 0. 165 | // You can do this by passing a 3rd parameter for this threshold. It's a bit 166 | // of fiddling to find the right value, but in general the faster the CPU the 167 | // higher the value. The default for a 16mhz AVR is a value of 6. For an 168 | // Arduino Due that runs at 84mhz a value of 30 works. 169 | // This is for the ESP8266 processor on ESP-01 170 | DHT dht(TEMP_PIN, TEMP_TYPE, 11); // 11 works fine for ESP8266 171 | #endif 172 | String tempStr = "--.-"; // displayable temperature string 173 | String humStr = "--.-"; // displayable humidity string, for Dallas sensors this will remain as --.- since they don't have humidity sensing 174 | float humidity, temp_f; // Values read from sensor 175 | 176 | 177 | // Graphics ----------------------------------------------------------------------------------------------------------------------------- 178 | #ifdef USE_OLED 179 | // OLED 128x64 displays 180 | // Haven't tested SPI OLED module, so you're on your own 181 | //#define OLED_RESET D0 // RESET 182 | //#define OLED_DC D2 // Data/Command 183 | //#define OLED_CS D8 // Chip select 184 | // SSD1306 display(true, OLED_RESET, OLED_DC, OLED_CS); // FOR SPI 185 | 186 | // Pin connections for I2C OLED 187 | // OLED pin -> NODEMCU pin 188 | // VCC -> any 3.3V NODEMCU pin 189 | // GND -> any NODEMCU GND 190 | // SCL -> D4 (GPIO2) 191 | // SDA -> D2 (GPIO4) 192 | #define OLED_SDA D2 193 | #define OLED_SCL D4 194 | #define OLED_ADDR 0x3C // I2C address for OLED, some might use 3D 195 | SSD1306 display(OLED_ADDR, OLED_SDA, OLED_SCL); // For I2C 196 | #endif //USE_OLED 197 | 198 | #ifdef USE_TFT 199 | // This is for the cheap touch screen ILI9341 based TFT display you find on Amazon and Ebay (less than $10), typically 320x240 200 | // Touch functionality not yet used in this code (I'll add it some day) 201 | // Pin connections for ILI9341 TFT (note that any 3.3V can be shared, same for GND) 202 | // ILI9341 pin -> NODEMCU pin 203 | // VCC -> any NODEMCU 3.3V pin 204 | // GND -> any NODEMCU GND 205 | // D/C -> D3 206 | // CS - > D8 207 | // SDO(MISO) -> D6 208 | // SDI(MOSI) -> D7 209 | // SCK -> D5 210 | // LED -> any 3.3V NODEMCU pin 211 | // RESET -> any 3.3V NODEMCU pin 212 | #define TFT_DC D3 213 | #define TFT_CS D8 214 | Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC); 215 | #define BACKGROUND_COLOR ILI9341_BLACK 216 | #define TOP_TEXT_COLOR ILI9341_YELLOW 217 | #define TEXT_COLOR 0x05FF 218 | #define STATUS_COLOR ILI9341_RED 219 | #define TEXT_SIZE 1 220 | #define ORIENTATION 1 // 2 is upside-down portrait 221 | #endif //USE_TFT 222 | #define ESP_SPI_FREQ 4000000 223 | 224 | // Master node stuff ------------------------------------------------------------------------------------- 225 | #define SLAVE_CHECK_INTERVAL_SEC 60 // interval at which to scan slaves, don't make it too fast, scanning nodes is slow especially if some nodes are not online and responding 226 | time_t slaveCheckNext = 0; 227 | int getsCount = 0; 228 | int bytesCount = 0; 229 | int attemptsCount = 0; 230 | 231 | // INIT functions ------------------------------------------------------------------------------------- 232 | void initDisplay() { 233 | // display stuff 234 | #ifdef USE_TFT 235 | SPI.setFrequency(ESP_SPI_FREQ); 236 | tft.begin(); 237 | tft.setRotation(ORIENTATION); // I flip display because of how I have the board in my setup, you may or may not need/want to flip 238 | tft.fillScreen(BACKGROUND_COLOR); 239 | tft.setFont(THE_FONT); 240 | #endif //USE_TFT 241 | #ifdef USE_OLED 242 | display.init(); 243 | display.flipScreenVertically(); // I flip display because of how I have the board in my setup, you may or may not need/want to flip 244 | #endif //USE_OLED 245 | } 246 | 247 | void connectWifiAccessPoint(String ssid, String password) { 248 | WiFi.reconnect(); 249 | WiFi.mode(WIFI_STA); 250 | WiFi.config(myIp, gateway, subnet, dns1, dns2); 251 | displayAll("Begin SSID:\n" + ssid); 252 | char ssidCC[60]; 253 | char passwordCC[60]; 254 | ssid.toCharArray(ssidCC, (unsigned int)ssid.length() + 1); 255 | password.toCharArray(passwordCC, (unsigned int)password.length() + 1); 256 | WiFi.begin(ssidCC, passwordCC); 257 | displayAll("Wait for SSID:\n" + String(ssid)); 258 | while (WiFi.waitForConnectResult() != WL_CONNECTED) { 259 | displayAll("Connection Failed!\nRebooting..."); 260 | delay(5000); 261 | ESP.restart(); 262 | } 263 | } 264 | 265 | // SETUP function ------------------------------------------------------------------------------------- 266 | void setup() { 267 | Serial.begin(115200); 268 | delay(100); 269 | initDisplay(); 270 | 271 | displayAll("Booting"); 272 | 273 | // Connect to WiFi network 274 | displayAll("Connecting SSID:\n" + String(AP_SSID)); 275 | connectWifiAccessPoint(AP_SSID, AP_PASSWORD); 276 | displayAll("WiFi connected"); 277 | 278 | // ArduinoOTA setup, for the very cool over-the-air wireless code update ability 279 | // Theoretically, once this code is programmed into a nodeMCU over USB, you can afterwards re-program it wirelessly. Just make sure 280 | // your module is running before starting Arduino IDE then you should should see it in the programming ports menu as something like: 281 | // "NODEMCU-192-168-1-n at 192.168.1.n (Generic ESP8266 Module)" where n is our IP octet defined in IP_LAST_OCTET 282 | // There are times where you will need to program over USB again, for example if the code you just updated wirelessly has a bug in how 283 | // it sets up ArduinoOTA, or gets hung up in a loop where the ArduinoOTA.handle() is not being called periodically anymore. 284 | // ------------------------------------------------------------------------------------------------------------------------------------------------- 285 | // NOTE: Always make sure you set up IP_LAST_OCTET correctly when you burn OTA, because if you're selecting one specific device over-the-air, but will be burning a different IP to it, it can cause you confusion, 286 | // especially if there is already another node online with that same IP. 287 | // -------------------------------------------------------------------------------------------------------------------------------------------------- 288 | String otahostStr = "NODEMCU-192-168-1-" + String(IP_LAST_OCTET); // this is the name you'll see available in the Arduino->Tools->Port menu for programming this device wirelessly 289 | char otahost[100]; 290 | otahostStr.toCharArray(otahost, (unsigned int)otahostStr.length() + 1); 291 | ArduinoOTA.setHostname(otahost); 292 | ArduinoOTA.onStart([]() { // this function gets called when you initiate a wireless code update, we'll just display info 293 | displayAll("OTA Starting."); 294 | }); 295 | ArduinoOTA.onEnd([]() { // this gets called when the code upload completes 296 | displayAll("OTA Complete."); 297 | }); 298 | ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { // this gets called periodically as the code upload progresses 299 | int percent = (progress / (total / 100)); 300 | displayAll("OTA Prog: " + String(percent) + "%"); 301 | }); 302 | ArduinoOTA.onError([](ota_error_t error) { // this gets called if there's an upload error 303 | String theError = "OTA Error[%u]: " + String(error) + "\n"; 304 | if (error == OTA_AUTH_ERROR) { 305 | theError = theError + "Auth Failed"; 306 | } else if (error == OTA_BEGIN_ERROR) { 307 | theError = theError + "Begin Failed"; 308 | } else if (error == OTA_CONNECT_ERROR) { 309 | theError = theError + "Connect Failed"; 310 | } else if (error == OTA_RECEIVE_ERROR) { 311 | theError = theError + "Receive Failed"; 312 | } else if (error == OTA_END_ERROR) { 313 | theError = theError + "End Failed"; 314 | } 315 | displayAll(theError); 316 | }); 317 | ArduinoOTA.begin(); // initialize the OTA module (note that in the program loop we will have to call ArduinoOTA.handle() so it can listen for update requests 318 | 319 | // Start the server 320 | server.begin(); 321 | Serial.println("Server started"); 322 | 323 | // Print the IP address 324 | displayAll("My HTTP host:\n" 325 | "IP: " + WiFi.localIP().toString() + "\n" 326 | "Port: " + String(PORT)); 327 | delay(2000); 328 | } 329 | 330 | // LOOP function ------------------------------------------------------------------------------------- 331 | void loop() { 332 | ArduinoOTA.handle(); 333 | 334 | updateTimeFromServer(); // sync our internal clock periodically with real time from remote server (also gets actual time rather than relative from boot time) 335 | updateTemperature(); 336 | 337 | // ***************************** if we're a Master node check slaves here 338 | if (ThisNode->isMaster && slaveCheckNext <= now()) { // am I a master and is it time to collect data from slaves ? 339 | collectSlaveData(); 340 | } 341 | 342 | // ************************** check and handle request for data from a browser 343 | WiFiClient client = server.available(); 344 | String request = acceptRequest(client); 345 | if (request.length() > 0) { // we have a request 346 | String payload = ""; 347 | if (request.indexOf("/reset") >= 0) { // this allows you to reset a node 348 | ESP.restart(); 349 | } else if (request.indexOf("/data") >= 0) { // this allows you to get temp data in a more easily machine readable format (used by a master to get slaves' data) 350 | payload = "[[temperature[" + tempStr + "]][[humidity[" + humStr + "]][name[" + nodeName + "]][time[" + timeStr + "]][date[" + dateStr + "]][uptime[" + (millis() / 1000) + "]]"; 351 | } else { // a regular request formats the data in a human readable page 352 | int textPtr = request.indexOf("text="); 353 | Serial.println("textPtr = " + textPtr); 354 | if (textPtr >= 0) { // if you submit text from the human readable page, you can change the node's name from defaultName 355 | int endPtr = request.indexOf("&", textPtr); 356 | if (endPtr == -1) { 357 | endPtr = request.indexOf(" ", textPtr); 358 | } 359 | Serial.println("endPtr = " + endPtr); 360 | nodeName = request.substring(textPtr + 5, endPtr); 361 | nodeName.replace("+", " "); 362 | Serial.println("text = " + nodeName); 363 | } 364 | String allNodes = ""; 365 | if (ThisNode->isMaster) { 366 | // if we're master add in all the responding slave nodes' data for display 367 | allNodes = ""; 368 | for (int i = 0; i < NODEMCU_NODE_COUNT; i++) { 369 | codeus::NODEMCU_NODE* node = &NODEMCU_NODES[i]; 370 | //TODO: build response from all nodes 371 | if (node->result != NULL && node->lastIPOctet != IP_LAST_OCTET) { 372 | codeus::SLAVE_RESULT* res = node->result; 373 | allNodes = allNodes + buildNodeHtml(res->name, res->temperature, res->humidity, node->lastIPOctet, now() - res->lastUpdateEpoch) + "
"; 374 | } 375 | } 376 | } 377 | 378 | // this is the webapge we'll render to the browser 379 | payload = 380 | "" 381 | "

" 382 | "Time: " + timeStr + "
" 383 | "Date: " + dateStr + "
" + 384 | "

" + 385 | buildNodeHtml(nodeName, tempStr, humStr, IP_LAST_OCTET, 0) + "
" + 386 | allNodes + 387 | "" 388 | ""; 389 | } 390 | 391 | // Return the response 392 | client.println( 393 | "HTTP/1.1 200 OK\r\n" 394 | "Content-Type: text/html\r\n" 395 | "\r\n" // do not forget this one 396 | + payload + 397 | "\r\n\r\n"); 398 | 399 | delay(1); 400 | Serial.println("Client disconnected"); 401 | Serial.println(""); 402 | } // end client check 403 | 404 | // display stuff to the OLED/TFT too 405 | displayText( 406 | tempStr + "F" + " " + "h:" + humStr + "%" + "\n" + 407 | nodeName + "\n" + 408 | dateStr + "\n" + 409 | timeStr + " \n" + 410 | "" 411 | ); 412 | } 413 | String buildNodeHtml(String nStr, String tStr, String hStr, int octet, time_t secondsSinceReading) { 414 | return 415 | "" + nStr + ":  " 416 | "" + tStr + "°  " 417 | "" + hStr +"% rh" + 418 | "[" + String(octet) + "] " + String(secondsSinceReading) +" seconds"; 419 | } 420 | 421 | void collectSlaveData() { 422 | slaveCheckNext = now() + SLAVE_CHECK_INTERVAL_SEC; 423 | for (int i = 0; i < NODEMCU_NODE_COUNT; i++) { 424 | codeus::NODEMCU_NODE* node = &NODEMCU_NODES[i]; 425 | if (node->lastIPOctet != IP_LAST_OCTET) { // skip ourselves 426 | String url = "http://192.168.1." + String(node->lastIPOctet) + ":" + PORT + "/data"; 427 | String n = node->defaultName; 428 | if (node->result != NULL && node->result->name != NULL) { 429 | n = node->result->name; 430 | } 431 | displayTextStatus(">: " + n); 432 | String res = getHttpPayload(url, 1000); 433 | if (res != NULL) { 434 | String n = extractSlaveValue(res, "name"); 435 | String t = extractSlaveValue(res, "temperature"); 436 | String h = extractSlaveValue(res, "humidity"); 437 | node->result = new codeus::SLAVE_RESULT {now(), n, t, h}; 438 | } 439 | } 440 | } 441 | displayTextStatus(""); 442 | } 443 | 444 | // parses a slave's returned data to extract a certain field, data is in format of: [fieldname1[fieldvalue1]][fieldname2[fieldvalue2]]... 445 | String extractSlaveValue(String payload, String fieldName) { 446 | String result = "---"; 447 | if (payload != NULL && fieldName != NULL) { 448 | int starter = payload.indexOf("[" + fieldName + "["); 449 | if (starter >= 0) { 450 | int ender = payload.indexOf("]]", starter + 1); 451 | if (ender >= 0) { 452 | result = payload.substring(starter + fieldName.length() + 2, ender); 453 | } 454 | } 455 | } 456 | return result; 457 | }; 458 | 459 | // helper string to display to both serial port and OLED - use only for initial startup or unusual (i.e. error) conditons, so you don't flash stuff to OLED in normal use 460 | void displayAll(String text) { 461 | Serial.println(text); 462 | displayText(text); 463 | } 464 | 465 | #ifdef USE_TFT 466 | String oldText = ""; // use this to prevent flashing by not updating if no text change 467 | #endif //USE_TFT 468 | 469 | // display text to OLED - every \n newline character advances to next line (4 lines available) 470 | void displayText(String lines) { 471 | #ifdef USE_OLED 472 | display.clear(); 473 | display.setTextAlignment(TEXT_ALIGN_RIGHT); 474 | display.setFont(ArialMT_Plain_16); 475 | #endif 476 | #ifdef USE_TFT 477 | if (lines == oldText) { 478 | return; 479 | } 480 | oldText = lines; 481 | // tft.fillScreen(BACKGROUND_COLOR); 482 | tft.fillRect(0, 0, tft.width(), FONT_HEIGHT*4, BACKGROUND_COLOR); 483 | tft.drawRect(0, 0, tft.width(), FONT_HEIGHT*4, TEXT_COLOR); 484 | tft.setTextSize(TEXT_SIZE); 485 | tft.setCursor(0, FONT_HEIGHT - FONT_OFFSET); 486 | tft.setTextColor(TOP_TEXT_COLOR); // first lines uses "special" color 487 | #endif 488 | int lineY = 0; 489 | int ptr = 0; 490 | int len = lines.length(); 491 | while (ptr < len) { 492 | int newLine = lines.indexOf("\n", ptr); 493 | if (newLine == -1) { 494 | newLine = len; 495 | } 496 | String line = lines.substring(ptr, newLine); 497 | #ifdef USE_OLED 498 | if (line.length() > 0) { 499 | display.drawString(128, lineY, line); 500 | } 501 | #endif 502 | #ifdef USE_TFT 503 | tft.println(line); 504 | if (lineY == 0) { 505 | tft.setTextColor(TEXT_COLOR); // subsequent lines use normal color 506 | } 507 | #endif 508 | lineY += 16; 509 | ptr = newLine + 1; 510 | } 511 | #ifdef USE_OLED 512 | display.display(); 513 | #endif //USE_OLED 514 | } 515 | 516 | // updates just the bottom line of the display 517 | void displayTextStatus(String line) { 518 | #ifdef USE_OLED 519 | display.setColor(BLACK); 520 | display.fillRect(0, 16*3, 128, 16); 521 | display.setColor(WHITE); 522 | display.setTextAlignment(TEXT_ALIGN_RIGHT); 523 | display.setFont(ArialMT_Plain_16); 524 | display.drawString(128, 16*3, line); 525 | display.display(); 526 | #endif //USE_OLED 527 | #ifdef USE_TFT 528 | tft.fillRect(0, tft.height() - FONT_HEIGHT, tft.width(), FONT_HEIGHT, BACKGROUND_COLOR); 529 | tft.drawRect(0, tft.height() - FONT_HEIGHT, tft.width(), FONT_HEIGHT, STATUS_COLOR); 530 | tft.setTextSize(TEXT_SIZE); 531 | tft.setCursor(0, tft.height() - FONT_OFFSET); 532 | tft.setTextColor(STATUS_COLOR); 533 | tft.println(line); 534 | #endif //USE_TFT 535 | } 536 | 537 | // helper to convert a float value to a string with given number of decimals after period (TODO: round value to given decimal) 538 | String floatToStr(float f, int decims) { 539 | float mult = 1.0; 540 | int l = decims; 541 | while (l > 0) { 542 | mult = mult * 10.0; 543 | l--; 544 | } 545 | int v = (f * mult); 546 | String res = String(v); 547 | if (decims != 0) { 548 | l = res.length(); 549 | int p = l - decims; 550 | if (p <= 0) { 551 | res = "0." + res; 552 | } else { 553 | String intStr = res.substring(0, p); 554 | String decStr = res.substring(p); 555 | res = intStr + "." + decStr; 556 | } 557 | } 558 | return res; 559 | } 560 | 561 | //------------------------------------------------------------------------------------------------------- 562 | void updateTemperature() { 563 | // read temperature sensors and update displayable strings 564 | unsigned long currentMillis = millis(); 565 | 566 | if(currentMillis - previousTempMillis >= tempInterval) { // read only at given interval, not every time in the loop 567 | // save the last time you read the sensor 568 | previousTempMillis = currentMillis; 569 | // Reading temperature for humidity takes about 250 milliseconds! 570 | // Sensor readings may also be up to 2 seconds 'old' (it's a very slow sensor) 571 | humidity = NAN; 572 | temp_f = NAN; 573 | #if TEMP_TYPE == DS18 574 | DS18B20.requestTemperatures(); 575 | temp_f = DS18B20.getTempFByIndex(0); 576 | humidity = 0.0; 577 | Serial.println("DS18 reads " + String(temp_f)); 578 | #else 579 | humidity = dht.readHumidity(); // Read humidity (percent) 580 | temp_f = dht.readTemperature(true); // Read temperature as Fahrenheit 581 | #endif 582 | // Check if any reads failed and exit early (to try again). 583 | if (isnan(humidity) || isnan(temp_f)) { 584 | Serial.println("Failed to read from temperature sensor!"); 585 | return; 586 | } 587 | tempStr = floatToStr(temp_f, 1); 588 | #if TEMP_TYPE != DS18 589 | humStr = floatToStr(humidity, 1); // DHT type sensors also have humidity 590 | #endif 591 | } 592 | } 593 | 594 | //------------------------------------------------------------------------------------------------------- 595 | // helper to left zero pad a value to a given string size 596 | String zeroPad(int value, int digits) { 597 | String s = String(value); 598 | while (s.length() < digits) { 599 | s = "0" + s; 600 | } 601 | return s; 602 | } 603 | 604 | // helper to check if we have an incoming HTTP request 605 | String acceptRequest(WiFiClient client) { 606 | String result = ""; 607 | unsigned long ms = 0; 608 | if (client) { 609 | // Wait until the client sends some data 610 | Serial.println("new client"); 611 | unsigned long timeout = millis() + 1000; 612 | while(!client.available() && millis() < timeout){ 613 | delay(1); 614 | } 615 | ms = millis(); 616 | if (ms >= timeout) { 617 | Serial.println("Timed out in client wait..."); 618 | client.flush(); 619 | } else { 620 | // Read the first line of the request 621 | result = client.readStringUntil('\r'); 622 | Serial.println(result); 623 | client.flush(); 624 | } 625 | } 626 | return result; 627 | } 628 | 629 | // helper to make an outgoing HTTP GET request 630 | String getHttpPayload(String url, unsigned long timeoutMaxMS) { 631 | attemptsCount++; 632 | String result = ""; 633 | HTTPClient http; 634 | Serial.println("http trying: " + url); 635 | http.setTimeout(timeoutMaxMS); 636 | if (!http.begin(url)) { 637 | Serial.println("http failed"); 638 | return result; 639 | } 640 | Serial.println("http connection made"); 641 | int httpCode = http.GET(); 642 | Serial.println("http connection status: " + httpCode); 643 | if(httpCode == HTTP_CODE_OK) { 644 | getsCount++; 645 | result = http.getString(); 646 | } else { 647 | Serial.println("http connection FAILED"); 648 | } 649 | http.end(); 650 | return result; 651 | } 652 | 653 | 654 | // NPT time server stuff ******************************************************************************** 655 | const long timeInterval = 60*60*1000; // interval at which to read time webpage (hourly) 656 | unsigned long previousTimeMillis = timeInterval; // will store last time was read 657 | 658 | void updateTimeFromServer() { 659 | unsigned long currentMillis = millis(); 660 | if(currentMillis - previousTimeMillis >= timeInterval) { 661 | // save the last time you read the server time 662 | previousTimeMillis = currentMillis; 663 | nptAttempts++; 664 | if (setNTPtime() || firstTimeGot) { 665 | previousTimeMillis = currentMillis; 666 | firstTimeGot = true; 667 | } else { 668 | previousTimeMillis = currentMillis - timeInterval + (30*1000); // if failed, try again in 30 seconds 669 | } 670 | } 671 | dateStr = String(dayShortStr(weekday())) + ", " + String(monthShortStr(month())) + " " + String(day()); 672 | String secStr = ""; 673 | #ifndef USE_TFT 674 | secStr = ":" + zeroPad(second(), 2); // add seconds in only if NOT tft because it causes too much flashing (need to figure out double buffering) 675 | #endif 676 | timeStr = zeroPad(hourFormat12(), 2) + ":" + zeroPad(minute(), 2) + secStr + " " + (isAM() ? "AM" : "PM"); 677 | } 678 | 679 | // NPT server time retrieve code ------------------------------------------------------------------------------------------------------- 680 | // Found at (and slightly munged) http://www.esp8266.com/viewtopic.php?p=18395 posted by user "nigelbe", it is all the info I have, thank you Nigel 681 | // Note that I've modified the dst function to (hopefully) get correct daylight savings time offset for USA 682 | #define localTimeOffset 21600UL // your localtime offset from UCT 683 | WiFiUDP udp; 684 | unsigned int localPort = 2390; // local port to listen for UDP packets 685 | const char* timeServer = "us.pool.ntp.org"; // fall back to regional time server 686 | const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message 687 | byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets 688 | bool setNTPtime() { 689 | time_t epoch = 0UL; 690 | if((epoch = getFromNTP(timeServer)) != 0){ // get from time server 691 | epoch -= 2208988800UL + localTimeOffset; 692 | setTime(epoch += dstUSA(epoch)); 693 | nptGets++; 694 | return true; 695 | } 696 | return false; 697 | } 698 | 699 | unsigned long getFromNTP(const char* server) { 700 | udp.begin(localPort); 701 | sendNTPpacket(server); // send an NTP packet to a time server 702 | delay(1000); // wait to see if a reply is available 703 | int cb = udp.parsePacket(); 704 | if (!cb) { 705 | Serial.println("no packet yet"); 706 | return 0UL; 707 | } 708 | Serial.print("packet received, length="); 709 | Serial.println(cb); 710 | // We've received a packet, read the data from it 711 | udp.read(packetBuffer, NTP_PACKET_SIZE); // read the packet into the buffer 712 | 713 | //the timestamp starts at byte 40 of the received packet and is four bytes, 714 | // or two words, long. First, extract the two words: 715 | 716 | unsigned long highWord = word(packetBuffer[40], packetBuffer[41]); 717 | unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]); 718 | // combine the four bytes (two words) into a long integer 719 | // this is NTP time (seconds since Jan 1 1900): 720 | udp.stop(); 721 | return (unsigned long) highWord << 16 | lowWord; 722 | } 723 | 724 | // send an NTP request to the time server at the given address 725 | unsigned long sendNTPpacket(const char* server) { 726 | Serial.print("sending NTP packet to "); 727 | Serial.println(server); 728 | // set all bytes in the buffer to 0 729 | memset(packetBuffer, 0, NTP_PACKET_SIZE); 730 | // Initialize values needed to form NTP request 731 | // (see URL above for details on the packets) 732 | packetBuffer[0] = 0b11100011; // LI, Version, Mode 733 | packetBuffer[1] = 0; // Stratum, or type of clock 734 | packetBuffer[2] = 6; // Polling Interval 735 | packetBuffer[3] = 0xEC; // Peer Clock Precision 736 | // 8 bytes of zero for Root Delay & Root Dispersion 737 | packetBuffer[12] = 49; 738 | packetBuffer[13] = 0x4E; 739 | packetBuffer[14] = 49; 740 | packetBuffer[15] = 52; 741 | 742 | // all NTP fields have been given values, now 743 | // you can send a packet requesting a timestamp: 744 | udp.beginPacket(server, 123); //NTP requests are to port 123 745 | udp.write(packetBuffer, NTP_PACKET_SIZE); 746 | udp.endPacket(); 747 | } 748 | 749 | int dstUSA (time_t t) // calculate if summertime in USA (2nd Sunday in Mar, first Sunday in Nov) 750 | { 751 | tmElements_t te; 752 | te.Year = year(t)-1970; 753 | te.Month = 3; 754 | te.Day = 1; 755 | te.Hour = 0; 756 | te.Minute = 0; 757 | te.Second = 0; 758 | time_t dstStart,dstEnd, current; 759 | dstStart = makeTime(te); 760 | dstStart = secondSunday(dstStart); 761 | dstStart += 2*SECS_PER_HOUR; //2AM 762 | te.Month=11; 763 | dstEnd = makeTime(te); 764 | dstEnd = firstSunday(dstEnd); 765 | dstEnd += SECS_PER_HOUR; //1AM 766 | if (t>=dstStart && t