├── CSS.h ├── ESP_Sensor_Monitor_V01a.ino ├── ESP_Sensor_Monitor_v01.ino ├── ESP_Sensor_Server_Advanced_4.ino ├── Icons ├── DINING.PNG ├── Readme ├── bedroom.png ├── building.png ├── conserve.png ├── factory.png ├── garage.png ├── outdoors.png └── study.png ├── Licence.txt ├── Network.h ├── README.md └── Sys_Variables.h /CSS.h: -------------------------------------------------------------------------------- 1 | void append_page_header(bool refresh_on) { 2 | webpage = F(""); 3 | webpage += F(""); 4 | webpage += F("Sensor Server"); // NOTE: 1em = 16px 5 | if (AUpdate && refresh_on) webpage += F(""); // 30-sec refresh time, test needed to prevent auto updates repeating some commands 6 | webpage += F(""); 7 | webpage += F("

Sensor Server "); webpage += String(ServerVersion) + "

"; 32 | } 33 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 34 | void append_page_footer(bool graph_on){ // Saves repeating many lines of code for HTML page footers 35 | if (graph_on) { 36 | webpage += F(""; 37 | webpage += F("

"; 38 | if (readingCnt > display_records) { 39 | webpage += ""; 40 | } 41 | else 42 | { 43 | webpage += ""; 44 | } 45 | } 46 | webpage += F(""); 62 | webpage += ""; 65 | webpage += F(""); 66 | } 67 | -------------------------------------------------------------------------------- /ESP_Sensor_Monitor_V01a.ino: -------------------------------------------------------------------------------- 1 | /* Version 1 2 | * 3 | * ESP32/ESP8266 Sensor Server 4 | * 5 | This software, the ideas and concepts is Copyright (c) David Bird 2018. All rights to this software are reserved. 6 | 7 | Any redistribution or reproduction of any part or all of the contents in any form is prohibited other than the following: 8 | 1. You may print or download to a local hard disk extracts for your personal and non-commercial use only. 9 | 2. You may copy the content to individual third parties for their personal use, but only if you acknowledge the author David Bird as the source of the material. 10 | 3. You may not, except with my express written permission, distribute or commercially exploit the content. 11 | 4. You may not transmit it or store it in any other website or other form of electronic retrieval system for commercial purposes. 12 | 13 | The above copyright ('as annotated') notice and this permission notice shall be included in all copies or substantial portions of the Software and where the 14 | software use is visible to an end-user. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS" FOR PRIVATE USE ONLY, IT IS NOT FOR COMMERCIAL USE IN WHOLE OR PART OR CONCEPT. FOR PERSONAL USE IT IS SUPPLIED WITHOUT WARRANTY 17 | OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHOR OR COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | See more at http://www.dsbird.org.uk 21 | * 22 | */ 23 | #ifdef ESP8266 24 | #include // Built-in 25 | #include // Built-in 26 | #include // Built-in 27 | #include 28 | #include // struct timeval 29 | #else 30 | #include // Built-in 31 | #include // Built-in 32 | #include // https://github.com/Pedroalbuquerque/ESP32WebServer download and place in your Libraries folder 33 | #include 34 | #include "FS.h" 35 | #endif 36 | #include "time.h" 37 | #include "Network.h" 38 | #include "Sys_Variables.h" 39 | #include "CSS.h" 40 | #include // Built-in 41 | #include // Built-in 42 | 43 | #ifdef ESP8266 44 | ESP8266WiFiMulti wifiMulti; 45 | ESP8266WebServer server(80); 46 | #else 47 | WiFiMulti wifiMulti; 48 | ESP32WebServer server(80); 49 | #endif 50 | 51 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 52 | void setup(void){ 53 | Serial.begin(115200); 54 | //WiFi.config(ip, gateway, subnet, dns1, dns2); 55 | if (!WiFi.config(local_IP, gateway, subnet, dns)) { 56 | Serial.println("WiFi STATION Failed to configure Correctly"); 57 | } 58 | wifiMulti.addAP(ssid_1, password_1); // add Wi-Fi networks you want to connect to, it connects from strongest to weakest signal strength 59 | wifiMulti.addAP(ssid_2, password_2); 60 | wifiMulti.addAP(ssid_3, password_3); 61 | wifiMulti.addAP(ssid_4, password_4); 62 | 63 | Serial.println("Connecting ..."); 64 | while (wifiMulti.run() != WL_CONNECTED) { // Wait for the Wi-Fi to connect: scan for Wi-Fi networks, and connect to the strongest of the networks above 65 | delay(250); Serial.print('.'); 66 | } 67 | Serial.println("\nConnected to "+WiFi.SSID()+" Use IP address: "+WiFi.localIP().toString()); // Report which SSID and IP is in use 68 | // The logical name http://fileserver.local will also access the device if you have 'Bonjour' running or your system supports multicast dns 69 | if (!MDNS.begin(servername)) { // Set your preferred server name, if you use "myserver" the address would be http://myserver.local/ 70 | Serial.println(F("Error setting up MDNS responder!")); 71 | ESP.restart(); 72 | } 73 | #ifdef ESP32 74 | SPI.begin(18,19,23); // (SCK,MOSI,MISO) SPI pins used by most ESP32 boards. 75 | // Note: SD_Card readers on the ESP32 will NOT work unless there is a pull-up on MISO, either do this or wire a resistor (1K to 4K7) to Vcc 76 | pinMode(19,INPUT_PULLUP); 77 | pinMode(23,INPUT_PULLUP); 78 | #endif 79 | Serial.print(F("Initializing SD card...")); 80 | if (!SD.begin(SD_CS_pin)) { // see if the card is present and can be initialised. Wemos D1 Mini SD-Card shields use D8 for CS 81 | Serial.println(F("Card failed or not present, no SD Card data logging possible...")); 82 | SD_present = false; 83 | } 84 | else 85 | { 86 | Serial.println(F("Card initialised... data logging enabled...")); 87 | SD_present = true; 88 | } 89 | // Note again: Using an ESP32 and SD Card readers requires a 1K to 4K7 pull-up to 3v3 on the MISO line, otherwise they do-not function. 90 | //---------------------------------------------------------------------- 91 | ///////////////////////////// Server Commands that will be responded to 92 | server.on("/", HomePage); 93 | server.on("/test", []() {server.send(200, "text/plain", "Server status is OK"); }); // Simple server test by providing a status response 94 | server.on("/sensor", HandleSensors); // Now associate the handler functions to the path of each function 95 | server.on("/Liveview", DisplaySensors); 96 | server.on("/Iconview", DisplayLocations); 97 | server.on("/Csetup", ChannelSetup); 98 | server.on("/AUpdate", Auto_Update); 99 | server.on("/Help", Help); 100 | server.on("/Cstream", Channel_File_Stream); 101 | server.on("/Cdownload", Channel_File_Download); 102 | server.on("/Odownload", File_Download); 103 | server.on("/Cupload", Channel_File_Upload); 104 | server.on("/Cerase", Channel_File_Erase); 105 | server.on("/Oerase", File_Erase); 106 | server.on("/upload", HTTP_POST,[](){ server.send(200);}, handleFileUpload); 107 | server.on("/SDdir", SD_dir); 108 | server.on("/chart", DrawChart); 109 | server.on("/forward", MoveChartForward); 110 | server.on("/reverse", MoveChartBack); 111 | server.onNotFound(handleNotFound); // When a client requests an unknown URI for example something other than "/") 112 | ///////////////////////////// End of Request commands 113 | server.begin(); 114 | Serial.println("HTTP server started"); 115 | for (int i = 0; i < number_of_channels; i++) {ChannelData[i].ID = i;} 116 | SetupTime(); 117 | ReadChannelData(); 118 | } 119 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 120 | void loop(void){ 121 | server.handleClient(); // Listen for client connections 122 | if (data_amended) { 123 | SaveChannelData(); 124 | data_amended = false; 125 | } 126 | } 127 | // Functions from here... 128 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 129 | void HomePage(){ 130 | SendHTML_Header(refresh_off); 131 | webpage += F(""); 132 | webpage += F(""); 133 | webpage += F(""); 134 | webpage += F(""); 135 | webpage += F("


"); 136 | webpage += F(""); 137 | webpage += F(""); 138 | webpage += F(""); 139 | webpage += F(""); 140 | webpage += F("


"); 141 | append_page_footer(graph_off); 142 | SendHTML_Content(); 143 | SendHTML_Stop(); 144 | webpage = ""; 145 | } 146 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 147 | void Help(){ 148 | SendHTML_Header(refresh_off); 149 | webpage += F("

Sensor Server Help

"); 150 | webpage += F("

Help section: The Server collects sensor readings and stores them in a Channel and then displayed according to the Channel settings.\ 151 | Data fields are numeric and can be interpreted as the units of your choice. All data from sensors is saved sequentially to the SD-Card and there is a file \ 152 | for each Channel.

"); 153 | webpage += F("

For example, Channel-1 data is saved in a file called '1.txt' and Channel-2 in '2.txt' and so-on. The contents of a Channel file can be streamed \ 154 | to the web-browser, graphed, downloaded, deleted or amended and then uploaded back again.


"); 155 | webpage += F(""); 156 | webpage += F(""); 157 | webpage += F(""); 158 | webpage += F(""); 159 | webpage += F(""); 160 | webpage += F(""); 161 | webpage += F(""); 162 | webpage += F(""); 163 | webpage += F(""); 164 | webpage += F(""); 165 | webpage += F(""); 166 | webpage += F(""); 167 | webpage += F(""); 168 | webpage += F(""); 169 | webpage += F(""); 170 | webpage += F(""); 171 | webpage += F("
Use the menu options to:
'Refresh'Toggle on/off automatic screen refresh of 30-secs
'View Channels'Shows sensor data for each channel as it is received
'View Locations'Shows an Icon and data for each channel as it is received
'Graph Channels'Graph Channel Readings
'Setup Channels'Edit the Channel Name, Description, Sensor Type and Units
'Stream Channel Data'Stream Channel Data to your browser
'Download Channel Data'Download Channel Data to a file
'[Download] File'Download any file
After download into Excel use the formula '=(((DataCell/60)/60)/24)+DATE(1970,1,1)' to convert UNIX time to a Date-Time
e.g If A1 = 1517059610 (unix time) and A2 is empty use A2 = (((A1/60)/60)/24)+DATE(1970,1,1)' now A2 = HH:MM;SS-DD/MM/YY
Set the format of cell A2 to Custom, then dd/mm/yyyy hh:mm
'Upload Channel Data'Upload Channel Data file
'Erase Channel Data'Erase Channel Data file
'[Erase] File'Erase any file
'File Directory'List files on the SD-Card

"); 172 | SendHTML_Content(); 173 | append_page_footer(graph_off); 174 | SendHTML_Content(); 175 | SendHTML_Stop(); 176 | webpage = ""; 177 | } 178 | //~~~~~~~~~~~~~~la~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 179 | void ChannelSetup(){ 180 | if (server.args() > 0 ) { // Arguments were received 181 | data_amended = true; 182 | for (byte ArgNum = 0; ArgNum <= server.args(); ArgNum++) { 183 | if (server.argName(ArgNum) == "chan_name") { ChannelData[channel_number].Name = server.arg(ArgNum); } 184 | if (server.argName(ArgNum) == "chan_desc") { ChannelData[channel_number].Description = server.arg(ArgNum); } 185 | if (server.argName(ArgNum) == "chan_type") { ChannelData[channel_number].Type = server.arg(ArgNum); } 186 | if (server.argName(ArgNum) == "chan_field1") { ChannelData[channel_number].Field1 = server.arg(ArgNum); } 187 | if (server.argName(ArgNum) == "chan_field1_units") { ChannelData[channel_number].Field1_Units = server.arg(ArgNum); } 188 | if (server.argName(ArgNum) == "chan_field2") { ChannelData[channel_number].Field2 = server.arg(ArgNum); } 189 | if (server.argName(ArgNum) == "chan_field2_units") { ChannelData[channel_number].Field2_Units = server.arg(ArgNum); } 190 | if (server.argName(ArgNum) == "chan_field3") { ChannelData[channel_number].Field3 = server.arg(ArgNum); } 191 | if (server.argName(ArgNum) == "chan_field3_units") { ChannelData[channel_number].Field3_Units = server.arg(ArgNum); } 192 | if (server.argName(ArgNum) == "chan_field4") { ChannelData[channel_number].Field4 = server.arg(ArgNum); } 193 | if (server.argName(ArgNum) == "chan_field4_units") { ChannelData[channel_number].Field4_Units = server.arg(ArgNum); } 194 | if (server.argName(ArgNum) == "iconname") { ChannelData[channel_number].IconName = server.arg(ArgNum); } 195 | ChannelData[channel_number].Created = TimeNow(); 196 | } 197 | } 198 | if (server.hasArg("edit_c1")) { channel_number = 1; ChannelEditor(channel_number); } 199 | else if (server.hasArg("edit_c2")) { channel_number = 2; ChannelEditor(channel_number); } 200 | else if (server.hasArg("edit_c3")) { channel_number = 3; ChannelEditor(channel_number); } 201 | else if (server.hasArg("edit_c4")) { channel_number = 4; ChannelEditor(channel_number); } 202 | else if (server.hasArg("edit_c5")) { channel_number = 5; ChannelEditor(channel_number); } 203 | else if (server.hasArg("edit_c6")) { channel_number = 6; ChannelEditor(channel_number); } 204 | else if (server.hasArg("edit_c7")) { channel_number = 7; ChannelEditor(channel_number); } 205 | else if (server.hasArg("edit_c8")) { channel_number = 8; ChannelEditor(channel_number); } 206 | else if (server.hasArg("edit_c9")) { channel_number = 9; ChannelEditor(channel_number); } 207 | else if (server.hasArg("edit_c10")) { channel_number = 10; ChannelEditor(channel_number); } 208 | else if (server.hasArg("edit_c11")) { channel_number = 11; ChannelEditor(channel_number); } 209 | else if (server.hasArg("edit_c12")) { channel_number = 12; ChannelEditor(channel_number); } // NOTE: *** Add more channels here to match the 'number_of_channels' value 210 | else 211 | { 212 | SendHTML_Header(refresh_off); 213 | webpage += F("

Channel Setup/Editor


"); 214 | webpage += F(""); 215 | webpage += F(""); 216 | webpage += F(""); 217 | for (byte cn = 1; cn < number_of_channels; cn++){ 218 | webpage += F(""; 220 | webpage += ""; 221 | if (ChannelData[cn].Updated == BaseTime) webpage += F(""); 222 | else 223 | { 224 | webpage += ""; 226 | } 227 | } 228 | webpage += F("
Channel IDNameDescriptionCreated on:Last Updated at:File Size
"); 219 | webpage += String(ChannelData[cn].ID)+""+ChannelData[cn].Name+""+ChannelData[cn].Description+""+Time(ChannelData[cn].Created).substring(9)+"-
"+Time(ChannelData[cn].Updated)+""; 225 | webpage += String(file_size(String(cn)))+"
"); 229 | webpage += F("

Select channel to View/Edit

"); 230 | webpage += F("
"); 231 | for (byte in_select = 1; in_select < number_of_channels; in_select++){ 232 | webpage += F(""; 233 | } 234 | webpage += F("

"); 235 | SendHTML_Content(); 236 | append_page_footer(graph_off); 237 | SendHTML_Content(); 238 | SendHTML_Stop(); 239 | webpage = ""; 240 | } 241 | } 242 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 243 | void ChannelEditor(int channel_number) { 244 | SendHTML_Header(refresh_off); 245 | webpage += F("

Channel Editor

"); 246 | webpage += F(""); 247 | webpage += F(""; 249 | //--- 250 | webpage += F(""; 251 | webpage += F(""; 253 | //--- 254 | webpage += F(""; 255 | webpage += F(""; 256 | //--- 257 | webpage += F(""; 258 | webpage += F(""; 259 | //--- 260 | webpage += F(""; 261 | webpage += F(""; 262 | webpage += F(""; 263 | SendHTML_Content(); 264 | //--- 265 | webpage += F(""; 266 | webpage += F(""; 267 | webpage += F(""; 268 | //--- 269 | webpage += F(""; 270 | webpage += F(""; 271 | webpage += F(""; 272 | //--- 273 | webpage += F(""; 274 | webpage += F(""; 275 | webpage += F(""; 276 | //--- 277 | webpage += F(""; 278 | webpage += F(""; 279 | //--- 280 | webpage += F("
ID:"); 248 | webpage += String(ChannelData[channel_number].ID)+"Edit EntriesEdit Units
Name:");webpage+=ChannelData[channel_number].Name+"
Description:"); webpage +=ChannelData[channel_number].Description+"
Sensor Type:"); webpage +=ChannelData[channel_number].Type+"
Field-1 Name:"); webpage +=ChannelData[channel_number].Field1+"
Field-2 Name:"); webpage +=ChannelData[channel_number].Field2+"
Field-3 Name:"); webpage +=ChannelData[channel_number].Field3+"
Field-4 Name:"); webpage +=ChannelData[channel_number].Field4+"
Icon Name:"); webpage +=ChannelData[channel_number].IconName+"
"); 281 | webpage += F("

"); 282 | webpage += F("[Back]

"); 283 | append_page_footer(graph_off); 284 | SendHTML_Content(); 285 | SendHTML_Stop(); 286 | webpage = ""; 287 | if (data_amended) SaveChannelData(); 288 | } 289 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 290 | boolean isValidNumber(String str) { 291 | str.trim(); 292 | if(!(str.charAt(0) == '+' || str.charAt(0) == '-' || isDigit(str.charAt(0)))) return false; // Failed if not starting with a unary +- sign or a number 293 | for(byte i=1;i number_of_channels) sensor_num = 0; // Channel-0 is a null channel, assigned wehn incorrect data is received 311 | if ((sensor_num >= 1 && sensor_num < number_of_channels) && ((server.argName(0) == "Sensor") || (server.argName(0) == "sensor"))) { 312 | ChannelData[sensor_num].Updated = TimeNow(); // A valid sensor has been received, so update time-received record 313 | sensor_reading = sensor_num; // Each sensor has its own record, so Sensor-1 readings go in record-1, etc 314 | for (int i = 0; i <= server.args(); i++) { 315 | if (sensor_num != 0 && sensor_num <= number_of_channels) { // If a valid sensor number then process 316 | SensorData[sensor_reading].sensornumber = (byte)sensor_num; // Max. 255 sensors 317 | if (i == 1) SensorData[sensor_reading].value1 = server.arg(i).toFloat(); 318 | if (i == 2) SensorData[sensor_reading].value2 = server.arg(i).toFloat(); 319 | if (i == 3) SensorData[sensor_reading].value3 = server.arg(i).toFloat(); 320 | if (i == 4) SensorData[sensor_reading].value4 = server.arg(i).toFloat(); 321 | if (i == 5) SensorData[sensor_reading].sensortype = server.arg(i); 322 | } 323 | else Serial.println("Sensor number rejected"); 324 | } 325 | SensorData[sensor_reading].readingtime = TimeNow(); 326 | if (SD_present){ // If the SD-Card is present and board fitted then append the next reading to the log file called 'datalog.txt' 327 | File dataFile = SD.open("/"+String(sensor_num)+".txt", FILE_READ); // Check to see if a file exists 328 | if (!dataFile) { // If not update its creation date 329 | ChannelData[sensor_num].Created = TimeNow(); 330 | } 331 | dataFile.close(); 332 | #ifdef ESP8266 333 | dataFile = SD.open("/"+String(sensor_num)+".txt", FILE_WRITE); //On the ESP8266 FILE_WRITE opens file for writing and move to the end of the file 334 | #else 335 | dataFile = SD.open("/"+String(sensor_num)+".txt", FILE_APPEND); //On the ESP32 it needs FILE_APPEND to open file for writing and movs to the end of the file 336 | #endif 337 | if (dataFile) { // if the file is available, write to it 338 | dataFile.print(SensorData[sensor_reading].readingtime); dataFile.print(char(9)); // TAB delimited data 339 | dataFile.print(SensorData[sensor_reading].value1); dataFile.print(char(9)); 340 | dataFile.print(SensorData[sensor_reading].value2); dataFile.print(char(9)); 341 | dataFile.print(SensorData[sensor_reading].value3); dataFile.print(char(9)); 342 | dataFile.print(SensorData[sensor_reading].value4); dataFile.print(char(9)); 343 | dataFile.println(SensorData[sensor_reading].sensortype); 344 | } 345 | dataFile.close(); 346 | } 347 | } 348 | DisplaySensors(); 349 | } 350 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 351 | void DisplaySensors(){ 352 | #define resolution 1 // 1 = 20.2 or 2 = 20.23 or 3 = 20.234 displayed data if the sensor supports the resolution 353 | SendHTML_Header(refresh_on); 354 | webpage += F("
"); 355 | if (!ReceivedAnySensor()) { 356 | webpage += F("

*** Waiting For Data Reception ***

"); 357 | Serial.println("No sensors received"); 358 | } 359 | else 360 | { 361 | webpage += F("

Current Sensor Readings

"); 362 | webpage += F("
"); // Add horizontal scrolling if number of fields exceeds page width 363 | webpage += F(""); 364 | for (int s = 1; s <= number_of_channels; s++) { 365 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 366 | {webpage += F("");} 367 | } 368 | webpage += F(""); 369 | for (int s = 1; s <= number_of_channels; s++) { 370 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 371 | webpage += ""; 372 | } 373 | webpage += F(""); 374 | for (int s = 1; s <= number_of_channels; s++) { 375 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 376 | webpage += ""; 377 | } 378 | webpage += F(""); 379 | for (int s = 1; s <= number_of_channels; s++) { 380 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 381 | webpage += ""; 382 | } 383 | SendHTML_Content(); 384 | webpage += F(""); 385 | for (int s = 1; s <= number_of_channels; s++) { 386 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 387 | webpage += ""; 388 | } 389 | webpage += F(""); 390 | for (int s = 1; s <= number_of_channels; s++) { 391 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 392 | webpage += ""; 393 | } 394 | webpage += F(""); 395 | for (int s = 1; s <= number_of_channels; s++) { 396 | if ((SensorData[s].sensornumber != 0) && (SensorData[s].sensornumber <= number_of_channels)) 397 | webpage += ""; 398 | } 399 | webpage += F(""); 400 | for (int s = 1; s <= number_of_channels; s++) { 401 | if ((SensorData[s].sensornumber != 0) && (SensorData[s].sensornumber <= number_of_channels)) 402 | webpage += ""; 403 | } 404 | webpage += F("
Sensor Name"); webpage += ChannelData[SensorData[s].sensornumber].Name; webpage += F("
Sensor Number"+String(SensorData[s].sensornumber)+"
Type"+ChannelData[s].Type +"
Field-1"+String(SensorData[s].value1,resolution)+ChannelData[s].Field1_Units+"
Field-2"+String(SensorData[s].value2,resolution)+ChannelData[s].Field2_Units+"
Field-3"+String(SensorData[s].value3,resolution)+ChannelData[s].Field3_Units+"
Field-4"+String(SensorData[s].value4,resolution)+ChannelData[s].Field4_Units+"
Updated"+Time(SensorData[s].readingtime).substring(0,8)+"
"); 405 | } 406 | webpage += F("

"); 407 | SendHTML_Content(); 408 | append_page_footer(graph_off); 409 | SendHTML_Content(); 410 | SendHTML_Stop(); 411 | webpage = ""; 412 | } 413 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 414 | void DisplayLocations(){ 415 | #define resolution 1 // 1 = 20.2 or 2 = 20.23 or 3 = 20.234 displayed data if the sensor supports the resolution 416 | SendHTML_Header(refresh_on); 417 | webpage += F("
"); 418 | if (!ReceivedAnySensor()) { 419 | webpage += F("

*** Waiting For Data Reception ***

"); 420 | } 421 | else 422 | { 423 | webpage += F("

Current Sensor Readings

"); 424 | webpage += F("
"); // Add horizontal scrolling if number of fields exceeds page width 425 | webpage += F(""); 426 | for (int s = 1; s <= number_of_channels; s++) { 427 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 428 | { webpage += F(""); } 429 | } 430 | webpage += F(""); 431 | for (int s = 1; s <= number_of_channels; s++) { 432 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 433 | { 434 | webpage += F(""); 436 | } 437 | } 438 | webpage += F(""); 439 | for (int s = 1; s <= number_of_channels; s++) { 440 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 441 | if (ChannelData[s].Field1 != "") { webpage += ""; } 442 | else webpage += F(""); 443 | } 444 | SendHTML_Content(); 445 | webpage += F(""); 446 | for (int s = 1; s <= number_of_channels; s++) { 447 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 448 | if (ChannelData[s].Field2 != "") { webpage += ""; } 449 | else webpage += F(""); 450 | } 451 | webpage += F(""); 452 | for (int s = 1; s <= number_of_channels; s++) { 453 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 454 | if (ChannelData[s].Field3 != "") { webpage += ""; } 455 | else webpage += F(""); 456 | } 457 | webpage += F(""); 458 | for (int s = 1; s <= number_of_channels; s++) { 459 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 460 | if (ChannelData[s].Field4 != "") { webpage += ""; } 461 | else webpage += F(""); 462 | } 463 | webpage += F(""); 464 | for (int s = 1; s <= number_of_channels; s++) { 465 | if ((SensorData[s].sensornumber != 0) && (SensorData[s].sensornumber <= number_of_channels)) 466 | webpage += ""; 467 | } 468 | webpage += F("
"); webpage += ChannelData[SensorData[s].sensornumber].Name; webpage += F("
"+String(SensorData[s].value1,resolution)+ChannelData[s].Field1_Units+"
"+String(SensorData[s].value2,resolution)+ChannelData[s].Field2_Units+"
"+String(SensorData[s].value3,resolution)+ChannelData[s].Field3_Units+"
"+String(SensorData[s].value4,resolution)+ChannelData[s].Field4_Units+"
" + Time(SensorData[s].readingtime).substring(0,8)+"
"); 469 | } 470 | webpage += F("

"); 471 | append_page_footer(graph_off); 472 | SendHTML_Content(); 473 | SendHTML_Stop(); 474 | webpage = ""; 475 | } 476 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 477 | void Auto_Update () { // Auto-refresh of the screen, this turns it on/off 478 | AUpdate = !AUpdate; 479 | HomePage(); 480 | } 481 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 482 | bool ReceivedAnySensor(){ 483 | bool sensor_received = false; 484 | for (int s = 1; s <= number_of_channels; s++) { 485 | if ((SensorData[s].sensornumber != 0) && (SensorData[s].sensornumber <= number_of_channels)) { sensor_received = true; } 486 | } 487 | return sensor_received; 488 | } 489 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 490 | void ReadChannelData(){ 491 | if (SD_present){ // If the SD-Card is present and board fitted then append the next reading to the log file called 'datalog.txt' 492 | File dataFile = SD.open("/chandata.txt", FILE_READ); 493 | int cn = 1; 494 | if (dataFile) { // if the file is available, read it 495 | String in_record; 496 | while (dataFile.available() && cn < number_of_channels) { 497 | // Note the trim function is essential for graphing to work! Fields are padded out with spaces otherwise, I don't know why... 498 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].ID = in_record.toInt(); 499 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Name = in_record; 500 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Description = in_record; 501 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Type = in_record; 502 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Field1 = in_record; 503 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Field1_Units = in_record; 504 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Field2 = in_record; 505 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Field2_Units = in_record; 506 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Field3 = in_record; 507 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Field3_Units = in_record; 508 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Field4 = in_record; 509 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Field4_Units = in_record; 510 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].IconName = in_record; 511 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Created = in_record.toInt(); 512 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Updated = in_record.toInt(); 513 | cn++; 514 | } 515 | } else ReportSDNotPresent(); 516 | dataFile.close(); 517 | } else ReportSDNotPresent(); 518 | } 519 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 520 | void SaveChannelData(){ 521 | if (SD_present){ // If the SD-Card is present and board fitted then append the next reading to the log file called 'datalog.txt' 522 | if (!SD.remove("/chandata.txt")) Serial.println(F("Failed to delete Channel Setting files")); 523 | File dataFile = SD.open("/chandata.txt", FILE_WRITE); 524 | if (dataFile) { // Save all possible channel data 525 | for (int cn= 1; cn < number_of_channels; cn++){ 526 | dataFile.println(String(ChannelData[cn].ID)+","); 527 | dataFile.println(ChannelData[cn].Name+","); 528 | dataFile.println(ChannelData[cn].Description+","); 529 | dataFile.println(ChannelData[cn].Type+","); 530 | dataFile.println(ChannelData[cn].Field1+","); 531 | dataFile.println(ChannelData[cn].Field1_Units+","); 532 | dataFile.println(ChannelData[cn].Field2+","); 533 | dataFile.println(ChannelData[cn].Field2_Units+","); 534 | dataFile.println(ChannelData[cn].Field3+","); 535 | dataFile.println(ChannelData[cn].Field3_Units+","); 536 | dataFile.println(ChannelData[cn].Field4+","); 537 | dataFile.println(ChannelData[cn].Field4_Units+","); 538 | dataFile.println(ChannelData[cn].IconName+","); 539 | dataFile.println(String(ChannelData[cn].Created)+","); 540 | dataFile.println(String(ChannelData[cn].Updated)+","); 541 | } 542 | } else ReportSDNotPresent(); 543 | dataFile.close(); 544 | } else ReportSDNotPresent(); 545 | } 546 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 547 | void Channel_File_Stream(){ 548 | if (server.args() > 0 ) { // Arguments were received 549 | if (server.hasArg("stream")) if (server.arg(0) == String(number_of_channels)) SD_file_stream("chandata"); else SD_file_stream(server.arg(0)); 550 | } 551 | else SelectInput(refresh_off,"Channel File Stream","Select a Channel to Stream","Cstream","stream",graph_on); 552 | } 553 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 554 | void SD_file_stream(String filename) { 555 | if (SD_present) { 556 | File dataFile = SD.open("/"+filename+".txt", FILE_READ); // Now read data from SD Card 557 | if (dataFile) { 558 | if (dataFile.available()) { // If data is available and present 559 | String dataType = "application/octet-stream"; 560 | if (server.streamFile(dataFile, dataType) != dataFile.size()) {Serial.print(F("Sent less data than expected!")); } 561 | } 562 | dataFile.close(); // close the file: 563 | } else ReportFileNotPresent("Cstream"); 564 | } else ReportSDNotPresent(); 565 | } 566 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 567 | void Channel_File_Download(){ 568 | if (server.args() > 0 ) { // Arguments were received 569 | if (server.hasArg("download")) if (server.arg(0) == String(number_of_channels)) SD_file_download("chandata",!open_download); else SD_file_download(server.arg(0),!open_download); 570 | } 571 | else SelectInput(refresh_off,"Channel File Download","Select a Channel to Download","Cdownload","download",graph_on); 572 | } 573 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 574 | void File_Download(){ 575 | if (server.args() > 0 ) { // Arguments were received 576 | Serial.println(server.arg(0)); 577 | if (server.hasArg("odownload")) SD_file_download(server.arg(0),open_download); 578 | } 579 | else OpenSelectInput("Enter a File Name to Download","Odownload","odownload"); 580 | } 581 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 582 | void SD_file_download(String filename, bool download_mode){ 583 | if (SD_present) { 584 | File download = SD.open("/"+filename+(download_mode?"":".txt"), FILE_READ); // Now read data from SD Card 585 | if (download) { 586 | if (!download_mode) filename += ".txt"; 587 | server.sendHeader("Content-Type", "text/text"); 588 | server.sendHeader("Content-Disposition", "attachment; filename="+filename); 589 | server.sendHeader("Connection", "close"); 590 | server.streamFile(download, "application/octet-stream"); 591 | download.close(); 592 | } else ReportFileNotPresent("Cdownload"); 593 | } else ReportSDNotPresent(); 594 | } 595 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 596 | void Channel_File_Upload(){ 597 | append_page_header(refresh_off); 598 | webpage += F("

Select File to Upload

"); 599 | webpage += F("
"); 600 | webpage += F("
"); 601 | webpage += F("

"); 602 | webpage += F("[Back]

"); 603 | append_page_footer(graph_off); 604 | server.send(200, "text/html",webpage); 605 | } 606 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 607 | File UploadFile; 608 | void handleFileUpload(){ // upload a new file to the Filing system 609 | HTTPUpload& uploadfile = server.upload(); // See https://github.com/esp8266/Arduino/tree/master/libraries/ESP8266WebServer/srcv 610 | // For further information on 'status' structure, there are other reasons such as a failed transfer that could be used 611 | if(uploadfile.status == UPLOAD_FILE_START) 612 | { 613 | String filename = uploadfile.filename; 614 | if(!filename.startsWith("/")) filename = "/"+filename; 615 | Serial.print("Upload File Name: "); Serial.println(filename); 616 | SD.remove(filename); // Remove a previous version, otherwise data is appended the file again 617 | UploadFile = SD.open(filename, FILE_WRITE); // Open the file for writing in SPIFFS (create it, if doesn't exist) 618 | filename = String(); 619 | } 620 | else if (uploadfile.status == UPLOAD_FILE_WRITE) 621 | { 622 | if(UploadFile) UploadFile.write(uploadfile.buf, uploadfile.currentSize); // Write the received bytes to the file 623 | } 624 | else if (uploadfile.status == UPLOAD_FILE_END) 625 | { 626 | if(UploadFile) // If the file was successfully created 627 | { 628 | UploadFile.close(); // Close the file again 629 | Serial.print("Upload Size: "); Serial.println(uploadfile.totalSize); 630 | webpage = ""; 631 | append_page_header(refresh_off); 632 | webpage += F("

File was successfully uploaded

"); 633 | webpage += F("

Uploaded File Name: "); webpage += uploadfile.filename+"

"; 634 | webpage += F("

File Size: "); webpage += file_size(uploadfile.totalSize) + "


"; 635 | append_page_footer(graph_off); 636 | server.send(200,"text/html",webpage); 637 | } 638 | else 639 | { 640 | ReportCouldNotCreateFile("Cupload"); 641 | } 642 | } 643 | } 644 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 645 | void Channel_File_Erase(){ 646 | if (server.args() > 0 ) { // Arguments were received 647 | if (server.hasArg("erase")) if (server.arg(0) == String(number_of_channels)) SD_file_erase("chandata",!open_erase); else SD_file_erase(server.arg(0),!open_erase); 648 | ChannelDataReset(server.arg(0).toInt()); 649 | SaveChannelData(); 650 | } 651 | else SelectInput(refresh_off,"Channel File Erase","Select a Channel to Erase","Cerase","erase",graph_off); 652 | } 653 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 654 | void File_Erase(){ 655 | if (server.args() > 0 ) { // Arguments were received 656 | Serial.println(server.arg(0)); 657 | if (server.hasArg("oerase")) SD_file_erase(server.arg(0),open_erase); 658 | } 659 | else OpenSelectInput("Enter a File Name to Erase","Oerase","oerase"); 660 | } 661 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 662 | void SD_file_erase(String filename, bool erase_mode) { // Erase the datalog file 663 | if (SD_present) { 664 | append_page_header(refresh_off); 665 | webpage += F("

Channel File Erase

"); 666 | File dataFile = SD.open("/"+filename+(erase_mode?"":".txt"), FILE_READ); // Now read data from SD Card 667 | if (dataFile) 668 | { 669 | if (SD.remove("/"+filename+(erase_mode?"":".txt"))) { 670 | Serial.println(F("File deleted successfully")); 671 | webpage += "

FILE: '"+filename+(erase_mode?"":".txt")+"' has been erased

"; 672 | if (erase_mode) webpage += F("[Back]

"); else webpage += F("[Back]

"); 673 | } 674 | else 675 | { 676 | webpage += F("

Channel File was not deleted - error

"); 677 | webpage += F("[Back]

"); 678 | } 679 | } else ReportFileNotPresent("Cerase"); 680 | append_page_footer(graph_off); 681 | server.send(200,"text/html",webpage); 682 | webpage = ""; 683 | } else ReportSDNotPresent(); 684 | } 685 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 686 | void SD_dir(){ 687 | if (SD_present) { 688 | File root = SD.open("/"); 689 | if (root) { 690 | root.rewindDirectory(); 691 | SendHTML_Header(refresh_off); 692 | webpage += F("

SD Card Contents

"); 693 | webpage += F(""); 694 | webpage += F(""); 695 | printDirectory("/",0); 696 | webpage += F("
Name/TypeFile/DirSize

"); 697 | SendHTML_Content(); 698 | root.close(); 699 | } 700 | else 701 | { 702 | SendHTML_Header(refresh_off); 703 | webpage += F("

No Channel Files Found

"); 704 | } 705 | append_page_footer(graph_off); 706 | SendHTML_Content(); 707 | SendHTML_Stop(); // Stop is needed because no content length was sent 708 | } else ReportSDNotPresent(); 709 | } 710 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 711 | void printDirectory(const char * dirname, uint8_t levels){ 712 | Serial.printf("Listing directory: %s\n", dirname); 713 | File root = SD.open(dirname); 714 | #ifdef ESP8266 715 | root.rewindDirectory(); //Only needed for ESP8266 716 | #endif 717 | if(!root){ 718 | Serial.println("Failed to open directory"); 719 | return; 720 | } 721 | if(!root.isDirectory()){ 722 | Serial.println("Not a directory"); 723 | return; 724 | } 725 | File file = root.openNextFile(); 726 | while(file){ 727 | if (webpage.length() > 1000) { 728 | SendHTML_Content(); 729 | } 730 | if(file.isDirectory()){ 731 | Serial.println(file.name()); 732 | webpage += ""+String(file.isDirectory()?"Dir":"File")+""+String(file.name())+""; 733 | printDirectory(file.name(), levels-1); 734 | } 735 | else 736 | { 737 | webpage += ""+String(file.name())+""; 738 | webpage += ""+String(file.isDirectory()?"Dir":"File")+""; 739 | webpage += ""+file_size(file.size())+""; 740 | } 741 | file = root.openNextFile(); 742 | } 743 | file.close(); 744 | } 745 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 746 | String file_size(int bytes){ 747 | String fsize = ""; 748 | if (bytes < 1024) fsize = String(bytes)+" B"; 749 | else if(bytes < (1024 * 1024)) fsize = String(bytes/1024.0,3)+" KB"; 750 | else if(bytes < (1024 * 1024 * 1024)) fsize = String(bytes/1024.0/1024.0,3)+" MB"; 751 | else fsize = String(bytes/1024.0/1024.0/1024.0,3)+" GB"; 752 | return fsize; 753 | } 754 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 755 | void DrawChart(){ 756 | graph_start = 0; 757 | graph_end = display_records; 758 | if (server.args() > 0 ) { // Arguments were received 759 | if (server.hasArg("draw")) drawchart(server.arg(0)); // parameter is sensor number 760 | } 761 | else SelectInput(refresh_off,"Channel Graph","Select a Channel to Graph","chart","draw",graph_off); 762 | } 763 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 764 | void drawchart(String filename){ // required to enable parameters with the call 765 | graph_filename = filename; // global variable requirement 766 | channel_number = filename.toInt(); 767 | readingCnt = CountFileRecords(filename); 768 | GetandGraphData(); 769 | } 770 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 771 | void GetDataForGraph(String filename, int start){ 772 | int recordCnt = 0, displayCnt = 0; 773 | String stype; 774 | graph_sensor = filename.toInt(); 775 | if (SD_present) { 776 | File dataFile = SD.open("/"+filename+".txt", FILE_READ); // Now read data from SD Card 777 | if (dataFile) { 778 | while (dataFile.available() && displayCnt < display_records) { // if the file is available, read from it 779 | DisplayData[displayCnt].ltime = dataFile.parseInt(); // 1517052559 21.58 43.71 0.00 0.00 SHT30-1 typically 780 | DisplayData[displayCnt].field1 = dataFile.parseFloat(); 781 | DisplayData[displayCnt].field2 = dataFile.parseFloat(); 782 | DisplayData[displayCnt].field3 = dataFile.parseFloat(); 783 | DisplayData[displayCnt].field4 = dataFile.parseFloat(); 784 | stype = dataFile.readStringUntil('\n'); // Needed to complete a record read 785 | if (recordCnt >= start) displayCnt++; 786 | recordCnt++; 787 | } 788 | } else ReportFileNotPresent("chart"); 789 | dataFile.close(); 790 | if (recordCnt == 0) displayCnt = 1; // In case the file is empty 791 | if (start > recordCnt) displayCnt = recordCnt; // In case the file is empty or there were not enough records. 792 | graph_start = start; 793 | graph_end = displayCnt; // Number of records to display 794 | } else ReportSDNotPresent(); 795 | } 796 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 797 | void MoveChartForward(){ 798 | graph_start += graph_step; 799 | graph_end += graph_step; 800 | if (graph_end > readingCnt) { 801 | graph_end = readingCnt; 802 | graph_start = readingCnt - display_records; 803 | if (graph_start < 0 ) graph_start = 0; 804 | } 805 | if ((graph_start > readingCnt - graph_step) && (readingCnt - graph_step > 0) ) graph_start = readingCnt - graph_step; 806 | GetandGraphData(); 807 | } 808 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 809 | void MoveChartBack(){ 810 | graph_start -= graph_step; 811 | graph_end -= graph_step; 812 | if (graph_start < 0) graph_start = 0; 813 | GetandGraphData(); 814 | } 815 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 816 | void GetandGraphData(){ 817 | GetDataForGraph(graph_filename,graph_start); 818 | GraphSelectedData(DisplayData, 819 | graph_start, 820 | graph_end, 821 | ChannelData[graph_sensor].Name); 822 | } 823 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 824 | void ConstructGraph(record_type displayed_SensorData[], int start, int number_of_records, String title, String ytitle, String yunits, String graphname){ 825 | webpage = ""; 826 | // https://developers.google.com/loader/ // https://developers.google.com/chart/interactive/docs/basic_load_libs 827 | // https://developers.google.com/chart/interactive/docs/basic_preparing_data 828 | // https://developers.google.com/chart/interactive/docs/reference#google.visualization.arraytodatatable and See appendix-A 829 | // data format is: [field-name,field-name,field-name] then [data,data,data], e.g. [12, 20.5, 70.3] 830 | webpage += F(""); 831 | webpage += F(""); 860 | SendHTML_Content(); 861 | } 862 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 863 | void GraphSelectedData(record_type displayed_SensorData[], 864 | int start, 865 | int number_of_records, 866 | String title) 867 | { 868 | // See google charts api for more details. To load the APIs, include the following script in the header of your web page. 869 | // 870 | // See https://developers.google.com/chart/interactive/docs/basic_load_libs 871 | bool graph1_on = false, graph2_on = false, graph3_on = false, graph4_on = false; 872 | String ytitle, yunits; 873 | SendHTML_Header(refresh_off); 874 | webpage += F("

"); webpage += ChannelData[graph_sensor].Name+" ("+String(readingCnt)+"-readings)

"; 875 | SendHTML_Content(); 876 | if (ChannelData[channel_number].Field1 != "") { 877 | graphfield = one; 878 | graphcolour = "red"; 879 | ytitle = ChannelData[graph_sensor].Field1; 880 | yunits = ChannelData[graph_sensor].Field1_Units; 881 | ConstructGraph(displayed_SensorData, start, number_of_records, title+" "+ytitle, ytitle, yunits, "graph1"); // 'graph1' is e.g. the tag to match the
statements below 882 | graph1_on = true; 883 | } 884 | //----------------- 885 | if (ChannelData[channel_number].Field2 != "") { 886 | graphfield = two; 887 | graphcolour = "blue"; 888 | ytitle = ChannelData[graph_sensor].Field2; 889 | yunits = ChannelData[graph_sensor].Field2_Units; 890 | ConstructGraph(displayed_SensorData, start, number_of_records, title+" "+ytitle, ytitle, yunits, "graph2"); 891 | graph2_on = true; 892 | } 893 | //----------------- 894 | if (ChannelData[channel_number].Field3 != "") { 895 | graphfield = three; 896 | graphcolour = "green"; 897 | ytitle = ChannelData[graph_sensor].Field3; 898 | yunits = ChannelData[graph_sensor].Field3_Units; 899 | ConstructGraph(displayed_SensorData, start, number_of_records, title+" "+ytitle, ytitle, yunits, "graph3"); 900 | graph3_on = true; 901 | } 902 | //----------------- 903 | if (ChannelData[channel_number].Field4 != "") { 904 | graphfield = four; 905 | graphcolour = "orange"; 906 | ytitle = ChannelData[graph_sensor].Field4; 907 | yunits = ChannelData[graph_sensor].Field4_Units; 908 | ConstructGraph(displayed_SensorData, start, number_of_records, title+" "+ytitle, ytitle, yunits, "graph4"); 909 | graph4_on = true; 910 | } 911 | //----------------- 912 | webpage += F("
"); 913 | webpage += F("
"); 914 | if (graph1_on) webpage += F("
"); 915 | if (graph2_on) webpage += F("
"); 916 | if (graph3_on) webpage += F("
"); 917 | if (graph4_on) webpage += F("
"); 918 | webpage += F("
"); 919 | webpage += F("
[Back]
"); 920 | SendHTML_Content(); // Send Content 921 | append_page_footer(graph_on); 922 | SendHTML_Content(); // Send footer 923 | SendHTML_Stop(); // Stop is needed because no content length was sent 924 | } 925 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 926 | void SendHTML_Header(bool refresh_mode){ 927 | server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); 928 | server.sendHeader("Pragma", "no-cache"); 929 | server.sendHeader("Connection", "Keep-Alive"); 930 | server.sendHeader("Expires", "-1"); 931 | server.setContentLength(CONTENT_LENGTH_UNKNOWN); 932 | server.send(200, "text/html", ""); // Empty content inhibits Content-length header 933 | append_page_header(refresh_mode); 934 | server.sendContent(webpage); 935 | server.sendContent("\n\r"); // A blank line seperates the header 936 | webpage = ""; 937 | } 938 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 939 | void SendHTML_Content(){ 940 | server.sendContent(webpage); 941 | webpage = ""; 942 | } 943 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 944 | void SendHTML_Stop(){ 945 | server.sendContent(""); 946 | server.client().stop(); // Stop is needed because no content length was sent 947 | } 948 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 949 | void SelectInput(bool refresh_mode, String heading1, String heading2, String command, String arg_calling_name, bool graph_mode_off){ 950 | SendHTML_Header(refresh_off); 951 | webpage += F("

