├── ESP32_CAM_WebSocket_Servo_Tests.ino ├── README.md └── data ├── edit.htm ├── favicon.ico ├── graphs.js.gz ├── index.htm ├── ws_arm.html ├── ws_basic.html ├── ws_gamepad.html ├── ws_gamepad_example_data.html └── ws_touchscreen_joystick.html /ESP32_CAM_WebSocket_Servo_Tests.ino: -------------------------------------------------------------------------------- 1 | /* 2 | FSWebServer - Example WebServer with SPIFFS backend for esp8266 3 | Copyright (c) 2015 Hristo Gochkov. All rights reserved. 4 | This file is part of the ESP8266WebServer library for Arduino environment. 5 | 6 | This library is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU Lesser General Public 8 | License as published by the Free Software Foundation; either 9 | version 2.1 of the License, or (at your option) any later version. 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | You should have received a copy of the GNU Lesser General Public 15 | License along with this library; if not, write to the Free Software 16 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | upload the contents of the data folder with MkSPIFFS Tool ("ESP8266 Sketch Data Upload" in Tools menu in Arduino IDE) 19 | or you can upload the contents of a folder if you CD in that folder and run the following command: 20 | for file in `\ls -A1`; do curl -F "file=@$PWD/$file" esp8266fs.local/edit; done 21 | 22 | access the sample web page at http://esp8266fs.local 23 | edit the page by going to http://esp8266fs.local/edit 24 | */ 25 | #if defined(ARDUINO_ARCH_ESP8266) 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #elif defined(ARDUINO_ARCH_ESP32) 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #endif 38 | //Add a below line for AutoConnect. 39 | #include 40 | #include 41 | 42 | #include 43 | #include 44 | Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver(); 45 | #define SERVOMIN 150 // This is the 'minimum' pulse length count (out of 4096) 46 | #define SERVOMAX 600 // This is the 'maximum' pulse length count (out of 4096) 47 | #define SERVO_FREQ 50 // Analog servos run at ~50 Hz updates 48 | 49 | #define DBG_OUTPUT_PORT Serial 50 | 51 | const char* ssid = "esp32001"; 52 | const char* password = "esp32001"; 53 | #if defined(ARDUINO_ARCH_ESP8266) 54 | const char* host = "esp8266fs"; 55 | 56 | ESP8266WebServer server(80); 57 | #elif defined(ARDUINO_ARCH_ESP32) 58 | const char* host = "esp32fs"; 59 | 60 | WebServer server(80); 61 | #endif 62 | 63 | #define AC_DEBUG 64 | WebSocketsServer webSocket = WebSocketsServer(81); 65 | 66 | //Add a below line for AutoConnect. 67 | AutoConnect portal(server); 68 | AutoConnectConfig config; 69 | AutoConnectAux FSBedit("/edit", "Edit"); 70 | AutoConnectAux FSBlist("/list?dir=\"/\"", "List"); 71 | //holds the current upload 72 | File fsUploadFile; 73 | 74 | //format bytes 75 | String formatBytes(size_t bytes){ 76 | if (bytes < 1024) { 77 | return String(bytes) + "B"; 78 | } else if (bytes < (1024 * 1024)) { 79 | return String(bytes / 1024.0) + "KB"; 80 | } else if (bytes < (1024 * 1024 * 1024)) { 81 | return String(bytes / 1024.0 / 1024.0) + "MB"; 82 | } else { 83 | return String(bytes / 1024.0 / 1024.0 / 1024.0) + "GB"; 84 | } 85 | } 86 | 87 | String getContentType(String filename) { 88 | if (server.hasArg("download")) { 89 | return "application/octet-stream"; 90 | } else if (filename.endsWith(".htm")) { 91 | return "text/html"; 92 | } else if (filename.endsWith(".html")) { 93 | return "text/html"; 94 | } else if (filename.endsWith(".css")) { 95 | return "text/css"; 96 | } else if (filename.endsWith(".js")) { 97 | return "application/javascript"; 98 | } else if (filename.endsWith(".png")) { 99 | return "image/png"; 100 | } else if (filename.endsWith(".gif")) { 101 | return "image/gif"; 102 | } else if (filename.endsWith(".jpg")) { 103 | return "image/jpeg"; 104 | } else if (filename.endsWith(".ico")) { 105 | return "image/x-icon"; 106 | } else if (filename.endsWith(".xml")) { 107 | return "text/xml"; 108 | } else if (filename.endsWith(".pdf")) { 109 | return "application/x-pdf"; 110 | } else if (filename.endsWith(".zip")) { 111 | return "application/x-zip"; 112 | } else if (filename.endsWith(".gz")) { 113 | return "application/x-gzip"; 114 | } 115 | return "text/plain"; 116 | } 117 | 118 | bool handleFileRead(String path) { 119 | DBG_OUTPUT_PORT.println("handleFileRead: " + path); 120 | if (path.endsWith("/")) { 121 | path += "index.htm"; 122 | } 123 | String contentType = getContentType(path); 124 | String pathWithGz = path + ".gz"; 125 | if (SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)) { 126 | if (SPIFFS.exists(pathWithGz)) { 127 | path += ".gz"; 128 | } 129 | File file = SPIFFS.open(path, "r"); 130 | server.streamFile(file, contentType); 131 | file.close(); 132 | return true; 133 | } 134 | return false; 135 | } 136 | 137 | void handleFileUpload() { 138 | if (server.uri() != "/edit") { 139 | return; 140 | } 141 | HTTPUpload& upload = server.upload(); 142 | if (upload.status == UPLOAD_FILE_START) { 143 | String filename = upload.filename; 144 | if (!filename.startsWith("/")) { 145 | filename = "/" + filename; 146 | } 147 | DBG_OUTPUT_PORT.print("handleFileUpload Name: "); DBG_OUTPUT_PORT.println(filename); 148 | fsUploadFile = SPIFFS.open(filename, "w"); 149 | filename = String(); 150 | } else if (upload.status == UPLOAD_FILE_WRITE) { 151 | //DBG_OUTPUT_PORT.print("handleFileUpload Data: "); DBG_OUTPUT_PORT.println(upload.currentSize); 152 | if (fsUploadFile) { 153 | fsUploadFile.write(upload.buf, upload.currentSize); 154 | } 155 | } else if (upload.status == UPLOAD_FILE_END) { 156 | if (fsUploadFile) { 157 | fsUploadFile.close(); 158 | } 159 | DBG_OUTPUT_PORT.print("handleFileUpload Size: "); DBG_OUTPUT_PORT.println(upload.totalSize); 160 | } 161 | } 162 | 163 | void handleFileDelete() { 164 | if (server.args() == 0) { 165 | return server.send(500, "text/plain", "BAD ARGS"); 166 | } 167 | String path = server.arg(0); 168 | DBG_OUTPUT_PORT.println("handleFileDelete: " + path); 169 | if (path == "/") { 170 | return server.send(500, "text/plain", "BAD PATH"); 171 | } 172 | if (!SPIFFS.exists(path)) { 173 | return server.send(404, "text/plain", "FileNotFound"); 174 | } 175 | SPIFFS.remove(path); 176 | server.send(200, "text/plain", ""); 177 | path = String(); 178 | } 179 | 180 | void handleFileCreate() { 181 | if (server.args() == 0) { 182 | return server.send(500, "text/plain", "BAD ARGS"); 183 | } 184 | String path = server.arg(0); 185 | DBG_OUTPUT_PORT.println("handleFileCreate: " + path); 186 | if (path == "/") { 187 | return server.send(500, "text/plain", "BAD PATH"); 188 | } 189 | if (SPIFFS.exists(path)) { 190 | return server.send(500, "text/plain", "FILE EXISTS"); 191 | } 192 | File file = SPIFFS.open(path, "w"); 193 | if (file) { 194 | file.close(); 195 | } else { 196 | return server.send(500, "text/plain", "CREATE FAILED"); 197 | } 198 | server.send(200, "text/plain", ""); 199 | path = String(); 200 | } 201 | 202 | void handleFileList() { 203 | if (!server.hasArg("dir")) { 204 | server.send(500, "text/plain", "BAD ARGS"); 205 | return; 206 | } 207 | 208 | String path = server.arg("dir"); 209 | DBG_OUTPUT_PORT.println("handleFileList: " + path); 210 | #if defined(ARDUINO_ARCH_ESP8266) 211 | Dir dir = SPIFFS.openDir(path); 212 | #elif defined(ARDUINO_ARCH_ESP32) 213 | File root = SPIFFS.open(path); 214 | #endif 215 | path = String(); 216 | 217 | String output = "["; 218 | #if defined(ARDUINO_ARCH_ESP8266) 219 | while (dir.next()) { 220 | File entry = dir.openFile("r"); 221 | if (output != "[") { 222 | output += ','; 223 | } 224 | bool isDir = false; 225 | output += "{\"type\":\""; 226 | output += (isDir) ? "dir" : "file"; 227 | output += "\",\"name\":\""; 228 | output += String(entry.name()).substring(1); 229 | output += "\",\"size\":\""; 230 | output += String(entry.size()); 231 | output += "\"}"; 232 | entry.close(); 233 | } 234 | #elif defined(ARDUINO_ARCH_ESP32) 235 | if(root.isDirectory()){ 236 | File file = root.openNextFile(); 237 | while(file){ 238 | if (output != "[") { 239 | output += ','; 240 | } 241 | output += "{\"type\":\""; 242 | output += (file.isDirectory()) ? "dir" : "file"; 243 | output += "\",\"name\":\""; 244 | output += String(file.name()).substring(1); 245 | output += "\",\"size\":\""; 246 | output += String(file.size()); 247 | output += "\"}"; 248 | file = root.openNextFile(); 249 | } 250 | } 251 | #endif 252 | 253 | output += "]"; 254 | server.send(200, "text/json", output); 255 | } 256 | 257 | void setup(void){ 258 | DBG_OUTPUT_PORT.begin(115200); 259 | DBG_OUTPUT_PORT.print("\n"); 260 | DBG_OUTPUT_PORT.setDebugOutput(true); 261 | SPIFFS.begin(); 262 | { 263 | #if defined(ARDUINO_ARCH_ESP8266) 264 | Dir dir = SPIFFS.openDir("/"); 265 | while (dir.next()) { 266 | String fileName = dir.fileName(); 267 | size_t fileSize = dir.fileSize(); 268 | DBG_OUTPUT_PORT.printf("FS File: %s, size: %s\n", fileName.c_str(), formatBytes(fileSize).c_str()); 269 | } 270 | #elif defined(ARDUINO_ARCH_ESP32) 271 | File root = SPIFFS.open("/"); 272 | File file = root.openNextFile(); 273 | while(file){ 274 | String fileName = file.name(); 275 | size_t fileSize = file.size(); 276 | DBG_OUTPUT_PORT.printf("FS File: %s, size: %s\n", fileName.c_str(), formatBytes(fileSize).c_str()); 277 | file = root.openNextFile(); 278 | } 279 | #endif 280 | DBG_OUTPUT_PORT.printf("\n"); 281 | } 282 | 283 | 284 | DBG_OUTPUT_PORT.println("PWM starting"); 285 | Wire.begin(13,15); 286 | pwm.begin(); 287 | pwm.setPWMFreq(SERVO_FREQ); // Analog servos run at ~50 Hz updates 288 | DBG_OUTPUT_PORT.println("PWM started"); 289 | 290 | //SERVER INIT 291 | //list directory 292 | server.on("/list", HTTP_GET, handleFileList); 293 | //load editor 294 | server.on("/edit", HTTP_GET, []() { 295 | if (!handleFileRead("/edit.htm")) { 296 | server.send(404, "text/plain", "FileNotFound"); 297 | } 298 | }); 299 | //create file 300 | server.on("/edit", HTTP_PUT, handleFileCreate); 301 | //delete file 302 | server.on("/edit", HTTP_DELETE, handleFileDelete); 303 | //first callback is called after the request has ended with all parsed arguments 304 | //second callback handles file uploads at that location 305 | server.on("/edit", HTTP_POST, []() { 306 | server.send(200, "text/plain", ""); 307 | }, handleFileUpload); 308 | 309 | DBG_OUTPUT_PORT.println("server handlers started"); 310 | //called when the url is not defined here 311 | //use it to load content from SPIFFS 312 | //Replacement as follows to make AutoConnect recognition. 313 | //server.onNotFound([](){ 314 | portal.onNotFound([](){ 315 | if(!handleFileRead(server.uri())) 316 | server.send(404, "text/plain", "FileNotFound"); 317 | }); 318 | DBG_OUTPUT_PORT.println("404 handler started"); 319 | 320 | //get heap status, analog input value and all GPIO statuses in one json call 321 | server.on("/all", HTTP_GET, [](){ 322 | String json = "{"; 323 | json += "\"heap\":"+String(ESP.getFreeHeap()); 324 | json += ", \"analog\":"+String(analogRead(A0)); 325 | #if defined(ARDUINO_ARCH_ESP8266) 326 | json += ", \"gpio\":"+String((uint32_t)(((GPI | GPO) & 0xFFFF) | ((GP16I & 0x01) << 16))); 327 | #elif defined(ARDUINO_ARCH_ESP32) 328 | json += ", \"gpio\":" + String((uint32_t)(0)); 329 | #endif 330 | json += "}"; 331 | server.send(200, "text/json", json); 332 | json = String(); 333 | }); 334 | 335 | DBG_OUTPUT_PORT.println("/all handler started"); 336 | //Set menu title 337 | config.title = "FSBrowser"; 338 | portal.config(config); 339 | //Register AutoConnect menu 340 | DBG_OUTPUT_PORT.println("portal about to join"); 341 | portal.join({ FSBedit, FSBlist }); 342 | //Replacement as follows to make AutoConnect recognition. 343 | //server.begin(); 344 | DBG_OUTPUT_PORT.println("portal about to begin"); 345 | portal.begin(); 346 | DBG_OUTPUT_PORT.println("HTTP server started"); 347 | 348 | //Relocation as follows to make AutoConnect recognition. 349 | DBG_OUTPUT_PORT.println(""); 350 | DBG_OUTPUT_PORT.print("Connected! IP address: "); 351 | DBG_OUTPUT_PORT.println(WiFi.localIP()); 352 | 353 | //Relocation as follows to make AutoConnect recognition. 354 | if (MDNS.begin(host)) { 355 | MDNS.addService("http", "tcp", 80); 356 | DBG_OUTPUT_PORT.print("Open http://"); 357 | DBG_OUTPUT_PORT.print(host); 358 | DBG_OUTPUT_PORT.println(".local/edit to see the file browser"); 359 | } 360 | else { 361 | DBG_OUTPUT_PORT.print("mDNS start failed"); 362 | } 363 | webSocket.begin(); 364 | webSocket.onEvent(webSocketEvent); 365 | 366 | centerServos(); 367 | } 368 | 369 | void loop(void){ 370 | //Replacement as follows to make AutoConnect recognition. 371 | //server.handleClient(); 372 | portal.handleClient(); 373 | #ifdef ARDUINO_ARCH_ESP8266 374 | MDNS.update(); 375 | #endif 376 | webSocket.loop(); 377 | if(Serial.available() > 0){ 378 | char c[] = {(char)Serial.read()}; 379 | webSocket.broadcastTXT(c, sizeof(c)); 380 | } 381 | } 382 | 383 | void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length){ 384 | if(type == WStype_TEXT){ 385 | if(payload[0] == 'S'){ // S0=90 386 | uint16_t servoNum = (uint16_t) strtol((const char *) &payload[1], NULL, 16); 387 | uint16_t servoVal = (uint16_t) strtol((const char *) &payload[3], NULL, 10); 388 | 389 | Serial.print("S"); 390 | Serial.print(servoNum); 391 | Serial.print("="); 392 | Serial.println(servoVal); 393 | 394 | uint16_t servoPWM = map(servoVal, 0, 180, SERVOMIN, SERVOMAX); // map(value, fromLow, fromHigh, toLow, toHigh) 395 | 396 | Serial.print(servoNum); 397 | Serial.print(" "); 398 | Serial.println(servoPWM); 399 | pwm.setPWM(servoNum, 0, servoPWM); 400 | } 401 | 402 | else{ 403 | for(int i = 0; i < length; i++) 404 | Serial.print((char) payload[i]); 405 | Serial.println(); 406 | } 407 | } 408 | 409 | } 410 | 411 | void centerServos(){ 412 | for(int i = 0; i < 16; i++){ 413 | Serial.print(i); 414 | Serial.print(" "); 415 | Serial.println(((SERVOMAX-SERVOMIN)/2)+SERVOMIN); 416 | pwm.setPWM(i, 0, ((SERVOMAX-SERVOMIN)/2)+SERVOMIN); 417 | } 418 | } 419 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP32_CAM_WebSocket_Servo_Tests 2 | Test/example code for the ESP32-CAM to run servos via a PCA9685 servo driver over I2C 3 | 4 | - Started with the [Autoconnect's FSBrowser example](https://github.com/Hieromon/AutoConnect/tree/master/examples/FSBrowser). 5 | - Allows automatic handling of WiFi access point selection. 6 | - Allows auto handling of web server set up. 7 | - Added/modified websockets from [this example](https://github.com/acrobotic/Ai_Tips_ESP8266/tree/master/webserver_websockets). 8 | - Gives good basic websocket example code with no extra fluff. 9 | - Added/modified [servo driver code](https://github.com/adafruit/Adafruit-PWM-Servo-Driver-Library/blob/master/examples/servo/servo.ino). 10 | - Allows driving 16 servos (expandable to more) with 2 pins (I2C). 11 | - Added/modified HTML joysticks from [here](https://github.com/stemkoski/HTML-Joysticks). 12 | - Example webpage with 4 touchscreen joysticks without extra fluff. Each joystick controls 2 servos 13 | - Added/modified [inverse kinematics example](https://www.instructables.com/id/Arduino-Control-Robot-Arm-Via-Web/). 14 | - Example webpage with SVG arm picture to position arm. Looks like a good way to control a robot arm via web page. 15 | - Added/modified [Gamepad API example](https://github.com/luser/gamepadtest). 16 | - Example webpage that uses browser's Gamepad API to get joystick data. Grab a joystick or game pad and control your robot. 17 | 18 | # To Do: 19 | - Add camera stream/capture. 20 | - Add camera tracking features. 21 | - Store joystick configuration. 22 | - Add error handling to both ESP32 side and javascript side. 23 | - Add socket closing/opening handling to both ESP32 side and javascript side. 24 | 25 | # Hardware set up 26 | - ESP32-CAM board 27 | - PCA9685 board 28 | - Serial board for programming 29 | 30 | # Software set up 31 | - Install Arduino IDE 32 | - Install ESP32 board to board manager 33 | - Install Autoconnect library (and its required dependancies) 34 | - Install Websocket library 35 | - Install Adafruit's PWM Servo Driver library 36 | - Install [SPIFFS file uploader](https://github.com/me-no-dev/arduino-esp32fs-plugin). 37 | - Compile and upload ESP32_CAM_WebSocket_Servo_Tests.ino 38 | - Upload data directory to SPIFFS 39 | - Play 40 | -------------------------------------------------------------------------------- /data/edit.htm: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | File manager 11 | 259 | 1109 | 1110 | 1111 | 1112 |
1113 |
1114 | 1115 |

