esp8266-remote-timer

ESP8266 based timer w/ remote control and NTP sync
Index Commits Files Refs README LICENSE
src/main.cpp (14331B)
   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 }