"); webpage += heading2 + "

"; 952 | webpage += F(""; // Must match the calling argument e.g. '/chart' calls '/chart' after selection but with arguments 953 | for (byte cn = 1; cn < number_of_channels; cn++){ 954 | webpage += F(""; 955 | } 956 | if (graph_mode_off) { webpage += F(""; } 957 | webpage += F("

"); 958 | append_page_footer(graph_off); 959 | SendHTML_Content(); 960 | SendHTML_Stop(); 961 | webpage = ""; 962 | } 963 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 964 | void OpenSelectInput(String heading1, String command, String arg_calling_name){ 965 | SendHTML_Header(refresh_off); 966 | webpage += F("

"); webpage += heading1 + "

"; 967 | webpage += F(""; // Must match the calling argument e.g. '/chart' calls '/chart' after selection but with arguments! 968 | webpage += F("
"); 969 | webpage += F("

"); 970 | append_page_footer(graph_off); 971 | SendHTML_Content(); 972 | SendHTML_Stop(); 973 | webpage = ""; 974 | } 975 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 976 | String file_size(String filename){ // Display file size of the datalog file 977 | String ftxtsize = ""; 978 | if (SD_present) { 979 | File dataFile = SD.open("/"+filename+".txt", FILE_READ); // Now read data from SD Card 980 | if (dataFile) { 981 | int bytes = dataFile.size(); 982 | if (bytes < 1024) ftxtsize = String(bytes)+" B"; 983 | else if(bytes < (1024 * 1024)) ftxtsize = String(bytes/1024.0)+" KB"; 984 | else if(bytes < (1024 * 1024 * 1024)) ftxtsize = String(bytes/1024.0/1024.0)+" MB"; 985 | else ftxtsize = String(bytes/1024.0/1024.0/1024.0)+" GB"; 986 | } 987 | else ftxtsize = ""; 988 | dataFile.close(); 989 | return ftxtsize; 990 | } else ReportSDNotPresent(); 991 | return ""; 992 | } 993 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 994 | void ReportSDNotPresent(){ 995 | append_page_header(refresh_off); 996 | webpage += F("