Loading...
1116 | 1117 | 1118 | 1119 | -------------------------------------------------------------------------------- /data/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobotHacker/ESP32_CAM_WebSocket_Servo_Tests/b2c4d1bc2be9bbeda37f6e10609d539364e38a0c/data/favicon.ico -------------------------------------------------------------------------------- /data/graphs.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobotHacker/ESP32_CAM_WebSocket_Servo_Tests/b2c4d1bc2be9bbeda37f6e10609d539364e38a0c/data/graphs.js.gz -------------------------------------------------------------------------------- /data/index.htm: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 22 | 23 | ESP Monitor 24 | 25 | 86 | 87 | 88 |
89 | 90 | 91 | 92 | 93 |
94 |
95 |
96 |
97 |

98 | AutoConnect menu 99 |

100 | 101 | -------------------------------------------------------------------------------- /data/ws_arm.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Arduino - Arm Robot - Web 5 | 6 | 10 | 486 | 487 | 488 |

Arduino - Control Arm Robot via Web

489 | 490 |

WebSocket : null

491 | 492 | 493 | 494 | -------------------------------------------------------------------------------- /data/ws_basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 20 | 21 |
22 | 23 |
24 |
25 |
26 | 27 |
28 |
29 |
0
30 |
31 |
1
32 |
33 |
2
34 |
35 |
3
36 | 37 | -------------------------------------------------------------------------------- /data/ws_gamepad.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 185 | 226 | 227 | 228 |
229 | 230 |
231 |
232 |
233 | 234 |
235 |
236 |

