├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── screenshot_esp32_webui.png └── screenshot_flash_button.png ├── platformio.ini └── src ├── ESP32-http-websocket.ino └── index.html /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build firmware 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-20.04 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | 16 | - name: Installing platformio 17 | run: pip3 install -U platformio 18 | 19 | - name: Building a firmware 20 | run: | 21 | pio lib install 22 | pio run -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode 3 | src/credentials.h -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 DominikN 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP32-http-websocket 2 | 3 | [![GitHub stars](https://img.shields.io/github/stars/dominikn/ESP32-http-websocket?style=social)](https://github.com/DominikN/ESP32-http-websocket/stargazers/) 4 | 5 | [![Build firmware](https://github.com/DominikN/ESP32-http-websocket/actions/workflows/build.yml/badge.svg)](https://github.com/DominikN/ESP32-http-websocket/actions/workflows/build.yml) 6 | [![GitHub license](https://img.shields.io/github/license/dominikn/ESP32-http-websocket.svg)](https://github.com/dominikn/ESP32-http-websocket/blob/master/LICENSE) 7 | 8 | 9 | **_ESP32 + HTTP server + websockets + Bootstrap + Husarnet. A simple project template showing how to use those technologies to create a fast, pretty and secure web UI hosted on ESP32. Works in LAN and over the internet. Written using Arduino framework._** 10 | 11 | ## Intro 12 | 13 | This template can be a base for your own ESP32 based projects needing a responsive web user interface. It is written using Arduino framework, AsyncWebServer and ArduinoJson. I show here how to develop both C++ and HTML part of the project in a clear way. 14 | 15 | The template combines all useful technologies in one: 16 | 17 | - **WebSockets** - to provide a fast and elegant communication between web browser and ESP32 withough reloading a page like in case of HTTP requests. We use [ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer) library to make a connection even faster. 18 | - **Bootstrap 4** - one of the most popular frameworks for rapid web page design. Thanks to Bootstrap you can easily write a pretty web UI, looking good both on mobile and desktop devices. 19 | - **JSON** - an elegant way to format data exchanged between web browser and ESP32. 20 | - **Husarnet** - a Virtual LAN network thanks to which you can access ESP32 both from LAN network and through the internet, without static IP adressess, setting port forwarding on your router etc. Basically you can make easily upgrade your existing project working in LAN to work over the Internet by just 4 extra lines of code (if your project is based on AsyncTCP - if it uses WiFiClient & WiFiServer classes you need also to replace them by, respectively, HusarnetClient & HusarnetServer classes). 21 | ```cpp 22 | #include 23 | 24 | ... 25 | 26 | /* Start Husarnet */ 27 | Husarnet.selfHostedSetup(dashboardURL); 28 | Husarnet.join(husarnetJoinCode, hostName); 29 | Husarnet.start(); 30 | 31 | ... 32 | ``` 33 | - **Platformio** - An extension for Visual Studio Code that handles all configuration for embedded project for you. Thanks to Platformio you can just clone the project repository, provide your network credentials and just click "Upload" button to program your board. Platformio handles all configuration and cloning appriopriate libraries from repos in the background. 34 | 35 | ## Full instruction 36 | 37 | A demo is really basic: 38 | 39 | - control a LED connected to ESP32 by using a button in web UI. 40 | - update cnt value in the web ui every 1s 41 | - ESP32 sends a button state to change a color of the dot in web UI. 42 | 43 | To run the project, open Visual Studio Code with Platformio extension and follow these steps: 44 | 45 | ### 1. Clone the project repo: 46 | 47 | ```bash 48 | git clone https://github.com/DominikN/ESP32-http-websocket.git 49 | ``` 50 | 51 | ### 2. Open ESP32-http-websocket project folder on your local machine from VSC with platformio extension installed 52 | 53 | File > Open Folder > ESP32-http-websocket 54 | 55 | ### 3. Add your network credentials 56 | 57 | - Open **ESP32-http-websocket.ino** file 58 | - Enter your Wi-Fi network credentials here: 59 | ```cpp 60 | // WiFi credentials 61 | #define NUM_NETWORKS 2 62 | // Add your networks credentials here 63 | const char* ssidTab[NUM_NETWORKS] = { 64 | "wifi-ssid-one", 65 | "wifi-ssid-two", 66 | }; 67 | const char* passwordTab[NUM_NETWORKS] = { 68 | "wifi-pass-one", 69 | "wifi-pass-two", 70 | }; 71 | ``` 72 | - Enter your **Husarnet Join Code** here: 73 | ```cpp 74 | const char* husarnetJoinCode = "xxxxxxxxxxxxxxxxxxxxxx"; 75 | ``` 76 | To find your Husarnet Join Code setup a free account at https://app.husarnet.com > click **[Create network]** button > go to **join code** tab . It will look like this: `fc94:b01d:1803:8dd8:b293:5c7d:7639:932a/XXXXXXXXXXXXXXXXXXXXX` 77 | 78 | ### 4. Upload code to your board 79 | 80 | Just click this button in VSC + Platformio 81 | ![Upload code](docs/screenshot_flash_button.png) 82 | 83 | ### 5. Open WebUI 84 | 85 | Add your laptop to the same Husarnet network as ESP32 board. In that scenerio no proxy servers are used and you connect to ESP32 with a very low latency directly with no port forwarding on your router! Currenly only Linux client is available, so open your Linux terminal and type: 86 | 87 | - `$ curl https://install.husarnet.com/install.sh | sudo bash` to install Husarnet. 88 | - `$ sudo systemctl restart husarnet` to start husarnet daemon after installation 89 | - `$ husarnet join XXXXXXXXXXXXXXXXXXXXXXX mylaptop` replace XXX...X with your own `join code` (the same as in 3), and click enter. 90 | 91 | At this stage your ESP32 and your laptop are in the same VLAN network. The best hostname support is in Mozilla Firefox web browser (other browsers also work, but you have to use IPv6 address of your device that you will find at https://app.husarnet.com) and type: 92 | `http://esp32websocket:8000` 93 | You should see a web UI to controll your ESP32 now. 94 | 95 | ![screenshot_esp32_webui](docs/screenshot_esp32_webui.png) 96 | -------------------------------------------------------------------------------- /docs/screenshot_esp32_webui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DominikN/ESP32-http-websocket/af24e3ec4eabd6ab402414b0ba391d0ae0607241/docs/screenshot_esp32_webui.png -------------------------------------------------------------------------------- /docs/screenshot_flash_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DominikN/ESP32-http-websocket/af24e3ec4eabd6ab402414b0ba391d0ae0607241/docs/screenshot_flash_button.png -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | [env] 2 | platform = espressif32 3 | framework = arduino 4 | platform_packages = 5 | framework-arduinoespressif32 @ https://github.com/husarnet/arduino-esp32/releases/download/1.0.4-1/arduino-husarnet-esp32.zip 6 | lib_deps = 7 | ; Until our pull requests are merged you need to use AsyncTCP with our fixes for IPv6 8 | https://github.com/husarnet/AsyncTCP.git 9 | ESP Async WebServer 10 | Husarnet ESP32 11 | bblanchon/ArduinoJson @ 6.17 12 | https://github.com/Bodmer/TFT_eSPI.git 13 | 14 | [env:esp32dev] 15 | board = esp32dev 16 | monitor_speed = 115200 17 | upload_speed = 921600 18 | 19 | monitor_filters = esp32_exception_decoder, default 20 | 21 | board_build.partitions = min_spiffs.csv 22 | board_build.embed_txtfiles = 23 | src/index.html 24 | 25 | build_flags = 26 | ; for AsyncTCP.h 27 | -DCONFIG_ASYNC_TCP_RUNNING_CORE=1 28 | -DCONFIG_ASYNC_TCP_USE_WDT=0 29 | 30 | ; for LCD/TFT display 31 | ; https://github.com/Bodmer/TFT_eSPI/blob/master/Tools/PlatformIO/Configuring%20options.txt 32 | ; https://github.com/Bodmer/TFT_eSPI/blob/master/User_Setups/Setup25_TTGO_T_Display.h 33 | -DUSER_SETUP_LOADED=1 34 | -DST7789_DRIVER=1 35 | -DTFT_WIDTH=135 36 | -DTFT_HEIGHT=240 37 | -DCGRAM_OFFSET=1 38 | -DTFT_MISO=-1 39 | -DTFT_MOSI=19 40 | -DTFT_SCLK=18 41 | -DTFT_CS=5 42 | -DTFT_DC=16 43 | -DTFT_RST=23 44 | -DTFT_BL=4 45 | -DLOAD_GLCD=1 46 | -DSMOOTH_FONT=1 47 | -TFT_BACKLIGHT_ON=HIGH 48 | -DLOAD_FONT2=1 49 | -DSPI_FREQUENCY=40000000 50 | -DSPI_READ_FREQUENCY=6000000 51 | -------------------------------------------------------------------------------- /src/ESP32-http-websocket.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | /* =============== config section start =============== */ 11 | 12 | #define ENABLE_TFT 0 // tested on TTGO T Display 13 | 14 | #define HTTP_PORT 8000 15 | 16 | const int BUTTON_PIN = 35; 17 | const int LED_PIN = 17; 18 | 19 | const char* hostName = "esp32websocket"; 20 | 21 | #if __has_include("credentials.h") 22 | #include "credentials.h" 23 | #else 24 | // WiFi credentials 25 | #define NUM_NETWORKS 2 26 | // Add your networks credentials here 27 | const char* ssidTab[NUM_NETWORKS] = { 28 | "wifi-ssid-one", 29 | "wifi-ssid-two", 30 | }; 31 | const char* passwordTab[NUM_NETWORKS] = { 32 | "wifi-pass-one", 33 | "wifi-pass-two", 34 | }; 35 | 36 | /* to get your join code go to https://app.husarnet.com 37 | -> select network 38 | -> click "Add element" 39 | -> select "join code" tab 40 | 41 | Keep it secret! 42 | */ 43 | const char* husarnetJoinCode = "xxxxxxxxxxxxxxxxxxxxxx"; 44 | const char* dashboardURL = "default"; 45 | #endif 46 | /* =============== config section end =============== */ 47 | 48 | #if ENABLE_TFT == 1 49 | TFT_eSPI tft = TFT_eSPI(); // Invoke custom library 50 | #define LOG(f_, ...) \ 51 | { \ 52 | if (tft.getCursorY() >= tft.height()) { \ 53 | tft.fillScreen(TFT_BLACK); \ 54 | tft.setCursor(0, 0); \ 55 | tft.printf("IP: %u.%u.%u.%u\r\n", myip[0], myip[1], myip[2], myip[3]); \ 56 | tft.printf("Hostname: %s\r\n--\r\n", Husarnet.getHostname().c_str()); \ 57 | } \ 58 | tft.printf((f_), ##__VA_ARGS__); \ 59 | Serial.printf((f_), ##__VA_ARGS__); \ 60 | } 61 | #else 62 | #define LOG(f_, ...) \ 63 | { Serial.printf((f_), ##__VA_ARGS__); } 64 | #endif 65 | 66 | // you can provide credentials to multiple WiFi networks 67 | WiFiMulti wifiMulti; 68 | IPAddress myip; 69 | 70 | // https://github.com/me-no-dev/ESPAsyncWebServer/issues/324 - sometimes 71 | AsyncWebServer server(HTTP_PORT); 72 | AsyncWebSocket ws("/ws"); 73 | 74 | StaticJsonDocument<200> jsonDocTx; 75 | StaticJsonDocument<100> jsonDocRx; 76 | 77 | extern const char index_html_start[] asm("_binary_src_index_html_start"); 78 | const String html = String((const char*)index_html_start); 79 | 80 | bool wsconnected = false; 81 | 82 | void notFound(AsyncWebServerRequest* request) { 83 | request->send(404, "text/plain", "Not found"); 84 | } 85 | 86 | void onWsEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, 87 | AwsEventType type, void* arg, uint8_t* data, size_t len) { 88 | if (type == WS_EVT_CONNECT) { 89 | wsconnected = true; 90 | LOG("ws[%s][%u] connect\n", server->url(), client->id()); 91 | // client->printf("Hello Client %u :)", client->id()); 92 | client->ping(); 93 | } else if (type == WS_EVT_DISCONNECT) { 94 | wsconnected = false; 95 | LOG("ws[%s][%u] disconnect\n", server->url(), client->id()); 96 | } else if (type == WS_EVT_ERROR) { 97 | LOG("ws[%s][%u] error(%u): %s\n", server->url(), client->id(), 98 | *((uint16_t*)arg), (char*)data); 99 | } else if (type == WS_EVT_PONG) { 100 | LOG("ws[%s][%u] pong[%u]: %s\n", server->url(), client->id(), len, 101 | (len) ? (char*)data : ""); 102 | } else if (type == WS_EVT_DATA) { 103 | AwsFrameInfo* info = (AwsFrameInfo*)arg; 104 | String msg = ""; 105 | if (info->final && info->index == 0 && info->len == len) { 106 | // the whole message is in a single frame and we got all of it's data 107 | LOG("ws[%s][%u] %s-msg[%llu]\r\n", server->url(), client->id(), 108 | (info->opcode == WS_TEXT) ? "txt" : "bin", info->len); 109 | 110 | if (info->opcode == WS_TEXT) { 111 | for (size_t i = 0; i < info->len; i++) { 112 | msg += (char)data[i]; 113 | } 114 | LOG("%s\r\n\r\n", msg.c_str()); 115 | 116 | deserializeJson(jsonDocRx, msg); 117 | 118 | uint8_t ledState = jsonDocRx["led"]; 119 | if (ledState == 1) { 120 | digitalWrite(LED_PIN, HIGH); 121 | } 122 | if (ledState == 0) { 123 | digitalWrite(LED_PIN, LOW); 124 | } 125 | jsonDocRx.clear(); 126 | } 127 | } 128 | } 129 | } 130 | 131 | void taskWifi(void* parameter); 132 | void taskStatus(void* parameter); 133 | 134 | void setup() { 135 | Serial.begin(115200); 136 | 137 | #if ENABLE_TFT == 1 138 | tft.init(); 139 | tft.setRotation(0); 140 | tft.fillScreen(TFT_BLACK); 141 | tft.setTextColor(TFT_WHITE, TFT_BLACK); 142 | tft.setTextSize(1); 143 | 144 | LOG("Starting...\r\n"); 145 | tft.setCursor(0, tft.height()); 146 | #endif 147 | 148 | pinMode(BUTTON_PIN, INPUT_PULLUP); 149 | 150 | pinMode(LED_PIN, OUTPUT); 151 | digitalWrite(LED_PIN, LOW); 152 | 153 | xTaskCreate(taskWifi, /* Task function. */ 154 | "taskWifi", /* String with name of task. */ 155 | 20000, /* Stack size in bytes. */ 156 | NULL, /* Parameter passed as input of the task */ 157 | 2, /* Priority of the task. */ 158 | NULL); /* Task handle. */ 159 | } 160 | 161 | void taskWifi(void* parameter) { 162 | uint8_t stat = WL_DISCONNECTED; 163 | static char output[200]; 164 | int cnt = 0; 165 | int lastButtonState = digitalRead(BUTTON_PIN); 166 | TickType_t xLastWakeTime = xTaskGetTickCount(); 167 | 168 | /* Configure Wi-Fi */ 169 | for (int i = 0; i < NUM_NETWORKS; i++) { 170 | wifiMulti.addAP(ssidTab[i], passwordTab[i]); 171 | Serial.printf("WiFi %d: SSID: \"%s\" ; PASS: \"%s\"\r\n", i, ssidTab[i], 172 | passwordTab[i]); 173 | } 174 | 175 | while (stat != WL_CONNECTED) { 176 | stat = wifiMulti.run(); 177 | Serial.printf("WiFi status: %d\r\n", (int)stat); 178 | delay(100); 179 | } 180 | 181 | Serial.printf("WiFi connected\r\n", (int)stat); 182 | Serial.printf("IP address: "); 183 | Serial.println(myip = WiFi.localIP()); 184 | 185 | /* Start Husarnet */ 186 | Husarnet.selfHostedSetup(dashboardURL); 187 | Husarnet.join(husarnetJoinCode, hostName); 188 | Husarnet.start(); 189 | 190 | /* Start web server and web socket server */ 191 | server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) { 192 | request->send(200, "text/html", html); 193 | }); 194 | server.onNotFound(notFound); 195 | ws.onEvent(onWsEvent); 196 | server.addHandler(&ws); 197 | server.begin(); 198 | 199 | LOG("Type in browser:\r\n%s:%d\r\n", hostName, HTTP_PORT); 200 | 201 | while (1) { 202 | while (WiFi.status() == WL_CONNECTED) { 203 | cnt++; 204 | if ((wsconnected == true) && 205 | ((lastButtonState != digitalRead(BUTTON_PIN)) || (cnt % 100 == 0))) { 206 | lastButtonState = digitalRead(BUTTON_PIN); 207 | jsonDocTx.clear(); 208 | jsonDocTx["counter"] = cnt; 209 | jsonDocTx["button"] = lastButtonState; 210 | 211 | serializeJson(jsonDocTx, output, 200); 212 | 213 | Serial.printf("Sending: %s", output); 214 | if (ws.availableForWriteAll()) { 215 | ws.textAll(output); 216 | Serial.printf("...done\r\n"); 217 | } else { 218 | Serial.printf("...queue is full\r\n"); 219 | } 220 | } else { 221 | vTaskDelayUntil(&xLastWakeTime, 10 / portTICK_PERIOD_MS); 222 | } 223 | } 224 | 225 | stat = wifiMulti.run(); 226 | myip = WiFi.localIP(); 227 | LOG("WiFi status: %d\r\n", (int)stat); 228 | delay(500); 229 | } 230 | } 231 | 232 | void loop() { 233 | Serial.printf("[RAM: %d]\r\n", esp_get_free_heap_size()); 234 | delay(1000); 235 | // add ping mechanism in case of websocket connection timeout 236 | // ws.cleanupClients(); 237 | // ws.pingAll(); 238 | } 239 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 12 | ESP32 + Bootstrap + WebSocket + JSON + Husarnet 13 | 14 | 85 | 86 | 87 | 88 |
89 |
90 |
91 |
92 |

ESP32 control

93 |
94 |
95 |
96 |
97 | 98 |
99 |
100 |
101 |
102 |
103 |
Input 1
104 |
105 |
Counter
106 |

107 | A counter value is updated every 1s by ESP32. 108 |

109 |

110 | 0 111 |

112 |
113 |
114 |
115 |
116 |
117 |
Input 2
118 |
119 |
Button
120 |

121 | Press the push button on ESP32 board to change a color of the 122 | dot below. 123 |

124 | 125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
Output
133 |
134 |
LED
135 |

136 | Press the button to turn LED on. Release to turn LED off. 137 |

138 | 142 |
143 |
144 |
145 |
146 |
147 |
148 | 149 | 150 | --------------------------------------------------------------------------------