No SD Card present

"); 997 | webpage += F("[Back]

"); 998 | append_page_footer(graph_off); 999 | server.send(200,"text/html",webpage); 1000 | webpage = ""; 1001 | } 1002 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1003 | void ReportFileNotPresent(String target){ 1004 | append_page_header(refresh_off); 1005 | webpage += F("

File does not exist

"); 1006 | webpage += F("[Back]

"; 1007 | append_page_footer(graph_off); 1008 | server.send(200,"text/html",webpage); 1009 | webpage = ""; 1010 | } 1011 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1012 | void ReportCommandNotFound(String target){ 1013 | append_page_header(refresh_off); 1014 | webpage += F("

Function does not exist

"); 1015 | webpage += F("[Back]

"; 1016 | append_page_footer(graph_off); 1017 | server.send(200,"text/html",webpage); 1018 | webpage = ""; 1019 | } 1020 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1021 | void ReportCouldNotCreateFile(String target){ 1022 | append_page_header(refresh_off); 1023 | webpage += F("

Could Not Create Uploaded File (write-protected?)

"); 1024 | webpage += F("[Back]

"; 1025 | append_page_footer(graph_off); 1026 | server.send(200,"text/html",webpage); 1027 | webpage = ""; 1028 | } 1029 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1030 | int CountFileRecords(String filename){ 1031 | int recordCnt = 0, temp_read_int = 0; 1032 | float temp_read_flt = 0; 1033 | String temp_read_txt = ""; 1034 | if (SD_present) { 1035 | File dataFile = SD.open("/"+filename+".txt", FILE_READ); // Now read data from SD Card 1036 | if (dataFile) { 1037 | while (dataFile.available()) { // if the file is available, read from it 1038 | recordCnt++; 1039 | temp_read_int = dataFile.parseInt(); // 1517052559 21.58 43.71 0.00 0.00 SHT30-1 typically 1040 | temp_read_flt = dataFile.parseFloat(); 1041 | temp_read_flt = dataFile.parseFloat(); 1042 | temp_read_flt = dataFile.parseFloat(); 1043 | temp_read_flt = dataFile.parseFloat(); 1044 | temp_read_txt = dataFile.readStringUntil('\n'); // Needed to complete a record read 1045 | } 1046 | } 1047 | dataFile.close(); 1048 | return recordCnt; 1049 | } else ReportSDNotPresent(); 1050 | return 0; 1051 | } 1052 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1053 | void SetupTime(){ 1054 | timeval tv = {BaseTime, 0 }; // 00:00:00 01/01/2018 1055 | timezone tz = { 0 , 0 }; 1056 | settimeofday(&tv, &tz); 1057 | configTime(0, 0, "pool.ntp.org"); 1058 | setenv("TZ", "GMT0BST,M3.5.0/2,M10.5.0/2", 1); 1059 | tzset(); 1060 | time_t tnow = time(nullptr); 1061 | delay(2000); 1062 | Serial.print(F("\nWaiting for time...")); 1063 | tnow = time(nullptr); 1064 | Serial.println("Time set "+Time(tnow)); 1065 | } 1066 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1067 | String Time(int unix_time){ 1068 | struct tm *now_tm; 1069 | int hour, min, second, day, month, year; 1070 | // timeval tv = {unix_time,0}; 1071 | time_t tm = unix_time; 1072 | now_tm = localtime(&tm); 1073 | hour = now_tm->tm_hour; 1074 | min = now_tm->tm_min; 1075 | second = now_tm->tm_sec; 1076 | day = now_tm->tm_mday; 1077 | month = now_tm->tm_mon+1; 1078 | year = 1900 + now_tm->tm_year; // To get just YY information 1079 | //String days[7] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"}; 1080 | time_str = (hour<10?"0"+String(hour):String(hour))+":"+(min<10?"0"+String(min):String(min))+":"+(second<10?"0"+String(second):String(second))+"-"; 1081 | time_str += (day<10?"0"+String(day):String(day))+"/"+ (month<10?"0"+String(month):String(month))+"/"+(year<10?"0"+String(year):String(year)); // HH:MM:SS 05/07/17 1082 | //Serial.println(time_str); 1083 | return time_str; 1084 | } 1085 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1086 | int TimeNow(){ 1087 | time_t tnow = time(nullptr); 1088 | return tnow; 1089 | } 1090 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1091 | void ChannelDataReset(byte CN){ 1092 | ChannelData[CN].ID = CN; 1093 | ChannelData[CN].Name = "Name-TBA"; 1094 | ChannelData[CN].Description = "Sensor Readings"; 1095 | ChannelData[CN].Type = "e.g.SHT30"; 1096 | ChannelData[CN].Field1 = "e.g.Temperature"; 1097 | ChannelData[CN].Field1_Units = "°C"; 1098 | ChannelData[CN].Field2 = "e.g.Humidity"; 1099 | ChannelData[CN].Field2_Units = "%"; 1100 | ChannelData[CN].Field3 = "e.g.Pressure"; 1101 | ChannelData[CN].Field3_Units = "hPa"; 1102 | ChannelData[CN].Field4 = "Unused"; 1103 | ChannelData[CN].Field4_Units = ""; 1104 | ChannelData[CN].IconName = "building.png"; 1105 | ChannelData[CN].Created = BaseTime; 1106 | ChannelData[CN].Updated = BaseTime; 1107 | } 1108 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1109 | // Read file from SD Card and display it 1110 | bool loadFromSdCard(String filename){ 1111 | String dataType = "text/plain"; 1112 | if(filename.endsWith(".htm")) dataType = "text/html"; 1113 | else if(filename.endsWith(".html")) dataType = "text/html"; 1114 | else if(filename.endsWith(".css")) dataType = "text/css"; 1115 | else if(filename.endsWith(".png")) dataType = "image/png"; 1116 | else if(filename.endsWith(".gif")) dataType = "image/gif"; 1117 | else if(filename.endsWith(".jpg")) dataType = "image/jpeg"; 1118 | else if(filename.endsWith(".bmp")) dataType = "image/bmp"; 1119 | else if(filename.endsWith(".ico")) dataType = "image/x-icon"; 1120 | Serial.println(filename); 1121 | File dataFile = SD.open(filename.c_str()); 1122 | if (!dataFile) return false; 1123 | if (server.hasArg("download")) dataType = "application/octet-stream"; 1124 | if (server.streamFile(dataFile, dataType) != dataFile.size()) { Serial.println("Sent less data than expected!"); } 1125 | dataFile.close(); 1126 | return true; 1127 | } 1128 | 1129 | -------------------------------------------------------------------------------- /ESP_Sensor_Monitor_v01.ino: -------------------------------------------------------------------------------- 1 | /* Version 1 2 | * 3 | * ESP32/ESP8266 Sensor Monitor 4 | * 5 | This software, the ideas and concepts is Copyright (c) David Bird 2018. All rights to this software are reserved. 6 | 7 | Any redistribution or reproduction of any part or all of the contents in any form is prohibited other than the following: 8 | 1. You may print or download to a local hard disk extracts for your personal and non-commercial use only. 9 | 2. You may copy the content to individual third parties for their personal use, but only if you acknowledge the author David Bird as the source of the material. 10 | 3. You may not, except with my express written permission, distribute or commercially exploit the content. 11 | 4. You may not transmit it or store it in any other website or other form of electronic retrieval system for commercial purposes. 12 | 13 | The above copyright ('as annotated') notice and this permission notice shall be included in all copies or substantial portions of the Software and where the 14 | software use is visible to an end-user. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS" FOR PRIVATE USE ONLY, IT IS NOT FOR COMMERCIAL USE IN WHOLE OR PART OR CONCEPT. FOR PERSONAL USE IT IS SUPPLIED WITHOUT WARRANTY 17 | OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHOR OR COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | See more at http://www.dsbird.org.uk 21 | * 22 | */ 23 | #ifdef ESP8266 24 | #include // Built-in 25 | #include // Built-in 26 | #include // Built-in 27 | #include 28 | #include // struct timeval 29 | #else 30 | #include // Built-in 31 | #include // Built-in 32 | #include // https://github.com/Pedroalbuquerque/ESP32WebServer download and place in your Libraries folder 33 | #include 34 | #include "FS.h" 35 | #endif 36 | #include "time.h" 37 | #include "Network.h" 38 | #include "Sys_Variables.h" 39 | #include "CSS.h" 40 | #include 41 | #include 42 | 43 | #ifdef ESP8266 44 | ESP8266WiFiMulti wifiMulti; 45 | ESP8266WebServer server(80); 46 | #else 47 | WiFiMulti wifiMulti; 48 | ESP32WebServer server(80); 49 | #endif 50 | 51 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 52 | void setup(void){ 53 | Serial.begin(115200); 54 | //WiFi.config(ip, gateway, subnet, dns1, dns2); 55 | if (!WiFi.config(local_IP, gateway, subnet, dns)) { 56 | Serial.println("WiFi STATION Failed to configure Correctly"); 57 | } 58 | wifiMulti.addAP(ssid_1, password_1); // add Wi-Fi networks you want to connect to, it connects strongest to weakest 59 | wifiMulti.addAP(ssid_2, password_2); 60 | wifiMulti.addAP(ssid_3, password_3); 61 | wifiMulti.addAP(ssid_4, password_4); 62 | 63 | Serial.println("Connecting ..."); 64 | while (wifiMulti.run() != WL_CONNECTED) { // Wait for the Wi-Fi to connect: scan for Wi-Fi networks, and connect to the strongest of the networks above 65 | delay(250); Serial.print('.'); 66 | } 67 | Serial.print("\nConnected to "+WiFi.SSID()+" Use IP address: "+WiFi.localIP().toString()); // Report which SSID and IP is in use 68 | // The logical name http://fileserver.local will also access the device if you have 'Bonjour' running or your system supports multicast dns 69 | if (!MDNS.begin(servername)) { // Set your preferred server name, if you use "myserver" the address would be http://myserver.local/ 70 | Serial.println(F("Error setting up MDNS responder!")); 71 | ESP.restart(); 72 | } 73 | #ifdef ESP32 74 | SPI.begin(18,19,23); // Pins for most ESP32 boards. (SCK,MOSI,MISO) 75 | // Note: SD_Card readers on the ESP32 and some/most ESP8266 will NOT work unless there is a pull-down on SS, either do this or wire one on (1K to 4K7) 76 | pinMode(19,INPUT_PULLUP); 77 | pinMode(23,INPUT_PULLUP); 78 | #endif 79 | Serial.print(F("Initializing SD card...")); 80 | if (!SD.begin(SD_CS_pin)) { // see if the card is present and can be initialised. Wemos SD-Card CS uses D8 81 | Serial.println(F("Card failed or not present, no SD Card data logging possible...")); 82 | SD_present = false; 83 | } 84 | else 85 | { 86 | Serial.println(F("Card initialised... data logging enabled...")); 87 | SD_present = true; 88 | } 89 | // Note: Using the ESP32 and SD_Card readers requires a 1K to 4K7 pull-down to Gnd on the SS line, otherwise they do-not function. 90 | //---------------------------------------------------------------------- 91 | ///////////////////////////// Request commands 92 | server.on("/", HomePage); 93 | server.on("/test", []() {server.send(200, "text/plain", "Server status is OK"); }); // Test the server by giving a status response 94 | server.on("/sensor", HandleSensors); // Associate the handler functions to the path 95 | server.on("/Liveview", DisplaySensors); 96 | server.on("/Iconview", DisplayLocations); 97 | server.on("/Csetup", ChannelSetup); 98 | server.on("/AUpdate", Auto_Update); 99 | server.on("/Help", Help); 100 | server.on("/Cstream", Channel_File_Stream); 101 | server.on("/Cdownload", Channel_File_Download); 102 | server.on("/Odownload", File_Download); 103 | server.on("/Cupload", Channel_File_Upload); 104 | server.on("/Cerase", Channel_File_Erase); 105 | server.on("/Oerase", File_Erase); 106 | server.on("/upload", HTTP_POST,[](){ server.send(200);}, handleFileUpload); 107 | server.on("/SDdir", SD_dir); 108 | server.on("/chart", DrawChart); 109 | server.on("/forward", MoveChartForward); 110 | server.on("/reverse", MoveChartBack); 111 | server.onNotFound(handleNotFound); // Handle when a client requests an unknown URI for example something other than "/") 112 | ///////////////////////////// End of Request commands 113 | server.begin(); 114 | Serial.println("HTTP server started"); 115 | for (int i = 0; i < number_of_channels; i++) {ChannelData[i].ID = i;} 116 | SetupTime(); 117 | ReadChannelData(); 118 | } 119 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 120 | void loop(void){ 121 | server.handleClient(); // Listen for client connections 122 | if (data_amended) { 123 | SaveChannelData(); 124 | data_amended = false; 125 | } 126 | } 127 | // Functions from here... 128 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 129 | void HomePage(){ 130 | append_page_header(refresh_off); 131 | webpage += F(""); 132 | webpage += F(""); 133 | webpage += F(""); 134 | webpage += F(""); 135 | webpage += F("


"); 136 | webpage += F(""); 137 | webpage += F(""); 138 | webpage += F(""); 139 | webpage += F(""); 140 | webpage += F("


"); 141 | append_page_footer(graph_off); 142 | server.send(200,"text/html",webpage); 143 | webpage = ""; 144 | } 145 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 146 | void Help(){ 147 | append_page_header(refresh_off); 148 | webpage += F("

Sensor Server Help

"); 149 | webpage += F("

Help section: The Server collects sensor readings and stores them in a Channel and then displayed according to the Channel settings.\ 150 | Data fields are numeric and can be interpreted as the units of your choice. All data from sensors is saved sequentially to the SD-Card and there is a file \ 151 | for each Channel.

"); 152 | webpage += F("

For example, Channel-1 data is saved in a file called '1.txt' and Channel-2 in '2.txt' and so-on. The contents of a Channel file can be streamed \ 153 | to the web-browser, graphed, downloaded, deleted or amended and then uploaded back again.


"); 154 | webpage += F(""); 155 | webpage += F(""); 156 | webpage += F(""); 157 | webpage += F(""); 158 | webpage += F(""); 159 | webpage += F(""); 160 | webpage += F(""); 161 | webpage += F(""); 162 | webpage += F(""); 163 | webpage += F(""); 164 | webpage += F(""); 165 | webpage += F(""); 166 | webpage += F(""); 167 | webpage += F(""); 168 | webpage += F(""); 169 | webpage += F("
Use the menu options to:
'Refresh'Toggle on/off automatic screen refresh of 30-secs
'View Channels'Displays sensor data for each channel as it is received
'Graph Channels'Graph Channel Readings
'Setup Channels'Edit the Channel Name, Description, Sensor Type and Units
'Stream Channel Data'Stream Channel Data to your browser
'Download Channel Data'Download Channel Data to a file
'[Download] File'Download any file
After download into Excel use the formula '=(((DataCell/60)/60)/24)+DATE(1970,1,1)' to convert UNIX time to a Date-Time
e.g If A1 = 1517059610 (unix time) and A2 is empty use A2 = (((A1/60)/60)/24)+DATE(1970,1,1)' now A2 = HH:MM;SS-DD/MM/YY
Set the format of cell A2 to Custom, then dd/mm/yyyy hh:mm
'Upload Channel Data'Upload Channel Data file
'Erase Channel Data'Erase Channel Data file
'[Erase] File'Erase any file
'File Directory'List files on the SD-Card

"); 170 | append_page_footer(graph_off); 171 | server.send(200,"text/html",webpage); 172 | webpage = ""; 173 | } 174 | //~~~~~~~~~~~~~~la~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 175 | void ChannelSetup(){ 176 | if (server.args() > 0 ) { // Arguments were received 177 | data_amended = true; 178 | for (byte ArgNum = 0; ArgNum <= server.args(); ArgNum++) { 179 | if (server.argName(ArgNum) == "chan_name") { ChannelData[channel_number].Name = server.arg(ArgNum); } 180 | if (server.argName(ArgNum) == "chan_desc") { ChannelData[channel_number].Description = server.arg(ArgNum); } 181 | if (server.argName(ArgNum) == "chan_type") { ChannelData[channel_number].Type = server.arg(ArgNum); } 182 | if (server.argName(ArgNum) == "chan_field1") { ChannelData[channel_number].Field1 = server.arg(ArgNum); } 183 | if (server.argName(ArgNum) == "chan_field1_units") { ChannelData[channel_number].Field1_Units = server.arg(ArgNum); } 184 | if (server.argName(ArgNum) == "chan_field2") { ChannelData[channel_number].Field2 = server.arg(ArgNum); } 185 | if (server.argName(ArgNum) == "chan_field2_units") { ChannelData[channel_number].Field2_Units = server.arg(ArgNum); } 186 | if (server.argName(ArgNum) == "chan_field3") { ChannelData[channel_number].Field3 = server.arg(ArgNum); } 187 | if (server.argName(ArgNum) == "chan_field3_units") { ChannelData[channel_number].Field3_Units = server.arg(ArgNum); } 188 | if (server.argName(ArgNum) == "chan_field4") { ChannelData[channel_number].Field4 = server.arg(ArgNum); } 189 | if (server.argName(ArgNum) == "chan_field4_units") { ChannelData[channel_number].Field4_Units = server.arg(ArgNum); } 190 | if (server.argName(ArgNum) == "iconname") { ChannelData[channel_number].IconName = server.arg(ArgNum); } 191 | ChannelData[channel_number].Created = TimeNow(); 192 | } 193 | } 194 | if (server.hasArg("edit_c1")) { channel_number = 1; ChannelEditor(channel_number); } 195 | else if (server.hasArg("edit_c2")) { channel_number = 2; ChannelEditor(channel_number); } 196 | else if (server.hasArg("edit_c3")) { channel_number = 3; ChannelEditor(channel_number); } 197 | else if (server.hasArg("edit_c4")) { channel_number = 4; ChannelEditor(channel_number); } 198 | else if (server.hasArg("edit_c5")) { channel_number = 5; ChannelEditor(channel_number); } 199 | else if (server.hasArg("edit_c6")) { channel_number = 6; ChannelEditor(channel_number); } 200 | else 201 | { 202 | append_page_header(refresh_off); 203 | webpage += F("

Channel Setup/Editor


"); 204 | webpage += F(""); 205 | webpage += F(""); 206 | webpage += F(""); 207 | for (byte cn = 1; cn < number_of_channels; cn++){ 208 | webpage += F(""; 210 | webpage += ""; 211 | if (ChannelData[cn].Updated == BaseTime) webpage += F(""); 212 | else 213 | { 214 | webpage += ""; 216 | } 217 | } 218 | webpage += F("
Channel IDNameDescriptionCreated on:Last Updated at:File Size
"); 209 | webpage += String(ChannelData[cn].ID)+""+ChannelData[cn].Name+""+ChannelData[cn].Description+""+Time(ChannelData[cn].Created).substring(9)+"-
"+Time(ChannelData[cn].Updated)+""; 215 | webpage += String(file_size(String(cn)))+"
"); 219 | webpage += F("

Select channel to View/Edit

"); 220 | webpage += F(""); 221 | for (byte in_select = 1; in_select < number_of_channels; in_select++){ 222 | webpage += F(""; 223 | } 224 | webpage += F("

"); 225 | append_page_footer(graph_off); 226 | server.send(200,"text/html",webpage); 227 | webpage = ""; 228 | } 229 | } 230 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 231 | void ChannelEditor(int channel_number) { 232 | append_page_header(refresh_off); 233 | webpage += F("

Channel Editor

"); 234 | webpage += F(""); 235 | webpage += F(""; 237 | //--- 238 | webpage += F(""; 239 | webpage += F(""; 241 | //--- 242 | webpage += F(""; 243 | webpage += F(""; 244 | //--- 245 | webpage += F(""; 246 | webpage += F(""; 247 | //--- 248 | webpage += F(""; 249 | webpage += F(""; 250 | webpage += F(""; 251 | SendHTML_Content(); 252 | //--- 253 | webpage += F(""; 254 | webpage += F(""; 255 | webpage += F(""; 256 | //--- 257 | webpage += F(""; 258 | webpage += F(""; 259 | webpage += F(""; 260 | //--- 261 | webpage += F(""; 262 | webpage += F(""; 263 | webpage += F(""; 264 | //--- 265 | webpage += F(""; 266 | webpage += F(""; 267 | //--- 268 | webpage += F("
ID:"); 236 | webpage += String(ChannelData[channel_number].ID)+"Edit EntriesEdit Units
Name:");webpage+=ChannelData[channel_number].Name+"
Description:"); webpage +=ChannelData[channel_number].Description+"
Sensor Type:"); webpage +=ChannelData[channel_number].Type+"
Field-1 Name:"); webpage +=ChannelData[channel_number].Field1+"
Field-2 Name:"); webpage +=ChannelData[channel_number].Field2+"
Field-3 Name:"); webpage +=ChannelData[channel_number].Field3+"
Field-4 Name:"); webpage +=ChannelData[channel_number].Field4+"
Icon Name:"); webpage +=ChannelData[channel_number].IconName+"
"); 269 | webpage += F("

"); 270 | webpage += F("[Back]

"); 271 | append_page_footer(graph_off); 272 | server.send(200,"text/html",webpage); 273 | webpage = ""; 274 | if (data_amended) SaveChannelData(); 275 | } 276 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 277 | boolean isValidNumber(String str) { 278 | str.trim(); 279 | if(!(str.charAt(0) == '+' || str.charAt(0) == '-' || isDigit(str.charAt(0)))) return false; // Failed if not starting with +- or a number 280 | for(byte i=1;i number_of_channels) sensor_num = 0; // Channel-0 is a null channel, assigned wehn incorrect data is received 298 | if ((sensor_num >= 1 && sensor_num < number_of_channels) && ((server.argName(0) == "Sensor") || (server.argName(0) == "sensor"))) { 299 | ChannelData[sensor_num].Updated = TimeNow(); // A valid sensor has been received, so update time-received record 300 | sensor_reading = sensor_num; // Each sensor has its own record, so Sensor-1 readings go in record-1, etc 301 | for (int i = 0; i <= server.args(); i++) { 302 | if (sensor_num != 0 && sensor_num <= number_of_channels) { // If a valid sensor number then process 303 | SensorData[sensor_reading].sensornumber = (byte)sensor_num; // Max. 255 sensors 304 | if (i == 1) SensorData[sensor_reading].value1 = server.arg(i).toFloat(); 305 | if (i == 2) SensorData[sensor_reading].value2 = server.arg(i).toFloat(); 306 | if (i == 3) SensorData[sensor_reading].value3 = server.arg(i).toFloat(); 307 | if (i == 4) SensorData[sensor_reading].value4 = server.arg(i).toFloat(); 308 | if (i == 5) SensorData[sensor_reading].sensortype = server.arg(i); 309 | } 310 | else Serial.println("Sensor number rejected"); 311 | } 312 | SensorData[sensor_reading].readingtime = TimeNow(); 313 | if (SD_present){ // If the SD-Card is present and board fitted then append the next reading to the log file called 'datalog.txt' 314 | File dataFile = SD.open("/"+String(sensor_num)+".txt", FILE_READ); // Check to see if a file exists 315 | if (!dataFile) { // If not update its creation date 316 | ChannelData[sensor_num].Created = TimeNow(); 317 | } 318 | dataFile.close(); 319 | #ifdef ESP8266 320 | dataFile = SD.open("/"+String(sensor_num)+".txt", FILE_WRITE); //On the ESP8266 FILE_WRITE opens file for writing and move to the end of the file 321 | #else 322 | dataFile = SD.open("/"+String(sensor_num)+".txt", FILE_APPEND); //On the ESP32 it needs FILE_APPEND to open file for writing and movs to the end of the file 323 | #endif 324 | if (dataFile) { // if the file is available, write to it 325 | dataFile.print(SensorData[sensor_reading].readingtime); dataFile.print(char(9)); // TAB delimited data 326 | dataFile.print(SensorData[sensor_reading].value1); dataFile.print(char(9)); 327 | dataFile.print(SensorData[sensor_reading].value2); dataFile.print(char(9)); 328 | dataFile.print(SensorData[sensor_reading].value3); dataFile.print(char(9)); 329 | dataFile.print(SensorData[sensor_reading].value4); dataFile.print(char(9)); 330 | dataFile.println(SensorData[sensor_reading].sensortype); 331 | } 332 | dataFile.close(); 333 | } 334 | } 335 | DisplaySensors(); 336 | } 337 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 338 | void DisplaySensors(){ 339 | #define resolution 1 // 1 = 20.2 or 2 = 20.23 or 3 = 20.234 displayed data if the sensor supports the resolution 340 | append_page_header(refresh_off); 341 | webpage += F("
"); 342 | if (!ReceivedAnySensor()) { 343 | webpage += F("