Press a button on your controller to start

237 | 238 | 239 | -------------------------------------------------------------------------------- /data/ws_gamepad_example_data.html: -------------------------------------------------------------------------------- 1 |
2 |

gamepad: AIRFLO (Vendor: 124b Product: 4d01)

3 |
4 | 0 5 | 1 6 | 2 7 | 3 8 | 4 9 | 5 10 | 6 11 | 7 12 | 8 13 | 9 14 | 10 15 | 11 16 | 12 17 |
18 |
19 | 0: 0.0039 20 | 1: 0.0039 21 | 2: 0.0039 22 | 3: 0.0000 23 | 4: 0.0000 24 | 5: 0.0039 25 | 6: 0.0000 26 | 7: 0.0000 27 | 8: 0.0000 28 | 9: -1.2857 29 |
30 |
31 |
32 |

gamepad: 6771 (Vendor: 06a3 Product: 053c)

33 |
34 | 0 35 | 1 36 | 2 37 | 3 38 | 4 39 | 5 40 | 6 41 | 7 42 | 8 43 | 9 44 | 10 45 | 11 46 | 12 47 | 13 48 | 14 49 | 15 50 | 16 51 | 17 52 | 18 53 | 19 54 | 20 55 | 21 56 | 22 57 | 23 58 | 24 59 | 25 60 |
61 |
62 | 0: -0.0357 63 | 1: -0.0268 64 | 2: 0.0000 65 | 3: 0.8734 66 | 4: 0.0000 67 | 5: 0.0779 68 | 6: 0.0000 69 | 7: 0.4390 70 | 8: 0.0000 71 | 9: -1.2857 72 |
73 |
74 | -------------------------------------------------------------------------------- /data/ws_touchscreen_joystick.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Joystick Controls 6 | 30 | 31 | 32 | 33 | 34 |
35 | 36 |
37 |
38 |
39 | 40 |
41 |
42 | 43 | Joystick Controls. 44 |
45 |
Joystick 1
46 |
Joystick 2
47 |
Joystick 3
48 |
Joystick 4
49 | 50 |
51 |
52 |
53 |
54 |
55 | 56 |
57 |
58 |
59 |
60 | 61 |
62 |
63 |
64 |
65 | 66 |
67 |
68 |
69 |
70 | 71 | 234 | 235 | 236 | --------------------------------------------------------------------------------