├── data ├── favicon.ico ├── index.html ├── index.js └── index.css ├── platformio.ini ├── Readme.md ├── LICENSE └── src └── main.cpp /data/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1cr0lab-esp32/remote-control-with-websocket/HEAD/data/favicon.ico -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | [env:esp32doit-devkit-v1] 2 | platform = espressif32 3 | board = esp32doit-devkit-v1 4 | framework = arduino 5 | upload_speed = 921600 6 | monitor_speed = 115200 7 | lib_deps = ESP Async WebServer, ArduinoJson -------------------------------------------------------------------------------- /data/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ESP32 remote control 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |

ESP32 remote control

19 |
20 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # ESP32 Remote Control with WebSocket 2 | 3 | [Please visit the tutorial][tutorial] 4 | 5 | Throughout the tutorial, you will be able to update your code using the different tagged versions of this repository. Each tag corresponds to the finalized code of a chapter: 6 | 7 | ``` 8 | $ git tag --list -n1 9 | v0.0 Project Bootstrap 10 | v0.1 LED Setup 11 | v0.2 Button Setup 12 | v0.3 Web UI Design 13 | v0.4 SPIFFS Setup 14 | v0.5 WiFi Setup 15 | v0.6 Web Server Setup 16 | v0.7 WebSocket Setup 17 | v0.8 WebSocket Data Exchange 18 | v1.0 WebSocket and JSON 19 | ``` 20 | 21 | For example, to switch to the version of the chapter that deals with setting up the WiFi connection, simply run: 22 | 23 | ``` 24 | $ git checkout v0.5 25 | ``` 26 | 27 | The complete project code corresponds to version 1.0: 28 | 29 | ``` 30 | $ git checkout v1.0 31 | ``` 32 | 33 | 34 | [tutorial]: https://m1cr0lab-esp32.github.io/remote-control-with-websocket/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Stéphane Calderoni 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. -------------------------------------------------------------------------------- /data/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ---------------------------------------------------------------------------- 3 | * ESP32 Remote Control with WebSocket 4 | * ---------------------------------------------------------------------------- 5 | * © 2020 Stéphane Calderoni 6 | * ---------------------------------------------------------------------------- 7 | */ 8 | 9 | var gateway = `ws://${window.location.hostname}/ws`; 10 | var websocket; 11 | 12 | // ---------------------------------------------------------------------------- 13 | // Initialization 14 | // ---------------------------------------------------------------------------- 15 | 16 | window.addEventListener('load', onLoad); 17 | 18 | function onLoad(event) { 19 | initWebSocket(); 20 | initButton(); 21 | } 22 | 23 | // ---------------------------------------------------------------------------- 24 | // WebSocket handling 25 | // ---------------------------------------------------------------------------- 26 | 27 | function initWebSocket() { 28 | console.log('Trying to open a WebSocket connection...'); 29 | websocket = new WebSocket(gateway); 30 | websocket.onopen = onOpen; 31 | websocket.onclose = onClose; 32 | websocket.onmessage = onMessage; 33 | } 34 | 35 | function onOpen(event) { 36 | console.log('Connection opened'); 37 | } 38 | 39 | function onClose(event) { 40 | console.log('Connection closed'); 41 | setTimeout(initWebSocket, 2000); 42 | } 43 | 44 | function onMessage(event) { 45 | let data = JSON.parse(event.data); 46 | document.getElementById('led').className = data.status; 47 | } 48 | 49 | // ---------------------------------------------------------------------------- 50 | // Button handling 51 | // ---------------------------------------------------------------------------- 52 | 53 | function initButton() { 54 | document.getElementById('toggle').addEventListener('click', onToggle); 55 | } 56 | 57 | function onToggle(event) { 58 | websocket.send(JSON.stringify({'action':'toggle'})); 59 | } -------------------------------------------------------------------------------- /data/index.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | /** 4 | * ---------------------------------------------------------------------------- 5 | * ESP32 Remote Control with WebSocket 6 | * ---------------------------------------------------------------------------- 7 | * © 2020 Stéphane Calderoni 8 | * ---------------------------------------------------------------------------- 9 | */ 10 | 11 | @import url("https://fonts.googleapis.com/css2?family=Roboto&display=swap"); 12 | * { 13 | margin: 0; 14 | padding: 0; 15 | box-sizing: border-box; 16 | } 17 | 18 | html, body { 19 | height: 100%; 20 | font-family: Roboto, sans-serif; 21 | font-size: 12pt; 22 | overflow: hidden; 23 | } 24 | 25 | body { 26 | display: grid; 27 | grid-template-rows: 1fr; 28 | align-items: center; 29 | justify-items: center; 30 | } 31 | 32 | .panel { 33 | display: grid; 34 | grid-gap: 3em; 35 | justify-items: center; 36 | } 37 | 38 | h1 { 39 | font-size: 1.5rem; 40 | text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); 41 | } 42 | 43 | #led { 44 | position: relative; 45 | width: 5em; 46 | height: 5em; 47 | border: 2px solid #000; 48 | border-radius: 2.5em; 49 | background-image: radial-gradient(farthest-corner at 50% 20%, #b30000 0%, #330000 100%); 50 | box-shadow: 0 0.5em 1em rgba(102, 0, 0, 0.3); 51 | } 52 | #led.on { 53 | background-image: radial-gradient(farthest-corner at 50% 75%, red 0%, #990000 100%); 54 | box-shadow: 0 1em 1.5em rgba(255, 0, 0, 0.5); 55 | } 56 | #led:after { 57 | content: ''; 58 | position: absolute; 59 | top: .3em; 60 | left: 1em; 61 | width: 60%; 62 | height: 40%; 63 | border-radius: 60%; 64 | background-image: linear-gradient(rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.1)); 65 | } 66 | 67 | button { 68 | padding: .5em .75em; 69 | font-size: 1.2rem; 70 | color: #fff; 71 | text-shadow: 0 -1px 1px #000; 72 | border: 1px solid #000; 73 | border-radius: .5em; 74 | background-image: linear-gradient(#2e3538, #73848c); 75 | box-shadow: inset 0 2px 4px rgba(255, 255, 255, 0.5), 0 0.2em 0.4em rgba(0, 0, 0, 0.4); 76 | outline: none; 77 | } 78 | button:active { 79 | transform: translateY(2px); 80 | } 81 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * ---------------------------------------------------------------------------- 3 | * ESP32 Remote Control with WebSocket 4 | * ---------------------------------------------------------------------------- 5 | * © 2020 Stéphane Calderoni 6 | * ---------------------------------------------------------------------------- 7 | */ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | // ---------------------------------------------------------------------------- 16 | // Definition of macros 17 | // ---------------------------------------------------------------------------- 18 | 19 | #define LED_PIN 26 20 | #define BTN_PIN 22 21 | #define HTTP_PORT 80 22 | 23 | // ---------------------------------------------------------------------------- 24 | // Definition of global constants 25 | // ---------------------------------------------------------------------------- 26 | 27 | // Button debouncing 28 | const uint8_t DEBOUNCE_DELAY = 10; // in milliseconds 29 | 30 | // WiFi credentials 31 | const char *WIFI_SSID = "YOUR_WIFI_SSID"; 32 | const char *WIFI_PASS = "YOUR_WIFI_PASSWORD"; 33 | 34 | // ---------------------------------------------------------------------------- 35 | // Definition of the LED component 36 | // ---------------------------------------------------------------------------- 37 | 38 | struct Led { 39 | // state variables 40 | uint8_t pin; 41 | bool on; 42 | 43 | // methods 44 | void update() { 45 | digitalWrite(pin, on ? HIGH : LOW); 46 | } 47 | }; 48 | 49 | // ---------------------------------------------------------------------------- 50 | // Definition of the Button component 51 | // ---------------------------------------------------------------------------- 52 | 53 | struct Button { 54 | // state variables 55 | uint8_t pin; 56 | bool lastReading; 57 | uint32_t lastDebounceTime; 58 | uint16_t state; 59 | 60 | // methods determining the logical state of the button 61 | bool pressed() { return state == 1; } 62 | bool released() { return state == 0xffff; } 63 | bool held(uint16_t count = 0) { return state > 1 + count && state < 0xffff; } 64 | 65 | // method for reading the physical state of the button 66 | void read() { 67 | // reads the voltage on the pin connected to the button 68 | bool reading = digitalRead(pin); 69 | 70 | // if the logic level has changed since the last reading, 71 | // we reset the timer which counts down the necessary time 72 | // beyond which we can consider that the bouncing effect 73 | // has passed. 74 | if (reading != lastReading) { 75 | lastDebounceTime = millis(); 76 | } 77 | 78 | // from the moment we're out of the bouncing phase 79 | // the actual status of the button can be determined 80 | if (millis() - lastDebounceTime > DEBOUNCE_DELAY) { 81 | // don't forget that the read pin is pulled-up 82 | bool pressed = reading == LOW; 83 | if (pressed) { 84 | if (state < 0xfffe) state++; 85 | else if (state == 0xfffe) state = 2; 86 | } else if (state) { 87 | state = state == 0xffff ? 0 : 0xffff; 88 | } 89 | } 90 | 91 | // finally, each new reading is saved 92 | lastReading = reading; 93 | } 94 | }; 95 | 96 | // ---------------------------------------------------------------------------- 97 | // Definition of global variables 98 | // ---------------------------------------------------------------------------- 99 | 100 | Led onboard_led = { LED_BUILTIN, false }; 101 | Led led = { LED_PIN, false }; 102 | Button button = { BTN_PIN, HIGH, 0, 0 }; 103 | 104 | AsyncWebServer server(HTTP_PORT); 105 | AsyncWebSocket ws("/ws"); 106 | 107 | // ---------------------------------------------------------------------------- 108 | // SPIFFS initialization 109 | // ---------------------------------------------------------------------------- 110 | 111 | void initSPIFFS() { 112 | if (!SPIFFS.begin()) { 113 | Serial.println("Cannot mount SPIFFS volume..."); 114 | while (1) { 115 | onboard_led.on = millis() % 200 < 50; 116 | onboard_led.update(); 117 | } 118 | } 119 | } 120 | 121 | // ---------------------------------------------------------------------------- 122 | // Connecting to the WiFi network 123 | // ---------------------------------------------------------------------------- 124 | 125 | void initWiFi() { 126 | WiFi.mode(WIFI_STA); 127 | WiFi.begin(WIFI_SSID, WIFI_PASS); 128 | Serial.printf("Trying to connect [%s] ", WiFi.macAddress().c_str()); 129 | while (WiFi.status() != WL_CONNECTED) { 130 | Serial.print("."); 131 | delay(500); 132 | } 133 | Serial.printf(" %s\n", WiFi.localIP().toString().c_str()); 134 | } 135 | 136 | // ---------------------------------------------------------------------------- 137 | // Web server initialization 138 | // ---------------------------------------------------------------------------- 139 | 140 | String processor(const String &var) { 141 | return String(var == "STATE" && led.on ? "on" : "off"); 142 | } 143 | 144 | void onRootRequest(AsyncWebServerRequest *request) { 145 | request->send(SPIFFS, "/index.html", "text/html", false, processor); 146 | } 147 | 148 | void initWebServer() { 149 | server.on("/", onRootRequest); 150 | server.serveStatic("/", SPIFFS, "/"); 151 | server.begin(); 152 | } 153 | 154 | // ---------------------------------------------------------------------------- 155 | // WebSocket initialization 156 | // ---------------------------------------------------------------------------- 157 | 158 | void notifyClients() { 159 | const uint8_t size = JSON_OBJECT_SIZE(1); 160 | StaticJsonDocument json; 161 | json["status"] = led.on ? "on" : "off"; 162 | 163 | char buffer[17]; 164 | size_t len = serializeJson(json, buffer); 165 | ws.textAll(buffer, len); 166 | } 167 | 168 | void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) { 169 | AwsFrameInfo *info = (AwsFrameInfo*)arg; 170 | if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) { 171 | 172 | const uint8_t size = JSON_OBJECT_SIZE(1); 173 | StaticJsonDocument json; 174 | DeserializationError err = deserializeJson(json, data); 175 | if (err) { 176 | Serial.print(F("deserializeJson() failed with code ")); 177 | Serial.println(err.c_str()); 178 | return; 179 | } 180 | 181 | const char *action = json["action"]; 182 | if (strcmp(action, "toggle") == 0) { 183 | led.on = !led.on; 184 | notifyClients(); 185 | } 186 | 187 | } 188 | } 189 | 190 | void onEvent(AsyncWebSocket *server, 191 | AsyncWebSocketClient *client, 192 | AwsEventType type, 193 | void *arg, 194 | uint8_t *data, 195 | size_t len) { 196 | 197 | switch (type) { 198 | case WS_EVT_CONNECT: 199 | Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str()); 200 | break; 201 | case WS_EVT_DISCONNECT: 202 | Serial.printf("WebSocket client #%u disconnected\n", client->id()); 203 | break; 204 | case WS_EVT_DATA: 205 | handleWebSocketMessage(arg, data, len); 206 | break; 207 | case WS_EVT_PONG: 208 | case WS_EVT_ERROR: 209 | break; 210 | } 211 | } 212 | 213 | void initWebSocket() { 214 | ws.onEvent(onEvent); 215 | server.addHandler(&ws); 216 | } 217 | 218 | // ---------------------------------------------------------------------------- 219 | // Initialization 220 | // ---------------------------------------------------------------------------- 221 | 222 | void setup() { 223 | pinMode(onboard_led.pin, OUTPUT); 224 | pinMode(led.pin, OUTPUT); 225 | pinMode(button.pin, INPUT); 226 | 227 | Serial.begin(115200); delay(500); 228 | 229 | initSPIFFS(); 230 | initWiFi(); 231 | initWebSocket(); 232 | initWebServer(); 233 | } 234 | 235 | // ---------------------------------------------------------------------------- 236 | // Main control loop 237 | // ---------------------------------------------------------------------------- 238 | 239 | void loop() { 240 | ws.cleanupClients(); 241 | 242 | button.read(); 243 | 244 | if (button.pressed()) { 245 | led.on = !led.on; 246 | notifyClients(); 247 | } 248 | 249 | onboard_led.on = millis() % 1000 < 50; 250 | 251 | led.update(); 252 | onboard_led.update(); 253 | } --------------------------------------------------------------------------------