*** Waiting For Data Reception ***

"); 344 | Serial.println("No sensors received"); 345 | } 346 | else 347 | { 348 | webpage += F("

Current Sensor Readings

"); 349 | webpage += F("
"); // Add horizontal scrolling if number of fields exceeds page width 350 | webpage += F(""); 351 | for (int s = 1; s <= number_of_channels; s++) { 352 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 353 | {webpage += F("");} 354 | } 355 | webpage += F(""); 356 | for (int s = 1; s <= number_of_channels; s++) { 357 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 358 | webpage += ""; 359 | } 360 | webpage += F(""); 361 | for (int s = 1; s <= number_of_channels; s++) { 362 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 363 | webpage += ""; 364 | } 365 | webpage += F(""); 366 | for (int s = 1; s <= number_of_channels; s++) { 367 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 368 | webpage += ""; 369 | } 370 | webpage += F(""); 371 | for (int s = 1; s <= number_of_channels; s++) { 372 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 373 | webpage += ""; 374 | } 375 | webpage += F(""); 376 | for (int s = 1; s <= number_of_channels; s++) { 377 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 378 | webpage += ""; 379 | } 380 | webpage += F(""); 381 | for (int s = 1; s <= number_of_channels; s++) { 382 | if ((SensorData[s].sensornumber != 0) && (SensorData[s].sensornumber <= number_of_channels)) 383 | webpage += ""; 384 | } 385 | webpage += F(""); 386 | for (int s = 1; s <= number_of_channels; s++) { 387 | if ((SensorData[s].sensornumber != 0) && (SensorData[s].sensornumber <= number_of_channels)) 388 | webpage += ""; 389 | } 390 | webpage += F("
Sensor Name"); webpage += ChannelData[SensorData[s].sensornumber].Name; webpage += F("
Sensor Number"+String(SensorData[s].sensornumber)+"
Type"+ChannelData[s].Type +"
Field-1"+String(SensorData[s].value1,resolution)+ChannelData[s].Field1_Units+"
Field-2"+String(SensorData[s].value2,resolution)+ChannelData[s].Field2_Units+"
Field-3"+String(SensorData[s].value3,resolution)+ChannelData[s].Field3_Units+"
Field-4"+String(SensorData[s].value4,resolution)+ChannelData[s].Field4_Units+"
Updated"+Time(SensorData[s].readingtime).substring(0,8)+"
"); 391 | } 392 | webpage += F("

"); 393 | append_page_footer(graph_off); 394 | server.send(200,"text/html",webpage); 395 | webpage = ""; 396 | } 397 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 398 | void DisplayLocations(){ 399 | #define resolution 1 // 1 = 20.2 or 2 = 20.23 or 3 = 20.234 displayed data if the sensor supports the resolution 400 | append_page_header(refresh_off); 401 | webpage += F("
"); 402 | if (!ReceivedAnySensor()) { 403 | webpage += F("

*** Waiting For Data Reception ***

"); 404 | } 405 | else 406 | { 407 | webpage += F("

Current Sensor Readings

"); 408 | webpage += F("
"); // Add horizontal scrolling if number of fields exceeds page width 409 | webpage += F(""); 410 | for (int s = 1; s <= number_of_channels; s++) { 411 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 412 | { webpage += F(""); } 413 | } 414 | webpage += F(""); 415 | for (int s = 1; s <= number_of_channels; s++) { 416 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 417 | { 418 | webpage += F(""); 420 | } 421 | } 422 | webpage += F(""); 423 | for (int s = 1; s <= number_of_channels; s++) { 424 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 425 | if (ChannelData[s].Field1 != "") { webpage += ""; } 426 | else webpage += F(""); 427 | } 428 | webpage += F(""); 429 | for (int s = 1; s <= number_of_channels; s++) { 430 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 431 | if (ChannelData[s].Field2 != "") { webpage += ""; } 432 | else webpage += F(""); 433 | } 434 | webpage += F(""); 435 | for (int s = 1; s <= number_of_channels; s++) { 436 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 437 | if (ChannelData[s].Field3 != "") { webpage += ""; } 438 | else webpage += F(""); 439 | } 440 | webpage += F(""); 441 | for (int s = 1; s <= number_of_channels; s++) { 442 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 443 | if (ChannelData[s].Field4 != "") { webpage += ""; } 444 | else webpage += F(""); 445 | } 446 | webpage += F(""); 447 | for (int s = 1; s <= number_of_channels; s++) { 448 | if ((SensorData[s].sensornumber != 0) && (SensorData[s].sensornumber <= number_of_channels)) 449 | webpage += ""; 450 | } 451 | webpage += F("
"); webpage += ChannelData[SensorData[s].sensornumber].Name; webpage += F("
"+String(SensorData[s].value1,resolution)+ChannelData[s].Field1_Units+"
"+String(SensorData[s].value2,resolution)+ChannelData[s].Field2_Units+"
"+String(SensorData[s].value3,resolution)+ChannelData[s].Field3_Units+"
"+String(SensorData[s].value4,resolution)+ChannelData[s].Field4_Units+"
" + Time(SensorData[s].readingtime).substring(0,8)+"
"); 452 | } 453 | webpage += F("

"); 454 | append_page_footer(graph_off); 455 | server.send(200,"text/html",webpage); 456 | webpage = ""; 457 | } 458 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 459 | void Auto_Update () { // Auto-refresh of the screen, this turns it on/off 460 | AUpdate = !AUpdate; 461 | HomePage(); 462 | } 463 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 464 | bool ReceivedAnySensor(){ 465 | bool sensor_received = false; 466 | for (int s = 1; s <= number_of_channels; s++) { 467 | if ((SensorData[s].sensornumber != 0) && (SensorData[s].sensornumber <= number_of_channels)) { sensor_received = true; } 468 | } 469 | return sensor_received; 470 | } 471 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 472 | void ReadChannelData(){ 473 | if (SD_present){ // If the SD-Card is present and board fitted then append the next reading to the log file called 'datalog.txt' 474 | File dataFile = SD.open("/chandata.txt", FILE_READ); 475 | int cn = 1; 476 | if (dataFile) { // if the file is available, read it 477 | String in_record; 478 | while (dataFile.available() && cn < number_of_channels) { 479 | // Note the trim function is essential for graphing to work! Fields are padded out with spaces otherwise, I don't know why... 480 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].ID = in_record.toInt(); 481 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Name = in_record; 482 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Description = in_record; 483 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Type = in_record; 484 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Field1 = in_record; 485 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Field1_Units = in_record; 486 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Field2 = in_record; 487 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Field2_Units = in_record; 488 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Field3 = in_record; 489 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Field3_Units = in_record; 490 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Field4 = in_record; 491 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Field4_Units = in_record; 492 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].IconName = in_record; 493 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Created = in_record.toInt(); 494 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Updated = in_record.toInt(); 495 | cn++; 496 | } 497 | } else ReportSDNotPresent(); 498 | dataFile.close(); 499 | } else ReportSDNotPresent(); 500 | } 501 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 502 | void SaveChannelData(){ 503 | if (SD_present){ // If the SD-Card is present and board fitted then append the next reading to the log file called 'datalog.txt' 504 | if (!SD.remove("/chandata.txt")) Serial.println(F("Failed to delete Channel Setting files")); 505 | File dataFile = SD.open("/chandata.txt", FILE_WRITE); 506 | if (dataFile) { // Save all possible channel data 507 | for (int cn= 1; cn < number_of_channels; cn++){ 508 | dataFile.println(String(ChannelData[cn].ID)+","); 509 | dataFile.println(ChannelData[cn].Name+","); 510 | dataFile.println(ChannelData[cn].Description+","); 511 | dataFile.println(ChannelData[cn].Type+","); 512 | dataFile.println(ChannelData[cn].Field1+","); 513 | dataFile.println(ChannelData[cn].Field1_Units+","); 514 | dataFile.println(ChannelData[cn].Field2+","); 515 | dataFile.println(ChannelData[cn].Field2_Units+","); 516 | dataFile.println(ChannelData[cn].Field3+","); 517 | dataFile.println(ChannelData[cn].Field3_Units+","); 518 | dataFile.println(ChannelData[cn].Field4+","); 519 | dataFile.println(ChannelData[cn].Field4_Units+","); 520 | dataFile.println(ChannelData[cn].IconName+","); 521 | dataFile.println(String(ChannelData[cn].Created)+","); 522 | dataFile.println(String(ChannelData[cn].Updated)+","); 523 | } 524 | } else ReportSDNotPresent(); 525 | dataFile.close(); 526 | } else ReportSDNotPresent(); 527 | } 528 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 529 | void Channel_File_Stream(){ 530 | if (server.args() > 0 ) { // Arguments were received 531 | if (server.hasArg("stream")) if (server.arg(0) == String(number_of_channels)) SD_file_stream("chandata"); else SD_file_stream(server.arg(0)); 532 | } 533 | else SelectInput(refresh_off,"Channel File Stream","Select a Channel to Stream","Cstream","stream",graph_on); 534 | } 535 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 536 | void SD_file_stream(String filename) { 537 | if (SD_present) { 538 | File dataFile = SD.open("/"+filename+".txt", FILE_READ); // Now read data from SD Card 539 | if (dataFile) { 540 | if (dataFile.available()) { // If data is available and present 541 | String dataType = "application/octet-stream"; 542 | if (server.streamFile(dataFile, dataType) != dataFile.size()) {Serial.print(F("Sent less data than expected!")); } 543 | } 544 | dataFile.close(); // close the file: 545 | } else ReportFileNotPresent("Cstream"); 546 | } else ReportSDNotPresent(); 547 | } 548 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 549 | void Channel_File_Download(){ 550 | if (server.args() > 0 ) { // Arguments were received 551 | if (server.hasArg("download")) if (server.arg(0) == String(number_of_channels)) SD_file_download("chandata",!open_download); else SD_file_download(server.arg(0),!open_download); 552 | } 553 | else SelectInput(refresh_off,"Channel File Download","Select a Channel to Download","Cdownload","download",graph_on); 554 | } 555 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 556 | void File_Download(){ 557 | if (server.args() > 0 ) { // Arguments were received 558 | Serial.println(server.arg(0)); 559 | if (server.hasArg("odownload")) SD_file_download(server.arg(0),open_download); 560 | } 561 | else OpenSelectInput("Select a File to Download","Odownload","odownload"); 562 | } 563 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 564 | void SD_file_download(String filename, bool download_mode){ 565 | if (SD_present) { 566 | File download = SD.open("/"+filename+(download_mode?"":".txt"), FILE_READ); // Now read data from SD Card 567 | if (download) { 568 | if (!download_mode) filename += ".txt"; 569 | server.sendHeader("Content-Type", "text/text"); 570 | server.sendHeader("Content-Disposition", "attachment; filename="+filename); 571 | server.sendHeader("Connection", "close"); 572 | server.streamFile(download, "application/octet-stream"); 573 | download.close(); 574 | } else ReportFileNotPresent("Cdownload"); 575 | } else ReportSDNotPresent(); 576 | } 577 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 578 | void Channel_File_Upload(){ 579 | append_page_header(refresh_off); 580 | webpage += F("

Select File to Upload

"); 581 | webpage += F("
"); 582 | webpage += F("
"); 583 | webpage += F("

"); 584 | webpage += F("[Back]

"); 585 | append_page_footer(graph_off); 586 | server.send(200, "text/html",webpage); 587 | } 588 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 589 | File UploadFile; 590 | void handleFileUpload(){ // upload a new file to the Filing system 591 | HTTPUpload& uploadfile = server.upload(); // See https://github.com/esp8266/Arduino/tree/master/libraries/ESP8266WebServer/srcv 592 | // For further information on 'status' structure, there are other reasons such as a failed transfer that could be used 593 | if(uploadfile.status == UPLOAD_FILE_START) 594 | { 595 | String filename = uploadfile.filename; 596 | if(!filename.startsWith("/")) filename = "/"+filename; 597 | Serial.print("Upload File Name: "); Serial.println(filename); 598 | SD.remove(filename); // Remove a previous version, otherwise data is appended the file again 599 | UploadFile = SD.open(filename, FILE_WRITE); // Open the file for writing in SPIFFS (create it, if doesn't exist) 600 | filename = String(); 601 | } 602 | else if (uploadfile.status == UPLOAD_FILE_WRITE) 603 | { 604 | if(UploadFile) UploadFile.write(uploadfile.buf, uploadfile.currentSize); // Write the received bytes to the file 605 | } 606 | else if (uploadfile.status == UPLOAD_FILE_END) 607 | { 608 | if(UploadFile) // If the file was successfully created 609 | { 610 | UploadFile.close(); // Close the file again 611 | Serial.print("Upload Size: "); Serial.println(uploadfile.totalSize); 612 | webpage = ""; 613 | append_page_header(refresh_off); 614 | webpage += F("

File was successfully uploaded

"); 615 | webpage += F("

Uploaded File Name: "); webpage += uploadfile.filename+"

"; 616 | webpage += F("

File Size: "); webpage += file_size(uploadfile.totalSize) + "


"; 617 | append_page_footer(graph_off); 618 | server.send(200,"text/html",webpage); 619 | } 620 | else 621 | { 622 | ReportCouldNotCreateFile("Cupload"); 623 | } 624 | } 625 | } 626 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 627 | void Channel_File_Erase(){ 628 | if (server.args() > 0 ) { // Arguments were received 629 | if (server.hasArg("erase")) if (server.arg(0) == String(number_of_channels)) SD_file_erase("chandata",!open_erase); else SD_file_erase(server.arg(0),!open_erase); 630 | ChannelDataReset(server.arg(0).toInt()); 631 | SaveChannelData(); 632 | } 633 | else SelectInput(refresh_off,"Channel File Erase","Select a Channel to Erase","Cerase","erase",graph_off); 634 | } 635 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 636 | void File_Erase(){ 637 | if (server.args() > 0 ) { // Arguments were received 638 | Serial.println(server.arg(0)); 639 | if (server.hasArg("oerase")) SD_file_erase(server.arg(0),open_erase); 640 | } 641 | else OpenSelectInput("Select a File to Erase","Oerase","oerase"); 642 | } 643 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 644 | void SD_file_erase(String filename, bool erase_mode) { // Erase the datalog file 645 | if (SD_present) { 646 | append_page_header(refresh_off); 647 | webpage += F("

Channel File Erase

"); 648 | File dataFile = SD.open("/"+filename+(erase_mode?"":".txt"), FILE_READ); // Now read data from SD Card 649 | if (dataFile) 650 | { 651 | if (SD.remove("/"+filename+(erase_mode?"":".txt"))) { 652 | Serial.println(F("File deleted successfully")); 653 | webpage += "

FILE: '"+filename+(erase_mode?"":".txt")+"' has been erased

"; 654 | if (erase_mode) webpage += F("[Back]

"); else webpage += F("[Back]

"); 655 | } 656 | else 657 | { 658 | webpage += F("

Channel File was not deleted - error

"); 659 | webpage += F("[Back]

"); 660 | } 661 | } else ReportFileNotPresent("Cerase"); 662 | append_page_footer(graph_off); 663 | server.send(200,"text/html",webpage); 664 | webpage = ""; 665 | } else ReportSDNotPresent(); 666 | } 667 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 668 | void SD_dir(){ 669 | if (SD_present) { 670 | File root = SD.open("/"); 671 | if (root) { 672 | root.rewindDirectory(); 673 | append_page_header(refresh_off); 674 | webpage += F("

SD Card Contents

"); 675 | webpage += F(""); 676 | webpage += F(""); 677 | printDirectory("/",0); 678 | webpage += F("
Name/TypeFile/DirSize

"); 679 | root.close(); 680 | } 681 | else 682 | { 683 | append_page_header(refresh_off); 684 | webpage += F("

No Channel Files Found

"); 685 | } 686 | append_page_footer(graph_off); 687 | server.send(200,"text/html",webpage); 688 | webpage = ""; 689 | } else ReportSDNotPresent(); 690 | } 691 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 692 | void printDirectory(const char * dirname, uint8_t levels){ 693 | Serial.printf("Listing directory: %s\n", dirname); 694 | File root = SD.open(dirname); 695 | #ifdef ESP8266 696 | root.rewindDirectory(); //Only needed for ESP8266 697 | #endif 698 | if(!root){ 699 | Serial.println("Failed to open directory"); 700 | return; 701 | } 702 | if(!root.isDirectory()){ 703 | Serial.println("Not a directory"); 704 | return; 705 | } 706 | File file = root.openNextFile(); 707 | while(file){ 708 | if(file.isDirectory()){ 709 | Serial.println(file.name()); 710 | webpage += ""+String(file.isDirectory()?"Dir":"File")+""+String(file.name())+""; 711 | printDirectory(file.name(), levels-1); 712 | } 713 | else 714 | { 715 | webpage += ""+String(file.name())+""; 716 | webpage += ""+String(file.isDirectory()?"Dir":"File")+""; 717 | webpage += ""+file_size(file.size())+""; 718 | } 719 | file = root.openNextFile(); 720 | } 721 | file.close(); 722 | } 723 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 724 | String file_size(int bytes){ 725 | String fsize = ""; 726 | if (bytes < 1024) fsize = String(bytes)+" B"; 727 | else if(bytes < (1024 * 1024)) fsize = String(bytes/1024.0,3)+" KB"; 728 | else if(bytes < (1024 * 1024 * 1024)) fsize = String(bytes/1024.0/1024.0,3)+" MB"; 729 | else fsize = String(bytes/1024.0/1024.0/1024.0,3)+" GB"; 730 | return fsize; 731 | } 732 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 733 | void DrawChart(){ 734 | graph_start = 0; 735 | graph_end = display_records; 736 | if (server.args() > 0 ) { // Arguments were received 737 | if (server.hasArg("draw")) drawchart(server.arg(0)); // parameter is sensor number 738 | } 739 | else SelectInput(refresh_off,"Channel Graph","Select a Channel to Graph","chart","draw",graph_off); 740 | } 741 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 742 | void drawchart(String filename){ // required to enable parameters with the call 743 | graph_filename = filename; // global variable requirement 744 | channel_number = filename.toInt(); 745 | readingCnt = CountFileRecords(filename); 746 | GetandGraphData(); 747 | } 748 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 749 | void GetDataForGraph(String filename, int start){ 750 | int recordCnt = 0, displayCnt = 0; 751 | String stype; 752 | graph_sensor = filename.toInt(); 753 | if (SD_present) { 754 | File dataFile = SD.open("/"+filename+".txt", FILE_READ); // Now read data from SD Card 755 | if (dataFile) { 756 | while (dataFile.available() && displayCnt < display_records) { // if the file is available, read from it 757 | DisplayData[displayCnt].ltime = dataFile.parseInt(); // 1517052559 21.58 43.71 0.00 0.00 SHT30-1 typically 758 | DisplayData[displayCnt].field1 = dataFile.parseFloat(); 759 | DisplayData[displayCnt].field2 = dataFile.parseFloat(); 760 | DisplayData[displayCnt].field3 = dataFile.parseFloat(); 761 | DisplayData[displayCnt].field4 = dataFile.parseFloat(); 762 | stype = dataFile.readStringUntil('\n'); // Needed to complete a record read 763 | if (recordCnt >= start) displayCnt++; 764 | recordCnt++; 765 | } 766 | } else ReportFileNotPresent("chart"); 767 | dataFile.close(); 768 | if (recordCnt == 0) displayCnt = 1; // In case the file is empty 769 | if (start > recordCnt) displayCnt = recordCnt; // In case the file is empty or there were not enough records. 770 | graph_start = start; 771 | graph_end = displayCnt; // Number of records to display 772 | } else ReportSDNotPresent(); 773 | } 774 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 775 | void MoveChartForward(){ 776 | graph_start += graph_step; 777 | graph_end += graph_step; 778 | if (graph_end > readingCnt) { 779 | graph_end = readingCnt; 780 | graph_start = readingCnt - display_records; 781 | if (graph_start < 0 ) graph_start = 0; 782 | } 783 | if ((graph_start > readingCnt - graph_step) && (readingCnt - graph_step > 0) ) graph_start = readingCnt - graph_step; 784 | GetandGraphData(); 785 | } 786 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 787 | void MoveChartBack(){ 788 | graph_start -= graph_step; 789 | graph_end -= graph_step; 790 | if (graph_start < 0) graph_start = 0; 791 | GetandGraphData(); 792 | } 793 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 794 | void GetandGraphData(){ 795 | GetDataForGraph(graph_filename,graph_start); 796 | GraphSelectedData(DisplayData, 797 | graph_start, 798 | graph_end, 799 | ChannelData[graph_sensor].Name); 800 | } 801 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 802 | void ConstructGraph(record_type displayed_SensorData[], int start, int number_of_records, String title, String ytitle, String yunits, String graphname){ 803 | webpage = ""; 804 | // https://developers.google.com/loader/ // https://developers.google.com/chart/interactive/docs/basic_load_libs 805 | // https://developers.google.com/chart/interactive/docs/basic_preparing_data 806 | // https://developers.google.com/chart/interactive/docs/reference#google.visualization.arraytodatatable and See appendix-A 807 | // data format is: [field-name,field-name,field-name] then [data,data,data], e.g. [12, 20.5, 70.3] 808 | webpage += F(""); 809 | webpage += F(""); 838 | SendHTML_Content(); 839 | } 840 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 841 | void GraphSelectedData(record_type displayed_SensorData[], 842 | int start, 843 | int number_of_records, 844 | String title) 845 | { 846 | // See google charts api for more details. To load the APIs, include the following script in the header of your web page. 847 | // 848 | // See https://developers.google.com/chart/interactive/docs/basic_load_libs 849 | bool graph1_on = false, graph2_on = false, graph3_on = false, graph4_on = false; 850 | String ytitle, yunits; 851 | SendHTML_Header(refresh_off); 852 | webpage += F("

"); webpage += ChannelData[graph_sensor].Name+" ("+String(readingCnt)+"-readings)

"; 853 | SendHTML_Content(); 854 | if (ChannelData[channel_number].Field1 != "") { 855 | graphfield = one; 856 | graphcolour = "red"; 857 | ytitle = ChannelData[graph_sensor].Field1; 858 | yunits = ChannelData[graph_sensor].Field1_Units; 859 | ConstructGraph(displayed_SensorData, start, number_of_records, title+" "+ytitle, ytitle, yunits, "graph1"); // 'graph1' is e.g. the tag to match the
statements below 860 | graph1_on = true; 861 | } 862 | //----------------- 863 | if (ChannelData[channel_number].Field2 != "") { 864 | graphfield = two; 865 | graphcolour = "blue"; 866 | ytitle = ChannelData[graph_sensor].Field2; 867 | yunits = ChannelData[graph_sensor].Field2_Units; 868 | ConstructGraph(displayed_SensorData, start, number_of_records, title+" "+ytitle, ytitle, yunits, "graph2"); 869 | graph2_on = true; 870 | } 871 | //----------------- 872 | if (ChannelData[channel_number].Field3 != "") { 873 | graphfield = three; 874 | graphcolour = "green"; 875 | ytitle = ChannelData[graph_sensor].Field3; 876 | yunits = ChannelData[graph_sensor].Field3_Units; 877 | ConstructGraph(displayed_SensorData, start, number_of_records, title+" "+ytitle, ytitle, yunits, "graph3"); 878 | graph3_on = true; 879 | } 880 | //----------------- 881 | if (ChannelData[channel_number].Field4 != "") { 882 | graphfield = four; 883 | graphcolour = "orange"; 884 | ytitle = ChannelData[graph_sensor].Field4; 885 | yunits = ChannelData[graph_sensor].Field4_Units; 886 | ConstructGraph(displayed_SensorData, start, number_of_records, title+" "+ytitle, ytitle, yunits, "graph4"); 887 | graph4_on = true; 888 | } 889 | //----------------- 890 | webpage += F("
"); 891 | webpage += F("
"); 892 | if (graph1_on) webpage += F("
"); 893 | if (graph2_on) webpage += F("
"); 894 | if (graph3_on) webpage += F("
"); 895 | if (graph4_on) webpage += F("
"); 896 | webpage += F("
"); 897 | webpage += F("
[Back]
"); 898 | SendHTML_Content(); // Send Content 899 | append_page_footer(graph_on); 900 | SendHTML_Content(); // Send footer 901 | SendHTML_Stop(); // Stop is needed because no content length was sent 902 | } 903 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 904 | void SendHTML_Header(bool refresh_mode){ 905 | server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); 906 | server.sendHeader("Pragma", "no-cache"); 907 | server.sendHeader("Connection", "Keep-Alive"); 908 | server.sendHeader("Expires", "-1"); 909 | server.setContentLength(CONTENT_LENGTH_UNKNOWN); 910 | server.send(200, "text/html", ""); // Empty content inhibits Content-length header 911 | append_page_header(refresh_mode); 912 | server.sendContent(webpage); 913 | server.sendContent("\n\r"); // A blank line seperates the header 914 | webpage = ""; 915 | } 916 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 917 | void SendHTML_Content(){ 918 | server.sendContent(webpage); 919 | webpage = ""; 920 | } 921 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 922 | void SendHTML_Stop(){ 923 | server.sendContent(""); 924 | server.client().stop(); // Stop is needed because no content length was sent 925 | } 926 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 927 | void SelectInput(bool refresh_mode, String heading1, String heading2, String command, String arg_calling_name, bool graph_mode_off){ 928 | append_page_header(refresh_off); 929 | webpage += F("

