├── 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 | }
--------------------------------------------------------------------------------