commit 88e2ad719e07c7c655d491b9cf1f1d0c69f3f99e
Author: Martin Kloeckner <mjkloeckner@gmail.com>
Date: Mon, 24 Mar 2025 19:09:36 -0300
first commit!
Diffstat:
| A | .gitignore | | | 1 | + |
| A | data/index.html | | | 87 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | data/main.js | | | 82 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | data/style.css | | | 95 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | platformio.ini | | | 23 | +++++++++++++++++++++++ |
| A | src/main.cpp | | | 268 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
6 files changed, 556 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1 @@
+.pio
diff --git a/data/index.html b/data/index.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>ESP8266 Web Server</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <link rel="icon" href="data:,">
+ <link rel="stylesheet" type="text/css" href="style.css">
+ <script type="text/javascript" src="main.js"></script>
+ </head>
+ <body>
+ <div id="main-div">
+ <h1 id="title">ESP8266 Web Server</h1>
+ <div id="buttons-div">
+ <div id="main-toggle-div" onclick="handle_click('main-toggle')">
+ <table>
+ <tr style="vertical-align: bottom;">
+ <td id="label-box">
+ Main output enable
+ </td>
+ <td>
+ <input type="checkbox" id="main-output-checkbox" name="main-output-checkbox"/>
+ </td>
+ </tr>
+ </table>
+ </div>
+
+ <!-- <div id="timer-div" onclick="handle_click('timer-toggle)"> -->
+ <!-- <table> -->
+ <!-- <tr style="vertical-align: bottom;"> -->
+ <!-- <td id="label-box"> -->
+ <!-- Timer enabled -->
+ <!-- </td> -->
+ <!-- <td> -->
+ <!-- <input type="checkbox" id="timer-checkbox" name="timer-checkbox"/> -->
+ <!-- </td> -->
+ <!-- </tr> -->
+ <!-- </table> -->
+ <!-- </div> -->
+ </div>
+
+ <div id="info-div">
+ <table style="width: 100%; padding: 1em;">
+ <tr>
+ <td style="text-align: left;">
+ WiFi SSID
+ </td>
+ <td style="text-align: right;">
+ <div id="wifi-ssid" style="font-family: monospace;"></div>
+ </td>
+ </tr>
+ <tr>
+ <td style="text-align: left;">
+ WiFi Strength
+ </td>
+ <td style="text-align: right;">
+ <div id="wifi-rssi" style="font-family: monospace;"></div>
+ </td>
+ </tr>
+ <tr>
+ <td style="text-align: left;">
+ Local domain
+ </td>
+ <td style="text-align: right;">
+ <a id="system-local-domain" style="font-family: monospace;"></a>
+ </td>
+ </tr>
+ <tr>
+ <td style="text-align: left;">
+ Local IP address
+ </td>
+ <td style="text-align: right;">
+ <a id="system-ip-addr" style="font-family: monospace;"></a>
+ </td>
+ </tr>
+ <tr>
+ <td style="text-align: left;">
+ System time
+ </td>
+ <td style="text-align: right;">
+ <div id="system-time" style="font-family: monospace;"></div>
+ </td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/data/main.js b/data/main.js
@@ -0,0 +1,82 @@
+var received_data;
+var time_div, main_output_checkbox, system_local_domain, system_ip_addr, system_time, wifi_ssid, wifi_rssi;
+var system_time_since_epoch;
+
+// Convert to GMT-3 using Intl.DateTimeFormat
+const time_formatter = new Intl.DateTimeFormat('en-GB', {
+ timeZone: 'America/Argentina/Buenos_Aires',
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: false
+});
+
+function rssi_to_percentage(rssi) {
+ var quality = 0;
+
+ if (rssi <= -100) {
+ quality = 0;
+ } else if (rssi >= -50) {
+ quality = 100;
+ } else {
+ quality = 2 * (rssi + 100);
+ }
+ return quality;
+}
+
+function socket_onmessage_handler(event) {
+ // console.log(event.data);
+ received_data = JSON.parse(event.data);
+
+ main_output_checkbox.checked = received_data["main-output-enabled"] == "1" ? true : false;
+
+ system_local_domain.href = "http://" + received_data["system-local-domain"] + ".local";
+ system_local_domain.innerHTML = "http://" + received_data["system-local-domain"] + ".local";
+
+ system_ip_addr.href = received_data["system-ip-addr"];
+ system_ip_addr.innerHTML = received_data["system-ip-addr"];
+
+ system_time_since_epoch = received_data["system-time"];
+ const tmp_system_time = new Date(system_time_since_epoch*1000);
+ const formatted_date = time_formatter.format(tmp_system_time);
+ system_time.innerHTML = formatted_date + " GMT-3";
+
+ wifi_ssid.innerHTML = received_data["wifi-ssid"];
+ const rssi = received_data["wifi-rssi"];
+ wifi_rssi.innerHTML = rssi + "dBm (" + rssi_to_percentage(rssi) + "%)";
+}
+
+function socket_onopen_handler(event) {
+ console.log("[SOCKET] Connection established")
+ socket.send("main-output-status");
+}
+
+function handle_click(cb) {
+ if(cb == 'main-toggle') {
+ socket.send("main-output-toggle");
+ } else {
+ console.log("not implemented");
+ }
+}
+
+function update_system_time() {
+ socket.send("main-output-status");
+}
+
+function query_data() {
+ main_output_checkbox = document.getElementById("main-output-checkbox");
+ system_local_domain = document.getElementById("system-local-domain");
+ system_ip_addr = document.getElementById("system-ip-addr");
+ system_time = document.getElementById("system-time");
+ wifi_ssid = document.getElementById("wifi-ssid");
+ wifi_rssi = document.getElementById("wifi-rssi");
+
+ setInterval(update_system_time, 30*1000); // update time every 30 secs
+}
+
+const socket = new WebSocket("ws:/" + "/" + location.host + ":81");
+socket.onmessage = socket_onmessage_handler;
+socket.onopen = socket_onopen_handler;
+window.onload = query_data;
diff --git a/data/style.css b/data/style.css
@@ -0,0 +1,95 @@
+body {
+ color: black;
+ background-color: white;
+ margin: 3em auto 3em auto;
+ text-align: center;
+ font-family: "Nimbus Sans", "Nimbus Sans L", "Helvetica", Inter;
+ font-size: 18px;
+}
+
+#title {
+ padding: 0em 0.5em 0em 0.5em;
+}
+
+#main-div {
+ max-width: 25em;
+ min-height: 85vh;
+ margin: auto auto;
+ border-radius: 10px;
+}
+
+#main-toggle-div {
+ cursor: pointer;
+ user-select: none;
+ /* padding: 1em 0em 1em 0em; */
+ padding: 1.20em 1.70em 1em 1.70em;
+}
+
+#buttons-div {
+ text-align: left;
+ margin-left: auto;
+ margin-right: auto;
+ border-radius: 14px;
+ border: 1px solid #BCCCDC;
+}
+
+#label-box {
+ width: 100%;
+ font-size: 20px;
+}
+
+#main-output-checkbox, #timer-checkbox {
+ width: 20px;
+ height: 20px;
+ cursor: pointer;
+ margin: 0;
+ padding: 0;
+}
+
+#timer-div {
+ padding-top: 0.5em;
+ padding: 0.5em 1.70em 1em 1.70em;
+ cursor: pointer;
+ user-select: none;
+}
+
+#info-div {
+ padding-top: .5em;
+ font-size: 16px;
+}
+
+a, a:visited {
+ color: #00E;
+}
+
+@media (prefers-color-scheme: dark) {
+ body {
+ color: #E2E2E2;
+ background-color: #202020;
+ }
+ a, a:visited {
+ color: #56C8FF;
+ }
+ #main-toggle-div {
+ border-color: #E2E2E2;
+ }
+ #main-output-checkbox, #timer-checkbox {
+ appearance: none;
+ background-color: #E2E2E2;
+ border-radius: 2px;
+ margin-bottom: -1px;
+ /* margin: 0; */
+ /* padding: 0; */
+ /* width: 20px; */
+ /* height: 20px; */
+ }
+
+ #main-output-checkbox:checked, #timer-checkbox:checked {
+ appearance: revert;
+ accent-color: #56C8FF;
+ /* margin-bottom: 1px; */
+ /* padding: 0; */
+ /* width: 20px; */
+ /* height: 20px; */
+ }
+}
diff --git a/platformio.ini b/platformio.ini
@@ -0,0 +1,23 @@
+; PlatformIO Project Configuration File
+;
+; Build options: build flags, source filter
+; Upload options: custom upload port, speed and extra flags
+; Library options: dependencies, extra library storages
+; Advanced options: extra scripting
+;
+; Please visit documentation for the other options and examples
+; https://docs.platformio.org/page/projectconf.html
+
+[env]
+lib_deps =
+ links2004/WebSockets
+ arduino-libraries/Arduino_JSON
+ arduino-libraries/NTPClient
+
+[env:nodemcuv2]
+platform = espressif8266
+board = nodemcuv2
+framework = arduino
+build_flags = -Wno-switch
+upload_speed = 921600
+board_build.filesystem = littlefs
diff --git a/src/main.cpp b/src/main.cpp
@@ -0,0 +1,268 @@
+#include <ESP8266WiFi.h>
+#include <ESP8266WebServer.h>
+#include <WebSocketsServer.h>
+#include <ESP8266mDNS.h>
+#include <LittleFS.h>
+#include <Arduino_JSON.h>
+#include <NTPClient.h>
+#include <WiFiUdp.h>
+
+uint8_t remote_devices = 0, main_output_enabled = 0;
+uint32_t t = 0, dt = 0, main_output_dt = 0;
+
+ESP8266WebServer server(80);
+WebSocketsServer web_socket = WebSocketsServer(81);
+JSONVar data, system_time_data;
+WiFiUDP ntpUDP;
+
+NTPClient system_time(ntpUDP, "pool.ntp.org", 0, 3600); // UTC Time
+
+#define REMOTE_LED_PIN LED_BUILTIN
+#define MAIN_OUTPUT_PIN 5
+#define MAIN_SWITCH_INPUT_PIN 4
+#define SETUP_COMPLETE_OUTPUT_PIN 14
+
+#define WIFI_SSID "hello-world"
+#define WIFI_PASSWD "12345678"
+#define MDNS_DOMAIN "esp8266"
+
+#define MAIN_SWITCH_REPEAT_TIME 200
+
+void webp_socket_event(uint8_t, WStype_t, uint8_t *, size_t);
+void webp_handler();
+
+void main_output_toggle();
+void update_all_socket_clients();
+
+void setup_wifi();
+void setup_websocket();
+void setup_webserver();
+void setup_mdns();
+void setup_fs();
+
+void setup() {
+ pinMode(REMOTE_LED_PIN, OUTPUT);
+ pinMode(MAIN_OUTPUT_PIN, OUTPUT);
+ pinMode(MAIN_SWITCH_INPUT_PIN, INPUT);
+ pinMode(SETUP_COMPLETE_OUTPUT_PIN, OUTPUT);
+
+ digitalWrite(REMOTE_LED_PIN, HIGH); // led builtin uses inverted logic
+ digitalWrite(MAIN_OUTPUT_PIN, LOW);
+ digitalWrite(SETUP_COMPLETE_OUTPUT_PIN, LOW);
+
+ // Serial.begin(921600);
+ Serial.begin(115200);
+ Serial.setDebugOutput(false);
+ Serial.printf("\n\n\n");
+ delay(1000);
+
+ Serial.printf("[SETUP] Booting");
+ for(uint8_t t = 20; t > 0; t--) {
+ Serial.print(".");
+ Serial.flush();
+ delay(150);
+ }
+ Serial.print("\n");
+
+ setup_wifi();
+ setup_fs();
+ setup_websocket();
+ setup_mdns();
+ setup_webserver();
+ system_time.begin();
+ digitalWrite(SETUP_COMPLETE_OUTPUT_PIN, HIGH);
+}
+
+void loop() {
+ t = millis();
+
+ if(digitalRead(MAIN_SWITCH_INPUT_PIN)) {
+ if((t - dt) > MAIN_SWITCH_REPEAT_TIME) {
+ dt = millis();
+ main_output_toggle();
+ update_all_socket_clients();
+ }
+ }
+
+ // if((t - dt) > MAIN_OUTPUT_PERIOD) {
+ // dt = millis();
+ // digitalWrite(MAIN_OUTPUT_PIN, HIGH);
+ // main_output_dt = millis();
+ // }
+
+ // if((t - main_output_dt) > MAIN_OUTPUT_ON_TIME) {
+ // digitalWrite(MAIN_OUTPUT_PIN, LOW);
+ // }
+
+ MDNS.update();
+ web_socket.loop();
+ server.handleClient();
+ system_time.update();
+}
+
+void setup_wifi() {
+ WiFi.begin(WIFI_SSID, WIFI_PASSWD);
+
+ Serial.printf("[SETUP] Connecting to WiFi");
+ while(WiFi.status() != WL_CONNECTED) {
+ delay(200);
+ Serial.print(".");
+ }
+ Serial.printf("\n[SETUP] Connected to '%s' IP address ", WIFI_SSID);
+ Serial.println(WiFi.localIP());
+}
+
+void setup_mdns() {
+ if (!MDNS.begin(MDNS_DOMAIN)) {
+ Serial.println("[SETUP] Error setting up MDNS responder!");
+ while(1) { delay(100); }
+ }
+
+ MDNS.addService("http", "tcp", 80);
+ Serial.printf("[SETUP] mDNS started domain '%s.local'\n", MDNS_DOMAIN);
+}
+
+void main_output_toggle() {
+ if(main_output_enabled) {
+ // GPOC = (1 << MAIN_OUTPUT_PIN); // low
+ digitalWrite(MAIN_OUTPUT_PIN, LOW);
+ } else {
+ // GPOS = (1 << MAIN_OUTPUT_PIN); // high
+ digitalWrite(MAIN_OUTPUT_PIN, HIGH);
+ }
+ main_output_enabled = !main_output_enabled;
+}
+
+void query_system_time() {
+ system_time_data["system-time"] = system_time.getEpochTime();
+}
+
+void update_data() {
+ data["main-output-enabled"] = String(main_output_enabled);
+ data["system-local-domain"] = String(MDNS_DOMAIN);
+ data["system-ip-addr"] = WiFi.localIP().toString();
+ data["system-time"] = system_time.getEpochTime();
+ data["wifi-ssid"] = WIFI_SSID;
+ data["wifi-rssi"] = WiFi.RSSI();
+}
+
+void update_all_socket_clients() {
+ update_data();
+ String data_as_json = JSON.stringify(data);
+ web_socket.broadcastTXT(data_as_json);
+}
+
+void webp_socket_event(uint8_t num, WStype_t type, uint8_t *payload, size_t len) {
+ switch(type) {
+ case WStype_DISCONNECTED:
+ Serial.printf("[SOCKET] %d: Disconnected\n", num);
+ remote_devices -= 1;
+ digitalWrite(REMOTE_LED_PIN, remote_devices ? LOW : HIGH);
+ break;
+ case WStype_CONNECTED: {
+ IPAddress ip = web_socket.remoteIP(num);
+ Serial.printf("[SOCKET] %u: Connected from %d.%d.%d.%d URL %s\n",
+ num, ip[0], ip[1], ip[2], ip[3], payload);
+ remote_devices += 1;
+ digitalWrite(REMOTE_LED_PIN, remote_devices ? LOW : HIGH);
+ }
+ break;
+ case WStype_TEXT: {
+ // Serial.printf("[SOCKET](%u) got text: %s\n", num, payload);
+
+ if(!strcmp((char *)payload, "main-output-toggle")) {
+ main_output_toggle();
+ update_all_socket_clients();
+ }
+
+ if(!strcmp((char *)payload, "main-output-status")) {
+ update_data();
+ String data_as_json = JSON.stringify(data);
+ web_socket.sendTXT(num, data_as_json);
+ }
+
+ if(!strcmp((char *)payload, "system-time")) {
+ String system_time_data_as_json = JSON.stringify(system_time_data);
+ web_socket.sendTXT(num, system_time_data_as_json);
+ }
+ }
+ break;
+ case WStype_BIN:
+ Serial.printf("[SOCKET][%u] get binary length: %u\n", num, len);
+ hexdump(payload, len);
+ // web_socket.sendBIN(num, payload, len);
+ break;
+ }
+}
+
+void setup_websocket() {
+ web_socket.begin();
+ web_socket.onEvent(webp_socket_event);
+}
+
+String webserver_get_file(String path) {
+ String return_page;
+
+ if(LittleFS.exists(path)) {
+ Serial.printf("[SERVER] Serving file '%s'\n", path.c_str());
+ File file = LittleFS.open(path.c_str(), "r");
+ while(file.available()) {
+ return_page += (char)file.read();
+ }
+ file.close();
+ } else {
+ Serial.printf("[SERVER] '%s' File Not Found\n", path.c_str());
+ return_page = R"==(<!DOCTYPE html>
+ <html>
+ <head>
+ <title>ERROR 404: File Not found!!</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ </head>
+ <body>
+ <h>ERROR 404: File Not Found!</h1>
+ <p>file '/index.html' not found</p>
+ </body>
+ </html>)==";
+ }
+ return return_page;
+}
+
+String webserver_file_content_type(String path) {
+ if (path.endsWith(".html")) return "text/html";
+ else if (path.endsWith(".css")) return "text/css";
+ else if (path.endsWith(".js")) return "application/javascript";
+ else if (path.endsWith(".ico")) return "image/x-icon";
+ else if (path.endsWith(".gz")) return "application/x-gzip";
+ return "text/plain";
+}
+
+void webserver_file_handler() {
+ String path = server.uri();
+ server.send(200, webserver_file_content_type(path), webserver_get_file(path));
+}
+
+void webserver_handle_root() {
+ String webp_index = webserver_get_file("index.html");
+ server.send(200, "text/html", webp_index.c_str());
+}
+
+void setup_webserver() {
+ Serial.println("[SETUP] loading server response from file 'index.html'");
+
+ server.on("/", webserver_handle_root);
+ server.onNotFound(webserver_file_handler);
+
+ server.begin();
+}
+
+void setup_fs() {
+ if(LittleFS.begin() == 0) {
+ Serial.println("[SETUP] Error couldn't begin filesystem!");
+ }
+
+ FSInfo fs_info;
+ LittleFS.info(fs_info);
+
+ uint8_t percentage_usad = (fs_info.usedBytes/fs_info.totalBytes)*100;
+ Serial.printf("[SETUP] LittleFS started: spaced used %d%%\n", percentage_usad);
+}