"); webpage += heading2 + "

"; 930 | webpage += F(""; // Must match the calling argument e.g. '/chart' calls '/chart' after selection but with arguments 931 | for (byte cn = 1; cn < number_of_channels; cn++){ 932 | webpage += F(""; 933 | } 934 | if (graph_mode_off) { webpage += F(""; } 935 | webpage += F("

"); 936 | append_page_footer(graph_off); 937 | server.send(200,"text/html",webpage); 938 | webpage = ""; 939 | } 940 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 941 | void OpenSelectInput(String heading1, String command, String arg_calling_name){ 942 | append_page_header(refresh_off); 943 | webpage += F("

"); webpage += heading1 + "

"; 944 | webpage += F(""; // Must match the calling argument e.g. '/chart' calls '/chart' after selection but with arguments! 945 | webpage += F("
"); 946 | webpage += F("

"); 947 | append_page_footer(graph_off); 948 | server.send(200,"text/html",webpage); 949 | webpage = ""; 950 | } 951 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 952 | String file_size(String filename){ // Display file size of the datalog file 953 | String ftxtsize = ""; 954 | if (SD_present) { 955 | File dataFile = SD.open("/"+filename+".txt", FILE_READ); // Now read data from SD Card 956 | if (dataFile) { 957 | int bytes = dataFile.size(); 958 | if (bytes < 1024) ftxtsize = String(bytes)+" B"; 959 | else if(bytes < (1024 * 1024)) ftxtsize = String(bytes/1024.0)+" KB"; 960 | else if(bytes < (1024 * 1024 * 1024)) ftxtsize = String(bytes/1024.0/1024.0)+" MB"; 961 | else ftxtsize = String(bytes/1024.0/1024.0/1024.0)+" GB"; 962 | } 963 | else ftxtsize = ""; 964 | dataFile.close(); 965 | return ftxtsize; 966 | } else ReportSDNotPresent(); 967 | return ""; 968 | } 969 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 970 | void ReportSDNotPresent(){ 971 | append_page_header(refresh_off); 972 | webpage += F("

No SD Card present

"); 973 | webpage += F("[Back]

"); 974 | append_page_footer(graph_off); 975 | server.send(200,"text/html",webpage); 976 | webpage = ""; 977 | } 978 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 979 | void ReportFileNotPresent(String target){ 980 | append_page_header(refresh_off); 981 | webpage += F("

File does not exist

"); 982 | webpage += F("[Back]

"; 983 | append_page_footer(graph_off); 984 | server.send(200,"text/html",webpage); 985 | webpage = ""; 986 | } 987 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 988 | void ReportCommandNotFound(String target){ 989 | append_page_header(refresh_off); 990 | webpage += F("

Function does not exist

"); 991 | webpage += F("[Back]

"; 992 | append_page_footer(graph_off); 993 | server.send(200,"text/html",webpage); 994 | webpage = ""; 995 | } 996 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 997 | void ReportCouldNotCreateFile(String target){ 998 | append_page_header(refresh_off); 999 | webpage += F("

Could Not Create Uploaded File (write-protected?)

"); 1000 | webpage += F("[Back]

"; 1001 | append_page_footer(graph_off); 1002 | server.send(200,"text/html",webpage); 1003 | webpage = ""; 1004 | } 1005 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1006 | int CountFileRecords(String filename){ 1007 | int recordCnt = 0, temp_read_int = 0; 1008 | float temp_read_flt = 0; 1009 | String temp_read_txt = ""; 1010 | if (SD_present) { 1011 | File dataFile = SD.open("/"+filename+".txt", FILE_READ); // Now read data from SD Card 1012 | if (dataFile) { 1013 | while (dataFile.available()) { // if the file is available, read from it 1014 | recordCnt++; 1015 | temp_read_int = dataFile.parseInt(); // 1517052559 21.58 43.71 0.00 0.00 SHT30-1 typically 1016 | temp_read_flt = dataFile.parseFloat(); 1017 | temp_read_flt = dataFile.parseFloat(); 1018 | temp_read_flt = dataFile.parseFloat(); 1019 | temp_read_flt = dataFile.parseFloat(); 1020 | temp_read_txt = dataFile.readStringUntil('\n'); // Needed to complete a record read 1021 | } 1022 | } 1023 | dataFile.close(); 1024 | return recordCnt; 1025 | } else ReportSDNotPresent(); 1026 | return 0; 1027 | } 1028 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1029 | void SetupTime(){ 1030 | timeval tv = {BaseTime, 0 }; // 00:00:00 01/01/2018 1031 | timezone tz = { 0 , 0 }; 1032 | settimeofday(&tv, &tz); 1033 | configTime(0, 0, "pool.ntp.org"); 1034 | setenv("TZ", "GMT0BST,M3.5.0/2,M10.5.0/2", 1); 1035 | tzset(); 1036 | time_t tnow = time(nullptr); 1037 | delay(2000); 1038 | Serial.print(F("\nWaiting for time...")); 1039 | tnow = time(nullptr); 1040 | Serial.println("Time set "+Time(tnow)); 1041 | } 1042 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1043 | String Time(int unix_time){ 1044 | struct tm *now_tm; 1045 | int hour, min, second, day, month, year; 1046 | // timeval tv = {unix_time,0}; 1047 | time_t tm = unix_time; 1048 | now_tm = localtime(&tm); 1049 | hour = now_tm->tm_hour; 1050 | min = now_tm->tm_min; 1051 | second = now_tm->tm_sec; 1052 | day = now_tm->tm_mday; 1053 | month = now_tm->tm_mon+1; 1054 | year = 1900 + now_tm->tm_year; // To get just YY information 1055 | //String days[7] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"}; 1056 | time_str = (hour<10?"0"+String(hour):String(hour))+":"+(min<10?"0"+String(min):String(min))+":"+(second<10?"0"+String(second):String(second))+"-"; 1057 | time_str += (day<10?"0"+String(day):String(day))+"/"+ (month<10?"0"+String(month):String(month))+"/"+(year<10?"0"+String(year):String(year)); // HH:MM:SS 05/07/17 1058 | //Serial.println(time_str); 1059 | return time_str; 1060 | } 1061 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1062 | int TimeNow(){ 1063 | time_t tnow = time(nullptr); 1064 | return tnow; 1065 | } 1066 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1067 | void ChannelDataReset(byte CN){ 1068 | ChannelData[CN].ID = CN; 1069 | ChannelData[CN].Name = "Name-TBA"; 1070 | ChannelData[CN].Description = "Sensor Readings"; 1071 | ChannelData[CN].Type = "e.g.SHT30"; 1072 | ChannelData[CN].Field1 = "Temperature"; 1073 | ChannelData[CN].Field1_Units = "°C"; 1074 | ChannelData[CN].Field2 = "Humidity"; 1075 | ChannelData[CN].Field2_Units = "%"; 1076 | ChannelData[CN].Field3 = "Pressure"; 1077 | ChannelData[CN].Field3_Units = "hPa"; 1078 | ChannelData[CN].Field4 = "Unused"; 1079 | ChannelData[CN].Field4_Units = ""; 1080 | ChannelData[CN].IconName = "building.png"; 1081 | ChannelData[CN].Created = BaseTime; 1082 | ChannelData[CN].Updated = BaseTime; 1083 | } 1084 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1085 | // Read file from SD Card and display it 1086 | bool loadFromSdCard(String filename){ 1087 | String dataType = "text/plain"; 1088 | if(filename.endsWith(".htm")) dataType = "text/html"; 1089 | else if(filename.endsWith(".html")) dataType = "text/html"; 1090 | else if(filename.endsWith(".css")) dataType = "text/css"; 1091 | else if(filename.endsWith(".png")) dataType = "image/png"; 1092 | else if(filename.endsWith(".gif")) dataType = "image/gif"; 1093 | else if(filename.endsWith(".jpg")) dataType = "image/jpeg"; 1094 | else if(filename.endsWith(".ico")) dataType = "image/x-icon"; 1095 | Serial.println(filename); 1096 | File dataFile = SD.open(filename.c_str()); 1097 | if (!dataFile) return false; 1098 | if(server.hasArg("download")) dataType = "application/octet-stream"; 1099 | if (server.streamFile(dataFile, dataType) != dataFile.size()) { 1100 | Serial.println("Sent less data than expected!"); 1101 | } 1102 | dataFile.close(); 1103 | return true; 1104 | } 1105 | 1106 | -------------------------------------------------------------------------------- /ESP_Sensor_Server_Advanced_4.ino: -------------------------------------------------------------------------------- 1 | /* Version 1 2 | 3 | ESP32/ESP8266 Sensor Server 4 | 5 | This software, the ideas and concepts is Copyright (c) David Bird 2018. All rights to this software are reserved. 6 | 7 | Any redistribution or reproduction of any part or all of the contents in any form is prohibited other than the following: 8 | 1. You may print or download to a local hard disk extracts for your personal and non-commercial use only. 9 | 2. You may copy the content to individual third parties for their personal use, but only if you acknowledge the author David Bird as the source of the material. 10 | 3. You may not, except with my express written permission, distribute or commercially exploit the content. 11 | 4. You may not transmit it or store it in any other website or other form of electronic retrieval system for commercial purposes. 12 | 13 | The above copyright ('as annotated') notice and this permission notice shall be included in all copies or substantial portions of the Software and where the 14 | software use is visible to an end-user. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS" FOR PRIVATE USE ONLY, IT IS NOT FOR COMMERCIAL USE IN WHOLE OR PART OR CONCEPT. FOR PERSONAL USE IT IS SUPPLIED WITHOUT WARRANTY 17 | OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHOR OR COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | See more at http://www.dsbird.org.uk 21 | 22 | */ 23 | #ifdef ESP8266 24 | #include // Built-in 25 | #include // Built-in 26 | #include // Built-in 27 | #include 28 | #include // struct timeval 29 | #else 30 | #include // Built-in 31 | #include // Built-in 32 | #include // https://github.com/Pedroalbuquerque/ESP32WebServer download and place in your Libraries folder 33 | #include 34 | #include "FS.h" 35 | #endif 36 | #include "time.h" 37 | #include "Network.h" 38 | #include "Sys_Variables.h" 39 | #include "CSS.h" 40 | #include // Built-in 41 | #include // Built-in 42 | 43 | #ifdef ESP8266 44 | ESP8266WiFiMulti wifiMulti; 45 | ESP8266WebServer server(80); 46 | #else 47 | WiFiMulti wifiMulti; 48 | WebServer server(80); 49 | #endif 50 | 51 | 52 | 53 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 54 | void setup(void) { 55 | Serial.begin(115200); 56 | //WiFi.config(ip, gateway, subnet, dns1, dns2); 57 | if (!WiFi.config(local_IP, gateway, subnet, dns)) { 58 | Serial.println("WiFi STATION Failed to configure Correctly"); 59 | } 60 | wifiMulti.addAP(ssid_1, password_1); // add Wi-Fi networks you want to connect to, it connects from strongest to weakest signal strength 61 | wifiMulti.addAP(ssid_2, password_2); 62 | wifiMulti.addAP(ssid_3, password_3); 63 | wifiMulti.addAP(ssid_4, password_4); 64 | 65 | Serial.println("Connecting ..."); 66 | while (wifiMulti.run() != WL_CONNECTED) { // Wait for the Wi-Fi to connect: scan for Wi-Fi networks, and connect to the strongest of the networks above 67 | delay(250); Serial.print('.'); 68 | } 69 | Serial.println("\nConnected to " + WiFi.SSID() + " Use IP address: " + WiFi.localIP().toString()); // Report which SSID and IP is in use 70 | // The logical name http://fileserver.local will also access the device if you have 'Bonjour' running or your system supports multicast dns 71 | if (!MDNS.begin(servername)) { // Set your preferred server name, if you use "myserver" the address would be http://myserver.local/ 72 | Serial.println(F("Error setting up MDNS responder!")); 73 | ESP.restart(); 74 | } 75 | #ifdef ESP32 76 | SPI.begin(18, 19, 23); // (SCK,MOSI,MISO) SPI pins used by most ESP32 boards. 77 | // Note: SD_Card readers on the ESP32 will NOT work unless there is a pull-up on MISO, either do this or wire a resistor (1K to 4K7) to Vcc 78 | pinMode(19, INPUT_PULLUP); 79 | pinMode(23, INPUT_PULLUP); 80 | #endif 81 | Serial.print(F("Initializing SD card...")); 82 | if (!SD.begin(SD_CS_pin)) { // see if the card is present and can be initialised. Wemos D1 Mini SD-Card shields use D8 for CS 83 | Serial.println(F("Card failed or not present, no SD Card data logging possible...")); 84 | SD_present = false; 85 | } 86 | else 87 | { 88 | Serial.println(F("Card initialised... data logging enabled...")); 89 | SD_present = true; 90 | } 91 | // Note again: Using an ESP32 and SD Card readers requires a 1K to 4K7 pull-up to 3v3 on the MISO line, otherwise they do-not function. 92 | //---------------------------------------------------------------------- 93 | ///////////////////////////// Server Commands that will be responded to 94 | server.on("/", HomePage); 95 | server.on("/test", []() { 96 | server.send(200, "text/plain", "Server status is OK"); 97 | }); // Simple server test by providing a status response 98 | server.on("/sensor", HandleSensors ); // Now associate the handler functions to the path of each function 99 | server.on("/Liveview", DisplaySensors ); 100 | server.on("/Iconview", DisplayLocations ); 101 | server.on("/Csetup", ChannelSetup ); 102 | server.on("/AUpdate", Auto_Update ); 103 | server.on("/Help", Help ); 104 | server.on("/Cstream", Channel_File_Stream ); 105 | server.on("/Cdownload", Channel_File_Download ); 106 | server.on("/Odownload", File_Download ); 107 | server.on("/Cupload", Channel_File_Upload ); 108 | server.on("/Cerase", Channel_File_Erase ); 109 | server.on("/Oerase", File_Erase ); 110 | server.on("/upload", HTTP_POST, []() { 111 | server.send(200); 112 | }, handleFileUpload); 113 | server.on("/SDdir", SD_dir ); 114 | server.on("/chart", DrawChart ); 115 | server.on("/forward", MoveChartForward ); 116 | server.on("/reverse", MoveChartBack ); 117 | server.onNotFound(handleNotFound); // When a client requests an unknown URI for example something other than "/") 118 | ///////////////////////////// End of Request commands 119 | server.begin(); 120 | Serial.println("HTTP server started"); 121 | for (int i = 0; i < number_of_channels; i++) { 122 | ChannelData[i].ID = i; 123 | } 124 | SetupTime(); 125 | ReadChannelData(); 126 | } 127 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 128 | void loop(void) { 129 | server.handleClient(); // Listen for client connections 130 | if (data_amended) { 131 | SaveChannelData(); 132 | data_amended = false; 133 | } 134 | } 135 | // Functions from here... 136 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 137 | void HomePage() { 138 | SendHTML_Header(refresh_off); 139 | webpage += F(""); 140 | webpage += F(""); 141 | webpage += F(""); 142 | webpage += F(""); 143 | webpage += F("


"); 144 | webpage += F(""); 145 | webpage += F(""); 146 | webpage += F(""); 147 | webpage += F(""); 148 | webpage += F("


"); 149 | append_page_footer(graph_off); 150 | SendHTML_Content(); 151 | SendHTML_Stop(); 152 | webpage = ""; 153 | } 154 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 155 | void Help() { 156 | SendHTML_Header(refresh_off); 157 | webpage += F("

Sensor Server Help

"); 158 | webpage += F("

Help section: The Server collects sensor readings and stores them in a Channel and then displayed according to the Channel settings.\ 159 | Data fields are numeric and can be interpreted as the units of your choice. All data from sensors is saved sequentially to the SD-Card and there is a file \ 160 | for each Channel.

"); 161 | webpage += F("

For example, Channel-1 data is saved in a file called '1.txt' and Channel-2 in '2.txt' and so-on. The contents of a Channel file can be streamed \ 162 | to the web-browser, graphed, downloaded, deleted or amended and then uploaded back again.


"); 163 | webpage += F(""); 164 | webpage += F(""); 165 | webpage += F(""); 166 | webpage += F(""); 167 | webpage += F(""); 168 | webpage += F(""); 169 | webpage += F(""); 170 | webpage += F(""); 171 | webpage += F(""); 172 | webpage += F(""); 173 | webpage += F(""); 174 | webpage += F(""); 175 | webpage += F(""); 176 | webpage += F(""); 177 | webpage += F(""); 178 | webpage += F(""); 179 | webpage += F("
Use the menu options to:
'Refresh'Toggle on/off automatic screen refresh of 30-secs
'View Channels'Shows sensor data for each channel as it is received
'View Locations'Shows an Icon and data for each channel as it is received
'Graph Channels'Graph Channel Readings
'Setup Channels'Edit the Channel Name, Description, Sensor Type and Units
'Stream Channel Data'Stream Channel Data to your browser
'Download Channel Data'Download Channel Data to a file
'[Download] File'Download any file
After download into Excel use the formula '=(((DataCell/60)/60)/24)+DATE(1970,1,1)' to convert UNIX time to a Date-Time
e.g If A1 = 1517059610 (unix time) and A2 is empty use A2 = (((A1/60)/60)/24)+DATE(1970,1,1)' now A2 = HH:MM;SS-DD/MM/YY
Set the format of cell A2 to Custom, then dd/mm/yyyy hh:mm
'Upload Channel Data'Upload Channel Data file
'Erase Channel Data'Erase Channel Data file
'[Erase] File'Erase any file
'File Directory'List files on the SD-Card

"); 180 | SendHTML_Content(); 181 | append_page_footer(graph_off); 182 | SendHTML_Content(); 183 | SendHTML_Stop(); 184 | webpage = ""; 185 | } 186 | //~~~~~~~~~~~~~~la~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 187 | void ChannelSetup() { 188 | if (server.args() > 0 ) { // Arguments were received 189 | data_amended = true; 190 | for (byte ArgNum = 0; ArgNum <= server.args(); ArgNum++) { 191 | if (server.argName(ArgNum) == "chan_name") { 192 | ChannelData[channel_number].Name = server.arg(ArgNum); 193 | } 194 | if (server.argName(ArgNum) == "chan_desc") { 195 | ChannelData[channel_number].Description = server.arg(ArgNum); 196 | } 197 | if (server.argName(ArgNum) == "chan_type") { 198 | ChannelData[channel_number].Type = server.arg(ArgNum); 199 | } 200 | if (server.argName(ArgNum) == "chan_field1") { 201 | ChannelData[channel_number].Field1 = server.arg(ArgNum); 202 | } 203 | if (server.argName(ArgNum) == "chan_field1_units") { 204 | ChannelData[channel_number].Field1_Units = server.arg(ArgNum); 205 | } 206 | if (server.argName(ArgNum) == "chan_field2") { 207 | ChannelData[channel_number].Field2 = server.arg(ArgNum); 208 | } 209 | if (server.argName(ArgNum) == "chan_field2_units") { 210 | ChannelData[channel_number].Field2_Units = server.arg(ArgNum); 211 | } 212 | if (server.argName(ArgNum) == "chan_field3") { 213 | ChannelData[channel_number].Field3 = server.arg(ArgNum); 214 | } 215 | if (server.argName(ArgNum) == "chan_field3_units") { 216 | ChannelData[channel_number].Field3_Units = server.arg(ArgNum); 217 | } 218 | if (server.argName(ArgNum) == "chan_field4") { 219 | ChannelData[channel_number].Field4 = server.arg(ArgNum); 220 | } 221 | if (server.argName(ArgNum) == "chan_field4_units") { 222 | ChannelData[channel_number].Field4_Units = server.arg(ArgNum); 223 | } 224 | if (server.argName(ArgNum) == "iconname") { 225 | ChannelData[channel_number].IconName = server.arg(ArgNum); 226 | } 227 | ChannelData[channel_number].Created = TimeNow(); 228 | } 229 | } 230 | if (server.hasArg("edit_c1")) { 231 | channel_number = 1; 232 | ChannelEditor(channel_number); 233 | } 234 | else if (server.hasArg("edit_c2")) { 235 | channel_number = 2; 236 | ChannelEditor(channel_number); 237 | } 238 | else if (server.hasArg("edit_c3")) { 239 | channel_number = 3; 240 | ChannelEditor(channel_number); 241 | } 242 | else if (server.hasArg("edit_c4")) { 243 | channel_number = 4; 244 | ChannelEditor(channel_number); 245 | } 246 | else if (server.hasArg("edit_c5")) { 247 | channel_number = 5; 248 | ChannelEditor(channel_number); 249 | } 250 | else if (server.hasArg("edit_c6")) { 251 | channel_number = 6; 252 | ChannelEditor(channel_number); 253 | } 254 | else if (server.hasArg("edit_c7")) { 255 | channel_number = 7; 256 | ChannelEditor(channel_number); 257 | } 258 | else if (server.hasArg("edit_c8")) { 259 | channel_number = 8; 260 | ChannelEditor(channel_number); 261 | } 262 | else if (server.hasArg("edit_c9")) { 263 | channel_number = 9; 264 | ChannelEditor(channel_number); 265 | } 266 | else if (server.hasArg("edit_c10")) { 267 | channel_number = 10; 268 | ChannelEditor(channel_number); 269 | } 270 | else if (server.hasArg("edit_c11")) { 271 | channel_number = 11; 272 | ChannelEditor(channel_number); 273 | } 274 | else if (server.hasArg("edit_c12")) { 275 | channel_number = 12; // NOTE: *** Add more channels here to match the 'number_of_channels' value 276 | ChannelEditor(channel_number); 277 | } 278 | else 279 | { 280 | SendHTML_Header(refresh_off); 281 | webpage += F("

Channel Setup/Editor


"); 282 | webpage += F(""); 283 | webpage += F(""); 284 | webpage += F(""); 285 | for (byte cn = 1; cn < number_of_channels; cn++) { 286 | webpage += F(""; 288 | webpage += ""; 289 | if (ChannelData[cn].Updated == BaseTime) webpage += F(""); 290 | else 291 | { 292 | webpage += ""; 294 | } 295 | } 296 | webpage += F("
Channel IDNameDescriptionCreated on:Last Updated at:File Size
"); 287 | webpage += String(ChannelData[cn].ID) + "" + ChannelData[cn].Name + "" + ChannelData[cn].Description + "" + Time(ChannelData[cn].Created).substring(9) + "-
" + Time(ChannelData[cn].Updated) + ""; 293 | webpage += String(file_size(String(cn))) + "
"); 297 | webpage += F("

Select channel to View/Edit

