1 #include <ESP8266WiFi.h> 2 #include <ESP8266WebServer.h> 3 #include <WebSocketsServer.h> 4 #include <ESP8266mDNS.h> 5 #include <LittleFS.h> 6 #include <Arduino_JSON.h> 7 #include <time.h> 8 #include <coredecls.h> // settimeofday_cb() callback 9 10 #define REMOTE_LED_PIN LED_BUILTIN 11 #define MAIN_OUTPUT_PIN 5 12 #define MAIN_SWITCH_INPUT_PIN 4 13 14 #define WIFI_SSID "hello-world" 15 #define WIFI_PASSWD "12345678" 16 #define MDNS_DOMAIN "esp8266" 17 18 #define MY_NTP_SERVER "pool.ntp.org" 19 #define MY_TZ "<-03>3" 20 21 #define MAIN_SWITCH_REPEAT_TIME 500 22 23 typedef enum { 24 MAIN_OUTPUT_STATUS = 0, 25 MAIN_OUTPUT_TOGGLE, 26 TIMER_TOGGLE, 27 TIMER_SET_VALUES 28 } query_t; 29 30 typedef struct { 31 uint8_t from_hour, from_minute; 32 uint8_t to_hour, to_minute; 33 } timer_values_t; 34 35 ESP8266WebServer server(80); 36 WebSocketsServer web_socket = WebSocketsServer(81); 37 JSONVar data, timer_data; 38 timer_values_t timer_values; 39 40 time_t system_time, last_ntp_sync; 41 uint8_t remote_devices = 0, timer_enabled = 0, main_output_enabled = 0; 42 uint32_t t = 0, system_time_dt = 0, main_output_dt = 0, timer_dt = 0; 43 44 void setup_wifi(); 45 void setup_websocket(); 46 void setup_webserver(); 47 void setup_mdns(); 48 void setup_fs(); 49 50 void update_timer_values(const timer_values_t); 51 void update_timer_data(); 52 void update_all_clients_checkbox(); 53 void update_all_socket_clients(); 54 55 void websocket_event_handler(uint8_t, WStype_t, uint8_t *, size_t); 56 57 void save_timer_values_to_file() { 58 const char* file_path = "timer_values.json"; 59 File file = LittleFS.open(file_path, "w"); 60 61 if (!file) { 62 Serial.printf("[LITTLEFS] Failed to open file `%s` for writing\n", file_path); 63 return; 64 } 65 66 update_timer_data(); 67 if (file.print(JSON.stringify(timer_data).c_str())) { 68 Serial.println("[LITTLEFS] Timer values saved"); 69 } else { 70 Serial.println("[LITTLEFS] Timer values write failed"); 71 } 72 73 // delay(1000); // Make sure the CREATE and LASTWRITE times are different 74 file.close(); 75 } 76 77 void read_timer_values_from_file() { 78 const char* file_path = "timer_values.json"; 79 File file = LittleFS.open(file_path, "r"); 80 81 if (!file) { 82 Serial.printf("[LITTLEFS] Failed to open file `%s`. Setting default values..\n", file_path); 83 timer_values_t new_values = {18, 0, 0, 0}; 84 update_timer_values(new_values); 85 return; 86 } 87 88 String timer_values_json = file.readString(); 89 file.close(); 90 91 JSONVar timer_values = JSON.parse(timer_values_json.c_str()); 92 if(JSON.typeof(timer_values) == "undefined") { 93 Serial.println("[SOCKET] Parsing file `timer-values.json` failed"); 94 return; 95 } 96 97 timer_values_t new_values = { 98 (uint8_t)String(timer_values["from"]["hour"]).toInt(), 99 (uint8_t)String(timer_values["from"]["minute"]).toInt(), 100 (uint8_t)String(timer_values["to"]["hour"]).toInt(), 101 (uint8_t)String(timer_values["to"]["minute"]).toInt() 102 }; 103 104 timer_enabled = String(timer_values["enabled"]).toInt(); 105 106 Serial.printf("[UPDATE_TIMER_VALUES] %02d:%02d to %02d:%02d\n", 107 new_values.from_hour, new_values.from_minute, 108 new_values.to_hour, new_values.to_minute); 109 110 update_timer_values(new_values); 111 update_timer_data(); 112 } 113 114 void show_time(bool from_sntp = false) { 115 tm tm; 116 117 time(&system_time); // read the current time 118 localtime_r(&system_time, &tm); // update the structure tm with the current time 119 120 const char* prompt = from_sntp ? "[SNTP]" : "[LOOP]"; 121 122 if(from_sntp) { 123 time(&last_ntp_sync); 124 } 125 126 // YYYY-MM-DD HH:MM:SS GMT-3 127 Serial.printf("%s %d-%02d-%02d %02d:%02d:%02d GMT-3\n", prompt, 128 tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec); 129 } 130 131 // ntp startup delay 132 uint32_t sntp_startup_delay_MS_rfc_not_less_than_60000 () { 133 randomSeed(A0); 134 return random(5000); 135 } 136 137 // ntp polling interval 138 uint32_t sntp_update_delay_MS_rfc_not_less_than_15000 () { 139 return 8 * 60 * 60 * 1000UL; // 8*60 mins 140 } 141 142 void main_output_set_to_if_not_already(bool state) { 143 if((bool)main_output_enabled != state) { 144 digitalWrite(MAIN_OUTPUT_PIN, main_output_enabled ^= 1); 145 update_all_clients_checkbox(); 146 } 147 } 148 149 void update_timer_output() { 150 tm now_tm; 151 uint16_t timer_from_24, timer_to_24, now_24; 152 153 if(!timer_enabled) return; 154 155 localtime_r(&system_time, &now_tm); 156 timer_from_24 = (timer_values.from_hour*100)+timer_values.from_minute; 157 timer_to_24 = (timer_values.to_hour*100)+timer_values.to_minute; 158 now_24 = (now_tm.tm_hour*100)+now_tm.tm_min; 159 160 Serial.printf("[TIMER] from: %04d; to: %04d; now: %04d\n", 161 timer_from_24, timer_to_24, now_24); 162 163 if(timer_to_24 > timer_from_24) { 164 if((now_24 >= timer_from_24) && (now_24 < timer_to_24)) { 165 main_output_set_to_if_not_already(true); 166 } 167 else { 168 main_output_set_to_if_not_already(false); 169 } 170 } else { 171 if((now_24 < timer_to_24) || now_24 >= timer_from_24) { 172 main_output_set_to_if_not_already(true); 173 } 174 else { 175 main_output_set_to_if_not_already(false); 176 } 177 } 178 } 179 180 void clear_data() { 181 JSONVar keys = data.keys(); 182 for (int i = 0; i < keys.length(); i++) { 183 data[keys[i]] = undefined; 184 } 185 } 186 187 void update_data() { 188 data["main-output-enabled"] = String(main_output_enabled); 189 data["system-local-domain"] = String(MDNS_DOMAIN); 190 data["system-ip-addr"] = WiFi.localIP().toString(); 191 data["last-ntp-sync"] = (long)last_ntp_sync; 192 data["system-time"] = (long)system_time; 193 data["wifi-ssid"] = WIFI_SSID; 194 data["wifi-rssi"] = WiFi.RSSI(); 195 196 update_timer_data(); 197 data["timer"] = timer_data; 198 } 199 200 void update_all_socket_clients() { 201 update_data(); 202 String data_as_json = JSON.stringify(data); 203 web_socket.broadcastTXT(data_as_json); 204 } 205 206 void update_all_clients_checkbox() { 207 JSONVar output_data; 208 output_data["type"] = "cb"; 209 output_data["main-output-enabled"] = String(main_output_enabled); 210 output_data["timer"]["enabled"] = String(timer_enabled); 211 String output_data_as_json = JSON.stringify(output_data); 212 web_socket.broadcastTXT(output_data_as_json); 213 } 214 215 String left_pad(uint8_t n) { 216 return n < 10 ? "0" + String(n) : String(n); 217 } 218 219 void update_timer_data() { 220 timer_data["enabled"] = String(timer_enabled); 221 timer_data["from"]["hour"] = left_pad(timer_values.from_hour); 222 timer_data["from"]["minute"] = left_pad(timer_values.from_minute); 223 timer_data["to"]["hour"] = left_pad(timer_values.to_hour); 224 timer_data["to"]["minute"] = left_pad(timer_values.to_minute); 225 } 226 227 void update_all_clients_timer_values() { 228 clear_data(); 229 update_timer_data(); 230 data["type"] = "timer"; 231 data["timer"] = timer_data; 232 web_socket.broadcastTXT(JSON.stringify(data).c_str()); 233 } 234 235 void update_timer_values(const timer_values_t new_values) { 236 timer_values = new_values; 237 Serial.printf("[UPDATE_TIMER_VALUES] %02d:%02d to %02d:%02d\n", 238 timer_values.from_hour, timer_values.from_minute, 239 timer_values.to_hour, timer_values.to_minute); 240 } 241 242 void websocket_event_handler(uint8_t num, WStype_t type, uint8_t *payload, size_t len) { 243 switch(type) { 244 case WStype_DISCONNECTED: 245 Serial.printf("[SOCKET] %d: Disconnected\n", num); 246 remote_devices -= 1; 247 digitalWrite(REMOTE_LED_PIN, remote_devices ? LOW : HIGH); 248 break; 249 case WStype_CONNECTED: { 250 IPAddress ip = web_socket.remoteIP(num); 251 Serial.printf("[SOCKET] %u: Connected from %d.%d.%d.%d URL %s\n", 252 num, ip[0], ip[1], ip[2], ip[3], payload); 253 remote_devices += 1; 254 digitalWrite(REMOTE_LED_PIN, remote_devices ? LOW : HIGH); 255 } 256 break; 257 case WStype_TEXT: { 258 int query_type = *payload - '0'; 259 260 switch(query_type) { 261 case MAIN_OUTPUT_TOGGLE: 262 digitalWrite(MAIN_OUTPUT_PIN, main_output_enabled ^= 1); 263 if(timer_enabled) { 264 timer_enabled = 0; 265 } 266 update_all_clients_checkbox(); 267 break; 268 case TIMER_TOGGLE: 269 timer_enabled = !timer_enabled; 270 update_timer_output(); 271 update_all_clients_checkbox(); 272 break; 273 case TIMER_SET_VALUES: { 274 payload++; // skip query type 275 Serial.printf("[SOCKET] %s\n", payload); 276 timer_data = JSON.parse((char *)payload); 277 278 if(JSON.typeof(timer_data) == "undefined") { 279 Serial.println("[SOCKET] Parsing payload failed!"); 280 break; 281 } 282 283 timer_values_t new_values = { 284 (uint8_t)String(timer_data["from"]["hour"]).toInt(), 285 (uint8_t)String(timer_data["from"]["minute"]).toInt(), 286 (uint8_t)String(timer_data["to"]["hour"]).toInt(), 287 (uint8_t)String(timer_data["to"]["minute"]).toInt() 288 }; 289 290 update_timer_values(new_values); 291 update_timer_output(); 292 save_timer_values_to_file(); 293 update_all_clients_timer_values(); 294 } 295 break; 296 case MAIN_OUTPUT_STATUS: 297 default: 298 update_data(); 299 data["type"] = "all"; 300 String data_as_json = JSON.stringify(data); 301 web_socket.sendTXT(num, data_as_json); 302 break; 303 } 304 } 305 break; 306 case WStype_BIN: 307 Serial.printf("[SOCKET][%u] get binary length: %u\n", num, len); 308 hexdump(payload, len); 309 break; 310 } 311 } 312 313 int webserver_get_file(String path, String &return_page) { 314 if(LittleFS.exists(path)) { 315 Serial.printf("[SERVER] Serving file '%s'\n", path.c_str()); 316 File file = LittleFS.open(path.c_str(), "r"); 317 while(file.available()) { 318 return_page += (char)file.read(); 319 } 320 file.close(); 321 } else { 322 Serial.printf("[SERVER] '%s' File Not Found\n", path.c_str()); 323 return_page = R"==(<!DOCTYPE html> 324 <html> 325 <head> 326 <title>ERROR 404: File Not found!!</title> 327 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 328 </head> 329 <body> 330 <h>ERROR 404: File Not Found!</h1> 331 <p>file '/index.html' not found</p> 332 </body> 333 </html>)=="; 334 return 1; 335 } 336 return 0; 337 } 338 339 String webserver_file_content_type(String path) { 340 if (path.endsWith(".html")) return "text/html"; 341 else if (path.endsWith(".css")) return "text/css"; 342 else if (path.endsWith(".js")) return "application/javascript"; 343 else if (path.endsWith(".ico")) return "image/x-icon"; 344 else if (path.endsWith(".gz")) return "application/x-gzip"; 345 return "text/plain"; 346 } 347 348 void webserver_file_handler() { 349 String path = server.uri(); 350 String requested_page; 351 int response_code; 352 response_code = webserver_get_file(path, requested_page) ? 404 : 200; 353 server.send(response_code, webserver_file_content_type(path), requested_page); 354 } 355 356 void webserver_handle_root() { 357 String index_page; 358 int response_code = 200; 359 if(webserver_get_file("index.html", index_page)) { 360 response_code = 404; 361 } 362 server.send(response_code, "text/html", index_page.c_str()); 363 } 364 365 void setup_wifi() { 366 WiFi.begin(WIFI_SSID, WIFI_PASSWD); 367 368 Serial.printf("[SETUP] Connecting to WiFi"); 369 while(WiFi.status() != WL_CONNECTED) { 370 delay(200); 371 Serial.print("."); 372 } 373 Serial.printf("\n[SETUP] Connected to '%s' IP address ", WIFI_SSID); 374 Serial.println(WiFi.localIP()); 375 } 376 377 void setup_mdns() { 378 if (!MDNS.begin(MDNS_DOMAIN)) { 379 Serial.println("[SETUP] Error setting up MDNS responder!"); 380 while(1) { delay(100); } 381 } 382 383 MDNS.addService("http", "tcp", 80); 384 Serial.printf("[SETUP] mDNS started domain '%s.local'\n", MDNS_DOMAIN); 385 } 386 387 void setup_websocket() { 388 web_socket.begin(); 389 web_socket.onEvent(websocket_event_handler); 390 } 391 392 void setup_webserver() { 393 Serial.println("[SETUP] loading server response from file 'index.html'"); 394 395 server.on("/", webserver_handle_root); 396 server.onNotFound(webserver_file_handler); 397 398 server.begin(); 399 } 400 401 void setup_fs() { 402 if(LittleFS.begin() == 0) { 403 Serial.println("[SETUP] Error couldn't begin filesystem!"); 404 } 405 406 FSInfo fs_info; 407 LittleFS.info(fs_info); 408 409 uint8_t percentage_usad = (fs_info.usedBytes/fs_info.totalBytes)*100; 410 Serial.printf("[SETUP] LittleFS started: spaced used %d%%\n", percentage_usad); 411 } 412 413 void setup() { 414 pinMode(REMOTE_LED_PIN, OUTPUT); 415 pinMode(MAIN_OUTPUT_PIN, OUTPUT); 416 pinMode(MAIN_SWITCH_INPUT_PIN, INPUT); 417 418 digitalWrite(REMOTE_LED_PIN, HIGH); // led builtin uses inverted logic 419 digitalWrite(MAIN_OUTPUT_PIN, LOW); 420 421 Serial.begin(115200); 422 Serial.setDebugOutput(false); 423 Serial.printf("\n\n\n"); 424 delay(1000); 425 426 Serial.printf("[SETUP] Booting"); 427 for(uint8_t i = 10; i > 0; i--) { 428 Serial.print("."); 429 Serial.flush(); 430 delay(150); 431 } 432 Serial.print("\n"); 433 434 setup_wifi(); 435 setup_fs(); 436 setup_websocket(); 437 setup_mdns(); 438 setup_webserver(); 439 440 configTime(MY_TZ, MY_NTP_SERVER); // configure builtin ntp! 441 settimeofday_cb(show_time); // ntp update callback 442 443 show_time(); 444 read_timer_values_from_file(); 445 } 446 447 void loop() { 448 t = millis(); 449 450 if((t - main_output_dt) > MAIN_SWITCH_REPEAT_TIME) { 451 if(digitalRead(MAIN_SWITCH_INPUT_PIN)) { 452 main_output_dt = millis(); 453 digitalWrite(MAIN_OUTPUT_PIN, main_output_enabled ^= 1); 454 if(timer_enabled) { 455 timer_enabled = 0; 456 } 457 update_all_clients_checkbox(); 458 } 459 } 460 461 // update `system_time` every 5s 462 if((t - system_time_dt) > 5000){ 463 system_time_dt = millis(); 464 time(&system_time); // read the current time 465 update_timer_output(); 466 } 467 468 MDNS.update(); 469 web_socket.loop(); 470 server.handleClient(); 471 }