"); 298 | webpage += F(""); 299 | for (byte in_select = 1; in_select < number_of_channels; in_select++) { 300 | webpage += F(""; 301 | } 302 | webpage += F("

"); 303 | SendHTML_Content(); 304 | append_page_footer(graph_off); 305 | SendHTML_Content(); 306 | SendHTML_Stop(); 307 | webpage = ""; 308 | } 309 | } 310 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 311 | void ChannelEditor(int channel_number) { 312 | SendHTML_Header(refresh_off); 313 | webpage += F("

Channel Editor

"); 314 | webpage += F(""); 315 | webpage += F(""; 317 | //--- 318 | webpage += F(""; 319 | webpage += F(""; 321 | //--- 322 | webpage += F(""; 323 | webpage += F(""; 324 | //--- 325 | webpage += F(""; 326 | webpage += F(""; 327 | //--- 328 | webpage += F(""; 329 | webpage += F(""; 330 | webpage += F(""; 331 | SendHTML_Content(); 332 | //--- 333 | webpage += F(""; 334 | webpage += F(""; 335 | webpage += F(""; 336 | //--- 337 | webpage += F(""; 338 | webpage += F(""; 339 | webpage += F(""; 340 | //--- 341 | webpage += F(""; 342 | webpage += F(""; 343 | webpage += F(""; 344 | //--- 345 | webpage += F(""; 346 | webpage += F(""; 347 | //--- 348 | webpage += F("
ID:"); 316 | webpage += String(ChannelData[channel_number].ID) + "Edit EntriesEdit Units
Name:"); webpage += ChannelData[channel_number].Name + "
Description:"); webpage += ChannelData[channel_number].Description + "
Sensor Type:"); webpage += ChannelData[channel_number].Type + "
Field-1 Name:"); webpage += ChannelData[channel_number].Field1 + "
Field-2 Name:"); webpage += ChannelData[channel_number].Field2 + "
Field-3 Name:"); webpage += ChannelData[channel_number].Field3 + "
Field-4 Name:"); webpage += ChannelData[channel_number].Field4 + "
Icon Name:"); webpage += ChannelData[channel_number].IconName + "
"); 349 | webpage += F("

"); 350 | webpage += F("[Back]

"); 351 | append_page_footer(graph_off); 352 | SendHTML_Content(); 353 | SendHTML_Stop(); 354 | webpage = ""; 355 | if (data_amended) SaveChannelData(); 356 | } 357 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 358 | boolean isValidNumber(String str) { 359 | str.trim(); 360 | if (!(str.charAt(0) == '+' || str.charAt(0) == '-' || isDigit(str.charAt(0)))) return false; // Failed if not starting with a unary +- sign or a number 361 | for (byte i = 1; i < str.length(); i++) { 362 | if (!(isDigit(str.charAt(i)) || str.charAt(i) == '.')) return false; // Anything other than a number or . is a failure 363 | } 364 | return true; 365 | } 366 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 367 | void handleNotFound() { 368 | if (SD_present && loadFromSdCard(server.uri())) return; // Process a file if requested 369 | ReportCommandNotFound(""); // go back to home page if erroneous command entered 370 | } 371 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 372 | // This function handles many arguments e.g. http://sensorserver.local/sensor?Sensor=1&Temperature=11.1&Humidity=22.2&Pressure=1000.0mb&Value=5.0&sensortype=BME280 373 | // e.g. http://sensorserver.local/sensor?Sensor=2&Temperature=22.2&Humidity=44.4&Pressure=1002.2mb 374 | // e.g. http://sensorserver.local/sensor?Sensor=3&Temperature=33.3&Humidity=66.6&Pressure=1003.3mb 375 | // Update the links above to your own name e.g. sensorserver becomes 'myserver.local' and click on the links to test the server! 376 | void HandleSensors() { //Server request handler 377 | Serial.println("Handle sensor reception"); 378 | if (server.argName(0) != "") Serial.print("Received from: "); 379 | for (int i = 0; i <= server.args(); i++) Serial.println(server.argName(i) + " " + server.arg(i)); // Display received arguments from sensor 380 | int sensor_num = server.arg(0).toInt(); 381 | if (sensor_num > number_of_channels) sensor_num = 0; // Channel-0 is a null channel, assigned when incorrect data is received 382 | if ((sensor_num >= 1 && sensor_num < number_of_channels) && ((server.argName(0) == "Sensor") || (server.argName(0) == "sensor"))) { 383 | ChannelData[sensor_num].Updated = TimeNow(); // A valid sensor has been received, so update time-received record 384 | sensor_reading = sensor_num; // Each sensor has its own record, so Sensor-1 readings go in record-1, etc 385 | for (int i = 0; i <= server.args(); i++) { 386 | if (sensor_num != 0 && sensor_num <= number_of_channels) { // If a valid sensor number then process 387 | SensorData[sensor_reading].sensornumber = (byte)sensor_num; // Max. 255 sensors 388 | if (i == 1) SensorData[sensor_reading].value1 = server.arg(i).toFloat(); 389 | if (i == 2) SensorData[sensor_reading].value2 = server.arg(i).toFloat(); 390 | if (i == 3) SensorData[sensor_reading].value3 = server.arg(i).toFloat(); 391 | if (i == 4) SensorData[sensor_reading].value4 = server.arg(i).toFloat(); 392 | if (i == 5) SensorData[sensor_reading].sensortype = server.arg(i); 393 | } 394 | else Serial.println("Sensor number rejected"); 395 | } 396 | SensorData[sensor_reading].readingtime = TimeNow(); 397 | if (SD_present) { // If the SD-Card is present and board fitted then append the next reading to the log file called 'datalog.txt' 398 | File dataFile = SD.open("/" + String(sensor_num) + ".txt", FILE_READ); // Check to see if a file exists 399 | if (!dataFile) { // If not update its creation date 400 | ChannelData[sensor_num].Created = TimeNow(); 401 | } 402 | dataFile.close(); 403 | #ifdef ESP8266 404 | dataFile = SD.open("/" + String(sensor_num) + ".txt", FILE_WRITE); //On the ESP8266 FILE_WRITE opens file for writing and move to the end of the file 405 | #else 406 | dataFile = SD.open("/" + String(sensor_num) + ".txt", FILE_APPEND); //On the ESP32 it needs FILE_APPEND to open file for writing and movs to the end of the file 407 | #endif 408 | if (dataFile) { // if the file is available, write to it 409 | dataFile.print(SensorData[sensor_reading].readingtime); dataFile.print(char(9)); // TAB delimited data 410 | dataFile.print(SensorData[sensor_reading].value1); dataFile.print(char(9)); 411 | dataFile.print(SensorData[sensor_reading].value2); dataFile.print(char(9)); 412 | dataFile.print(SensorData[sensor_reading].value3); dataFile.print(char(9)); 413 | dataFile.print(SensorData[sensor_reading].value4); dataFile.print(char(9)); 414 | dataFile.println(SensorData[sensor_reading].sensortype); 415 | } 416 | dataFile.close(); 417 | } 418 | } 419 | DisplaySensors(); 420 | } 421 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 422 | void DisplaySensors() { 423 | const byte resolution = 1; // 1 = 20.2 or 2 = 20.23 or 3 = 20.234 displayed data if the sensor supports the resolution 424 | SendHTML_Header(refresh_on); 425 | webpage = F("
"); 426 | if (!ReceivedAnySensor()) { 427 | webpage += F("

*** Waiting For Data Reception ***

"); 428 | Serial.println("No sensors received"); 429 | } 430 | else 431 | { 432 | webpage += F("

Current Sensor Readings

"); 433 | webpage += F("
"); // Add horizontal scrolling if number of fields exceeds page width 434 | webpage += F(""); 435 | // NOTE: ****** You might think there is a more efficient way to do the next section, there is, except using a FOR loop, but the ESP32 gives stack errors, it's a compiler error!! 436 | if (SensorData[0].sensornumber != 0 && SensorData[0].sensornumber <= number_of_channels) webpage += ""; 437 | if (SensorData[1].sensornumber != 0 && SensorData[1].sensornumber <= number_of_channels) webpage += ""; 438 | if (SensorData[2].sensornumber != 0 && SensorData[2].sensornumber <= number_of_channels) webpage += ""; 439 | if (SensorData[3].sensornumber != 0 && SensorData[3].sensornumber <= number_of_channels) webpage += ""; 440 | if (SensorData[4].sensornumber != 0 && SensorData[4].sensornumber <= number_of_channels) webpage += ""; 441 | if (SensorData[5].sensornumber != 0 && SensorData[5].sensornumber <= number_of_channels) webpage += ""; 442 | if (SensorData[6].sensornumber != 0 && SensorData[6].sensornumber <= number_of_channels) webpage += ""; 443 | // if (SensorData[7].sensornumber != 0 && SensorData[7].sensornumber <= number_of_channels) webpage += ""; 444 | // if (SensorData[8].sensornumber != 0 && SensorData[8].sensornumber <= number_of_channels) webpage += ""; 445 | // if (SensorData[9].sensornumber != 0 && SensorData[9].sensornumber <= number_of_channels) webpage += ""; 446 | // if (SensorData[10].sensornumber != 0 && SensorData[10].sensornumber <= number_of_channels) webpage += ""; 447 | // if (SensorData[11].sensornumber != 0 && SensorData[11].sensornumber <= number_of_channels) webpage += ""; 448 | // if (SensorData[12].sensornumber != 0 && SensorData[12].sensornumber <= number_of_channels) webpage += ""; 449 | webpage += F(""); 450 | if ((SensorData[0].sensornumber != 0) && (SensorData[0].sensornumber <= number_of_channels)) webpage += ""; 451 | if ((SensorData[1].sensornumber != 0) && (SensorData[1].sensornumber <= number_of_channels)) webpage += ""; 452 | if ((SensorData[2].sensornumber != 0) && (SensorData[2].sensornumber <= number_of_channels)) webpage += ""; 453 | if ((SensorData[3].sensornumber != 0) && (SensorData[3].sensornumber <= number_of_channels)) webpage += ""; 454 | if ((SensorData[4].sensornumber != 0) && (SensorData[4].sensornumber <= number_of_channels)) webpage += ""; 455 | if ((SensorData[5].sensornumber != 0) && (SensorData[5].sensornumber <= number_of_channels)) webpage += ""; 456 | if ((SensorData[6].sensornumber != 0) && (SensorData[6].sensornumber <= number_of_channels)) webpage += ""; 457 | // if ((SensorData[7].sensornumber != 0) && (SensorData[7].sensornumber <= number_of_channels)) webpage += ""; 458 | // if ((SensorData[8].sensornumber != 0) && (SensorData[8].sensornumber <= number_of_channels)) webpage += ""; 459 | // if ((SensorData[9].sensornumber != 0) && (SensorData[9].sensornumber <= number_of_channels)) webpage += ""; 460 | // if ((SensorData[10].sensornumber != 0) && (SensorData[10].sensornumber <= number_of_channels)) webpage += ""; 461 | // if ((SensorData[11].sensornumber != 0) && (SensorData[11].sensornumber <= number_of_channels)) webpage += ""; 462 | // if ((SensorData[12].sensornumber != 0) && (SensorData[12].sensornumber <= number_of_channels)) webpage += ""; 463 | webpage += F(""); 464 | if (SensorData[0].sensornumber != 0 && SensorData[0].sensornumber <= number_of_channels) webpage += ""; 465 | if (SensorData[1].sensornumber != 0 && SensorData[1].sensornumber <= number_of_channels) webpage += ""; 466 | if (SensorData[2].sensornumber != 0 && SensorData[2].sensornumber <= number_of_channels) webpage += ""; 467 | if (SensorData[3].sensornumber != 0 && SensorData[3].sensornumber <= number_of_channels) webpage += ""; 468 | if (SensorData[4].sensornumber != 0 && SensorData[4].sensornumber <= number_of_channels) webpage += ""; 469 | if (SensorData[5].sensornumber != 0 && SensorData[5].sensornumber <= number_of_channels) webpage += ""; 470 | if (SensorData[6].sensornumber != 0 && SensorData[6].sensornumber <= number_of_channels) webpage += ""; 471 | // if (SensorData[7].sensornumber != 0 && SensorData[7].sensornumber <= number_of_channels) webpage += ""; 472 | // if (SensorData[8].sensornumber != 0 && SensorData[8].sensornumber <= number_of_channels) webpage += ""; 473 | // if (SensorData[9].sensornumber != 0 && SensorData[9].sensornumber <= number_of_channels) webpage += ""; 474 | // if (SensorData[10].sensornumber != 0 && SensorData[10].sensornumber <= number_of_channels) webpage += ""; 475 | // if (SensorData[11].sensornumber != 0 && SensorData[11].sensornumber <= number_of_channels) webpage += ""; 476 | // if (SensorData[12].sensornumber != 0 && SensorData[12].sensornumber <= number_of_channels) webpage += ""; 477 | webpage += F(""); 478 | if (SensorData[0].sensornumber != 0 && SensorData[0].sensornumber <= number_of_channels) webpage += ""; 479 | if (SensorData[1].sensornumber != 0 && SensorData[1].sensornumber <= number_of_channels) webpage += ""; 480 | if (SensorData[2].sensornumber != 0 && SensorData[2].sensornumber <= number_of_channels) webpage += ""; 481 | if (SensorData[3].sensornumber != 0 && SensorData[3].sensornumber <= number_of_channels) webpage += ""; 482 | if (SensorData[4].sensornumber != 0 && SensorData[4].sensornumber <= number_of_channels) webpage += ""; 483 | if (SensorData[5].sensornumber != 0 && SensorData[5].sensornumber <= number_of_channels) webpage += ""; 484 | if (SensorData[6].sensornumber != 0 && SensorData[6].sensornumber <= number_of_channels) webpage += ""; 485 | // if (SensorData[7].sensornumber != 0 && SensorData[7].sensornumber <= number_of_channels) webpage += ""; 486 | // if (SensorData[8].sensornumber != 0 && SensorData[8].sensornumber <= number_of_channels) webpage += ""; 487 | // if (SensorData[9].sensornumber != 0 && SensorData[9].sensornumber <= number_of_channels) webpage += ""; 488 | // if (SensorData[10].sensornumber != 0 && SensorData[10].sensornumber <= number_of_channels) webpage += ""; 489 | // if (SensorData[11].sensornumber != 0 && SensorData[11].sensornumber <= number_of_channels) webpage += ""; 490 | // if (SensorData[12].sensornumber != 0 && SensorData[12].sensornumber <= number_of_channels) webpage += ""; 491 | SendHTML_Content(); 492 | webpage += F(""); 493 | if (SensorData[0].sensornumber != 0 && SensorData[0].sensornumber <= number_of_channels) webpage += ""; 494 | if (SensorData[1].sensornumber != 0 && SensorData[1].sensornumber <= number_of_channels) webpage += ""; 495 | if (SensorData[2].sensornumber != 0 && SensorData[2].sensornumber <= number_of_channels) webpage += ""; 496 | if (SensorData[3].sensornumber != 0 && SensorData[3].sensornumber <= number_of_channels) webpage += ""; 497 | if (SensorData[4].sensornumber != 0 && SensorData[4].sensornumber <= number_of_channels) webpage += ""; 498 | if (SensorData[5].sensornumber != 0 && SensorData[5].sensornumber <= number_of_channels) webpage += ""; 499 | if (SensorData[6].sensornumber != 0 && SensorData[6].sensornumber <= number_of_channels) webpage += ""; 500 | // if (SensorData[7].sensornumber != 0 && SensorData[7].sensornumber <= number_of_channels) webpage += ""; 501 | // if (SensorData[8].sensornumber != 0 && SensorData[8].sensornumber <= number_of_channels) webpage += ""; 502 | // if (SensorData[9].sensornumber != 0 && SensorData[9].sensornumber <= number_of_channels) webpage += ""; 503 | // if (SensorData[10].sensornumber != 0 && SensorData[10].sensornumber <= number_of_channels) webpage += ""; 504 | // if (SensorData[11].sensornumber != 0 && SensorData[11].sensornumber <= number_of_channels) webpage += ""; 505 | // if (SensorData[12].sensornumber != 0 && SensorData[12].sensornumber <= number_of_channels) webpage += ""; 506 | webpage += F(""); 507 | if (SensorData[0].sensornumber != 0 && SensorData[0].sensornumber <= number_of_channels) webpage += ""; 508 | if (SensorData[1].sensornumber != 0 && SensorData[1].sensornumber <= number_of_channels) webpage += ""; 509 | if (SensorData[2].sensornumber != 0 && SensorData[2].sensornumber <= number_of_channels) webpage += ""; 510 | if (SensorData[3].sensornumber != 0 && SensorData[3].sensornumber <= number_of_channels) webpage += ""; 511 | if (SensorData[4].sensornumber != 0 && SensorData[4].sensornumber <= number_of_channels) webpage += ""; 512 | if (SensorData[5].sensornumber != 0 && SensorData[5].sensornumber <= number_of_channels) webpage += ""; 513 | if (SensorData[6].sensornumber != 0 && SensorData[6].sensornumber <= number_of_channels) webpage += ""; 514 | // if (SensorData[7].sensornumber != 0 && SensorData[7].sensornumber <= number_of_channels) webpage += ""; 515 | // if (SensorData[8].sensornumber != 0 && SensorData[8].sensornumber <= number_of_channels) webpage += ""; 516 | // if (SensorData[9].sensornumber != 0 && SensorData[9].sensornumber <= number_of_channels) webpage += ""; 517 | // if (SensorData[10].sensornumber != 0 && SensorData[10].sensornumber <= number_of_channels) webpage += ""; 518 | // if (SensorData[11].sensornumber != 0 && SensorData[11].sensornumber <= number_of_channels) webpage += ""; 519 | // if (SensorData[12].sensornumber != 0 && SensorData[12].sensornumber <= number_of_channels) webpage += ""; 520 | webpage += F(""); 521 | if ((SensorData[0].sensornumber != 0) && (SensorData[0].sensornumber <= number_of_channels)) webpage += ""; 522 | if ((SensorData[1].sensornumber != 0) && (SensorData[1].sensornumber <= number_of_channels)) webpage += ""; 523 | if ((SensorData[2].sensornumber != 0) && (SensorData[2].sensornumber <= number_of_channels)) webpage += ""; 524 | if ((SensorData[3].sensornumber != 0) && (SensorData[3].sensornumber <= number_of_channels)) webpage += ""; 525 | if ((SensorData[4].sensornumber != 0) && (SensorData[4].sensornumber <= number_of_channels)) webpage += ""; 526 | if ((SensorData[5].sensornumber != 0) && (SensorData[5].sensornumber <= number_of_channels)) webpage += ""; 527 | if ((SensorData[6].sensornumber != 0) && (SensorData[6].sensornumber <= number_of_channels)) webpage += ""; 528 | // if ((SensorData[7].sensornumber != 0) && (SensorData[7].sensornumber <= number_of_channels)) webpage += ""; 529 | // if ((SensorData[8].sensornumber != 0) && (SensorData[8].sensornumber <= number_of_channels)) webpage += ""; 530 | // if ((SensorData[9].sensornumber != 0) && (SensorData[9].sensornumber <= number_of_channels)) webpage += ""; 531 | // if ((SensorData[10].sensornumber != 0) && (SensorData[10].sensornumber <= number_of_channels)) webpage += ""; 532 | // if ((SensorData[11].sensornumber != 0) && (SensorData[11].sensornumber <= number_of_channels)) webpage += ""; 533 | // if ((SensorData[12].sensornumber != 0) && (SensorData[12].sensornumber <= number_of_channels)) webpage += ""; 534 | webpage += F(""); 535 | if ((SensorData[0].sensornumber != 0) && (SensorData[0].sensornumber <= number_of_channels)); webpage += ""; 536 | if ((SensorData[1].sensornumber != 0) && (SensorData[1].sensornumber <= number_of_channels)); webpage += ""; 537 | if ((SensorData[2].sensornumber != 0) && (SensorData[2].sensornumber <= number_of_channels)); webpage += ""; 538 | if ((SensorData[3].sensornumber != 0) && (SensorData[3].sensornumber <= number_of_channels)); webpage += ""; 539 | if ((SensorData[4].sensornumber != 0) && (SensorData[4].sensornumber <= number_of_channels)); webpage += ""; 540 | if ((SensorData[5].sensornumber != 0) && (SensorData[5].sensornumber <= number_of_channels)); webpage += ""; 541 | if ((SensorData[6].sensornumber != 0) && (SensorData[6].sensornumber <= number_of_channels)); webpage += ""; 542 | // if ((SensorData[7].sensornumber != 0) && (SensorData[7].sensornumber <= number_of_channels)) webpage += ""; 543 | // if ((SensorData[8].sensornumber != 0) && (SensorData[8].sensornumber <= number_of_channels)) webpage += ""; 544 | // if ((SensorData[9].sensornumber != 0) && (SensorData[9].sensornumber <= number_of_channels)) webpage += ""; 545 | // if ((SensorData[10].sensornumber != 0) && (SensorData[10].sensornumber <= number_of_channels)) webpage += ""; 546 | // if ((SensorData[11].sensornumber != 0) && (SensorData[11].sensornumber <= number_of_channels)) webpage += ""; 547 | // if ((SensorData[12].sensornumber != 0) && (SensorData[12].sensornumber <= number_of_channels)) webpage += ""; 548 | webpage += F("
Sensor Name" + String(ChannelData[SensorData[0].sensornumber].Name) + "" + String(ChannelData[SensorData[1].sensornumber].Name) + "" + String(ChannelData[SensorData[2].sensornumber].Name) + "" + String(ChannelData[SensorData[3].sensornumber].Name) + "" + String(ChannelData[SensorData[4].sensornumber].Name) + "" + String(ChannelData[SensorData[5].sensornumber].Name) + "" + String(ChannelData[SensorData[6].sensornumber].Name) + ""+String(ChannelData[SensorData[7].sensornumber].Name)+""+String(ChannelData[SensorData[8].sensornumber].Name)+""+String(ChannelData[SensorData[9].sensornumber].Name)+""+String(ChannelData[SensorData[10].sensornumber].Name)+""+String(ChannelData[SensorData[11].sensornumber].Name)+""+String(ChannelData[SensorData[12].sensornumber].Name)+"
Sensor Number" + String(SensorData[0].sensornumber) + "" + String(SensorData[1].sensornumber) + "" + String(SensorData[2].sensornumber) + "" + String(SensorData[3].sensornumber) + "" + String(SensorData[4].sensornumber) + "" + String(SensorData[5].sensornumber) + "" + String(SensorData[6].sensornumber) + "" + String(SensorData[7].sensornumber) + "" + String(SensorData[8].sensornumber) + "" + String(SensorData[9].sensornumber) + "" + String(SensorData[10].sensornumber) + "" + String(SensorData[11].sensornumber) + "" + String(SensorData[12].sensornumber) + "
Type" + ChannelData[0].Type + "" + ChannelData[1].Type + "" + ChannelData[2].Type + "" + ChannelData[3].Type + "" + ChannelData[4].Type + "" + ChannelData[5].Type + "" + ChannelData[6].Type + "" + ChannelData[7].Type + "" + ChannelData[8].Type + "" + ChannelData[9].Type + "" + ChannelData[10].Type + "" + ChannelData[11].Type + "" + ChannelData[12].Type + "
Field-1" + (String)(SensorData[0].value1, resolution) + ChannelData[0].Field1_Units + "" + (String)(SensorData[1].value1, resolution) + ChannelData[1].Field1_Units + "" + (String)(SensorData[2].value1, resolution) + ChannelData[2].Field1_Units + "" + (String)(SensorData[3].value1, resolution) + ChannelData[3].Field1_Units + "" + (String)(SensorData[4].value1, resolution) + ChannelData[4].Field1_Units + "" + (String)(SensorData[5].value1, resolution) + ChannelData[5].Field1_Units + "" + (String)(SensorData[6].value1, resolution) + ChannelData[6].Field1_Units + "" + String(SensorData[7].value1, resolution) + ChannelData[7].Field1_Units + "" + String(SensorData[8].value1, resolution) + ChannelData[8].Field1_Units + "" + String(SensorData[9].value1, resolution) + ChannelData[9].Field1_Units + "" + String(SensorData[10].value1, resolution) + ChannelData[10].Field1_Units + "" + String(SensorData[11].value1, resolution) + ChannelData[11].Field1_Units + "" + String(SensorData[12].value1, resolution) + ChannelData[12].Field1_Units + "
Field-2" + (String)(SensorData[0].value2, resolution) + ChannelData[0].Field2_Units + "" + (String)(SensorData[1].value2, resolution) + ChannelData[1].Field2_Units + "" + (String)(SensorData[2].value2, resolution) + ChannelData[2].Field2_Units + "" + (String)(SensorData[3].value2, resolution) + ChannelData[3].Field2_Units + "" + (String)(SensorData[4].value2, resolution) + ChannelData[4].Field2_Units + "" + (String)(SensorData[5].value2, resolution) + ChannelData[5].Field2_Units + "" + (String)(SensorData[6].value2, resolution) + ChannelData[6].Field2_Units + "" + (String)(SensorData[7].value2, resolution) + ChannelData[7].Field2_Units + "" + (String)(SensorData[8].value2, resolution) + ChannelData[8].Field2_Units + "" + (String)(SensorData[9].value2, resolution) + ChannelData[9].Field2_Units + "" + (String)(SensorData[10].value2, resolution) + ChannelData[10].Field2_Units + "" + (String)(SensorData[11].value2, resolution) + ChannelData[11].Field2_Units + "" + (String)(SensorData[12].value2, resolution) + ChannelData[12].Field2_Units + "
Field-3" + (String)(SensorData[0].value3, resolution) + ChannelData[0].Field3_Units + "" + (String)(SensorData[1].value3, resolution) + ChannelData[1].Field3_Units + "" + (String)(SensorData[2].value3, resolution) + ChannelData[2].Field3_Units + "" + (String)(SensorData[3].value3, resolution) + ChannelData[3].Field3_Units + "" + (String)(SensorData[4].value3, resolution) + ChannelData[4].Field3_Units + "" + (String)(SensorData[5].value3, resolution) + ChannelData[5].Field3_Units + "" + (String)(SensorData[6].value3, resolution) + ChannelData[6].Field3_Units + "" + (String)(SensorData[7].value3, resolution) + ChannelData[7].Field3_Units + "" + (String)(SensorData[8].value3, resolution) + ChannelData[8].Field3_Units + "" + (String)(SensorData[9].value3, resolution) + ChannelData[9].Field3_Units + "" + (String)(SensorData[10].value3, resolution) + ChannelData[10].Field3_Units + "" + (String)(SensorData[11].value3, resolution) + ChannelData[11].Field3_Units + "" + (String)(SensorData[12].value3, resolution) + ChannelData[12].Field3_Units + "
Field-4" + (String)(SensorData[0].value4, resolution) + ChannelData[0].Field4_Units + "" + (String)(SensorData[1].value4, resolution) + ChannelData[1].Field4_Units + "" + (String)(SensorData[2].value4, resolution) + ChannelData[2].Field4_Units + "" + (String)(SensorData[3].value4, resolution) + ChannelData[3].Field4_Units + "" + (String)(SensorData[4].value4, resolution) + ChannelData[4].Field4_Units + "" + (String)(SensorData[5].value4, resolution) + ChannelData[5].Field4_Units + "" + (String)(SensorData[6].value4, resolution) + ChannelData[6].Field4_Units + "" + (String)(SensorData[7].value4, resolution) + ChannelData[7].Field4_Units + "" + (String)(SensorData[8].value4, resolution) + ChannelData[8].Field4_Units + "" + (String)(SensorData[9].value4, resolution) + ChannelData[9].Field4_Units + "" + (String)(SensorData[10].value4, resolution) + ChannelData[10].Field4_Units + "" + (String)(SensorData[11].value4, resolution) + ChannelData[11].Field4_Units + "" + (String)(SensorData[12].value4, resolution) + ChannelData[12].Field4_Units + "
Updated" + Time(SensorData[0].readingtime).substring(0, 8) + "" + Time(SensorData[1].readingtime).substring(0, 8) + "" + Time(SensorData[2].readingtime).substring(0, 8) + "" + Time(SensorData[3].readingtime).substring(0, 8) + "" + Time(SensorData[4].readingtime).substring(0, 8) + "" + Time(SensorData[5].readingtime).substring(0, 8) + "" + Time(SensorData[6].readingtime).substring(0, 8) + "" + Time(SensorData[7].readingtime).substring(0, 8) + "" + Time(SensorData[8].readingtime).substring(0, 8) + "" + Time(SensorData[9].readingtime).substring(0, 8) + "" + Time(SensorData[10].readingtime).substring(0, 8) + "" + Time(SensorData[11].readingtime).substring(0, 8) + "" + Time(SensorData[12].readingtime).substring(0, 8) + "
"); 549 | } 550 | webpage += F("

"); 551 | SendHTML_Content(); 552 | append_page_footer(graph_off); 553 | SendHTML_Content(); 554 | SendHTML_Stop(); 555 | webpage = ""; 556 | } 557 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 558 | void DisplayLocations() { 559 | const byte resolution = 1; // 1 = 20.2 or 2 = 20.23 or 3 = 20.234 displayed data if the sensor supports the resolution 560 | SendHTML_Header(refresh_on); 561 | webpage += F("
"); 562 | if (!ReceivedAnySensor()) { 563 | webpage += F("

*** Waiting For Data Reception ***

"); 564 | } 565 | else 566 | { 567 | webpage += F("

Current Sensor Readings

"); 568 | webpage += F("
"); // Add horizontal scrolling if number of fields exceeds page width 569 | webpage += F(""); 570 | for (int s = 1; s <= number_of_channels; s++) { 571 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 572 | { 573 | webpage += F(""); 576 | } 577 | } 578 | webpage += F(""); 579 | for (int s = 1; s <= number_of_channels; s++) { 580 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 581 | { 582 | webpage += F(""); 584 | } 585 | } 586 | webpage += F(""); 587 | for (int s = 1; s <= number_of_channels; s++) { 588 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 589 | { 590 | if (ChannelData[s].Field1 != "") { 591 | webpage += ""; 592 | } 593 | } 594 | else webpage += F(""); 595 | } 596 | SendHTML_Content(); 597 | webpage += F(""); 598 | for (int s = 1; s <= number_of_channels; s++) { 599 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 600 | { 601 | if (ChannelData[s].Field2 != "") { 602 | webpage += ""; 603 | } 604 | } 605 | else webpage += F(""); 606 | } 607 | webpage += F(""); 608 | for (int s = 1; s <= number_of_channels; s++) { 609 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) 610 | { 611 | if (ChannelData[s].Field3 != "") { 612 | webpage += ""; 613 | } 614 | } 615 | else webpage += F(""); 616 | } 617 | webpage += F(""); 618 | for (int s = 1; s <= number_of_channels; s++) { 619 | if (SensorData[s].sensornumber != 0 && SensorData[s].sensornumber <= number_of_channels) { 620 | if (ChannelData[s].Field4 != "") 621 | { 622 | webpage += ""; 623 | } 624 | } else webpage += F(""); 625 | } 626 | webpage += F(""); 627 | for (int s = 1; s <= number_of_channels; s++) { 628 | if ((SensorData[s].sensornumber != 0) && (SensorData[s].sensornumber <= number_of_channels)) 629 | webpage += ""; 630 | } 631 | webpage += F("
"); 574 | webpage += ChannelData[SensorData[s].sensornumber].Name; 575 | webpage += F("
" + (String)(SensorData[s].value1, resolution) + ChannelData[s].Field1_Units + "
" + (String)(SensorData[s].value2, resolution) + ChannelData[s].Field2_Units + "
" + (String)(SensorData[s].value3, resolution) + ChannelData[s].Field3_Units + "
" + (String)(SensorData[s].value4, resolution) + ChannelData[s].Field4_Units + "
" + Time(SensorData[s].readingtime).substring(0, 8) + "
"); 632 | } 633 | webpage += F("

"); 634 | append_page_footer(graph_off); 635 | SendHTML_Content(); 636 | SendHTML_Stop(); 637 | webpage = ""; 638 | } 639 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 640 | void Auto_Update () { // Auto-refresh of the screen, this turns it on/off 641 | AUpdate = !AUpdate; 642 | HomePage(); 643 | } 644 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 645 | bool ReceivedAnySensor() { 646 | bool sensor_received = false; 647 | for (int s = 1; s <= number_of_channels; s++) { 648 | if ((SensorData[s].sensornumber != 0) && (SensorData[s].sensornumber <= number_of_channels)) { 649 | sensor_received = true; 650 | } 651 | } 652 | return sensor_received; 653 | } 654 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 655 | void ReadChannelData() { 656 | if (SD_present) { // If the SD-Card is present and board fitted then append the next reading to the log file called 'datalog.txt' 657 | File dataFile = SD.open("/chandata.txt", FILE_READ); 658 | int cn = 1; 659 | if (dataFile) { // if the file is available, read it 660 | String in_record; 661 | while (dataFile.available() && cn < number_of_channels) { 662 | // Note the trim function is essential for graphing to work! Fields are padded out with spaces otherwise, I don't know why... 663 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].ID = in_record.toInt(); 664 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Name = in_record; 665 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Description = in_record; 666 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Type = in_record; 667 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Field1 = in_record; 668 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Field1_Units = in_record; 669 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Field2 = in_record; 670 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Field2_Units = in_record; 671 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Field3 = in_record; 672 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Field3_Units = in_record; 673 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Field4 = in_record; 674 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Field4_Units = in_record; 675 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].IconName = in_record; 676 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Created = in_record.toInt(); 677 | in_record = dataFile.readStringUntil(','); in_record.trim(); ChannelData[cn].Updated = in_record.toInt(); 678 | cn++; 679 | } 680 | } else ReportSDNotPresent(); 681 | dataFile.close(); 682 | } else ReportSDNotPresent(); 683 | } 684 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 685 | void SaveChannelData() { 686 | if (SD_present) { // If the SD-Card is present and board fitted then append the next reading to the log file called 'datalog.txt' 687 | if (!SD.remove("/chandata.txt")) Serial.println(F("Failed to delete Channel Setting files")); 688 | File dataFile = SD.open("/chandata.txt", FILE_WRITE); 689 | if (dataFile) { // Save all possible channel data 690 | for (int cn = 1; cn < number_of_channels; cn++) { 691 | dataFile.println(String(ChannelData[cn].ID) + ","); 692 | dataFile.println(ChannelData[cn].Name + ","); 693 | dataFile.println(ChannelData[cn].Description + ","); 694 | dataFile.println(ChannelData[cn].Type + ","); 695 | dataFile.println(ChannelData[cn].Field1 + ","); 696 | dataFile.println(ChannelData[cn].Field1_Units + ","); 697 | dataFile.println(ChannelData[cn].Field2 + ","); 698 | dataFile.println(ChannelData[cn].Field2_Units + ","); 699 | dataFile.println(ChannelData[cn].Field3 + ","); 700 | dataFile.println(ChannelData[cn].Field3_Units + ","); 701 | dataFile.println(ChannelData[cn].Field4 + ","); 702 | dataFile.println(ChannelData[cn].Field4_Units + ","); 703 | dataFile.println(ChannelData[cn].IconName + ","); 704 | dataFile.println(String(ChannelData[cn].Created) + ","); 705 | dataFile.println(String(ChannelData[cn].Updated) + ","); 706 | } 707 | } else ReportSDNotPresent(); 708 | dataFile.close(); 709 | } else ReportSDNotPresent(); 710 | } 711 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 712 | void Channel_File_Stream() { 713 | if (server.args() > 0 ) { // Arguments were received 714 | if (server.hasArg("stream")) { 715 | if (server.arg(0) == String(number_of_channels)) SD_file_stream("chandata"); 716 | } else SD_file_stream(server.arg(0)); 717 | } 718 | else SelectInput(refresh_off, "Channel File Stream", "Select a Channel to Stream", "Cstream", "stream", graph_on); 719 | } 720 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 721 | void SD_file_stream(String filename) { 722 | if (SD_present) { 723 | File dataFile = SD.open("/" + filename + ".txt", FILE_READ); // Now read data from SD Card 724 | if (dataFile) { 725 | if (dataFile.available()) { // If data is available and present 726 | String dataType = "application/octet-stream"; 727 | if (server.streamFile(dataFile, dataType) != dataFile.size()) { 728 | Serial.print(F("Sent less data than expected!")); 729 | } 730 | } 731 | dataFile.close(); // close the file: 732 | } else ReportFileNotPresent("Cstream"); 733 | } else ReportSDNotPresent(); 734 | } 735 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 736 | void Channel_File_Download() { 737 | if (server.args() > 0 ) { // Arguments were received 738 | if (server.hasArg("download")) { 739 | if (server.arg(0) == String(number_of_channels)) SD_file_download("chandata", !open_download); 740 | } else SD_file_download(server.arg(0), !open_download); 741 | } 742 | else SelectInput(refresh_off, "Channel File Download", "Select a Channel to Download", "Cdownload", "download", graph_on); 743 | } 744 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 745 | void File_Download() { 746 | if (server.args() > 0 ) { // Arguments were received 747 | Serial.println(server.arg(0)); 748 | if (server.hasArg("odownload")) SD_file_download(server.arg(0), open_download); 749 | } 750 | else OpenSelectInput("Enter a File Name to Download", "Odownload", "odownload"); 751 | } 752 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 753 | void SD_file_download(String filename, bool download_mode) { 754 | if (SD_present) { 755 | File download = SD.open("/" + filename + (download_mode ? "" : ".txt"), FILE_READ); // Now read data from SD Card 756 | if (download) { 757 | if (!download_mode) filename += ".txt"; 758 | server.sendHeader("Content-Type", "text/text"); 759 | server.sendHeader("Content-Disposition", "attachment; filename=" + filename); 760 | server.sendHeader("Connection", "close"); 761 | server.streamFile(download, "application/octet-stream"); 762 | download.close(); 763 | } else ReportFileNotPresent("Cdownload"); 764 | } else ReportSDNotPresent(); 765 | } 766 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 767 | void Channel_File_Upload() { 768 | append_page_header(refresh_off); 769 | webpage += F("

Select File to Upload

"); 770 | webpage += F("
"); 771 | webpage += F("
"); 772 | webpage += F("

"); 773 | webpage += F("[Back]

"); 774 | append_page_footer(graph_off); 775 | server.send(200, "text/html", webpage); 776 | } 777 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 778 | File UploadFile; 779 | void handleFileUpload() { // upload a new file to the Filing system 780 | HTTPUpload& uploadfile = server.upload(); // See https://github.com/esp8266/Arduino/tree/master/libraries/ESP8266WebServer/srcv 781 | // For further information on 'status' structure, there are other reasons such as a failed transfer that could be used 782 | if (uploadfile.status == UPLOAD_FILE_START) 783 | { 784 | String filename = uploadfile.filename; 785 | if (!filename.startsWith("/")) filename = "/" + filename; 786 | Serial.print("Upload File Name: "); Serial.println(filename); 787 | SD.remove(filename); // Remove a previous version, otherwise data is appended the file again 788 | UploadFile = SD.open(filename, FILE_WRITE); // Open the file for writing in SPIFFS (create it, if doesn't exist) 789 | filename = String(); 790 | } 791 | else if (uploadfile.status == UPLOAD_FILE_WRITE) 792 | { 793 | if (UploadFile) UploadFile.write(uploadfile.buf, uploadfile.currentSize); // Write the received bytes to the file 794 | } 795 | else if (uploadfile.status == UPLOAD_FILE_END) 796 | { 797 | if (UploadFile) // If the file was successfully created 798 | { 799 | UploadFile.close(); // Close the file again 800 | Serial.print("Upload Size: "); Serial.println(uploadfile.totalSize); 801 | webpage = ""; 802 | append_page_header(refresh_off); 803 | webpage += F("

File was successfully uploaded

"); 804 | webpage += F("

Uploaded File Name: "); webpage += uploadfile.filename + "

"; 805 | webpage += F("

File Size: "); webpage += file_size(uploadfile.totalSize) + "


"; 806 | append_page_footer(graph_off); 807 | server.send(200, "text/html", webpage); 808 | } 809 | else 810 | { 811 | ReportCouldNotCreateFile("Cupload"); 812 | } 813 | } 814 | } 815 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 816 | void Channel_File_Erase() { 817 | if (server.args() > 0 ) { // Arguments were received 818 | if (server.hasArg("erase")) { 819 | if (server.arg(0) == String(number_of_channels)) SD_file_erase("chandata", !open_erase); 820 | } else SD_file_erase(server.arg(0), !open_erase); 821 | ChannelDataReset(server.arg(0).toInt()); 822 | SaveChannelData(); 823 | } 824 | else SelectInput(refresh_off, "Channel File Erase", "Select a Channel to Erase", "Cerase", "erase", graph_off); 825 | } 826 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 827 | void File_Erase() { 828 | if (server.args() > 0 ) { // Arguments were received 829 | Serial.println(server.arg(0)); 830 | if (server.hasArg("oerase")) SD_file_erase(server.arg(0), open_erase); 831 | } 832 | else OpenSelectInput("Enter a File Name to Erase", "Oerase", "oerase"); 833 | } 834 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 835 | void SD_file_erase(String filename, bool erase_mode) { // Erase the datalog file 836 | if (SD_present) { 837 | append_page_header(refresh_off); 838 | webpage += F("

Channel File Erase

"); 839 | File dataFile = SD.open("/" + filename + (erase_mode ? "" : ".txt"), FILE_READ); // Now read data from SD Card 840 | if (dataFile) 841 | { 842 | if (SD.remove("/" + filename + (erase_mode ? "" : ".txt"))) { 843 | Serial.println(F("File deleted successfully")); 844 | webpage += "

FILE: '" + filename + (erase_mode ? "" : ".txt") + "' has been erased

"; 845 | if (erase_mode) webpage += F("[Back]

"); else webpage += F("[Back]

"); 846 | } 847 | else 848 | { 849 | webpage += F("

Channel File was not deleted - error

"); 850 | webpage += F("[Back]

"); 851 | } 852 | } else ReportFileNotPresent("Cerase"); 853 | append_page_footer(graph_off); 854 | server.send(200, "text/html", webpage); 855 | webpage = ""; 856 | } else ReportSDNotPresent(); 857 | } 858 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 859 | void SD_dir() { 860 | if (SD_present) { 861 | File root = SD.open("/"); 862 | if (root) { 863 | root.rewindDirectory(); 864 | SendHTML_Header(refresh_off); 865 | webpage += F("

SD Card Contents

"); 866 | webpage += F(""); 867 | webpage += F(""); 868 | printDirectory("/", 0); 869 | webpage += F("
Name/TypeFile/DirSize

"); 870 | SendHTML_Content(); 871 | root.close(); 872 | } 873 | else 874 | { 875 | SendHTML_Header(refresh_off); 876 | webpage += F("

No Channel Files Found

"); 877 | } 878 | append_page_footer(graph_off); 879 | SendHTML_Content(); 880 | SendHTML_Stop(); // Stop is needed because no content length was sent 881 | } else ReportSDNotPresent(); 882 | } 883 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 884 | void printDirectory(const char * dirname, uint8_t levels) { 885 | Serial.printf("Listing directory: %s\n", dirname); 886 | File root = SD.open(dirname); 887 | #ifdef ESP8266 888 | root.rewindDirectory(); //Only needed for ESP8266 889 | #endif 890 | if (!root) { 891 | Serial.println("Failed to open directory"); 892 | return; 893 | } 894 | if (!root.isDirectory()) { 895 | Serial.println("Not a directory"); 896 | return; 897 | } 898 | File file = root.openNextFile(); 899 | while (file) { 900 | if (webpage.length() > 1000) { 901 | SendHTML_Content(); 902 | } 903 | if (file.isDirectory()) { 904 | Serial.println(file.name()); 905 | webpage += "" + String(file.isDirectory() ? "Dir" : "File") + "" + String(file.name()) + ""; 906 | printDirectory(file.name(), levels - 1); 907 | } 908 | else 909 | { 910 | webpage += "" + String(file.name()) + ""; 911 | webpage += "" + String(file.isDirectory() ? "Dir" : "File") + ""; 912 | webpage += "" + file_size(file.size()) + ""; 913 | } 914 | file = root.openNextFile(); 915 | } 916 | file.close(); 917 | } 918 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 919 | String file_size(int bytes) { 920 | String fsize = ""; 921 | if (bytes < 1024) fsize = String(bytes) + " B"; 922 | else if (bytes < (1024 * 1024)) fsize = String(bytes / 1024.0, 3) + " KB"; 923 | else if (bytes < (1024 * 1024 * 1024)) fsize = String(bytes / 1024.0 / 1024.0, 3) + " MB"; 924 | else fsize = String(bytes / 1024.0 / 1024.0 / 1024.0, 3) + " GB"; 925 | return fsize; 926 | } 927 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 928 | void DrawChart() { 929 | graph_start = 0; 930 | graph_end = display_records; 931 | if (server.args() > 0 ) { // Arguments were received 932 | if (server.hasArg("draw")) drawchart(server.arg(0)); // parameter is sensor number 933 | } 934 | else SelectInput(refresh_off, "Channel Graph", "Select a Channel to Graph", "chart", "draw", graph_off); 935 | } 936 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 937 | void drawchart(String filename) { // required to enable parameters with the call 938 | graph_filename = filename; // global variable requirement 939 | channel_number = filename.toInt(); 940 | readingCnt = CountFileRecords(filename); 941 | GetandGraphData(); 942 | } 943 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 944 | void GetDataForGraph(String filename, int start) { 945 | int recordCnt = 0, displayCnt = 0; 946 | String stype; 947 | graph_sensor = filename.toInt(); 948 | if (SD_present) { 949 | File dataFile = SD.open("/" + filename + ".txt", FILE_READ); // Now read data from SD Card 950 | if (dataFile) { 951 | while (dataFile.available() && displayCnt < display_records) { // if the file is available, read from it 952 | DisplayData[displayCnt].ltime = dataFile.parseInt(); // 1517052559 21.58 43.71 0.00 0.00 SHT30-1 typically 953 | DisplayData[displayCnt].field1 = dataFile.parseFloat(); 954 | DisplayData[displayCnt].field2 = dataFile.parseFloat(); 955 | DisplayData[displayCnt].field3 = dataFile.parseFloat(); 956 | DisplayData[displayCnt].field4 = dataFile.parseFloat(); 957 | stype = dataFile.readStringUntil('\n'); // Needed to complete a record read 958 | if (recordCnt >= start) displayCnt++; 959 | recordCnt++; 960 | } 961 | } else ReportFileNotPresent("chart"); 962 | dataFile.close(); 963 | if (recordCnt == 0) displayCnt = 1; // In case the file is empty 964 | if (start > recordCnt) displayCnt = recordCnt; // In case the file is empty or there were not enough records. 965 | graph_start = start; 966 | graph_end = displayCnt; // Number of records to display 967 | } else ReportSDNotPresent(); 968 | } 969 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 970 | void MoveChartForward() { 971 | graph_start += graph_step; 972 | graph_end += graph_step; 973 | if (graph_end > readingCnt) { 974 | graph_end = readingCnt; 975 | graph_start = readingCnt - display_records; 976 | if (graph_start < 0 ) graph_start = 0; 977 | } 978 | if ((graph_start > readingCnt - graph_step) && (readingCnt - graph_step > 0) ) graph_start = readingCnt - graph_step; 979 | GetandGraphData(); 980 | } 981 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 982 | void MoveChartBack() { 983 | graph_start -= graph_step; 984 | graph_end -= graph_step; 985 | if (graph_start < 0) graph_start = 0; 986 | GetandGraphData(); 987 | } 988 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 989 | void GetandGraphData() { 990 | GetDataForGraph(graph_filename, graph_start); 991 | GraphSelectedData(DisplayData, 992 | graph_start, 993 | graph_end, 994 | ChannelData[graph_sensor].Name); 995 | } 996 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 997 | void ConstructGraph(record_type displayed_SensorData[], int start, int number_of_records, String title, String ytitle, String yunits, String graphname) { 998 | webpage = ""; 999 | // https://developers.google.com/loader/ // https://developers.google.com/chart/interactive/docs/basic_load_libs 1000 | // https://developers.google.com/chart/interactive/docs/basic_preparing_data 1001 | // https://developers.google.com/chart/interactive/docs/reference#google.visualization.arraytodatatable and See appendix-A 1002 | // data format is: [field-name,field-name,field-name] then [data,data,data], e.g. [12, 20.5, 70.3] 1003 | webpage += F(""); 1004 | webpage += F(""); 1049 | SendHTML_Content(); 1050 | } 1051 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1052 | void GraphSelectedData(record_type displayed_SensorData[], 1053 | int start, 1054 | int number_of_records, 1055 | String title) 1056 | { 1057 | // See google charts api for more details. To load the APIs, include the following script in the header of your web page. 1058 | // 1059 | // See https://developers.google.com/chart/interactive/docs/basic_load_libs 1060 | bool graph1_on = false, graph2_on = false, graph3_on = false, graph4_on = false; 1061 | String ytitle, yunits; 1062 | SendHTML_Header(refresh_off); 1063 | webpage += F("

"); webpage += ChannelData[graph_sensor].Name + " (" + String(readingCnt) + "-readings)

"; 1064 | SendHTML_Content(); 1065 | if (ChannelData[channel_number].Field1 != "") { 1066 | graphfield = one; 1067 | graphcolour = "red"; 1068 | ytitle = ChannelData[graph_sensor].Field1; 1069 | yunits = ChannelData[graph_sensor].Field1_Units; 1070 | ConstructGraph(displayed_SensorData, start, number_of_records, title + " " + ytitle, ytitle, yunits, "graph1"); // 'graph1' is e.g. the tag to match the
statements below 1071 | graph1_on = true; 1072 | } 1073 | //----------------- 1074 | if (ChannelData[channel_number].Field2 != "") { 1075 | graphfield = two; 1076 | graphcolour = "blue"; 1077 | ytitle = ChannelData[graph_sensor].Field2; 1078 | yunits = ChannelData[graph_sensor].Field2_Units; 1079 | ConstructGraph(displayed_SensorData, start, number_of_records, title + " " + ytitle, ytitle, yunits, "graph2"); 1080 | graph2_on = true; 1081 | } 1082 | //----------------- 1083 | if (ChannelData[channel_number].Field3 != "") { 1084 | graphfield = three; 1085 | graphcolour = "green"; 1086 | ytitle = ChannelData[graph_sensor].Field3; 1087 | yunits = ChannelData[graph_sensor].Field3_Units; 1088 | ConstructGraph(displayed_SensorData, start, number_of_records, title + " " + ytitle, ytitle, yunits, "graph3"); 1089 | graph3_on = true; 1090 | } 1091 | //----------------- 1092 | if (ChannelData[channel_number].Field4 != "") { 1093 | graphfield = four; 1094 | graphcolour = "orange"; 1095 | ytitle = ChannelData[graph_sensor].Field4; 1096 | yunits = ChannelData[graph_sensor].Field4_Units; 1097 | ConstructGraph(displayed_SensorData, start, number_of_records, title + " " + ytitle, ytitle, yunits, "graph4"); 1098 | graph4_on = true; 1099 | } 1100 | //----------------- 1101 | webpage += F("
"); 1102 | webpage += F("
"); 1103 | if (graph1_on) webpage += F("
"); 1104 | if (graph2_on) webpage += F("
"); 1105 | if (graph3_on) webpage += F("
"); 1106 | if (graph4_on) webpage += F("
"); 1107 | webpage += F("
"); 1108 | webpage += F("
[Back]
"); 1109 | SendHTML_Content(); // Send Content 1110 | append_page_footer(graph_on); 1111 | SendHTML_Content(); // Send footer 1112 | SendHTML_Stop(); // Stop is needed because no content length was sent 1113 | } 1114 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1115 | void SendHTML_Header(bool refresh_mode) { 1116 | server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); 1117 | server.sendHeader("Pragma", "no-cache"); 1118 | server.sendHeader("Connection", "Keep-Alive"); 1119 | server.sendHeader("Expires", "-1"); 1120 | server.setContentLength(CONTENT_LENGTH_UNKNOWN); 1121 | server.send(200, "text/html", ""); // Empty content inhibits Content-length header 1122 | append_page_header(refresh_mode); 1123 | server.sendContent(webpage); 1124 | server.sendContent("\n\r"); // A blank line seperates the header 1125 | webpage = ""; 1126 | } 1127 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1128 | void SendHTML_Content() { 1129 | server.sendContent(webpage); 1130 | webpage = ""; 1131 | } 1132 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1133 | void SendHTML_Stop() { 1134 | server.sendContent(""); 1135 | server.client().stop(); // Stop is needed because no content length was sent 1136 | } 1137 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1138 | void SelectInput(bool refresh_mode, String heading1, String heading2, String command, String arg_calling_name, bool graph_mode_off) { 1139 | SendHTML_Header(refresh_off); 1140 | webpage += F("

"); webpage += heading2 + "

"; 1141 | webpage += F(""; // Must match the calling argument e.g. '/chart' calls '/chart' after selection but with arguments 1142 | for (byte cn = 1; cn < number_of_channels; cn++) { 1143 | webpage += F(""; 1144 | } 1145 | if (graph_mode_off) { 1146 | webpage += F(""; 1148 | } 1149 | webpage += F("

"); 1150 | append_page_footer(graph_off); 1151 | SendHTML_Content(); 1152 | SendHTML_Stop(); 1153 | webpage = ""; 1154 | } 1155 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1156 | void OpenSelectInput(String heading1, String command, String arg_calling_name) { 1157 | SendHTML_Header(refresh_off); 1158 | webpage += F("

"); webpage += heading1 + "

"; 1159 | webpage += F(""; // Must match the calling argument e.g. '/chart' calls '/chart' after selection but with arguments! 1160 | webpage += F("
"); 1161 | webpage += F("

"); 1162 | append_page_footer(graph_off); 1163 | SendHTML_Content(); 1164 | SendHTML_Stop(); 1165 | webpage = ""; 1166 | } 1167 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1168 | String file_size(String filename) { // Display file size of the datalog file 1169 | String ftxtsize = ""; 1170 | if (SD_present) { 1171 | File dataFile = SD.open("/" + filename + ".txt", FILE_READ); // Now read data from SD Card 1172 | if (dataFile) { 1173 | int bytes = dataFile.size(); 1174 | if (bytes < 1024) ftxtsize = String(bytes) + " B"; 1175 | else if (bytes < (1024 * 1024)) ftxtsize = String(bytes / 1024.0) + " KB"; 1176 | else if (bytes < (1024 * 1024 * 1024)) ftxtsize = String(bytes / 1024.0 / 1024.0) + " MB"; 1177 | else ftxtsize = String(bytes / 1024.0 / 1024.0 / 1024.0) + " GB"; 1178 | } 1179 | else ftxtsize = ""; 1180 | dataFile.close(); 1181 | return ftxtsize; 1182 | } else ReportSDNotPresent(); 1183 | return ""; 1184 | } 1185 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1186 | void ReportSDNotPresent() { 1187 | append_page_header(refresh_off); 1188 | webpage += F("

No SD Card present

"); 1189 | webpage += F("[Back]

"); 1190 | append_page_footer(graph_off); 1191 | server.send(200, "text/html", webpage); 1192 | webpage = ""; 1193 | } 1194 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1195 | void ReportFileNotPresent(String target) { 1196 | append_page_header(refresh_off); 1197 | webpage += F("

File does not exist

"); 1198 | webpage += F("[Back]

"; 1199 | append_page_footer(graph_off); 1200 | server.send(200, "text/html", webpage); 1201 | webpage = ""; 1202 | } 1203 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1204 | void ReportCommandNotFound(String target) { 1205 | append_page_header(refresh_off); 1206 | webpage += F("

Function does not exist

"); 1207 | webpage += F("[Back]

"; 1208 | append_page_footer(graph_off); 1209 | server.send(200, "text/html", webpage); 1210 | webpage = ""; 1211 | } 1212 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1213 | void ReportCouldNotCreateFile(String target) { 1214 | append_page_header(refresh_off); 1215 | webpage += F("

Could Not Create Uploaded File (write-protected?)

"); 1216 | webpage += F("[Back]

"; 1217 | append_page_footer(graph_off); 1218 | server.send(200, "text/html", webpage); 1219 | webpage = ""; 1220 | } 1221 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1222 | int CountFileRecords(String filename) { 1223 | int recordCnt = 0, temp_read_int = 0; 1224 | float temp_read_flt = 0; 1225 | String temp_read_txt = ""; 1226 | if (SD_present) { 1227 | File dataFile = SD.open("/" + filename + ".txt", FILE_READ); // Now read data from SD Card 1228 | if (dataFile) { 1229 | while (dataFile.available()) { // if the file is available, read from it 1230 | recordCnt++; 1231 | temp_read_int = dataFile.parseInt(); // 1517052559 21.58 43.71 0.00 0.00 SHT30-1 typically 1232 | temp_read_flt = dataFile.parseFloat(); 1233 | temp_read_flt = dataFile.parseFloat(); 1234 | temp_read_flt = dataFile.parseFloat(); 1235 | temp_read_flt = dataFile.parseFloat(); 1236 | temp_read_txt = dataFile.readStringUntil('\n'); // Needed to complete a record read 1237 | } 1238 | } 1239 | dataFile.close(); 1240 | return recordCnt; 1241 | } else ReportSDNotPresent(); 1242 | return 0; 1243 | } 1244 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1245 | void SetupTime() { 1246 | timeval tv = {BaseTime, 0 }; // 00:00:00 01/01/2018 1247 | timezone tz = { 0 , 0 }; 1248 | settimeofday(&tv, &tz); 1249 | configTime(0, 0, "pool.ntp.org"); 1250 | setenv("TZ", "GMT0BST,M3.5.0/2,M10.5.0/2", 1); 1251 | tzset(); 1252 | time_t tnow = time(nullptr); 1253 | delay(2000); 1254 | Serial.print(F("\nWaiting for time...")); 1255 | tnow = time(nullptr); 1256 | Serial.println("Time set " + Time(tnow)); 1257 | } 1258 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1259 | String Time(int unix_time) { 1260 | struct tm *now_tm; 1261 | int hour, min, second, day, month, year; 1262 | // timeval tv = {unix_time,0}; 1263 | time_t tm = unix_time; 1264 | now_tm = localtime(&tm); 1265 | hour = now_tm->tm_hour; 1266 | min = now_tm->tm_min; 1267 | second = now_tm->tm_sec; 1268 | day = now_tm->tm_mday; 1269 | month = now_tm->tm_mon + 1; 1270 | year = 1900 + now_tm->tm_year; // To get just YY information 1271 | //String days[7] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"}; 1272 | time_str = (hour < 10 ? "0" + String(hour) : String(hour)) + ":" + (min < 10 ? "0" + String(min) : String(min)) + ":" + (second < 10 ? "0" + String(second) : String(second)) + "-"; 1273 | time_str += (day < 10 ? "0" + String(day) : String(day)) + "/" + (month < 10 ? "0" + String(month) : String(month)) + "/" + (year < 10 ? "0" + String(year) : String(year)); // HH:MM:SS 05/07/17 1274 | //Serial.println(time_str); 1275 | return time_str; 1276 | } 1277 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1278 | int TimeNow() { 1279 | time_t tnow = time(nullptr); 1280 | return tnow; 1281 | } 1282 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1283 | void ChannelDataReset(byte CN) { 1284 | ChannelData[CN].ID = CN; 1285 | ChannelData[CN].Name = "Name-TBA"; 1286 | ChannelData[CN].Description = "Sensor Readings"; 1287 | ChannelData[CN].Type = "e.g.SHT30"; 1288 | ChannelData[CN].Field1 = "e.g.Temperature"; 1289 | ChannelData[CN].Field1_Units = "°C"; 1290 | ChannelData[CN].Field2 = "e.g.Humidity"; 1291 | ChannelData[CN].Field2_Units = "%"; 1292 | ChannelData[CN].Field3 = "e.g.Pressure"; 1293 | ChannelData[CN].Field3_Units = "hPa"; 1294 | ChannelData[CN].Field4 = "Unused"; 1295 | ChannelData[CN].Field4_Units = ""; 1296 | ChannelData[CN].IconName = "building.png"; 1297 | ChannelData[CN].Created = BaseTime; 1298 | ChannelData[CN].Updated = BaseTime; 1299 | } 1300 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1301 | // Read file from SD Card and display it 1302 | bool loadFromSdCard(String filename) { 1303 | String dataType = "text/plain"; 1304 | if (filename.endsWith(".htm")) dataType = "text/html"; 1305 | else if (filename.endsWith(".html")) dataType = "text/html"; 1306 | else if (filename.endsWith(".css")) dataType = "text/css"; 1307 | else if (filename.endsWith(".png")) dataType = "image/png"; 1308 | else if (filename.endsWith(".gif")) dataType = "image/gif"; 1309 | else if (filename.endsWith(".jpg")) dataType = "image/jpeg"; 1310 | else if (filename.endsWith(".bmp")) dataType = "image/bmp"; 1311 | else if (filename.endsWith(".ico")) dataType = "image/x-icon"; 1312 | Serial.println(filename); 1313 | File dataFile = SD.open(filename.c_str()); 1314 | if (!dataFile) return false; 1315 | if (server.hasArg("download")) dataType = "application/octet-stream"; 1316 | if (server.streamFile(dataFile, dataType) != dataFile.size()) { 1317 | Serial.println("Sent less data than expected!"); 1318 | } 1319 | dataFile.close(); 1320 | return true; 1321 | } 1322 | -------------------------------------------------------------------------------- /Icons/DINING.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/G6EJD/ESP32-8266-Sensor-Monitor/7b3e20c65d98a4f61e9dbb602538880c87a32100/Icons/DINING.PNG -------------------------------------------------------------------------------- /Icons/Readme: -------------------------------------------------------------------------------- 1 | 2 | You can use PNG, JPEG, GIF or BMP files, but don't make them too big as the ESP struggles to handle larger files, it just takes longer! 3 | -------------------------------------------------------------------------------- /Icons/bedroom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/G6EJD/ESP32-8266-Sensor-Monitor/7b3e20c65d98a4f61e9dbb602538880c87a32100/Icons/bedroom.png -------------------------------------------------------------------------------- /Icons/building.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/G6EJD/ESP32-8266-Sensor-Monitor/7b3e20c65d98a4f61e9dbb602538880c87a32100/Icons/building.png -------------------------------------------------------------------------------- /Icons/conserve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/G6EJD/ESP32-8266-Sensor-Monitor/7b3e20c65d98a4f61e9dbb602538880c87a32100/Icons/conserve.png -------------------------------------------------------------------------------- /Icons/factory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/G6EJD/ESP32-8266-Sensor-Monitor/7b3e20c65d98a4f61e9dbb602538880c87a32100/Icons/factory.png -------------------------------------------------------------------------------- /Icons/garage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/G6EJD/ESP32-8266-Sensor-Monitor/7b3e20c65d98a4f61e9dbb602538880c87a32100/Icons/garage.png -------------------------------------------------------------------------------- /Icons/outdoors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/G6EJD/ESP32-8266-Sensor-Monitor/7b3e20c65d98a4f61e9dbb602538880c87a32100/Icons/outdoors.png -------------------------------------------------------------------------------- /Icons/study.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/G6EJD/ESP32-8266-Sensor-Monitor/7b3e20c65d98a4f61e9dbb602538880c87a32100/Icons/study.png -------------------------------------------------------------------------------- /Licence.txt: -------------------------------------------------------------------------------- 1 | This software, the ideas and concepts is Copyright (c) David Bird 2014 and beyond. 2 | 3 | All rights to this software are reserved. 4 | 5 | It is prohibited to redistribute or reproduce of any part or all of the software contents in any form other than the following: 6 | 7 | 1. You may print or download to a local hard disk extracts for your personal and non-commercial use only. 8 | 9 | 2. You may copy the content to individual third parties for their personal use, but only if you acknowledge the author David Bird as the source of the material. 10 | 11 | 3. You may not, except with my express written permission, distribute or commercially exploit the content. 12 | 13 | 4. You may not transmit it or store it in any other website or other form of electronic retrieval system for commercial purposes. 14 | 15 | 5. You MUST include all of this copyright and permission notice ('as annotated') and this shall be included in all copies or substantial portions of the software and where the software use is visible to an end-user. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS" FOR PRIVATE USE ONLY, IT IS NOT FOR COMMERCIAL USE IN WHOLE OR PART OR CONCEPT. 18 | 19 | FOR PERSONAL USE IT IS SUPPLIED WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | 21 | IN NO EVENT SHALL THE AUTHOR OR COPYRIGHT HOLDER 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. 22 | -------------------------------------------------------------------------------- /Network.h: -------------------------------------------------------------------------------- 1 | // Adjust the following values to match your needs 2 | // ----------------------------------------------- 3 | #define servername "sensorserver" // Set your server's logical name here e.g. if myserver then address is http://myserver.local/ 4 | IPAddress local_IP(192, 168, 0, 99); // Set your server's fixed IP address here 5 | IPAddress gateway(192, 168, 0, 1); // Set your network Gateway usually your Router base address 6 | IPAddress subnet(255, 255, 255, 0); 7 | IPAddress dns(192,168,0,1); // Set your network DNS usually your Router base address 8 | const char ssid_1[] = "your_SSID1"; 9 | const char password_1[] = "your_PASSWORD_for SSID1"; 10 | 11 | const char ssid_2[] = "your_SSID2"; 12 | const char password_2[] = "your_PASSWORD_for SSID2"; 13 | 14 | const char ssid_3[] = "your_SSID3"; 15 | const char password_3[] = "your_PASSWORD_for SSID3"; 16 | 17 | const char ssid_4[] = "your_SSID4"; 18 | const char password_4[] = "your_PASSWORD_for SSID4"; 19 | 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP32-8266-Sensor-Monitor 2 | An ESP32/8266 is used to receive sensor data from clients and then log and display the results 3 | 4 | A sensor monitor that receives and displays on a webpage client data, usually from an ESP configured as a sensor. The design does nto use units for any data, you decide on that. For example if the sensor uploads 70 (in deg-F) it will display 70, you then need to set the channel units to deg-F. The software preloads some fileds with °C which may be displayed on your browser as A°C to resolve this go back to the channel setup and delete the leading (escape character) A then the display will be correct. so a fil;ed contens for units that was showing 'A°C' becomes '°C'. 5 | 6 | Download the files to your IDE location.Locate the files referenced in the Server and Clients, download those and place in your Libraries folderChoose an IP address for your Server e.g. 192.168.0.99 7 | 8 | Edit the Server IP address accordingly. 9 | 10 | Test the Server by complining and uploading to either an ESP8266 or ESP32, the code adapts accordingly with conditional compile statments. The address of the libraries is included. 11 | 12 | Make sure you choose the correct board type! 13 | 14 | Test the server by typing this in a browser address bar: http://sensorserver.local/sensor?Sensor=1&temperature=21.2&humidity=50.1&pressure=1001&spare=0&sensortype="Mine" 15 | 16 | Or if your PC does not have 'Bonjour' installed, this is needed to resolve mult-cast DNS packets and resolves the address sesnorserver.local to the IP address, in which case enter in the browser address bar:http://192.168.0.99/sensor?Sensor=1&temperature=21.2&humidity=50.1&pressure=1001&spare=0&sensortype="Mine" 17 | 18 | Setup: 19 | 20 | 1. Determine your Routers address typically http://192.168.0.1 or https://192.168.5.1 21 | 22 | 2. Choose an IP addrress on your network, I used IPAddress local_IP(192, 168, 0, 99); // Set your server's fixed IP address 23 | 24 | 3. Set the Gateway address to match your Routers. IPAddress gateway(192, 168, 0, 1); // Set your network Gateway usually your Router base address 25 | 26 | 4. Set a sub-net mask IPAddress subnet(255, 255, 255, 0); 27 | 28 | 5. Now set DOmain Name Server Address, again the address of your Router. IPAddress dns(192,168,0,1); // Set your network DNS usually your Router base address 29 | 30 | 6. Change these entries to match your ssid and password: 31 | 32 | const char ssid_1[] = "your_SSID1"; 33 | 34 | const char password_1[] = "your_PASSWORD_for SSID1"; 35 | 36 | For Client programming ensure they use the same fixed IP address, in this example 192.168.0.99. 37 | 38 | For the Clients, wire your sensors accordingly and change the sensor pins in the Source code to match your pins. 39 | 40 | Compile and uploadAdjust the names and types and units as desired. 41 | 42 | To see the sensors use http://sensorserver.local/sensor or http://192.168.0.99/sensor 43 | 44 | To test the server use http://sensorserver.local/ or http://192.168.0.99/ 45 | 46 | Upload the example icons to your SD Card using the ESP upload function. 47 | -------------------------------------------------------------------------------- /Sys_Variables.h: -------------------------------------------------------------------------------- 1 | #define ServerVersion "1.0" 2 | String webpage = ""; 3 | bool AUpdate = true; // Default value is On 4 | const byte number_of_channels = 7; // **** MAXIMUM Of 12 and ensure this is the required Number of Channels + 1 e.g. for 6 channels set this value to 7 5 | // NOTE: ******************* FOR EACH increase or decrease in number_of_channels change lines 441 onwards accordingly, otherwise there will be compilation errors out-of-bounds 6 | // This is until the compiler errors can be fixed. Currently 1.0.0 for ESP32 7 | 8 | #define display_records 500 // 500 is the maximum of readings that can be displayed on the graph 9 | #define graph_step 250 // The amount the graph is moved forwards/backwards 10 | #define BaseTime 1514764800 // 00:00:00 01/01/2018 11 | #ifdef ESP8266 12 | #define SD_CS_pin D8 // The pin on Wemos D1 Mini for SD Card Chip-Select 13 | #else 14 | #define SD_CS_pin 5 // The pin on MH-T Live ESP32 (version of Wemos D1 Mini) for SD Card Chip-Select 15 | #endif 16 | String time_str; 17 | bool refresh_on = true; 18 | bool refresh_off = false; 19 | bool data_amended = false; 20 | String graph_filename = ""; 21 | bool graph_on = true; 22 | bool graph_off = false; 23 | bool open_erase = true; // Enables only Channel Data files to be deleted 24 | bool open_download = true; // Enables only Channel Data files to be downloaded 25 | enum fieldselector {one, two, three, four}; 26 | String graphcolour = "red"; 27 | int readingCnt = 0; 28 | fieldselector graphfield = one; 29 | 30 | /////////////////////////////////////////////////////////////////////////////////////// 31 | typedef struct { 32 | int ID = 0; 33 | String Name = "Name-TBA"; 34 | String Description = "Sensor Readings"; 35 | String Type = "SHT30"; 36 | String Field1 = "Temperature"; 37 | String Field1_Units = "°C"; 38 | String Field2 = "Humidity"; 39 | String Field2_Units = "%"; 40 | String Field3 = "Pressure"; 41 | String Field3_Units = "hPa"; 42 | String Field4 = "Unused"; 43 | String Field4_Units = ""; 44 | String IconName = "building.png"; 45 | int Created = BaseTime; 46 | int Updated = BaseTime; 47 | } sensor_details_type; 48 | 49 | sensor_details_type ChannelData[number_of_channels]; 50 | 51 | typedef struct { 52 | byte sensornumber; // Sensor number provided by e.g. Sensor=3 53 | float value1; // For example Temperature 54 | float value2; // For example Humidity 55 | float value3; // For example Pressure 56 | float value4; // Spare 57 | String sensortype = "N/A"; // The sensor type e.g. an SHT30 or BMP180 58 | int readingtime = BaseTime; 59 | } sensor_record_type; // total bytes per record = (1+4+4+4+4+String(6)+4) = 27Bytes ~ 4-years of records/MByte at 24 readings/day 60 | 61 | sensor_record_type SensorData[number_of_channels]; // Define the data array 62 | 63 | int channel_number = 0; 64 | int sensor_reading = 0; // Default value 65 | int day_count = 1; // Default value 66 | int graph_sensor = 1; 67 | bool SD_present; 68 | 69 | int graph_start, graph_end, index_ptr=0; 70 | 71 | typedef struct { 72 | int ltime; // Time reading arrived 73 | float field1; // usually Temperature values 74 | float field2; // usually Humidity values 75 | float field3; // usually Pressure values 76 | float field4; // spare 77 | } record_type; 78 | 79 | record_type DisplayData[display_records]; // Define the data array for display on a graph 80 | --------------------------------------------------------------------------------