From 948c4bfaedc1025193d728f6a4645986c4bc0454 Mon Sep 17 00:00:00 2001 From: bdring Date: Sat, 10 Nov 2018 15:33:04 -0600 Subject: [PATCH] Merging WebUI with master --- .travis.yml | 11 + Grbl_Esp32/Grbl_Esp32.ino | 4 +- Grbl_Esp32/config.h | 17 +- Grbl_Esp32/cpu_map.h | 16 +- Grbl_Esp32/data/favicon.ico | Bin 0 -> 1150 bytes Grbl_Esp32/data/index.html.gz | Bin 0 -> 101976 bytes Grbl_Esp32/gcode.cpp | 7 +- Grbl_Esp32/grbl.h | 17 +- Grbl_Esp32/grbl_sd.cpp | 293 +- Grbl_Esp32/grbl_sd.h | 2 +- Grbl_Esp32/limits.cpp | 2 +- Grbl_Esp32/motion_control.cpp | 11 +- Grbl_Esp32/nofile.h | 329 ++ Grbl_Esp32/nuts_bolts.h | 9 +- Grbl_Esp32/probe.cpp | 7 +- Grbl_Esp32/protocol.cpp | 3 - Grbl_Esp32/report.cpp | 26 +- Grbl_Esp32/report.h | 4 +- Grbl_Esp32/serial.cpp | 32 +- Grbl_Esp32/serial2socket.cpp | 176 ++ Grbl_Esp32/serial2socket.h | 81 + Grbl_Esp32/settings.cpp | 5 + Grbl_Esp32/spindle_control.cpp | 39 +- Grbl_Esp32/spindle_control.h | 4 +- Grbl_Esp32/stepper.cpp | 1 + Grbl_Esp32/system.cpp | 11 + Grbl_Esp32/system.h | 5 + Grbl_Esp32/telnet_server.cpp | 208 ++ Grbl_Esp32/telnet_server.h | 63 + Grbl_Esp32/web_server.cpp | 2643 +++++++++++++++++ Grbl_Esp32/web_server.h | 122 + Grbl_Esp32/wificonfig.cpp | 550 ++++ Grbl_Esp32/wificonfig.h | 123 + Grbl_Esp32/wifiservices.cpp | 154 + Grbl_Esp32/wifiservices.h | 39 + libraries/ESP32SSDP/ESP32SSDP.cpp | 442 +++ libraries/ESP32SSDP/ESP32SSDP.h | 126 + libraries/ESP32SSDP/README.rst | 22 + libraries/ESP32SSDP/examples/SSDP/SSDP.ino | 51 + libraries/ESP32SSDP/keywords.txt | 53 + libraries/ESP32SSDP/library.properties | 9 + libraries/arduinoWebSockets/.gitignore | 29 + libraries/arduinoWebSockets/.travis.yml | 40 + libraries/arduinoWebSockets/LICENSE | 502 ++++ libraries/arduinoWebSockets/README.md | 98 + .../Nginx/esp8266.ssl.reverse.proxy.conf | 83 + .../WebSocketClientAVR/WebSocketClientAVR.ino | 84 + .../esp32/WebSocketClient/WebSocketClient.ino | 110 + .../WebSocketClientSSL/WebSocketClientSSL.ino | 106 + .../esp32/WebSocketServer/WebSocketServer.ino | 104 + .../WebSocketClient/WebSocketClient.ino | 92 + .../WebSocketClientSSL/WebSocketClientSSL.ino | 88 + .../WebSocketClientSocketIO.ino | 113 + .../WebSocketClientStomp.ino | 149 + .../WebSocketClientStompOverSockJs.ino | 150 + .../WebSocketServer/WebSocketServer.ino | 86 + .../WebSocketServerAllFunctionsDemo.ino | 132 + .../WebSocketServerFragmentation.ino | 94 + .../WebSocketServerHttpHeaderValidation.ino | 86 + .../WebSocketServer_LEDcontrol.ino | 121 + .../ParticleWebSocketClient/application.cpp | 46 + libraries/arduinoWebSockets/library.json | 25 + .../arduinoWebSockets/library.properties | 9 + .../arduinoWebSockets/src/WebSockets.cpp | 655 ++++ libraries/arduinoWebSockets/src/WebSockets.h | 311 ++ .../src/WebSocketsClient.cpp | 762 +++++ .../arduinoWebSockets/src/WebSocketsClient.h | 136 + .../src/WebSocketsServer.cpp | 873 ++++++ .../arduinoWebSockets/src/WebSocketsServer.h | 212 ++ .../arduinoWebSockets/src/libb64/AUTHORS | 7 + .../arduinoWebSockets/src/libb64/LICENSE | 29 + .../arduinoWebSockets/src/libb64/cdecode.c | 98 + .../src/libb64/cdecode_inc.h | 28 + .../arduinoWebSockets/src/libb64/cencode.c | 119 + .../src/libb64/cencode_inc.h | 31 + .../arduinoWebSockets/src/libsha1/libsha1.c | 202 ++ .../arduinoWebSockets/src/libsha1/libsha1.h | 21 + .../arduinoWebSockets/tests/webSocket.html | 49 + .../tests/webSocketServer/index.js | 57 + .../tests/webSocketServer/package.json | 27 + libraries/arduinoWebSockets/travis/common.sh | 53 + 81 files changed, 11548 insertions(+), 186 deletions(-) create mode 100644 Grbl_Esp32/data/favicon.ico create mode 100644 Grbl_Esp32/data/index.html.gz create mode 100644 Grbl_Esp32/nofile.h create mode 100644 Grbl_Esp32/serial2socket.cpp create mode 100644 Grbl_Esp32/serial2socket.h create mode 100644 Grbl_Esp32/telnet_server.cpp create mode 100644 Grbl_Esp32/telnet_server.h create mode 100644 Grbl_Esp32/web_server.cpp create mode 100644 Grbl_Esp32/web_server.h create mode 100644 Grbl_Esp32/wificonfig.cpp create mode 100644 Grbl_Esp32/wificonfig.h create mode 100644 Grbl_Esp32/wifiservices.cpp create mode 100644 Grbl_Esp32/wifiservices.h create mode 100644 libraries/ESP32SSDP/ESP32SSDP.cpp create mode 100644 libraries/ESP32SSDP/ESP32SSDP.h create mode 100644 libraries/ESP32SSDP/README.rst create mode 100644 libraries/ESP32SSDP/examples/SSDP/SSDP.ino create mode 100644 libraries/ESP32SSDP/keywords.txt create mode 100644 libraries/ESP32SSDP/library.properties create mode 100644 libraries/arduinoWebSockets/.gitignore create mode 100644 libraries/arduinoWebSockets/.travis.yml create mode 100644 libraries/arduinoWebSockets/LICENSE create mode 100644 libraries/arduinoWebSockets/README.md create mode 100644 libraries/arduinoWebSockets/examples/Nginx/esp8266.ssl.reverse.proxy.conf create mode 100644 libraries/arduinoWebSockets/examples/avr/WebSocketClientAVR/WebSocketClientAVR.ino create mode 100644 libraries/arduinoWebSockets/examples/esp32/WebSocketClient/WebSocketClient.ino create mode 100644 libraries/arduinoWebSockets/examples/esp32/WebSocketClientSSL/WebSocketClientSSL.ino create mode 100644 libraries/arduinoWebSockets/examples/esp32/WebSocketServer/WebSocketServer.ino create mode 100644 libraries/arduinoWebSockets/examples/esp8266/WebSocketClient/WebSocketClient.ino create mode 100644 libraries/arduinoWebSockets/examples/esp8266/WebSocketClientSSL/WebSocketClientSSL.ino create mode 100644 libraries/arduinoWebSockets/examples/esp8266/WebSocketClientSocketIO/WebSocketClientSocketIO.ino create mode 100644 libraries/arduinoWebSockets/examples/esp8266/WebSocketClientStomp/WebSocketClientStomp.ino create mode 100644 libraries/arduinoWebSockets/examples/esp8266/WebSocketClientStompOverSockJs/WebSocketClientStompOverSockJs.ino create mode 100644 libraries/arduinoWebSockets/examples/esp8266/WebSocketServer/WebSocketServer.ino create mode 100644 libraries/arduinoWebSockets/examples/esp8266/WebSocketServerAllFunctionsDemo/WebSocketServerAllFunctionsDemo.ino create mode 100644 libraries/arduinoWebSockets/examples/esp8266/WebSocketServerFragmentation/WebSocketServerFragmentation.ino create mode 100644 libraries/arduinoWebSockets/examples/esp8266/WebSocketServerHttpHeaderValidation/WebSocketServerHttpHeaderValidation.ino create mode 100644 libraries/arduinoWebSockets/examples/esp8266/WebSocketServer_LEDcontrol/WebSocketServer_LEDcontrol.ino create mode 100644 libraries/arduinoWebSockets/examples/particle/ParticleWebSocketClient/application.cpp create mode 100644 libraries/arduinoWebSockets/library.json create mode 100644 libraries/arduinoWebSockets/library.properties create mode 100644 libraries/arduinoWebSockets/src/WebSockets.cpp create mode 100644 libraries/arduinoWebSockets/src/WebSockets.h create mode 100644 libraries/arduinoWebSockets/src/WebSocketsClient.cpp create mode 100644 libraries/arduinoWebSockets/src/WebSocketsClient.h create mode 100644 libraries/arduinoWebSockets/src/WebSocketsServer.cpp create mode 100644 libraries/arduinoWebSockets/src/WebSocketsServer.h create mode 100644 libraries/arduinoWebSockets/src/libb64/AUTHORS create mode 100644 libraries/arduinoWebSockets/src/libb64/LICENSE create mode 100644 libraries/arduinoWebSockets/src/libb64/cdecode.c create mode 100644 libraries/arduinoWebSockets/src/libb64/cdecode_inc.h create mode 100644 libraries/arduinoWebSockets/src/libb64/cencode.c create mode 100644 libraries/arduinoWebSockets/src/libb64/cencode_inc.h create mode 100644 libraries/arduinoWebSockets/src/libsha1/libsha1.c create mode 100644 libraries/arduinoWebSockets/src/libsha1/libsha1.h create mode 100644 libraries/arduinoWebSockets/tests/webSocket.html create mode 100644 libraries/arduinoWebSockets/tests/webSocketServer/index.js create mode 100644 libraries/arduinoWebSockets/tests/webSocketServer/package.json create mode 100644 libraries/arduinoWebSockets/travis/common.sh diff --git a/.travis.yml b/.travis.yml index be2a2cbb..3628f2c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,12 +22,23 @@ before_script: - python get.py - cd .. - echo 'build.flash_freq=40m' >> platform.txt + - mv $TRAVIS_BUILD_DIR/libraries/ESP32SSDP $HOME/arduino_ide/libraries/ + - mv $TRAVIS_BUILD_DIR/libraries/arduinoWebSockets $HOME/arduino_ide/libraries/ script: - cd $TRAVIS_BUILD_DIR - source command.sh - export PATH="$HOME/arduino_ide:$PATH" - arduino --board esp32:esp32:esp32 --save-prefs + - sed -n '48,72p;73q' $TRAVIS_BUILD_DIR/Grbl_Esp32/config.h + - sed -i "s/\/\/#define ENABLE_BLUETOOTH/#define ENABLE_BLUETOOTH/g" $TRAVIS_BUILD_DIR/Grbl_Esp32/config.h + - sed -i "s/#define ENABLE_BLUETOOTH/\/\/#define ENABLE_BLUETOOTH/g" $TRAVIS_BUILD_DIR/Grbl_Esp32/config.h + - sed -i "s/\/\/#define ENABLE_WIFI/#define ENABLE_WIFI/g" $TRAVIS_BUILD_DIR/Grbl_Esp32/config.h + - sed -n '48,72p;73q' $TRAVIS_BUILD_DIR/Grbl_Esp32/config.h + - build_sketch $TRAVIS_BUILD_DIR/Grbl_Esp32/Grbl_Esp32.ino + - sed -i "s/\/\/#define ENABLE_BLUETOOTH/#define ENABLE_BLUETOOTH/g" $TRAVIS_BUILD_DIR/Grbl_Esp32/config.h + - sed -i "s/#define ENABLE_WIFI/\/\/#define ENABLE_WIFI/g" $TRAVIS_BUILD_DIR/Grbl_Esp32/config.h + - sed -n '48,72p;73q' $TRAVIS_BUILD_DIR/Grbl_Esp32/config.h - build_sketch $TRAVIS_BUILD_DIR/Grbl_Esp32/Grbl_Esp32.ino notifications: diff --git a/Grbl_Esp32/Grbl_Esp32.ino b/Grbl_Esp32/Grbl_Esp32.ino index 08fd033d..0be7514d 100644 --- a/Grbl_Esp32/Grbl_Esp32.ino +++ b/Grbl_Esp32/Grbl_Esp32.ino @@ -81,7 +81,9 @@ void setup() { #ifdef HOMING_INIT_LOCK if (bit_istrue(settings.flags,BITFLAG_HOMING_ENABLE)) { sys.state = STATE_ALARM; } #endif - +#ifdef ENABLE_WIFI + wifi_config.begin(); +#endif } void loop() { diff --git a/Grbl_Esp32/config.h b/Grbl_Esp32/config.h index 6da95f92..c5a2ec7e 100644 --- a/Grbl_Esp32/config.h +++ b/Grbl_Esp32/config.h @@ -52,10 +52,24 @@ Some features should not be changed. See notes below. // Serial baud rate #define BAUD_RATE 115200 -#define ENABLE_BLUETOOTH // enable bluetooth ... turns of if $I= something +//#define ENABLE_BLUETOOTH // enable bluetooth ... turns of if $I= something #define ENABLE_SD_CARD // enable use of SD Card to run jobs +#define ENABLE_WIFI //enable wifi + +#define ENABLE_HTTP //enable HTTP and all related services +#define ENABLE_OTA //enable OTA +#define ENABLE_TELNET //enable telnet +#define ENABLE_MDNS //enable mDNS discovery +#define ENABLE_SSDP //enable UPNP discovery + +#define ENABLE_SERIAL2SOCKET_IN +#define ENABLE_SERIAL2SOCKET_OUT + +#define ENABLE_CAPTIVE_PORTAL +#define ENABLE_AUTHENTICATION + // Define realtime command special characters. These characters are 'picked-off' directly from the // serial read data stream and are not passed to the grbl line execution parser. Select characters // that do not and must not exist in the streamed g-code program. ASCII control characters may be @@ -654,4 +668,3 @@ Some features should not be changed. See notes below. #endif - diff --git a/Grbl_Esp32/cpu_map.h b/Grbl_Esp32/cpu_map.h index a452dda3..8235a069 100644 --- a/Grbl_Esp32/cpu_map.h +++ b/Grbl_Esp32/cpu_map.h @@ -44,7 +44,7 @@ // be handy if you are using a servo, etc. for another axis. #define X_STEP_PIN GPIO_NUM_12 #define Y_STEP_PIN GPIO_NUM_14 - #define Z_STEP_PIN GPIO_NUM_27 + #define Z_STEP_PIN GPIO_NUM_27 #define X_DIRECTION_PIN GPIO_NUM_26 #define Y_DIRECTION_PIN GPIO_NUM_25 @@ -62,10 +62,18 @@ // use a virtual spindle. Do not comment out the other parameters for the spindle. #define SPINDLE_PWM_PIN GPIO_NUM_17 #define SPINDLE_PWM_CHANNEL 0 + // PWM Generator is based on 80,000,000 Hz counter + // Therefor the freq determines the resolution + // 80,000,000 / freq = max resolution + // For 5000 that is 80,000,000 / 5000 = 16000 + // round down to nearest bit count for SPINDLE_PWM_MAX_VALUE = 13bits (8192) #define SPINDLE_PWM_BASE_FREQ 5000 // Hz - #define SPINDLE_PWM_BIT_PRECISION 8 + #define SPINDLE_PWM_BIT_PRECISION 12 // be sure to match this with SPINDLE_PWM_MAX_VALUE #define SPINDLE_PWM_OFF_VALUE 0 - #define SPINDLE_PWM_MAX_VALUE 255 // TODO ESP32 Calc from resolution + #define SPINDLE_PWM_MAX_VALUE 4096 // (2^SPINDLE_PWM_BIT_PRECISION) +#ifndef SPINDLE_PWM_MIN_VALUE + #define SPINDLE_PWM_MIN_VALUE 1 // Must be greater than zero. +#endif #define SPINDLE_PWM_RANGE (SPINDLE_PWM_MAX_VALUE-SPINDLE_PWM_MIN_VALUE) // if these spindle function pins are defined, they will be activated in the code @@ -117,4 +125,4 @@ // ======================================================================= -#endif \ No newline at end of file +#endif diff --git a/Grbl_Esp32/data/favicon.ico b/Grbl_Esp32/data/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..6794fd9f7f0c3cae5d15f4640439b6c14e7537d8 GIT binary patch literal 1150 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 literal 0 HcmV?d00001 diff --git a/Grbl_Esp32/gcode.cpp b/Grbl_Esp32/gcode.cpp index 22584f06..a098eb57 100644 --- a/Grbl_Esp32/gcode.cpp +++ b/Grbl_Esp32/gcode.cpp @@ -129,7 +129,7 @@ uint8_t gc_execute_line(char *line, uint8_t client) // NOTE: Mantissa is multiplied by 100 to catch non-integer command values. This is more // accurate than the NIST gcode requirement of x10 when used for commands, but not quite // accurate enough for value words that require integers to within 0.0001. This should be - // a good enough comprimise and catch most all non-integer errors. To make it compliant, + // a good enough compromise and catch most all non-integer errors. To make it compliant, // we would simply need to change the mantissa to int16, but this add compiled flash space. // Maybe update this later. int_value = trunc(value); @@ -163,7 +163,10 @@ uint8_t gc_execute_line(char *line, uint8_t client) mantissa = 0; // Set to zero to indicate valid non-integer G command. } break; - case 0: case 1: case 2: case 3: case 38: + case 0: case 1: case 2: case 3: +#ifdef PROBE_PIN //only allow G38 "Probe" commands if a probe pin is defined. + case 38: +#endif // Check for G0/1/2/3/38 being called with G10/28/30/92 on same block. // * G43.1 is also an axis command but is not explicitly defined this way. if (axis_command) { FAIL(STATUS_GCODE_AXIS_COMMAND_CONFLICT); } // [Axis word/command conflict] diff --git a/Grbl_Esp32/grbl.h b/Grbl_Esp32/grbl.h index decfd210..5b772a32 100644 --- a/Grbl_Esp32/grbl.h +++ b/Grbl_Esp32/grbl.h @@ -20,7 +20,7 @@ // Grbl versioning system #define GRBL_VERSION "1.1f" -#define GRBL_VERSION_BUILD "20180919" +#define GRBL_VERSION_BUILD "20180917" //#include #include @@ -33,9 +33,10 @@ // Define the Grbl system include files. NOTE: Do not alter organization. #include "config.h" +#include "nuts_bolts.h" #include "cpu_map.h" #include "tdef.h" -#include "nuts_bolts.h" + #include "defaults.h" #include "settings.h" #include "system.h" @@ -61,4 +62,14 @@ #ifdef ENABLE_SD_CARD #include "grbl_sd.h" -#endif \ No newline at end of file +#endif + +#ifdef ENABLE_WIFI + #include "wificonfig.h" + #ifdef ENABLE_HTTP + #include "serial2socket.h" + #endif + #ifdef ENABLE_TELNET + #include "telnet_server.h" + #endif +#endif diff --git a/Grbl_Esp32/grbl_sd.cpp b/Grbl_Esp32/grbl_sd.cpp index 2e5f28b9..ed41b663 100644 --- a/Grbl_Esp32/grbl_sd.cpp +++ b/Grbl_Esp32/grbl_sd.cpp @@ -1,9 +1,9 @@ /* - grbl_sd.cpp - Adds SD Card Features to Grbl_ESP32 + grbl_sd.cpp - Adds SD Card Features to Grbl_ESP32 Part of Grbl_ESP32 Copyright (c) 2018 Barton Dring Buildlog.net - + Grbl is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or @@ -25,82 +25,71 @@ #define LINE_FLAG_COMMENT_PARENTHESES bit(1) #define LINE_FLAG_COMMENT_SEMICOLON bit(2) - - File myFile; -char fileTypes[FILE_TYPE_COUNT][8] = {".NC", ".TXT", ".GCODE", ".TAP", ".NGC"}; // filter out files not of these types (s/b UPPERCASE) bool SD_ready_next = false; // Grbl has processed a line and is waiting for another - +uint32_t sd_current_line_number; // stores the most recent line number read from the SD // attempt to mount the SD card -bool sd_mount() { - if(!SD.begin()){ - report_status_message(STATUS_SD_FAILED_MOUNT, CLIENT_SERIAL); - return false; - } - return true; +bool sd_mount() +{ + if(!SD.begin()) { + report_status_message(STATUS_SD_FAILED_MOUNT, CLIENT_SERIAL); + return false; + } + return true; } -void listDir(fs::FS &fs, const char * dirname, uint8_t levels){ - char temp_filename[128]; // to help filter by extension TODO: 128 needs a definition based on something - - File root = fs.open(dirname); - if(!root){ - report_status_message(STATUS_SD_FAILED_OPEN_DIR, CLIENT_SERIAL); - return; - } - if(!root.isDirectory()){ - report_status_message(STATUS_SD_DIR_NOT_FOUND, CLIENT_SERIAL); - return; - } +void listDir(fs::FS &fs, const char * dirname, uint8_t levels) +{ + char temp_filename[128]; // to help filter by extension TODO: 128 needs a definition based on something - File file = root.openNextFile(); - while(file){ - if(file.isDirectory()){ - if(levels){ - listDir(fs, file.name(), levels -1); - } - } else { - strcpy(temp_filename, file.name()); // make a copy - - // convert it to uppercase so it is easy to filter - for(int i = 0; i <= strlen(file.name()); i++){ - temp_filename[i] = toupper(temp_filename[i]); - } - - // now filter for accetable file types - for (uint8_t i=0; i < FILE_TYPE_COUNT; i++) // make sure it is a valid file type - { - if (strstr(temp_filename, fileTypes[i])) { - grbl_sendf(CLIENT_ALL, "[FILE:%s,SIZE:%d]\r\n", file.name(), file.size()); - break; - } - } - } - file = root.openNextFile(); + File root = fs.open(dirname); + if(!root) { + report_status_message(STATUS_SD_FAILED_OPEN_DIR, CLIENT_SERIAL); + return; + } + if(!root.isDirectory()) { + report_status_message(STATUS_SD_DIR_NOT_FOUND, CLIENT_SERIAL); + return; + } + + File file = root.openNextFile(); + while(file) { + if(file.isDirectory()) { + if(levels) { + listDir(fs, file.name(), levels -1); + } + } else { + grbl_sendf(CLIENT_ALL, "[FILE:%s|SIZE:%d]\r\n", file.name(), file.size()); } + file = root.openNextFile(); + } } -boolean openFile(fs::FS &fs, const char * path){ +boolean openFile(fs::FS &fs, const char * path) +{ myFile = fs.open(path); - - if(!myFile){ - report_status_message(STATUS_SD_FAILED_READ, CLIENT_SERIAL); - return false; + + if(!myFile) { + report_status_message(STATUS_SD_FAILED_READ, CLIENT_SERIAL); + return false; } - + set_sd_state(SDCARD_BUSY_PRINTING); - SD_ready_next = false; // this will get set to true when Grbl issues "ok" message - return true; + SD_ready_next = false; // this will get set to true when Grbl issues "ok" message + sd_current_line_number = 0; + return true; } -boolean closeFile(){ - if(!myFile){ - return false; +boolean closeFile() +{ + if(!myFile) { + return false; } - - set_sd_state(SDCARD_IDLE); - SD_ready_next = false; + + set_sd_state(SDCARD_IDLE); + SD_ready_next = false; + sd_current_line_number = 0; myFile.close(); return true; } @@ -110,115 +99,129 @@ boolean closeFile(){ strip whitespace strip comments per http://linuxcnc.org/docs/ja/html/gcode/overview.html#gcode:comments make uppercase - return true if a line is + return true if a line is */ -boolean readFileLine(char *line) { +boolean readFileLine(char *line) +{ char c; uint8_t index = 0; uint8_t line_flags = 0; - + if (!myFile) { - report_status_message(STATUS_SD_FAILED_READ, CLIENT_SERIAL); + report_status_message(STATUS_SD_FAILED_READ, CLIENT_SERIAL); return false; } - - while(myFile.available()){ - c = myFile.read(); - - - if (c == '\r' || c == ' ' ) { - // ignore these whitespace items - } - else if (c == '(') { - line_flags |= LINE_FLAG_COMMENT_PARENTHESES; - } - else if (c == ')') { - // End of '()' comment. Resume line allowed. - if (line_flags & LINE_FLAG_COMMENT_PARENTHESES) { line_flags &= ~(LINE_FLAG_COMMENT_PARENTHESES); } - } - else if (c == ';') { - // NOTE: ';' comment to EOL is a LinuxCNC definition. Not NIST. - if (!(line_flags & LINE_FLAG_COMMENT_PARENTHESES)) // semi colon inside parentheses do not mean anything - line_flags |= LINE_FLAG_COMMENT_SEMICOLON; - } - else if (c == '\n') { // found the newline, so mark the end and return true - line[index] = '\0'; - return true; - } - else { // add characters to the line - if (!line_flags) { - c = toupper(c); // make upper case - line[index] = c; - index++; - } - } + sd_current_line_number += 1; - if (index == 255) // name is too long so return false - { - line[index] = '\0'; - report_status_message(STATUS_OVERFLOW, CLIENT_SERIAL); - return false; - } + while(myFile.available()) { + c = myFile.read(); + + if (c == '\r' || c == ' ' ) { + // ignore these whitespace items + } else if (c == '(') { + line_flags |= LINE_FLAG_COMMENT_PARENTHESES; + } else if (c == ')') { + // End of '()' comment. Resume line allowed. + if (line_flags & LINE_FLAG_COMMENT_PARENTHESES) { + line_flags &= ~(LINE_FLAG_COMMENT_PARENTHESES); + } + } else if (c == ';') { + // NOTE: ';' comment to EOL is a LinuxCNC definition. Not NIST. + if (!(line_flags & LINE_FLAG_COMMENT_PARENTHESES)) { // semi colon inside parentheses do not mean anything + line_flags |= LINE_FLAG_COMMENT_SEMICOLON; + } + } else if (c == '\n') { // found the newline, so mark the end and return true + line[index] = '\0'; + return true; + } else { // add characters to the line + if (!line_flags) { + c = toupper(c); // make upper case + line[index] = c; + index++; + } + } + + if (index == 255) { // name is too long so return false + line[index] = '\0'; + report_status_message(STATUS_OVERFLOW, CLIENT_SERIAL); + return false; + } } - // some files end without a newline + // some files end without a newline if (index !=0) { line[index] = '\0'; return true; - } - else // empty line after new line + } else { // empty line after new line return false; + } } // return a percentage complete 50.5 = 50.5% -float sd_report_perc_complete() { - if (!myFile) - return 0.0; - - return ((float)myFile.position() / (float)myFile.size() * 100.0); +float sd_report_perc_complete() +{ + if (!myFile) { + return 0.0; + } + + return ((float)myFile.position() / (float)myFile.size() * 100.0); +} + +uint32_t sd_get_current_line_number() +{ + return sd_current_line_number; } uint8_t sd_state = SDCARD_IDLE; -uint8_t get_sd_state(bool refresh){ +uint8_t get_sd_state(bool refresh) +{ #if defined(SDCARD_DET_PIN) && SDCARD_SD_PIN != -1 - //no need to go further if SD detect is not correct - if (!((digitalRead (SDCARD_DET_PIN) == SDCARD_DET_VAL) ? true : false)){ - sd_state = SDCARD_NOT_PRESENT; - return sd_state; - } -#endif - //if busy doing something return state - if (!((sd_state == SDCARD_NOT_PRESENT) || (sd_state == SDCARD_IDLE))) return sd_state; - if (!refresh) return sd_state; //to avoid refresh=true + busy to reset SD and waste time - //SD is idle or not detected, let see if still the case - - if (sd_state == SDCARD_IDLE) { - SD.end(); - //using default value for speed ? should be parameter - //refresh content if card was removed - if (!SD.begin()) sd_state = SDCARD_NOT_PRESENT; - else { - if ( !(SD.cardSize() > 0 )) sd_state = SDCARD_NOT_PRESENT; - } - } -return sd_state; -} - -uint8_t set_sd_state(uint8_t flag){ - sd_state = flag; + //no need to go further if SD detect is not correct + if (!((digitalRead (SDCARD_DET_PIN) == SDCARD_DET_VAL) ? true : false)) { + sd_state = SDCARD_NOT_PRESENT; return sd_state; + } +#endif + //if busy doing something return state + if (!((sd_state == SDCARD_NOT_PRESENT) || (sd_state == SDCARD_IDLE))) { + return sd_state; + } + if (!refresh) { + return sd_state; //to avoid refresh=true + busy to reset SD and waste time + } + //SD is idle or not detected, let see if still the case + + if (sd_state == SDCARD_IDLE) { + SD.end(); + //using default value for speed ? should be parameter + //refresh content if card was removed + if (!SD.begin()) { + sd_state = SDCARD_NOT_PRESENT; + } else { + if ( !(SD.cardSize() > 0 )) { + sd_state = SDCARD_NOT_PRESENT; + } + } + } + return sd_state; } -void sd_get_current_filename(char* name) { - - if (myFile != NULL) - { - strcpy(name, myFile.name()); - } - else - name[0] = 0; +uint8_t set_sd_state(uint8_t flag) +{ + sd_state = flag; + return sd_state; +} + +void sd_get_current_filename(char* name) +{ + + if (myFile != NULL) { + strcpy(name, myFile.name()); + } else { + name[0] = 0; + } } diff --git a/Grbl_Esp32/grbl_sd.h b/Grbl_Esp32/grbl_sd.h index c24942d7..a221efb2 100644 --- a/Grbl_Esp32/grbl_sd.h +++ b/Grbl_Esp32/grbl_sd.h @@ -46,7 +46,7 @@ boolean closeFile(); boolean readFileLine(char *line); void readFile(fs::FS &fs, const char * path); float sd_report_perc_complete(); - +uint32_t sd_get_current_line_number(); void sd_get_current_filename(char* name); #endif diff --git a/Grbl_Esp32/limits.cpp b/Grbl_Esp32/limits.cpp index eae28bf1..ab52a220 100644 --- a/Grbl_Esp32/limits.cpp +++ b/Grbl_Esp32/limits.cpp @@ -33,7 +33,7 @@ #define HOMING_AXIS_LOCATE_SCALAR 5.0 // Must be > 1 to ensure limit switch is cleared. #endif -void isr_limit_switches() +void IRAM_ATTR isr_limit_switches() { // Ignore limit switches if already in an alarm state or in-process of executing an alarm. // When in the alarm state, Grbl should have been reset or will force a reset, so any pending diff --git a/Grbl_Esp32/motion_control.cpp b/Grbl_Esp32/motion_control.cpp index 9de1f6d1..65944e1a 100644 --- a/Grbl_Esp32/motion_control.cpp +++ b/Grbl_Esp32/motion_control.cpp @@ -366,7 +366,15 @@ void mc_reset() // Kill spindle and coolant. spindle_stop(); - coolant_stop(); + coolant_stop(); + + #ifdef ENABLE_SD_CARD + // do we need to stop a running SD job? + if (get_sd_state(false) == SDCARD_BUSY_PRINTING) { + report_feedback_message(MESSAGE_SD_FILE_QUIT); + closeFile(); + } + #endif // Kill steppers only if in any motion state, i.e. cycle, actively holding, or homing. // NOTE: If steppers are kept enabled via the step idle delay setting, this also keeps @@ -379,5 +387,6 @@ void mc_reset() } else { system_set_exec_alarm(EXEC_ALARM_ABORT_CYCLE); } st_go_idle(); // Force kill steppers. Position has likely been lost. } + } } diff --git a/Grbl_Esp32/nofile.h b/Grbl_Esp32/nofile.h new file mode 100644 index 00000000..39ee9ccc --- /dev/null +++ b/Grbl_Esp32/nofile.h @@ -0,0 +1,329 @@ +/* + nofile.h - ESP3D data file + + Copyright (c) 2014 Luc Lebosse. All rights reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +//data generated by https://github.com/AraHaan/bin2c +//bin2c Conversion Tool v0.14.0 - Windows - [FINAL]. +#define PAGE_NOFILES_SIZE 4862 +const char PAGE_NOFILES [] = { + 0x1F, 0x8B, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0xED, 0x5C, 0x7D, 0x93, 0xDA, 0x46, + 0x93, 0xFF, 0x2A, 0xB2, 0x52, 0x36, 0x70, 0x2B, 0x40, 0x12, 0xAF, 0x8B, 0x16, 0xF2, 0x24, 0xB1, + 0x7D, 0xF1, 0x95, 0x13, 0xBB, 0xBC, 0xEB, 0x7B, 0xAE, 0x2A, 0x4E, 0xB9, 0x84, 0x34, 0x80, 0xCE, + 0x42, 0xD2, 0x49, 0xC3, 0xEE, 0x62, 0xC2, 0x77, 0xBF, 0xEE, 0x79, 0x91, 0x46, 0x42, 0xB0, 0xEC, + 0x26, 0x79, 0xF2, 0xFC, 0x91, 0x60, 0x23, 0x98, 0x99, 0xEE, 0xE9, 0xE9, 0xE9, 0xFE, 0x75, 0x4F, + 0x0F, 0xCE, 0xD5, 0x8A, 0xAE, 0xC3, 0xD9, 0xD5, 0x8A, 0xB8, 0xFE, 0xEC, 0x2A, 0xA3, 0xDB, 0x90, + 0xCC, 0xB0, 0x65, 0xB7, 0x88, 0x23, 0xDA, 0x5E, 0xB8, 0xEB, 0x20, 0xDC, 0x4E, 0x32, 0x37, 0xCA, + 0xDA, 0x19, 0x49, 0x83, 0x85, 0xD3, 0x5E, 0x67, 0x6D, 0x4A, 0xEE, 0x69, 0x3B, 0x0B, 0xBE, 0x92, + 0xB6, 0xEB, 0xFF, 0xEF, 0x26, 0xA3, 0x13, 0xCB, 0x34, 0x9F, 0x3B, 0xED, 0x3B, 0x32, 0xFF, 0x12, + 0xD0, 0x23, 0xBD, 0x8C, 0x1D, 0xB6, 0xC2, 0xD7, 0xE4, 0x7E, 0x3F, 0x8F, 0xFD, 0x6D, 0x69, 0x0A, + 0xFD, 0x47, 0x12, 0xDE, 0x12, 0x1A, 0x78, 0xAE, 0xF6, 0x33, 0xD9, 0x10, 0xDD, 0xC8, 0xBF, 0x1B, + 0xDF, 0xA5, 0x81, 0x1B, 0x1A, 0x8A, 0x0C, 0x0A, 0xAF, 0x7E, 0x72, 0xEF, 0x84, 0x41, 0x44, 0xDA, + 0x2B, 0x12, 0x2C, 0x57, 0x30, 0x57, 0xA7, 0x6F, 0x8F, 0x07, 0x23, 0xAB, 0xDF, 0x73, 0xBC, 0x38, + 0x8C, 0xD3, 0xC9, 0x37, 0xBD, 0x5E, 0xCF, 0x99, 0xBB, 0xDE, 0x97, 0x65, 0x1A, 0x6F, 0x22, 0xBF, + 0x2D, 0x5A, 0x17, 0x8B, 0xC5, 0xBE, 0xE3, 0x01, 0x1F, 0x17, 0x88, 0xD3, 0xDD, 0xDA, 0x4D, 0x97, + 0x41, 0xD4, 0x4E, 0x19, 0x0F, 0x77, 0x43, 0x63, 0x47, 0xB4, 0x84, 0x64, 0x21, 0x1A, 0x12, 0xD7, + 0xF7, 0x83, 0x68, 0xC9, 0x5B, 0xAC, 0x01, 0xCC, 0x2B, 0x5B, 0x38, 0x15, 0x36, 0xED, 0xA9, 0x3B, + 0x0F, 0xC9, 0x6E, 0x1E, 0xA7, 0x3E, 0x49, 0x27, 0xA6, 0xC3, 0x3F, 0xB4, 0xB3, 0xC4, 0xF5, 0x60, + 0x20, 0x34, 0xAC, 0xDD, 0xFB, 0xF6, 0x5D, 0xE0, 0xD3, 0x15, 0x53, 0xCA, 0xBE, 0xC3, 0xC6, 0xB7, + 0xF9, 0x30, 0xE2, 0xEF, 0x8A, 0x2E, 0x41, 0x3A, 0xB1, 0x92, 0x7B, 0x2D, 0x8B, 0xC3, 0xC0, 0xD7, + 0xBE, 0xF1, 0x7D, 0x5F, 0x4A, 0x35, 0x8F, 0x29, 0x8D, 0xD7, 0x13, 0x1B, 0x35, 0x49, 0x81, 0x6C, + 0x15, 0x50, 0xC2, 0x66, 0x21, 0x93, 0x28, 0xBE, 0x4B, 0xDD, 0x44, 0xCA, 0x36, 0xB1, 0xD7, 0xEB, + 0x3D, 0x5D, 0xED, 0xD8, 0x9E, 0xB8, 0x61, 0xB0, 0x8C, 0x26, 0x28, 0xBF, 0x98, 0x78, 0x46, 0x71, + 0x1B, 0x66, 0x34, 0x9D, 0x51, 0xDF, 0x38, 0x68, 0x5A, 0xE5, 0x4D, 0xCC, 0x36, 0xCA, 0xA3, 0xF2, + 0xA6, 0xD5, 0x4E, 0x4E, 0x35, 0x3E, 0xBE, 0x15, 0xB7, 0x24, 0xC5, 0x9D, 0x0C, 0x85, 0x08, 0x34, + 0x4E, 0xA4, 0x6A, 0xE0, 0x63, 0x65, 0x8D, 0x55, 0xA5, 0xD4, 0x08, 0x59, 0xD7, 0xB7, 0x3A, 0xEC, + 0x3B, 0x10, 0xBB, 0xAE, 0x6F, 0xB5, 0xAB, 0xD5, 0xF4, 0xA1, 0x14, 0x8F, 0xE2, 0x26, 0x76, 0x48, + 0xEC, 0xB5, 0x0D, 0xDB, 0x24, 0x68, 0x32, 0x9A, 0x06, 0x89, 0x22, 0xF8, 0x24, 0xA2, 0xAB, 0x76, + 0xBC, 0x68, 0xD3, 0x6D, 0x42, 0x9A, 0xB1, 0xEF, 0xB7, 0x76, 0x35, 0xB6, 0x7A, 0x89, 0xAF, 0xFD, + 0x3F, 0xD6, 0xC4, 0x0F, 0x5C, 0xAD, 0xB9, 0x06, 0x03, 0xE0, 0x7C, 0x47, 0x43, 0xD0, 0x79, 0x6B, + 0xA7, 0xD8, 0xB1, 0x68, 0x1F, 0xA0, 0x61, 0xD4, 0x10, 0x5C, 0x5E, 0xDA, 0xB5, 0x04, 0x97, 0xA3, + 0x23, 0x04, 0x96, 0x6D, 0x9A, 0xB5, 0x14, 0x96, 0xC5, 0x49, 0x3A, 0x91, 0x7B, 0xAB, 0x9A, 0xAD, + 0x10, 0xD9, 0xF3, 0xBC, 0x8A, 0xC3, 0x98, 0x55, 0x77, 0x31, 0xC1, 0x58, 0x32, 0x70, 0x63, 0x44, + 0x1C, 0xB0, 0xDA, 0x88, 0xD4, 0x78, 0x29, 0xF3, 0x5D, 0xAE, 0xD0, 0xD4, 0xF5, 0x83, 0x4D, 0x36, + 0x19, 0x82, 0x91, 0xD5, 0x38, 0x81, 0xBB, 0x4B, 0xE2, 0x2C, 0xA0, 0x41, 0x1C, 0x4D, 0x52, 0x12, + 0xBA, 0x34, 0xB8, 0x25, 0x8E, 0x1F, 0x64, 0x49, 0xE8, 0x6E, 0x27, 0xF3, 0x30, 0xF6, 0xBE, 0xE4, + 0x0E, 0x81, 0xE8, 0xA3, 0x31, 0xF7, 0x65, 0x3E, 0xE1, 0x13, 0x2F, 0x4E, 0x5D, 0x46, 0xC8, 0x64, + 0x28, 0xE4, 0xDF, 0x77, 0x5C, 0x0F, 0xF9, 0xEC, 0x0A, 0xC4, 0xA8, 0x91, 0xD0, 0x34, 0x4D, 0x39, + 0x50, 0x73, 0x0D, 0x77, 0xB2, 0x88, 0xBD, 0x4D, 0x06, 0xCF, 0x55, 0x0C, 0x36, 0xBF, 0x53, 0xC1, + 0x26, 0x71, 0x23, 0x12, 0xEE, 0x0E, 0x65, 0xAF, 0x07, 0xA7, 0x23, 0xFE, 0x5F, 0x56, 0x06, 0x82, + 0x9F, 0x44, 0xDD, 0x79, 0x7C, 0xDF, 0xCE, 0x56, 0xAE, 0x1F, 0xDF, 0x4D, 0x4C, 0x0D, 0xA9, 0xF0, + 0x6F, 0xBA, 0x9C, 0xBB, 0x4D, 0xD3, 0xC0, 0x57, 0xC7, 0x1C, 0xB4, 0x9C, 0x73, 0x06, 0x09, 0x49, + 0xDB, 0x0C, 0xA1, 0x73, 0xAD, 0x21, 0xB8, 0x89, 0x0E, 0x34, 0x76, 0x68, 0xDB, 0x1D, 0x6A, 0xF4, + 0x34, 0xE2, 0x0E, 0xF0, 0x25, 0x57, 0x20, 0x1A, 0x95, 0x35, 0x01, 0x12, 0x70, 0xD3, 0x90, 0xAB, + 0xEB, 0xA1, 0x6E, 0x8A, 0x3E, 0x34, 0xA3, 0x9A, 0x2E, 0xA1, 0xC9, 0x8A, 0xF7, 0x86, 0xEE, 0x1C, + 0x94, 0x2D, 0x2D, 0x20, 0x88, 0x18, 0x2E, 0x71, 0x43, 0x28, 0x43, 0x70, 0xC5, 0x98, 0x70, 0x15, + 0x2C, 0xBA, 0xDC, 0x71, 0x0C, 0x1B, 0xE1, 0xF6, 0x32, 0x43, 0x09, 0xA2, 0x45, 0x2C, 0xF7, 0xB3, + 0x07, 0xC6, 0x3F, 0x86, 0x2D, 0x5D, 0xC4, 0xE9, 0xBA, 0x8D, 0x9E, 0x91, 0xC6, 0xC5, 0x64, 0x7C, + 0x16, 0x3E, 0x03, 0x0B, 0x1C, 0x02, 0x0E, 0x7B, 0xFD, 0x22, 0x64, 0xA0, 0x19, 0x6B, 0x96, 0x2D, + 0x27, 0x3B, 0x37, 0x94, 0x0D, 0x06, 0x83, 0x63, 0xD6, 0x52, 0xB4, 0x06, 0x6B, 0x77, 0x29, 0x1D, + 0xEA, 0xC0, 0x86, 0xD0, 0x2F, 0xCF, 0xB2, 0xA1, 0x20, 0xCA, 0x08, 0xD5, 0x8E, 0x18, 0xC9, 0xA8, + 0x6C, 0x4A, 0x0F, 0x8E, 0x6D, 0xC7, 0x6D, 0x9A, 0x42, 0xF8, 0xE6, 0x0E, 0xAA, 0x5A, 0x80, 0x46, + 0xDC, 0x8C, 0x80, 0x6E, 0xDB, 0xF1, 0x86, 0x6A, 0x1D, 0x6B, 0x90, 0x19, 0x05, 0xDF, 0x83, 0xBE, + 0xB2, 0xC2, 0xB9, 0xAB, 0xED, 0xCA, 0xF6, 0x34, 0x1C, 0xBA, 0x0B, 0x72, 0xE9, 0x00, 0x05, 0x6A, + 0x12, 0x02, 0xEE, 0x13, 0x96, 0x66, 0x98, 0xD0, 0x39, 0x96, 0x1D, 0x96, 0x69, 0x1B, 0xD6, 0x68, + 0x60, 0xD8, 0xBD, 0x9E, 0xD1, 0x19, 0xB6, 0x84, 0x0C, 0xA8, 0xEB, 0xA4, 0xE2, 0xCC, 0xDC, 0x47, + 0xE6, 0x34, 0x3A, 0x66, 0x77, 0xEA, 0x60, 0xB3, 0x64, 0x66, 0x7D, 0xD3, 0x74, 0x94, 0x10, 0xED, + 0x91, 0x88, 0x92, 0xB4, 0x1A, 0x35, 0xD7, 0x81, 0xEF, 0x87, 0x84, 0x27, 0x60, 0xF1, 0xC6, 0x5B, + 0xB5, 0x11, 0x76, 0x40, 0x9F, 0x6B, 0x37, 0x0A, 0x92, 0x4D, 0xC8, 0x40, 0xCC, 0x39, 0xDE, 0xE3, + 0x6D, 0xD2, 0x0C, 0x54, 0x94, 0xC4, 0x01, 0x63, 0x7E, 0xA6, 0xC5, 0xB0, 0x7D, 0x4B, 0xDC, 0x14, + 0x24, 0x72, 0x4E, 0xA4, 0x19, 0x8F, 0xB4, 0xE7, 0x1A, 0x13, 0x5C, 0xC7, 0x5F, 0xDB, 0x9B, 0x0C, + 0x93, 0x25, 0x12, 0x12, 0x8F, 0x72, 0x71, 0x70, 0xAD, 0x07, 0x8D, 0xD5, 0x06, 0xA6, 0xF3, 0x76, + 0x92, 0xC2, 0x32, 0xD2, 0xED, 0x69, 0xB4, 0xEE, 0xF5, 0x46, 0xEE, 0x7C, 0x54, 0xC1, 0x20, 0x9B, + 0x0C, 0x7D, 0xB7, 0x5F, 0xE2, 0x22, 0x10, 0xDD, 0x28, 0xB5, 0x71, 0x68, 0x2F, 0x35, 0x31, 0x94, + 0x2F, 0x35, 0x4D, 0x6A, 0x28, 0x27, 0x87, 0x94, 0x07, 0xF1, 0xA1, 0x46, 0x58, 0x7B, 0x3C, 0x34, + 0x2F, 0xCD, 0x8A, 0xB0, 0x96, 0x6D, 0xCF, 0xFB, 0xE6, 0xDE, 0x73, 0x13, 0xDC, 0x54, 0x89, 0xC1, + 0x2C, 0x8D, 0x1A, 0x2B, 0x29, 0xA9, 0xB0, 0xB2, 0x71, 0x01, 0xCA, 0xA3, 0xD1, 0xC8, 0x39, 0xC8, + 0x02, 0xDD, 0x10, 0x4C, 0xAC, 0x04, 0xF2, 0x35, 0xC1, 0xF5, 0xB4, 0x51, 0x1C, 0x6C, 0xA5, 0xE0, + 0xDA, 0xCE, 0x36, 0x9E, 0x47, 0xB2, 0xAC, 0x26, 0x9F, 0xF1, 0x17, 0x0B, 0xD3, 0x1F, 0x57, 0x23, + 0xC1, 0x90, 0x5C, 0x7A, 0xC3, 0x3C, 0x84, 0x78, 0xA3, 0x61, 0xCF, 0x97, 0xAC, 0x7C, 0x37, 0x5A, + 0x82, 0xB6, 0x6A, 0xA0, 0xCF, 0xF6, 0x89, 0x4F, 0x2A, 0x9C, 0xC8, 0xDC, 0xF3, 0x7C, 0x4B, 0x72, + 0x72, 0x2F, 0xFB, 0xFD, 0xBE, 0xBD, 0xEF, 0xAC, 0xDC, 0xAC, 0x4D, 0xD2, 0x14, 0x20, 0xA7, 0x0C, + 0xDB, 0x65, 0x5A, 0x3E, 0xFA, 0xCF, 0x06, 0xC4, 0xA3, 0xD2, 0xD4, 0x62, 0xDA, 0xB8, 0xDF, 0x1B, + 0xF4, 0xFA, 0x4F, 0x46, 0x32, 0x74, 0xCD, 0x6F, 0x3C, 0x32, 0xEE, 0x8F, 0x7B, 0x8F, 0x91, 0xB1, + 0x4A, 0x5B, 0x92, 0x59, 0x88, 0xDB, 0xE6, 0x61, 0xB6, 0x46, 0xD3, 0x62, 0xF3, 0x4F, 0xEA, 0x9A, + 0xEF, 0xF1, 0xBF, 0x46, 0xD7, 0xB5, 0xF2, 0xD4, 0x6A, 0xDB, 0x9E, 0x0F, 0xFA, 0xB6, 0xF7, 0xFB, + 0xB4, 0x3D, 0x1C, 0xCD, 0xAD, 0xE1, 0xF8, 0x69, 0xDA, 0xE6, 0xB4, 0x15, 0xA9, 0x6B, 0xF5, 0x2D, + 0x7D, 0x04, 0x61, 0x45, 0x78, 0xC8, 0x49, 0x3C, 0xF1, 0x2F, 0xC1, 0x8C, 0x16, 0x55, 0xB7, 0xEB, + 0xF7, 0x16, 0x3D, 0x57, 0x65, 0x52, 0xC2, 0x3E, 0xD1, 0xA4, 0x00, 0x98, 0x68, 0x51, 0x90, 0x8F, + 0xB7, 0x4C, 0x0E, 0xC9, 0x26, 0x07, 0x64, 0xE7, 0xC0, 0x9E, 0x77, 0xD9, 0x33, 0x6D, 0xAF, 0x22, + 0xE6, 0x68, 0x68, 0x79, 0xD6, 0x25, 0x13, 0x33, 0x58, 0x2F, 0x77, 0x22, 0x96, 0xAD, 0xDC, 0xA8, + 0x9A, 0x12, 0x0F, 0xEB, 0xF0, 0x8A, 0x27, 0xE0, 0x9C, 0x56, 0x88, 0x50, 0x83, 0x25, 0x26, 0xBE, + 0x2A, 0xF3, 0x9A, 0x20, 0xE2, 0x5F, 0xEE, 0x78, 0x20, 0x38, 0x93, 0xF4, 0xF4, 0xCA, 0x7B, 0xA6, + 0x48, 0x3F, 0xE4, 0xD8, 0x87, 0x56, 0xFA, 0xD7, 0xAF, 0x2B, 0x04, 0xD1, 0x20, 0x43, 0xF8, 0x22, + 0x0D, 0x82, 0x1D, 0xA6, 0xF2, 0xD6, 0x89, 0xB0, 0xB1, 0x45, 0x10, 0x12, 0xF6, 0x9D, 0xBB, 0x6B, + 0x3E, 0xF6, 0xB2, 0x0F, 0xBB, 0x1A, 0x44, 0xC9, 0x86, 0xFE, 0x82, 0xA7, 0xE7, 0x29, 0x8E, 0xFB, + 0x75, 0x32, 0x91, 0xCB, 0xC2, 0xAF, 0xED, 0x4D, 0x12, 0xC6, 0xAE, 0xDF, 0x9E, 0x6F, 0x20, 0x9A, + 0xFD, 0x9D, 0x97, 0xFD, 0x6B, 0xF3, 0x32, 0xE7, 0xA4, 0x9B, 0x0F, 0xE6, 0x9E, 0x79, 0x10, 0xBA, + 0xFB, 0xC3, 0xF9, 0xD8, 0x77, 0x1F, 0xB5, 0xA9, 0xC2, 0x2A, 0xFE, 0xDE, 0xDA, 0x7F, 0x9F, 0xAD, + 0xED, 0x59, 0x73, 0xD3, 0xAF, 0x9E, 0xF4, 0xAD, 0xF9, 0xD0, 0x1F, 0x0F, 0x1E, 0xB7, 0xB5, 0x1C, + 0xC0, 0xFE, 0xDE, 0xDA, 0x7F, 0xF3, 0xAD, 0xB5, 0x87, 0x97, 0xEE, 0xDC, 0xDB, 0xE7, 0x40, 0x5D, + 0x82, 0xF3, 0x32, 0x7A, 0x2B, 0x68, 0x5E, 0x4A, 0x05, 0x04, 0x9A, 0x8B, 0x0A, 0xD3, 0x22, 0x8E, + 0x41, 0xA9, 0x27, 0x0A, 0x4C, 0xAC, 0xFE, 0xF2, 0xB4, 0x1A, 0xD3, 0x41, 0x9D, 0x17, 0x0D, 0x0E, + 0xC3, 0x24, 0xDF, 0xAB, 0xBE, 0x92, 0x34, 0xF4, 0xF0, 0xA5, 0x92, 0x2A, 0x9D, 0xBD, 0xFE, 0xE5, + 0xD8, 0x9F, 0x57, 0x54, 0x3F, 0x30, 0x9F, 0x3B, 0xB2, 0x6E, 0x0A, 0xD2, 0xCA, 0x9D, 0xC2, 0xCF, + 0x60, 0x3B, 0x6B, 0x5E, 0x66, 0xCC, 0x92, 0x20, 0xD2, 0xEC, 0x4C, 0xC3, 0xCD, 0x74, 0x53, 0x2D, + 0x88, 0x16, 0x41, 0x04, 0x96, 0xB0, 0xFF, 0xC7, 0x17, 0xB2, 0x5D, 0xA4, 0xEE, 0x9A, 0x64, 0x1A, + 0x0E, 0xD9, 0x99, 0xCF, 0x77, 0xCC, 0x5C, 0x30, 0x63, 0x9D, 0xA4, 0x31, 0x75, 0x29, 0x69, 0x9A, + 0xAD, 0x3D, 0x16, 0xAD, 0x0E, 0x3B, 0x7A, 0x43, 0x00, 0xD3, 0x65, 0x6B, 0xFF, 0x97, 0x68, 0x70, + 0x1D, 0xFB, 0x6E, 0x51, 0xFF, 0x62, 0x46, 0x94, 0x57, 0x63, 0x17, 0xC1, 0x3D, 0xF1, 0x9D, 0xAF, + 0xED, 0x20, 0xF2, 0xC9, 0x3D, 0x56, 0xDC, 0xCC, 0xA2, 0x10, 0xCC, 0x78, 0x61, 0x7D, 0xD9, 0x61, + 0x25, 0x62, 0x70, 0x5A, 0x68, 0x30, 0x1D, 0xA5, 0x38, 0x27, 0x35, 0x88, 0x9F, 0xD1, 0x5C, 0x16, + 0x21, 0x24, 0x1A, 0xAC, 0xA8, 0x56, 0x5B, 0x89, 0x3D, 0x6C, 0x55, 0x93, 0x90, 0x7E, 0x4B, 0x88, + 0xCA, 0xF2, 0x7F, 0x70, 0xC1, 0x5D, 0xB1, 0xA6, 0x52, 0x75, 0xD1, 0x32, 0xCB, 0x95, 0xC7, 0x52, + 0x55, 0x52, 0xED, 0x14, 0x45, 0xFE, 0x63, 0xB4, 0xA2, 0xFB, 0x18, 0x39, 0x5E, 0x0B, 0xE4, 0xE6, + 0x24, 0x0B, 0x13, 0x4A, 0x7D, 0x16, 0x4B, 0x50, 0x16, 0x42, 0x81, 0x59, 0xCA, 0xA5, 0xEC, 0x96, + 0x73, 0x58, 0xEB, 0xE6, 0x70, 0x58, 0xBA, 0xA8, 0x9A, 0xD4, 0xA8, 0xE3, 0x9B, 0x05, 0xC1, 0x97, + 0xD4, 0x03, 0x56, 0x72, 0x15, 0x2B, 0xB1, 0xC5, 0x84, 0x4E, 0x9E, 0xFC, 0xE2, 0xAB, 0x8E, 0x8B, + 0x8D, 0xAF, 0x63, 0xC5, 0xD9, 0x47, 0xAA, 0xAF, 0x54, 0x9E, 0x5C, 0xE0, 0x4B, 0x8A, 0x57, 0xAE, + 0x40, 0x9B, 0x42, 0x3A, 0xD9, 0x5B, 0x35, 0xF1, 0xA1, 0x94, 0x5E, 0x18, 0x4D, 0xBF, 0x33, 0x20, + 0xEB, 0xC7, 0x2F, 0xE5, 0x50, 0x9C, 0xDF, 0xB9, 0xDB, 0x27, 0xEE, 0x6D, 0xCA, 0xD6, 0xC8, 0xFB, + 0x06, 0x63, 0xF5, 0x2A, 0x26, 0xF3, 0x52, 0x42, 0x22, 0x0D, 0xB2, 0x7D, 0xA0, 0xCF, 0x0B, 0xD7, + 0xA3, 0xE1, 0xE8, 0x28, 0x3D, 0xBB, 0x57, 0xDC, 0x5F, 0x75, 0xF9, 0x4D, 0xEE, 0x55, 0x97, 0xDF, + 0xEB, 0xB2, 0xDB, 0xA6, 0x2B, 0x3F, 0xB8, 0xD5, 0x58, 0xFB, 0x54, 0xCF, 0x4D, 0xC8, 0x9D, 0xC3, + 0x62, 0x37, 0x94, 0x08, 0xE7, 0xE3, 0x97, 0x33, 0xA6, 0x3E, 0xFB, 0x6F, 0xAB, 0x63, 0x6B, 0x2F, + 0xA2, 0x79, 0x96, 0x38, 0xFC, 0xFD, 0xAA, 0x0B, 0xE4, 0xB3, 0x2B, 0x1E, 0x4D, 0x67, 0x57, 0x2B, + 0x7B, 0xF6, 0x86, 0x6A, 0x19, 0x21, 0xEB, 0x4C, 0xDB, 0xC6, 0x1B, 0xCD, 0x8F, 0xB5, 0x28, 0xA6, + 0xDA, 0xCA, 0xC5, 0x8B, 0x90, 0x68, 0xAB, 0x31, 0x87, 0xEF, 0xE0, 0x4D, 0xB2, 0x16, 0x91, 0x80, + 0xAE, 0x48, 0xAA, 0x34, 0x75, 0x96, 0x5F, 0x0D, 0x2D, 0x09, 0xB1, 0xC0, 0xAB, 0xF1, 0x90, 0xAF, + 0x05, 0x54, 0x8B, 0x53, 0xF8, 0xE2, 0x03, 0x9C, 0x21, 0xC3, 0x54, 0x5B, 0x04, 0xE9, 0xFA, 0x0E, + 0x62, 0xA5, 0x16, 0x2C, 0x80, 0x05, 0x1E, 0x84, 0xB1, 0xE4, 0x06, 0x2B, 0xB2, 0x67, 0x38, 0xA1, + 0xE7, 0x46, 0x30, 0x04, 0x14, 0x03, 0x78, 0xA3, 0x01, 0x7B, 0xA2, 0x4D, 0xB4, 0x2B, 0x57, 0xF3, + 0x42, 0x37, 0xCB, 0xA6, 0x7A, 0x7E, 0x8A, 0xD0, 0xB5, 0x55, 0x4A, 0x16, 0x53, 0x7D, 0x45, 0x69, + 0x92, 0x4D, 0xBA, 0xDD, 0x25, 0xC8, 0xB2, 0x99, 0xC3, 0x89, 0x7A, 0xDD, 0x0D, 0x37, 0x5E, 0x9B, + 0x7F, 0xED, 0xBE, 0xBA, 0x7E, 0xDF, 0x7B, 0xD9, 0xFE, 0xE7, 0xAB, 0xEF, 0x3F, 0xBE, 0xD1, 0x67, + 0x67, 0x0F, 0xBD, 0xEA, 0xBA, 0xA0, 0x61, 0xA9, 0x11, 0xD4, 0xAE, 0x98, 0x9D, 0x81, 0xB0, 0xAE, + 0x05, 0xFE, 0x54, 0xBF, 0x7E, 0xFF, 0xE6, 0xF5, 0xEB, 0x6B, 0xFD, 0xB0, 0x5B, 0xDE, 0xA3, 0xE8, + 0xB3, 0xD7, 0xD0, 0xBA, 0xD2, 0x5E, 0x43, 0x60, 0xCC, 0xB6, 0x19, 0x25, 0x6B, 0xA1, 0xE9, 0x03, + 0x02, 0xDC, 0x44, 0x60, 0xC4, 0x52, 0x28, 0x8D, 0xA5, 0x50, 0x3A, 0x46, 0x53, 0x3E, 0x0F, 0x4B, + 0x9F, 0x78, 0x1C, 0xD7, 0xB5, 0x08, 0xC2, 0xC8, 0x54, 0x5F, 0x6F, 0xB1, 0x31, 0xFB, 0xE5, 0x57, + 0x5D, 0x5B, 0x6F, 0x42, 0x1A, 0x24, 0xB8, 0xF1, 0xF2, 0x93, 0x3E, 0xD3, 0x04, 0x27, 0xA9, 0x31, + 0x1A, 0x69, 0x4A, 0x85, 0x52, 0x17, 0x33, 0xF0, 0x54, 0x8C, 0xCF, 0x51, 0xCA, 0xCE, 0x74, 0x50, + 0xBC, 0x17, 0x06, 0xDE, 0x17, 0x58, 0x23, 0x89, 0x7C, 0x9C, 0xAA, 0xD9, 0x72, 0x74, 0xED, 0xD6, + 0x0D, 0x37, 0x40, 0xF7, 0x91, 0x8D, 0xD5, 0x67, 0x25, 0x13, 0x4A, 0xD2, 0x78, 0x99, 0x62, 0x45, + 0x43, 0x58, 0xE1, 0x6D, 0x90, 0x05, 0xF3, 0x20, 0x0C, 0xE8, 0x76, 0xB2, 0x82, 0x7C, 0x8C, 0x44, + 0x52, 0xF4, 0x24, 0x5D, 0xF2, 0x29, 0xD9, 0x07, 0xB0, 0xFC, 0xA9, 0x0E, 0x86, 0x0D, 0x8B, 0xEF, + 0x4A, 0x16, 0x60, 0xD3, 0x29, 0xFF, 0x7B, 0xA0, 0xF7, 0xE3, 0xAA, 0xE3, 0x97, 0xD7, 0x57, 0x14, + 0xA8, 0xA8, 0xAF, 0x31, 0x87, 0x99, 0xEA, 0xE6, 0xF3, 0x5C, 0xA9, 0xE7, 0xA9, 0xA2, 0xB4, 0xEE, + 0x1F, 0xE2, 0x35, 0x24, 0x86, 0x7E, 0xB3, 0x81, 0xB7, 0x99, 0x0D, 0xA3, 0xE1, 0x86, 0x61, 0x43, + 0x51, 0xC3, 0x07, 0xB2, 0x00, 0x69, 0x57, 0x28, 0x39, 0xF5, 0x0F, 0x66, 0x45, 0x39, 0x73, 0x6E, + 0x3F, 0xA4, 0x04, 0x6C, 0xDF, 0x0F, 0xD2, 0x66, 0x4B, 0x57, 0x24, 0x81, 0x93, 0x3C, 0x8C, 0xCC, + 0x6E, 0x97, 0x92, 0xB2, 0x6F, 0x82, 0x4D, 0x33, 0x8C, 0xE3, 0x9F, 0x6F, 0x03, 0x72, 0xF7, 0x7D, + 0x0C, 0x1A, 0xC2, 0x03, 0x76, 0x1F, 0xFF, 0xC0, 0xF8, 0x14, 0xEC, 0x40, 0x83, 0xB6, 0x81, 0xAE, + 0x6D, 0x51, 0x77, 0xBA, 0xA4, 0xEE, 0x29, 0xD4, 0x36, 0x7C, 0x4E, 0x61, 0x90, 0x0D, 0x8F, 0x2D, + 0x7B, 0xC0, 0x2E, 0x86, 0x53, 0x5D, 0xA4, 0x79, 0x7A, 0xB7, 0xE0, 0x83, 0x43, 0xB7, 0x8C, 0x9D, + 0xE0, 0x63, 0x0D, 0x0A, 0x3E, 0xF8, 0xF9, 0x01, 0x3E, 0x98, 0x8F, 0x23, 0x1F, 0x8B, 0x0B, 0x64, + 0xC3, 0x23, 0x4F, 0x6E, 0xA1, 0x75, 0x2C, 0xBE, 0xDE, 0x09, 0x8E, 0x63, 0xD8, 0x6C, 0xC1, 0x84, + 0xE5, 0xC9, 0xFA, 0xEC, 0x02, 0x14, 0x08, 0x3C, 0x40, 0x8F, 0xA0, 0x8A, 0x99, 0x70, 0x11, 0xA1, + 0x53, 0xAE, 0x48, 0x34, 0x17, 0x9E, 0xCC, 0xE5, 0xEA, 0x13, 0x5F, 0x2B, 0xC3, 0xF3, 0x25, 0x98, + 0xF9, 0x26, 0x30, 0x53, 0x73, 0xE9, 0x2A, 0xA7, 0xC4, 0xEB, 0x3E, 0x69, 0xBC, 0x2A, 0x75, 0x17, + 0x6D, 0xA7, 0x2B, 0xED, 0x08, 0x1F, 0x92, 0x82, 0x7F, 0x29, 0xDD, 0xF2, 0xEB, 0xD2, 0xCE, 0x8B, + 0x83, 0x00, 0x1A, 0x21, 0x87, 0x64, 0x66, 0x84, 0x2B, 0xD5, 0x1C, 0x6E, 0xC0, 0xCA, 0x80, 0xF7, + 0x0A, 0xDB, 0x67, 0x3F, 0x83, 0x1F, 0xE4, 0x5F, 0xAE, 0x41, 0x4B, 0xF2, 0x4B, 0xC9, 0x80, 0x2A, + 0x6D, 0x62, 0x45, 0xAC, 0x55, 0x48, 0x2A, 0x26, 0x43, 0x07, 0xC8, 0x71, 0xE2, 0x33, 0xDA, 0x2A, + 0x1B, 0xC7, 0xE3, 0x82, 0x5C, 0xCF, 0x11, 0xDC, 0xE1, 0x91, 0x96, 0xFB, 0x63, 0x06, 0xE9, 0xE6, + 0x26, 0x2B, 0x34, 0x7A, 0xF0, 0x7E, 0x8E, 0x37, 0x16, 0xC8, 0x27, 0x21, 0xFE, 0x23, 0xC3, 0xFD, + 0x07, 0x80, 0xAF, 0xE4, 0xBD, 0x47, 0x71, 0xF0, 0xEE, 0x28, 0x0A, 0x2A, 0xF6, 0xF2, 0x34, 0xE4, + 0x03, 0xDE, 0x07, 0x18, 0xC0, 0x71, 0xEE, 0x10, 0xFD, 0x70, 0x3D, 0xEA, 0x8C, 0x8F, 0x81, 0xBE, + 0xC5, 0x5D, 0x0E, 0x7E, 0xF8, 0xB1, 0x1E, 0xFE, 0x72, 0xCE, 0x70, 0x84, 0x8C, 0xD8, 0xF0, 0x75, + 0xB6, 0xD4, 0x8F, 0xB3, 0x9F, 0x7D, 0x20, 0xB0, 0x79, 0x70, 0x06, 0x8E, 0x96, 0x79, 0xEC, 0xBD, + 0x73, 0x03, 0xDA, 0x81, 0xFF, 0xC0, 0xA9, 0x80, 0x89, 0xC2, 0xCA, 0x83, 0x1C, 0x89, 0x72, 0xCF, + 0xE1, 0x3D, 0x87, 0xC6, 0x5F, 0xDD, 0x74, 0xEE, 0x7E, 0x90, 0x85, 0x26, 0x70, 0xFE, 0xCD, 0xFD, + 0x88, 0xA5, 0x28, 0x65, 0x1B, 0x28, 0x65, 0x2D, 0x75, 0x5D, 0x3C, 0x2D, 0x85, 0x9E, 0x55, 0x6F, + 0xF6, 0x06, 0x44, 0xA7, 0xC1, 0x02, 0x0E, 0xEE, 0x98, 0xAD, 0x40, 0xF0, 0xEF, 0xD5, 0x18, 0x5A, + 0x91, 0x2E, 0xEA, 0x7C, 0x0D, 0x62, 0x25, 0xA5, 0x6E, 0x44, 0x0F, 0x9D, 0x9F, 0xC0, 0x21, 0x69, + 0x26, 0xB3, 0x8F, 0x70, 0xF0, 0x9D, 0x88, 0xE5, 0x55, 0x42, 0xA1, 0x7A, 0x49, 0x20, 0xAD, 0x81, + 0x93, 0xE7, 0x8B, 0xFC, 0x8C, 0xE7, 0xE6, 0xCF, 0xBC, 0x51, 0xA8, 0xBC, 0xB8, 0xF2, 0x2F, 0xD4, + 0xB6, 0x4A, 0xCF, 0x17, 0xE8, 0x3D, 0xF4, 0xDD, 0x01, 0x50, 0x3C, 0x42, 0xA8, 0x44, 0x90, 0xA8, + 0x82, 0xC9, 0xB6, 0x87, 0x85, 0xC3, 0xE0, 0x79, 0x44, 0x97, 0xC2, 0xE5, 0xCB, 0x4E, 0x26, 0x6D, + 0xFF, 0x98, 0xDB, 0x14, 0x71, 0x71, 0x33, 0x5F, 0x07, 0xF4, 0x03, 0xF9, 0xBF, 0x0D, 0x98, 0x1C, + 0x46, 0x33, 0xE1, 0x15, 0xBC, 0xBD, 0x16, 0x3C, 0x20, 0xD1, 0x0D, 0x12, 0x3A, 0x5B, 0x6C, 0x22, + 0x56, 0x6C, 0x01, 0x5F, 0xB8, 0x9D, 0xBB, 0x10, 0x09, 0x77, 0xB7, 0x70, 0x46, 0x06, 0x52, 0xC5, + 0xF9, 0x75, 0x83, 0x4E, 0xBD, 0x4D, 0x8A, 0x45, 0x14, 0x84, 0xEC, 0x0E, 0x1C, 0x3B, 0x03, 0xDA, + 0xD4, 0xBB, 0x7A, 0xCB, 0x88, 0xA6, 0xF0, 0x30, 0x82, 0xA9, 0xE5, 0x80, 0xB6, 0x9A, 0xE4, 0x02, + 0xE9, 0x7C, 0x21, 0x6F, 0x83, 0xC7, 0xD2, 0x86, 0x96, 0xCB, 0xF9, 0x49, 0x57, 0xD8, 0x4C, 0x1B, + 0xDD, 0x86, 0xA3, 0x1D, 0x8F, 0xE8, 0x9F, 0xF4, 0x59, 0x97, 0x79, 0x81, 0xEE, 0x04, 0x57, 0xB4, + 0x13, 0x92, 0x68, 0x49, 0x57, 0x6D, 0xCB, 0x69, 0x45, 0x17, 0x53, 0xFA, 0x4B, 0xF0, 0xEB, 0x05, + 0xCE, 0x7C, 0x64, 0xC6, 0x23, 0x13, 0xEA, 0x17, 0xD1, 0x85, 0xFE, 0xD0, 0xA4, 0xFA, 0x05, 0xE7, + 0x9E, 0xFB, 0xBB, 0x90, 0xC2, 0x08, 0x2E, 0x2E, 0x9C, 0x94, 0xD0, 0x4D, 0x1A, 0x69, 0x6C, 0x5A, + 0xD5, 0x39, 0xF5, 0x7D, 0xAE, 0x48, 0xB0, 0xAF, 0x6C, 0xF5, 0x39, 0x00, 0xC3, 0x51, 0x94, 0x59, + 0x64, 0x13, 0x0D, 0xBB, 0xDF, 0x90, 0x71, 0x9C, 0x7D, 0x96, 0xD9, 0x44, 0x03, 0xB3, 0x09, 0xCB, + 0x1E, 0xE3, 0xDF, 0x06, 0x2C, 0x5A, 0x9D, 0x4A, 0x24, 0x05, 0x8D, 0x81, 0xDD, 0x80, 0x60, 0xDE, + 0xB0, 0xE0, 0x01, 0xE1, 0xBF, 0x31, 0x6C, 0x60, 0xF8, 0xC7, 0x87, 0xE4, 0x3D, 0x28, 0x78, 0x8F, + 0x1A, 0xC2, 0x14, 0x1B, 0x18, 0xD6, 0xE1, 0xE4, 0xEA, 0x3B, 0x0D, 0xAD, 0x3B, 0x13, 0x3A, 0xAB, + 0x72, 0xAC, 0xE7, 0x61, 0x97, 0x79, 0xB0, 0xCC, 0xA0, 0x8E, 0x4B, 0xCF, 0xE4, 0x5C, 0xC6, 0x47, + 0xE4, 0x1A, 0x8E, 0x0A, 0x9E, 0x80, 0xAB, 0x67, 0x49, 0x66, 0x97, 0x79, 0x5A, 0x26, 0x67, 0x8A, + 0x4F, 0xC1, 0x75, 0xAC, 0x72, 0xED, 0x3F, 0x86, 0xA9, 0x7D, 0x59, 0xCB, 0xA4, 0x77, 0xE6, 0x72, + 0xFB, 0x9C, 0x4B, 0xBF, 0xC7, 0x45, 0x1B, 0x71, 0xC9, 0x46, 0x39, 0x4F, 0x85, 0xE5, 0xF0, 0x5C, + 0x9E, 0xC3, 0x3F, 0x81, 0xE7, 0xF8, 0x8F, 0xE0, 0xC9, 0xF3, 0x3F, 0xC5, 0xC0, 0xF1, 0x4C, 0x2F, + 0xED, 0x9B, 0x1B, 0xE9, 0xF9, 0xF6, 0x6D, 0xF7, 0xE1, 0x4F, 0x03, 0x82, 0x33, 0xF8, 0xA3, 0xE6, + 0x4F, 0x1B, 0x3F, 0x8D, 0x8C, 0x9E, 0xF6, 0xD6, 0x36, 0xC6, 0xDA, 0xDB, 0x91, 0x61, 0xF5, 0xD8, + 0xBB, 0xA9, 0xBD, 0xB5, 0xC4, 0x63, 0x6C, 0x58, 0x16, 0x7F, 0x0C, 0x78, 0xE3, 0x10, 0x1E, 0x26, + 0x7B, 0x5C, 0x1A, 0xD6, 0x88, 0xBD, 0x5F, 0xB2, 0x26, 0x1B, 0x86, 0xDB, 0xE2, 0x61, 0x1B, 0xD6, + 0x98, 0x3D, 0xC6, 0xAC, 0x6D, 0x88, 0x5C, 0x87, 0xDA, 0x57, 0x5C, 0x60, 0x1A, 0x7F, 0x81, 0x15, + 0xB2, 0xB3, 0x6A, 0x83, 0xA7, 0xBB, 0x0D, 0xB6, 0xD2, 0xDA, 0x85, 0xF2, 0xB4, 0xE6, 0x33, 0x1E, + 0x10, 0x48, 0x6B, 0xA7, 0x20, 0xC9, 0xC5, 0x94, 0x30, 0xF4, 0x51, 0x71, 0x44, 0x67, 0x29, 0x9E, + 0xA1, 0x03, 0x8E, 0xE8, 0xAD, 0x82, 0x07, 0x9C, 0x65, 0xB1, 0xFC, 0x7C, 0x0D, 0xF9, 0x69, 0xB4, + 0xCC, 0x9A, 0xC4, 0xA0, 0x52, 0x69, 0x80, 0x0B, 0xA4, 0x43, 0xE3, 0xB7, 0xF1, 0x1D, 0x49, 0x7F, + 0x80, 0xDC, 0xA0, 0xD9, 0x02, 0x98, 0xA5, 0x95, 0x16, 0x72, 0x45, 0xBF, 0x6D, 0x5B, 0x13, 0x32, + 0xA3, 0xDF, 0x5A, 0x13, 0xB3, 0x60, 0x8B, 0xB5, 0x3F, 0x97, 0x7A, 0x2B, 0x96, 0x69, 0xB1, 0x0C, + 0x11, 0x45, 0x44, 0xB4, 0x81, 0x5C, 0x1E, 0x00, 0x73, 0xD1, 0x84, 0xA7, 0x7A, 0x12, 0xBC, 0x66, + 0x83, 0x26, 0x9A, 0x7E, 0x41, 0x3A, 0x9C, 0xC0, 0xA0, 0x17, 0xE5, 0x21, 0xBF, 0xA9, 0x5F, 0x6E, + 0x62, 0xEA, 0x86, 0x1A, 0x2F, 0x96, 0x33, 0x22, 0x8A, 0x0D, 0xA7, 0x69, 0x20, 0xC0, 0xFB, 0x2A, + 0x09, 0x44, 0x6C, 0xFF, 0x34, 0xC5, 0x3B, 0xCF, 0xDB, 0x24, 0xBC, 0x4A, 0xAB, 0xE9, 0x6C, 0xE8, + 0xD5, 0x9A, 0x40, 0x1C, 0xD4, 0xD6, 0x41, 0x04, 0x06, 0xD3, 0x60, 0x99, 0x18, 0x47, 0x8C, 0x15, + 0x58, 0xD4, 0xB4, 0x71, 0x09, 0x9F, 0x78, 0x6C, 0x6B, 0xE0, 0x0C, 0x71, 0x4E, 0x0F, 0xB8, 0x0E, + 0x1B, 0xC8, 0x88, 0xC5, 0x21, 0xA2, 0xDA, 0xFF, 0x5C, 0x37, 0xFC, 0xD8, 0xDB, 0xAC, 0x61, 0x0F, + 0x3B, 0x4B, 0x42, 0x5F, 0x85, 0x04, 0x3F, 0x7E, 0xBF, 0x7D, 0x03, 0x7B, 0x27, 0x92, 0xEC, 0x56, + 0x27, 0x88, 0x22, 0x92, 0xFE, 0x78, 0xF3, 0xD3, 0xDB, 0x29, 0x35, 0x50, 0x93, 0x06, 0x6C, 0xF3, + 0x33, 0x35, 0xF8, 0x71, 0x25, 0x47, 0xA5, 0x78, 0x08, 0xB1, 0x87, 0xBE, 0xC1, 0x52, 0xCB, 0xBB, + 0x05, 0x46, 0x45, 0xA3, 0xD4, 0xC7, 0xC3, 0x96, 0xDD, 0x72, 0xD8, 0xEA, 0x68, 0x2A, 0xBD, 0x4C, + 0xBD, 0x57, 0x3E, 0x11, 0xB0, 0x4A, 0x71, 0x17, 0x86, 0x90, 0xA6, 0x69, 0x44, 0x17, 0x56, 0xEB, + 0xE1, 0x38, 0x86, 0x61, 0x11, 0x82, 0x99, 0xE2, 0xAD, 0x45, 0x4C, 0x03, 0xB3, 0x0C, 0x31, 0x25, + 0x01, 0x6C, 0x68, 0xCC, 0xE0, 0x2C, 0x80, 0x09, 0xA9, 0xCC, 0x37, 0xF5, 0x3D, 0x61, 0x77, 0x06, + 0x59, 0x07, 0xE4, 0xA3, 0x4D, 0x69, 0x72, 0x25, 0xD3, 0xAD, 0x5A, 0x75, 0x07, 0x33, 0x69, 0x83, + 0xB2, 0x47, 0x6B, 0xDF, 0x62, 0xC9, 0x00, 0xEA, 0x29, 0x98, 0x9A, 0x10, 0xBC, 0x25, 0x3F, 0xAE, + 0x0B, 0x07, 0xE2, 0x68, 0x4B, 0x6F, 0x5B, 0xA0, 0x57, 0x4E, 0xDF, 0x14, 0xFD, 0x10, 0x75, 0x3B, + 0x78, 0x3A, 0x6D, 0xBD, 0x78, 0xD1, 0x64, 0xCA, 0xBA, 0xF9, 0x30, 0x13, 0x46, 0xC1, 0xB2, 0x6E, + 0x80, 0x18, 0x15, 0x57, 0x14, 0xB8, 0xA9, 0x81, 0x18, 0x6D, 0xF6, 0x89, 0x16, 0x28, 0x63, 0x19, + 0x36, 0xA0, 0x84, 0x61, 0x5B, 0x88, 0x35, 0x36, 0x7E, 0x1E, 0xF2, 0xC7, 0x88, 0xB5, 0x59, 0x88, + 0x0F, 0x6F, 0x2D, 0x5B, 0xBC, 0x5B, 0x1A, 0x0E, 0xB3, 0xCE, 0x40, 0x0C, 0xBC, 0x52, 0xD0, 0xEE, + 0x2D, 0x1E, 0x91, 0xB7, 0xF8, 0x6C, 0x68, 0xF7, 0x36, 0x3C, 0x00, 0x59, 0xB7, 0x36, 0x8B, 0x80, + 0x15, 0x0E, 0xFC, 0x6B, 0x5B, 0x08, 0x6F, 0x35, 0xBA, 0x72, 0x89, 0x39, 0xAB, 0x81, 0xE0, 0x64, + 0x0A, 0x56, 0x3D, 0xCE, 0xCA, 0x32, 0xCF, 0xE0, 0x05, 0x6B, 0x3E, 0xE0, 0xD3, 0xAF, 0xF0, 0xE9, + 0x3F, 0x91, 0xCF, 0xB8, 0xC2, 0x67, 0x7C, 0x06, 0x1F, 0x59, 0x3B, 0x60, 0xF9, 0x13, 0x2C, 0xB3, + 0x71, 0x75, 0xF3, 0x52, 0xE4, 0x6A, 0x9F, 0x44, 0xB2, 0xF6, 0xA9, 0x91, 0x57, 0x48, 0x65, 0x19, + 0x3D, 0xB9, 0x77, 0xC0, 0x7C, 0x5D, 0x51, 0x47, 0x6C, 0x80, 0x1F, 0x33, 0xD0, 0x55, 0xCC, 0x04, + 0xED, 0xEC, 0xA2, 0x01, 0x49, 0xB9, 0x9B, 0x82, 0x2B, 0x4F, 0x3F, 0x83, 0x00, 0xD1, 0x97, 0x52, + 0x56, 0x5D, 0x94, 0x23, 0x67, 0x0D, 0x9C, 0xB9, 0x42, 0xCC, 0x75, 0x2E, 0xF2, 0x61, 0xAC, 0x29, + 0xDE, 0xBC, 0x9C, 0x81, 0x6C, 0x5C, 0xCA, 0x8A, 0x3D, 0x8A, 0xB1, 0x7C, 0x84, 0x34, 0x3B, 0xF3, + 0x79, 0x43, 0x9D, 0xEF, 0x93, 0x28, 0x1C, 0x7D, 0xD2, 0x15, 0x57, 0x7E, 0x09, 0xF1, 0x83, 0x92, + 0x26, 0x43, 0xAA, 0xB2, 0xE8, 0x7A, 0xA3, 0x85, 0x99, 0x26, 0x72, 0x56, 0xD3, 0xC5, 0x92, 0x54, + 0x38, 0x1F, 0xF7, 0x49, 0x7C, 0x43, 0x5F, 0x28, 0xFC, 0x2A, 0x04, 0xBF, 0x0A, 0xAB, 0x7E, 0x15, + 0x0A, 0xBF, 0x9A, 0x56, 0xFD, 0x2A, 0xFC, 0x43, 0xFD, 0x4A, 0xF1, 0xAA, 0x4B, 0x1E, 0x9E, 0x2F, + 0x31, 0xD0, 0x42, 0x90, 0x86, 0x78, 0x2C, 0xDE, 0x06, 0x18, 0x72, 0xFB, 0xE8, 0x45, 0x7D, 0xF4, + 0xBB, 0x01, 0x73, 0x3E, 0x9B, 0x0D, 0xC5, 0x07, 0x06, 0x6A, 0x74, 0xC5, 0x1E, 0xA3, 0x1F, 0xB0, + 0x77, 0x9B, 0x7B, 0x22, 0xF4, 0x9F, 0x17, 0xA7, 0x0B, 0xA3, 0xD2, 0x71, 0x5B, 0xCA, 0x27, 0x00, + 0x2D, 0xDF, 0xFE, 0x3C, 0xAB, 0x39, 0xBC, 0x99, 0x53, 0x51, 0x57, 0x09, 0xF5, 0xCA, 0x76, 0x85, + 0xC5, 0x76, 0x39, 0x72, 0xBF, 0x2A, 0x7D, 0x25, 0xE3, 0x90, 0xF2, 0x3C, 0xD9, 0x56, 0x8E, 0xCF, + 0xFF, 0x24, 0x73, 0x39, 0x1A, 0xEF, 0x8A, 0x9A, 0x54, 0x39, 0xE4, 0x1D, 0x25, 0x60, 0xA5, 0x3A, + 0x75, 0xAC, 0x3C, 0x2E, 0x16, 0x29, 0x89, 0xB0, 0x76, 0xCC, 0x94, 0xE2, 0x08, 0xAF, 0x12, 0x9A, + 0xFA, 0x0F, 0xFC, 0x83, 0xE6, 0x63, 0x17, 0x8E, 0x89, 0x17, 0xB8, 0x99, 0x3C, 0x3D, 0x00, 0x73, + 0x2C, 0xE5, 0x4F, 0x6C, 0x10, 0x81, 0x0C, 0xF6, 0x80, 0xA7, 0x4C, 0xC0, 0x4E, 0xB1, 0x85, 0x31, + 0xB0, 0x81, 0x71, 0xBA, 0x3D, 0xC1, 0x1B, 0xC6, 0x94, 0xD9, 0x2B, 0x05, 0x60, 0x71, 0x52, 0x4B, + 0x52, 0x88, 0x6C, 0x70, 0xB6, 0x7D, 0xCF, 0xCB, 0x35, 0xEC, 0xA2, 0xA1, 0xE0, 0xCD, 0x0A, 0x46, + 0x90, 0x13, 0x80, 0x6A, 0xA3, 0x4D, 0x18, 0x3E, 0x9B, 0x92, 0xCA, 0x3C, 0x9E, 0x64, 0x08, 0xF3, + 0x74, 0xC0, 0x09, 0xD7, 0xCD, 0x96, 0x32, 0x9D, 0x3A, 0x94, 0x45, 0x53, 0x9E, 0x4A, 0x44, 0xE4, + 0x4E, 0xFB, 0x9F, 0x9F, 0xDE, 0xFE, 0x48, 0x69, 0x22, 0x4E, 0xF0, 0x70, 0xA0, 0xD6, 0xBB, 0xCC, + 0x04, 0xBE, 0xE5, 0x3F, 0x85, 0x98, 0xC2, 0x9A, 0x20, 0x6E, 0x42, 0x26, 0x85, 0xAD, 0xBC, 0x6C, + 0x75, 0x41, 0x22, 0x2F, 0xF6, 0xC9, 0xC7, 0x0F, 0x6F, 0x9A, 0xB4, 0x65, 0xB0, 0x4E, 0x96, 0x34, + 0xA8, 0x1D, 0x6A, 0xE2, 0x72, 0x7C, 0x73, 0x45, 0xD1, 0xB6, 0xD5, 0x61, 0xAE, 0xD2, 0x29, 0x2A, + 0x59, 0xA2, 0xAA, 0x15, 0xC2, 0x9A, 0xA3, 0x4E, 0x1C, 0xC1, 0xE2, 0xFC, 0x2D, 0xA6, 0x4A, 0xC4, + 0x5B, 0xE1, 0xAF, 0x0B, 0xA7, 0x79, 0x6E, 0xD0, 0xDA, 0x41, 0xC6, 0xD9, 0x9F, 0x4E, 0xA3, 0x0E, + 0x1B, 0x83, 0xC9, 0x26, 0x69, 0x41, 0x93, 0x6D, 0x9A, 0xD8, 0xC8, 0xD3, 0x2B, 0xA9, 0xE3, 0xFF, + 0xBA, 0x7E, 0xF7, 0x33, 0x20, 0x7A, 0x0A, 0x09, 0x2E, 0x8E, 0xCF, 0x92, 0x38, 0xCA, 0xC8, 0x0D, + 0xB9, 0xA7, 0x27, 0x0C, 0xF6, 0x84, 0x88, 0xA2, 0xDA, 0x66, 0xD4, 0xA6, 0xC4, 0x7B, 0x12, 0xC2, + 0x36, 0x56, 0xCA, 0x23, 0x7B, 0x5C, 0x4D, 0x42, 0xA2, 0xA6, 0xFE, 0x9F, 0xAF, 0x6E, 0xE0, 0x5C, + 0x6F, 0x3C, 0x33, 0x5B, 0xD0, 0x94, 0xC1, 0xF6, 0x34, 0x2B, 0xDB, 0xC5, 0xCB, 0x8C, 0xBB, 0xBC, + 0x8D, 0xD9, 0x37, 0x2C, 0x8C, 0x08, 0xD0, 0x85, 0xED, 0x4C, 0x36, 0xAC, 0x04, 0x20, 0xB3, 0x6F, + 0xC2, 0x7F, 0xDF, 0xE0, 0x77, 0x45, 0xCA, 0xFC, 0x1F, 0x90, 0xBD, 0x1E, 0x5F, 0x17, 0xDE, 0xB6, + 0xB4, 0x3A, 0x3C, 0xA3, 0x3D, 0xE1, 0x7E, 0xE5, 0x7B, 0x20, 0x49, 0x20, 0x4A, 0xA1, 0x80, 0x6A, + 0x5A, 0xA7, 0xD3, 0xD1, 0x2F, 0xF0, 0xF0, 0xF0, 0x1A, 0xAF, 0xFF, 0x9B, 0x66, 0x0B, 0xF3, 0xDD, + 0xFD, 0x9E, 0x8B, 0x74, 0x12, 0x06, 0x64, 0xF1, 0xB6, 0xC5, 0x31, 0x07, 0x8F, 0x0E, 0xE6, 0xB3, + 0xA9, 0xAC, 0xB7, 0xB4, 0x76, 0x4F, 0x96, 0x09, 0x45, 0x3A, 0x01, 0x28, 0x6C, 0xE1, 0xC7, 0x0D, + 0xCE, 0x29, 0x7C, 0xE3, 0x75, 0x9C, 0xAE, 0x5F, 0xBA, 0xD4, 0x75, 0xA2, 0x8E, 0x9B, 0x24, 0xB8, + 0x49, 0x1C, 0x8E, 0xD4, 0x3C, 0xBB, 0x9A, 0x72, 0x52, 0x35, 0xD9, 0xDC, 0xF1, 0x90, 0x89, 0x65, + 0x1D, 0xC3, 0x57, 0x33, 0xF7, 0x8B, 0x50, 0xE0, 0xEB, 0xB5, 0x5E, 0x30, 0xF7, 0x8D, 0x90, 0x87, + 0x4B, 0xA3, 0x98, 0xAF, 0x28, 0x69, 0x1B, 0xA1, 0x71, 0xC8, 0xA0, 0xC5, 0x14, 0xED, 0xD6, 0x78, + 0xB2, 0xE3, 0x0A, 0x4B, 0x7B, 0xFF, 0xEE, 0xFA, 0x06, 0x4F, 0x13, 0x8C, 0x8F, 0xCE, 0x2C, 0xCE, + 0xED, 0x70, 0x15, 0x76, 0x20, 0x32, 0xBD, 0xBA, 0x05, 0x8E, 0x6F, 0x01, 0x90, 0x09, 0x00, 0x2C, + 0x6A, 0x87, 0x17, 0x9D, 0x01, 0x46, 0x8C, 0x67, 0x16, 0x0E, 0x8D, 0x23, 0x1C, 0x5A, 0xF1, 0x38, + 0xE6, 0x5E, 0x53, 0x37, 0xF7, 0xAF, 0xA7, 0xED, 0xD5, 0x13, 0x76, 0x29, 0xF7, 0xB9, 0xF3, 0x2C, + 0x4B, 0x4C, 0xA7, 0x3B, 0x35, 0x3E, 0xAA, 0xA0, 0x81, 0x5B, 0x46, 0x03, 0xE1, 0xBC, 0xEC, 0x5F, + 0x24, 0x34, 0xF5, 0xEF, 0xC0, 0xF9, 0xD8, 0x6F, 0xE1, 0xF1, 0x4C, 0x07, 0x1B, 0xE0, 0x3F, 0x83, + 0x63, 0x36, 0x28, 0x86, 0x79, 0x6D, 0xD4, 0xDA, 0x17, 0x7E, 0xAB, 0x5E, 0x10, 0xFC, 0xA9, 0x9E, + 0xBB, 0xB8, 0x3B, 0xC7, 0x77, 0xF1, 0x92, 0x40, 0x8D, 0x9C, 0x0F, 0x7B, 0x2D, 0x48, 0x79, 0x10, + 0xEB, 0x2A, 0x17, 0x37, 0xDA, 0xB7, 0x7A, 0x4B, 0xCA, 0x7D, 0x7C, 0x13, 0xEE, 0x7E, 0xA7, 0x73, + 0x17, 0xD7, 0x2F, 0x4F, 0x33, 0x02, 0x65, 0xFE, 0x27, 0x90, 0x73, 0xC5, 0x9D, 0x88, 0x49, 0xE7, + 0xAB, 0xFC, 0xC4, 0x58, 0xF1, 0x73, 0x01, 0x39, 0x91, 0xF8, 0x51, 0xD5, 0x54, 0xC7, 0x5F, 0x55, + 0x9D, 0xF6, 0x0C, 0xB6, 0xFD, 0x27, 0x10, 0x4C, 0x22, 0x52, 0x19, 0xC5, 0x8C, 0x07, 0x01, 0x4A, + 0xEF, 0xEA, 0x8F, 0x04, 0x26, 0x8E, 0x4B, 0x05, 0xDD, 0x23, 0xF0, 0x88, 0xFF, 0xFE, 0x03, 0x96, + 0xF2, 0xD7, 0x40, 0x92, 0x6A, 0x61, 0xE7, 0x82, 0xD2, 0xC1, 0xEE, 0x9E, 0xB8, 0x67, 0x3B, 0xC1, + 0x46, 0xDE, 0xB8, 0x3D, 0xCD, 0xC4, 0xFE, 0x10, 0x07, 0x39, 0xC6, 0x44, 0x5E, 0x38, 0x27, 0xF7, + 0x67, 0x7A, 0x97, 0x04, 0xD8, 0x3F, 0xCD, 0x17, 0x0F, 0xC9, 0x15, 0x19, 0x9D, 0x83, 0xBC, 0xAE, + 0x82, 0xE4, 0x88, 0x3B, 0x3A, 0x16, 0x7C, 0x64, 0xED, 0xF1, 0xC5, 0x0B, 0xBD, 0x5F, 0xFE, 0xAA, + 0xF6, 0xFE, 0xF6, 0x9B, 0xC0, 0x7C, 0x81, 0x75, 0x0B, 0x17, 0x6C, 0xDC, 0xD7, 0x5B, 0x86, 0x6E, + 0xC3, 0xE1, 0x56, 0x8E, 0x6A, 0x95, 0x07, 0x79, 0x6E, 0xE4, 0x81, 0x84, 0x18, 0x16, 0x1C, 0x16, + 0x37, 0x70, 0xCE, 0x9E, 0x3A, 0x9E, 0xC3, 0xA5, 0x11, 0x4D, 0x4D, 0xF0, 0xC1, 0x87, 0x1C, 0xDB, + 0x09, 0x3A, 0x58, 0x7B, 0xEC, 0x9B, 0x06, 0x9D, 0x66, 0x84, 0xBE, 0x41, 0x53, 0x01, 0x2D, 0x37, + 0x15, 0x6B, 0x8F, 0x2E, 0xA6, 0xD6, 0xC3, 0x00, 0xC1, 0xB7, 0x26, 0x3A, 0xC7, 0x0E, 0x0B, 0x93, + 0xEE, 0x5B, 0xED, 0xC8, 0x88, 0x66, 0x7D, 0x13, 0x8E, 0xEC, 0x1E, 0x58, 0x74, 0x9A, 0xCF, 0x0F, + 0xA9, 0x7E, 0x18, 0xF3, 0xBB, 0x59, 0xD0, 0x31, 0x9A, 0x0F, 0x1E, 0x31, 0x0C, 0x8B, 0xF4, 0xCA, + 0xE1, 0xB2, 0xA4, 0x3A, 0x0C, 0x95, 0x8F, 0x8A, 0xA5, 0x45, 0x30, 0x15, 0xB8, 0xF1, 0x16, 0x6F, + 0x3A, 0x9B, 0x27, 0xDC, 0xF9, 0x9C, 0x04, 0xFD, 0x38, 0xAD, 0xBC, 0xC4, 0x3E, 0x40, 0x61, 0xF6, + 0xDB, 0x73, 0xA5, 0x24, 0x5F, 0xC9, 0xE4, 0x4F, 0xC9, 0x73, 0x94, 0x27, 0x43, 0x76, 0x61, 0xB3, + 0xA7, 0xE9, 0x95, 0x3B, 0x67, 0xB1, 0x91, 0xE2, 0x50, 0x67, 0x9C, 0x88, 0xBA, 0x75, 0x97, 0xC2, + 0x15, 0x6A, 0xBC, 0x35, 0x65, 0xC3, 0xBE, 0xFD, 0x78, 0xFD, 0xEA, 0x83, 0x7A, 0x62, 0xC3, 0xAC, + 0x04, 0xC4, 0x8B, 0x28, 0x64, 0x2B, 0x17, 0xFA, 0x8B, 0xF7, 0xDF, 0x5D, 0x5F, 0xFF, 0xF3, 0xDD, + 0x87, 0x97, 0xF5, 0x43, 0x28, 0x0E, 0xB9, 0xFE, 0xF8, 0xFD, 0x4F, 0x6F, 0x6E, 0xA6, 0x5B, 0xCC, + 0x2A, 0x83, 0x3A, 0xC4, 0x0F, 0x1E, 0x38, 0xB9, 0xC1, 0xB1, 0x2D, 0x50, 0x8E, 0x6D, 0x2F, 0x5E, + 0x00, 0x84, 0x3F, 0x83, 0x26, 0xE9, 0x9A, 0x65, 0x0B, 0xD8, 0x1B, 0x81, 0x7A, 0x76, 0x8A, 0x58, + 0xD8, 0x08, 0xE4, 0xD9, 0x09, 0x95, 0xAA, 0x16, 0xB0, 0x21, 0x24, 0x39, 0x77, 0x41, 0xE4, 0xC7, + 0x77, 0x35, 0xD1, 0xE2, 0xF8, 0xED, 0x89, 0x73, 0xD5, 0x15, 0xD7, 0xD3, 0x57, 0x5D, 0xF1, 0x83, + 0x19, 0xF6, 0xFF, 0xCC, 0xF9, 0x7F, 0xE5, 0xCC, 0x32, 0xCA, 0x3A, 0x47, 0x00, 0x00 +}; diff --git a/Grbl_Esp32/nuts_bolts.h b/Grbl_Esp32/nuts_bolts.h index 6fe1bf49..6671ca39 100644 --- a/Grbl_Esp32/nuts_bolts.h +++ b/Grbl_Esp32/nuts_bolts.h @@ -21,8 +21,6 @@ #ifndef nuts_bolts_h #define nuts_bolts_h - - #define false 0 #define true 1 @@ -35,6 +33,13 @@ #define Z_AXIS 2 // #define A_AXIS 3 +// CoreXY motor assignments. DO NOT ALTER. +// NOTE: If the A and B motor axis bindings are changed, this effects the CoreXY equations. +#define A_MOTOR X_AXIS // Must be X_AXIS +#define B_MOTOR Y_AXIS // Must be Y_AXIS + + + // Conversions #define MM_PER_INCH (25.40) #define INCH_PER_MM (0.0393701) diff --git a/Grbl_Esp32/probe.cpp b/Grbl_Esp32/probe.cpp index 3a9c81e1..4c5093f7 100644 --- a/Grbl_Esp32/probe.cpp +++ b/Grbl_Esp32/probe.cpp @@ -31,7 +31,7 @@ uint8_t probe_invert_mask; // Probe pin initialization routine. void probe_init() { - +#ifdef PROBE_PIN #ifdef DISABLE_PROBE_PIN_PULL_UP pinMode(PROBE_PIN, INPUT); #else @@ -40,6 +40,7 @@ void probe_init() probe_configure_invert_mask(false); // Initialize invert mask. +#endif } @@ -56,7 +57,11 @@ void probe_configure_invert_mask(uint8_t is_probe_away) // Returns the probe pin state. Triggered = true. Called by gcode parser and probe state monitor. uint8_t probe_get_state() { +#ifdef PROBE_PIN return((digitalRead(PROBE_PIN)) ^ probe_invert_mask); +#else + return false; +#endif } diff --git a/Grbl_Esp32/protocol.cpp b/Grbl_Esp32/protocol.cpp index e561a437..badf0c1f 100644 --- a/Grbl_Esp32/protocol.cpp +++ b/Grbl_Esp32/protocol.cpp @@ -202,9 +202,6 @@ void protocol_main_loop() set_stepper_disable(true); } } -#ifdef ENABLE_WIFI - wifi_config.handle(); -#endif } return; /* Never reached */ diff --git a/Grbl_Esp32/report.cpp b/Grbl_Esp32/report.cpp index f6510aa9..c6f3a176 100644 --- a/Grbl_Esp32/report.cpp +++ b/Grbl_Esp32/report.cpp @@ -65,6 +65,12 @@ void grbl_send(uint8_t client, char *text) if ( client == CLIENT_WEBUI || client == CLIENT_ALL ) Serial2Socket.write((const uint8_t*)text, strlen(text)); #endif + +#if defined (ENABLE_WIFI) && defined(ENABLE_TELNET) + if ( client == CLIENT_TELNET || client == CLIENT_ALL ){ + telnet_server.write((const uint8_t*)text, strlen(text)); + } +#endif if ( client == CLIENT_SERIAL || client == CLIENT_ALL ) Serial.print(text); @@ -148,7 +154,15 @@ void report_status_message(uint8_t status_code, uint8_t client) grbl_send(client,"ok\r\n"); #endif break; - default: + default: + #ifdef ENABLE_SD_CARD + // do we need to stop a running SD job? + if (get_sd_state(false) == SDCARD_BUSY_PRINTING) { + grbl_sendf(CLIENT_ALL, "error:%d in SD file at line %d\r\n", status_code, sd_get_current_line_number()); + closeFile(); + return; + } + #endif grbl_sendf(client, "error:%d\r\n", status_code); } } @@ -192,6 +206,8 @@ void report_feedback_message(uint8_t message_code) // OK to send to all clients grbl_send(CLIENT_ALL, "[MSG:Restoring spindle]\r\n"); break; case MESSAGE_SLEEP_MODE: grbl_send(CLIENT_ALL, "[MSG:Sleeping]\r\n"); break; + case MESSAGE_SD_FILE_QUIT: + grbl_sendf(CLIENT_ALL, "[MSG:Reset during SD file at line: %d]\r\n", sd_get_current_line_number()); break; } } @@ -484,6 +500,9 @@ void report_build_info(char *line, uint8_t client) #ifdef ENABLE_SD_CARD strcat(build_info,"S"); #endif + #if defined (ENABLE_WIFI) + strcat(build_info,"W"); + #endif #ifndef ENABLE_RESTORE_EEPROM_WIPE_ALL // NOTE: Shown when disabled. strcat(build_info,"*"); #endif @@ -507,6 +526,9 @@ void report_build_info(char *line, uint8_t client) strcat(build_info,"]\r\n"); grbl_send(client, build_info); // ok to send to all + #if defined (ENABLE_WIFI) + grbl_send(client, (char *)wifi_config.info()); + #endif } @@ -729,4 +751,4 @@ void report_realtime_steps() for (idx=0; idx< N_AXIS; idx++) { grbl_sendf(CLIENT_ALL, "%ld\n", sys_position[idx]); // OK to send to all ... debug stuff } -} \ No newline at end of file +} diff --git a/Grbl_Esp32/report.h b/Grbl_Esp32/report.h index 7477120b..36bd57c5 100644 --- a/Grbl_Esp32/report.h +++ b/Grbl_Esp32/report.h @@ -96,12 +96,14 @@ #define MESSAGE_RESTORE_DEFAULTS 9 #define MESSAGE_SPINDLE_RESTORE 10 #define MESSAGE_SLEEP_MODE 11 +#define MESSAGE_SD_FILE_QUIT 60 // mc_reset was called during an SD job #define CLIENT_SERIAL 1 #define CLIENT_BT 2 #define CLIENT_WEBUI 3 +#define CLIENT_TELNET 4 #define CLIENT_ALL 0xFF -#define CLIENT_COUNT 3 // total number of client types regardless if they are used +#define CLIENT_COUNT 4 // total number of client types regardless if they are used // functions to send data to the user. void grbl_send(uint8_t client, char *text); diff --git a/Grbl_Esp32/serial.cpp b/Grbl_Esp32/serial.cpp index e83b6488..94f48274 100644 --- a/Grbl_Esp32/serial.cpp +++ b/Grbl_Esp32/serial.cpp @@ -47,7 +47,7 @@ void serial_init() // create a task to check for incoming data xTaskCreatePinnedToCore( serialCheckTask, // task "servoSyncTask", // name for task - 2048, // size of task stack + 8192, // size of task stack NULL, // parameters 1, // priority &serialCheckTaskHandle, @@ -76,6 +76,9 @@ void serialCheckTask(void *pvParameters) #if defined (ENABLE_WIFI) && defined(ENABLE_HTTP) && defined(ENABLE_SERIAL2SOCKET_IN) || Serial2Socket.available() #endif + #if defined (ENABLE_WIFI) && defined(ENABLE_TELNET) + || telnet_server.available() + #endif ) { if (Serial.available()) @@ -93,8 +96,21 @@ void serialCheckTask(void *pvParameters) } else { #endif #if defined (ENABLE_WIFI) && defined(ENABLE_HTTP) && defined(ENABLE_SERIAL2SOCKET_IN) - client = CLIENT_WEBUI; - data = Serial2Socket.read(); + if (Serial2Socket.available()) { + client = CLIENT_WEBUI; + data = Serial2Socket.read(); + } + else + { + #endif + #if defined (ENABLE_WIFI) && defined(ENABLE_TELNET) + if(telnet_server.available()){ + client = CLIENT_TELNET; + data = telnet_server.read(); + } + #endif + #if defined (ENABLE_WIFI) && defined(ENABLE_HTTP) && defined(ENABLE_SERIAL2SOCKET_IN) + } #endif #ifdef ENABLE_BLUETOOTH } @@ -163,8 +179,14 @@ void serialCheckTask(void *pvParameters) vTaskExitCritical(&myMutex); } } // switch data - } // if something available - vTaskDelay(1 / portTICK_RATE_MS); // Yield to other tasks + } // if something available +#ifdef ENABLE_WIFI + wifi_config.handle(); +#endif +#if defined (ENABLE_WIFI) && defined(ENABLE_HTTP) && defined(ENABLE_SERIAL2SOCKET_IN) + Serial2Socket.handle_flush(); +#endif + vTaskDelay(1 / portTICK_RATE_MS); // Yield to other tasks } // while(true) } diff --git a/Grbl_Esp32/serial2socket.cpp b/Grbl_Esp32/serial2socket.cpp new file mode 100644 index 00000000..bd0a9b94 --- /dev/null +++ b/Grbl_Esp32/serial2socket.cpp @@ -0,0 +1,176 @@ +/* + serial2socket.cpp - serial 2 socket functions class + + Copyright (c) 2014 Luc Lebosse. All rights reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + + +#ifdef ARDUINO_ARCH_ESP32 + +//#include "grbl.h" +#include "config.h" + +#if defined (ENABLE_WIFI) && defined(ENABLE_HTTP) + + +#include "serial2socket.h" +#include "web_server.h" +#include +#include +Serial_2_Socket Serial2Socket; + + +Serial_2_Socket::Serial_2_Socket(){ + _web_socket = NULL; + _TXbufferSize = 0; + _RXbufferSize = 0; + _RXbufferpos = 0; +} +Serial_2_Socket::~Serial_2_Socket(){ + if (_web_socket) detachWS(); + _TXbufferSize = 0; + _RXbufferSize = 0; + _RXbufferpos = 0; +} +void Serial_2_Socket::begin(long speed){ + _TXbufferSize = 0; + _RXbufferSize = 0; + _RXbufferpos = 0; +} + +void Serial_2_Socket::end(){ + _TXbufferSize = 0; + _RXbufferSize = 0; + _RXbufferpos = 0; +} + +long Serial_2_Socket::baudRate(){ + return 0; +} + +bool Serial_2_Socket::attachWS(void * web_socket){ + if (web_socket) { + _web_socket = web_socket; + _TXbufferSize=0; + return true; + } + return false; +} + +bool Serial_2_Socket::detachWS(){ + _web_socket = NULL; +} + +Serial_2_Socket::operator bool() const +{ + return true; +} +int Serial_2_Socket::available(){ + return _RXbufferSize; +} + + +size_t Serial_2_Socket::write(uint8_t c) +{ + if(!_web_socket) return 0; + write(&c,1); + return 1; +} + +size_t Serial_2_Socket::write(const uint8_t *buffer, size_t size) +{ + if((buffer == NULL) ||(!_web_socket)) { + if(buffer == NULL)log_i("[SOCKET]No buffer"); + if(!_web_socket)log_i("[SOCKET]No socket"); + return 0; + } +#if defined(ENABLE_SERIAL2SOCKET_OUT) + if (_TXbufferSize==0)_lastflush = millis(); + //send full line + if (_TXbufferSize + size > TXBUFFERSIZE) flush(); + //need periodic check to force to flush in case of no end + for (int i = 0; i < size;i++){ + _TXbuffer[_TXbufferSize] = buffer[i]; + _TXbufferSize++; + } + log_i("[SOCKET]buffer size %d",_TXbufferSize); + handle_flush(); +#endif + return size; +} + +int Serial_2_Socket::peek(void){ + if (_RXbufferSize > 0)return _RXbuffer[_RXbufferpos]; + else return -1; +} + +bool Serial_2_Socket::push (const char * data){ +#if defined(ENABLE_SERIAL2SOCKET_IN) + int data_size = strlen(data); + if ((data_size + _RXbufferSize) <= RXBUFFERSIZE){ + int current = _RXbufferpos + _RXbufferSize; + if (current > RXBUFFERSIZE) current = current - RXBUFFERSIZE; + for (int i = 0; i < data_size; i++){ + if (current > (RXBUFFERSIZE-1)) current = 0; + _RXbuffer[current] = data[i]; + current ++; + } + _RXbufferSize+=strlen(data); + return true; + } + return false; +#else + return true; +#endif +} + +int Serial_2_Socket::read(void){ + if (_RXbufferSize > 0) { + int v = _RXbuffer[_RXbufferpos]; + _RXbufferpos++; + if (_RXbufferpos > (RXBUFFERSIZE-1))_RXbufferpos = 0; + _RXbufferSize--; + return v; + } else return -1; +} + +void Serial_2_Socket::handle_flush() { + if (_TXbufferSize > 0) { + if ((_TXbufferSize>=TXBUFFERSIZE) || ((millis()- _lastflush) > FLUSHTIMEOUT)) { + log_i("[SOCKET]need flush, buffer size %d",_TXbufferSize); + flush(); + } + } +} +void Serial_2_Socket::flush(void){ + if (_TXbufferSize > 0){ + //if ((((AsyncWebSocket *)_web_socket)->count() > 0) && (((AsyncWebSocket *)_web_socket)->availableForWriteAll())) { + log_i("[SOCKET]flush data, buffer size %d",_TXbufferSize); + ((WebSocketsServer *)_web_socket)->broadcastBIN(_TXbuffer,_TXbufferSize); + // } else { + // log_i("[SOCKET]Cannot flush, buffer size %d",_TXbufferSize); + // } + //refresh timout + _lastflush = millis(); + //reset buffer + _TXbufferSize = 0; + } +} + +#endif // ENABLE_WIFI + +#endif // ARDUINO_ARCH_ESP32 diff --git a/Grbl_Esp32/serial2socket.h b/Grbl_Esp32/serial2socket.h new file mode 100644 index 00000000..30a2cb04 --- /dev/null +++ b/Grbl_Esp32/serial2socket.h @@ -0,0 +1,81 @@ +/* + serial2socket.h - serial 2 socket functions class + + Copyright (c) 2014 Luc Lebosse. All rights reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + + +#ifndef _SERIAL_2_SOCKET_H_ +#define _SERIAL_2_SOCKET_H_ + +#include "Print.h" +#define TXBUFFERSIZE 1200 +#define RXBUFFERSIZE 128 +#define FLUSHTIMEOUT 500 +class Serial_2_Socket: public Print{ + public: + Serial_2_Socket(); + ~Serial_2_Socket(); + size_t write(uint8_t c); + size_t write(const uint8_t *buffer, size_t size); + + inline size_t write(const char * s) + { + return write((uint8_t*) s, strlen(s)); + } + inline size_t write(unsigned long n) + { + return write((uint8_t) n); + } + inline size_t write(long n) + { + return write((uint8_t) n); + } + inline size_t write(unsigned int n) + { + return write((uint8_t) n); + } + inline size_t write(int n) + { + return write((uint8_t) n); + } + long baudRate(); + void begin(long speed); + void end(); + int available(); + int peek(void); + int read(void); + bool push (const char * data); + void flush(void); + void handle_flush(); + operator bool() const; + bool attachWS(void * web_socket); + bool detachWS(); + private: + uint32_t _lastflush; + void * _web_socket; + uint8_t _TXbuffer[TXBUFFERSIZE]; + uint16_t _TXbufferSize; + uint8_t _RXbuffer[RXBUFFERSIZE]; + uint16_t _RXbufferSize; + uint16_t _RXbufferpos; +}; + + +extern Serial_2_Socket Serial2Socket; + +#endif diff --git a/Grbl_Esp32/settings.cpp b/Grbl_Esp32/settings.cpp index 334003c3..43fc64f2 100644 --- a/Grbl_Esp32/settings.cpp +++ b/Grbl_Esp32/settings.cpp @@ -49,6 +49,11 @@ void settings_init() // Method to restore EEPROM-saved Grbl global settings back to defaults. void settings_restore(uint8_t restore_flag) { + if (restore_flag & SETTINGS_RESTORE_ALL){ +#ifdef ENABLE_WIFI + wifi_config.reset_ESP(); +#endif + } if (restore_flag & SETTINGS_RESTORE_DEFAULTS) { settings.pulse_microseconds = DEFAULT_STEP_PULSE_MICROSECONDS; settings.stepper_idle_lock_time = DEFAULT_STEPPER_IDLE_LOCK_TIME; diff --git a/Grbl_Esp32/spindle_control.cpp b/Grbl_Esp32/spindle_control.cpp index 14bf6db2..f0d78ec2 100644 --- a/Grbl_Esp32/spindle_control.cpp +++ b/Grbl_Esp32/spindle_control.cpp @@ -20,8 +20,11 @@ #include "grbl.h" +static float pwm_gradient; // Precalulated value to speed up rpm to PWM conversions. + void spindle_init() { + pwm_gradient = SPINDLE_PWM_RANGE/(settings.rpm_max-settings.rpm_min); // Use DIR and Enable if pins are defined #ifdef SPINDLE_ENABLE_PIN @@ -33,7 +36,7 @@ void spindle_init() #endif #ifdef SPINDLE_PWM_PIN - // use the LED control feature to setup PWM https://esp-idf.readthedocs.io/en/v1.0/api/ledc.html + // use the LED control feature to setup PWM https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/peripherals/ledc.html ledcSetup(SPINDLE_PWM_CHANNEL, SPINDLE_PWM_BASE_FREQ, SPINDLE_PWM_BIT_PRECISION); // setup the channel ledcAttachPin(SPINDLE_PWM_PIN, SPINDLE_PWM_CHANNEL); // attach the PWM to the pin #endif @@ -72,7 +75,7 @@ uint8_t spindle_get_state() // returns SPINDLE_STATE_DISABLE, SPINDLE_STATE_CW } } -void spindle_set_speed(uint8_t pwm_value) +void spindle_set_speed(uint32_t pwm_value) { #ifndef SPINDLE_PWM_PIN return; @@ -87,16 +90,31 @@ void spindle_set_speed(uint8_t pwm_value) } // Called by spindle_set_state() and step segment generator. Keep routine small and efficient. -uint8_t spindle_compute_pwm_value(float rpm) +uint32_t spindle_compute_pwm_value(float rpm) { - uint8_t pwm_value; - - rpm *= (0.010*sys.spindle_speed_ovr); - - pwm_value = map(rpm, settings.rpm_min, settings.rpm_max, SPINDLE_PWM_OFF_VALUE, SPINDLE_PWM_MAX_VALUE); - // TODO_ESP32 .. make it 16 bit - + uint32_t pwm_value; + rpm *= (0.010*sys.spindle_speed_ovr); // Scale by spindle speed override value. + // Calculate PWM register value based on rpm max/min settings and programmed rpm. + if ((settings.rpm_min >= settings.rpm_max) || (rpm >= settings.rpm_max)) { + // No PWM range possible. Set simple on/off spindle control pin state. + sys.spindle_speed = settings.rpm_max; + pwm_value = SPINDLE_PWM_MAX_VALUE; + } else if (rpm <= settings.rpm_min) { + if (rpm == 0.0) { // S0 disables spindle + sys.spindle_speed = 0.0; + pwm_value = SPINDLE_PWM_OFF_VALUE; + } else { // Set minimum PWM output + sys.spindle_speed = settings.rpm_min; + pwm_value = SPINDLE_PWM_MIN_VALUE; + } + } else { + // Compute intermediate PWM value with linear spindle speed model. + // NOTE: A nonlinear model could be installed here, if required, but keep it VERY light-weight. + sys.spindle_speed = rpm; + pwm_value = floor((rpm-settings.rpm_min)*pwm_gradient) + SPINDLE_PWM_MIN_VALUE; + } return(pwm_value); + } @@ -136,6 +154,7 @@ void grbl_analogWrite(uint8_t chan, uint32_t duty) { if (ledcRead(chan) != duty) // reduce unnecessary calls to ledcWrite() { + // grbl_sendf(CLIENT_SERIAL, "[MSG: Spindle duty: %d of %d]\r\n", duty, SPINDLE_PWM_MAX_VALUE); // debug statement ledcWrite(chan, duty); } } diff --git a/Grbl_Esp32/spindle_control.h b/Grbl_Esp32/spindle_control.h index 0349c3cf..81df7a7e 100644 --- a/Grbl_Esp32/spindle_control.h +++ b/Grbl_Esp32/spindle_control.h @@ -33,8 +33,8 @@ void spindle_init(); void spindle_stop(); uint8_t spindle_get_state(); - void spindle_set_speed(uint8_t pwm_value); - uint8_t spindle_compute_pwm_value(float rpm); + void spindle_set_speed(uint32_t pwm_value); + uint32_t spindle_compute_pwm_value(float rpm); void spindle_set_state(uint8_t state, float rpm); void spindle_sync(uint8_t state, float rpm); void grbl_analogWrite(uint8_t chan, uint32_t duty); diff --git a/Grbl_Esp32/stepper.cpp b/Grbl_Esp32/stepper.cpp index 001d813a..fd29fe57 100644 --- a/Grbl_Esp32/stepper.cpp +++ b/Grbl_Esp32/stepper.cpp @@ -365,6 +365,7 @@ void stepper_init() // make the stepper disable pin an output #ifdef STEPPERS_DISABLE_PIN pinMode(STEPPERS_DISABLE_PIN, OUTPUT); + set_stepper_disable(true); #endif // setup stepper timer interrupt diff --git a/Grbl_Esp32/system.cpp b/Grbl_Esp32/system.cpp index f205ea86..e29a9e0f 100644 --- a/Grbl_Esp32/system.cpp +++ b/Grbl_Esp32/system.cpp @@ -442,3 +442,14 @@ uint8_t get_limit_pin_mask(uint8_t axis_idx) return((1< +#include +#include "report.h" + + +Telnet_Server telnet_server; +bool Telnet_Server::_setupdone = false; +uint16_t Telnet_Server::_port = 0; +WiFiServer * Telnet_Server::_telnetserver = NULL; +WiFiClient Telnet_Server::_telnetClients[MAX_TLNT_CLIENTS]; + +Telnet_Server::Telnet_Server(){ + _RXbufferSize = 0; + _RXbufferpos = 0; +} +Telnet_Server::~Telnet_Server(){ + end(); +} + + +bool Telnet_Server::begin(){ + + bool no_error = true; + _setupdone = false; + Preferences prefs; + _RXbufferSize = 0; + _RXbufferpos = 0;; + prefs.begin(NAMESPACE, true); + int8_t penabled = prefs.getChar(TELNET_ENABLE_ENTRY, DEFAULT_TELNET_STATE); + //Get telnet port + _port = prefs.getUShort(TELNET_PORT_ENTRY, DEFAULT_TELNETSERVER_PORT); + prefs.end(); + + if (penabled == 0) return false; + //create instance + _telnetserver= new WiFiServer(_port); + _telnetserver->setNoDelay(true); + String s = "[MSG:TELNET Started " + String(_port) + "]\r\n"; + grbl_send(CLIENT_ALL,(char *)s.c_str()); + //start telnet server + _telnetserver->begin(); + _setupdone = true; + return no_error; +} + +void Telnet_Server::end(){ + _setupdone = false; + _RXbufferSize = 0; + _RXbufferpos = 0; + if (_telnetserver) { + delete _telnetserver; + _telnetserver = NULL; + } +} + +void Telnet_Server::clearClients(){ + //check if there are any new clients + if (_telnetserver->hasClient()){ + uint8_t i; + for(i = 0; i < MAX_TLNT_CLIENTS; i++){ + //find free/disconnected spot + if (!_telnetClients[i] || !_telnetClients[i].connected()){ + if(_telnetClients[i]) _telnetClients[i].stop(); + _telnetClients[i] = _telnetserver->available(); + break; + } + } + if (i >= MAX_TLNT_CLIENTS) { + //no free/disconnected spot so reject + _telnetserver->available().stop(); + } + } +} + +size_t Telnet_Server::write(const uint8_t *buffer, size_t size){ + + if ( !_setupdone || _telnetserver == NULL) { + log_i("[TELNET out blocked]"); + return 0; + } + clearClients(); + log_i("[TELNET out]"); + //push UART data to all connected telnet clients + for(uint8_t i = 0; i < MAX_TLNT_CLIENTS; i++){ + if (_telnetClients[i] && _telnetClients[i].connected()){ + log_i("[TELNET out connected]"); + _telnetClients[i].write(buffer, size); + wifi_config.wait(0); + } + } +} + +void Telnet_Server::handle(){ + //check if can read + if ( !_setupdone || _telnetserver == NULL) { + return; + } + clearClients(); + //check clients for data + uint8_t c; + for(uint8_t i = 0; i < MAX_TLNT_CLIENTS; i++){ + if (_telnetClients[i] && _telnetClients[i].connected()){ + if(_telnetClients[i].available()){ + //get data from the telnet client and push it to grbl + while(_telnetClients[i].available() && (available() < TELNETRXBUFFERSIZE)) { + wifi_config.wait(0); + c = _telnetClients[i].read(); + if ((char)c != '\r')push(c); + if ((char)c == '\n')return; + } + } + } + else { + if (_telnetClients[i]) { + _telnetClients[i].stop(); + } + } + wifi_config.wait(0); + } +} + +int Telnet_Server::peek(void){ + if (_RXbufferSize > 0)return _RXbuffer[_RXbufferpos]; + else return -1; +} + +int Telnet_Server::available(){ + return _RXbufferSize; +} + +bool Telnet_Server::push (uint8_t data){ + log_i("[TELNET]push %c",data); + if ((1 + _RXbufferSize) <= TELNETRXBUFFERSIZE){ + int current = _RXbufferpos + _RXbufferSize; + if (current > TELNETRXBUFFERSIZE) current = current - TELNETRXBUFFERSIZE; + if (current > (TELNETRXBUFFERSIZE-1)) current = 0; + _RXbuffer[current] = data; + _RXbufferSize++; + log_i("[TELNET]buffer size %d",_RXbufferSize); + return true; + } + return false; +} + +bool Telnet_Server::push (const char * data){ + int data_size = strlen(data); + if ((data_size + _RXbufferSize) <= TELNETRXBUFFERSIZE){ + int current = _RXbufferpos + _RXbufferSize; + if (current > TELNETRXBUFFERSIZE) current = current - TELNETRXBUFFERSIZE; + for (int i = 0; i < data_size; i++){ + if (current > (TELNETRXBUFFERSIZE-1)) current = 0; + _RXbuffer[current] = data[i]; + current ++; + wifi_config.wait(0); + } + _RXbufferSize+=strlen(data); + return true; + } + return false; +} + +int Telnet_Server::read(void){ + + if (_RXbufferSize > 0) { + int v = _RXbuffer[_RXbufferpos]; + log_i("[TELNET]read %c",v); + _RXbufferpos++; + if (_RXbufferpos > (TELNETRXBUFFERSIZE-1))_RXbufferpos = 0; + _RXbufferSize--; + return v; + } else return -1; +} + +#endif // Enable TELNET && ENABLE_WIFI + +#endif // ARDUINO_ARCH_ESP32 diff --git a/Grbl_Esp32/telnet_server.h b/Grbl_Esp32/telnet_server.h new file mode 100644 index 00000000..0a17dcdf --- /dev/null +++ b/Grbl_Esp32/telnet_server.h @@ -0,0 +1,63 @@ +/* + telnet_server.h - telnet service functions class + + Copyright (c) 2014 Luc Lebosse. All rights reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +//how many clients should be able to telnet to this ESP32 +#define MAX_TLNT_CLIENTS 1 + +#ifndef _TELNET_SERVER_H +#define _TELNET_SERVER_H + + +#include "config.h" +class WiFiServer; +class WiFiClient; + +#define TELNETRXBUFFERSIZE 1200 +#define FLUSHTIMEOUT 500 + +class Telnet_Server { + public: + Telnet_Server(); + ~Telnet_Server(); + bool begin(); + void end(); + void handle(); + size_t write(const uint8_t *buffer, size_t size); + int read(void); + int peek(void); + int available(); + bool push (uint8_t data); + bool push (const char * data); + private: + static bool _setupdone; + static WiFiServer * _telnetserver; + static WiFiClient _telnetClients[MAX_TLNT_CLIENTS]; + static uint16_t _port; + void clearClients(); + uint32_t _lastflush; + uint8_t _RXbuffer[TELNETRXBUFFERSIZE]; + uint16_t _RXbufferSize; + uint16_t _RXbufferpos; +}; + +extern Telnet_Server telnet_server; + +#endif + diff --git a/Grbl_Esp32/web_server.cpp b/Grbl_Esp32/web_server.cpp new file mode 100644 index 00000000..16baa0e5 --- /dev/null +++ b/Grbl_Esp32/web_server.cpp @@ -0,0 +1,2643 @@ +/* + web_server.cpp - web server functions class + + Copyright (c) 2014 Luc Lebosse. All rights reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifdef ARDUINO_ARCH_ESP32 + +#include "config.h" + +#if defined (ENABLE_WIFI) && defined (ENABLE_HTTP) + +#include "wifiservices.h" + +#include "grbl.h" + +#include "serial2socket.h" +#include "web_server.h" +#include +#include "wificonfig.h" +#include +#include +#include +#ifdef ENABLE_SD_CARD +#include +#include "grbl_sd.h" +#endif +#include +#include "report.h" +#include +#include +#include +#include +#include +#include +#ifdef ENABLE_MDNS +#include +#endif +#ifdef ENABLE_SSDP +#include +#endif +#ifdef ENABLE_CAPTIVE_PORTAL +#include +const byte DNS_PORT = 53; +DNSServer dnsServer; +#endif + +//embedded response file if no files on SPIFFS +#include "nofile.h" + +// Define line flags. Includes comment type tracking and line overflow detection. +#define LINE_FLAG_OVERFLOW bit(0) +#define LINE_FLAG_COMMENT_PARENTHESES bit(1) +#define LINE_FLAG_COMMENT_SEMICOLON bit(2) + +//Upload status +typedef enum { + UPLOAD_STATUS_NONE = 0, + UPLOAD_STATUS_FAILED = 1, + UPLOAD_STATUS_CANCELLED = 2, + UPLOAD_STATUS_SUCCESSFUL = 3, + UPLOAD_STATUS_ONGOING = 4 +} upload_status_type; + +#ifdef ENABLE_AUTHENTICATION +#define DEFAULT_ADMIN_PWD "admin" +#define DEFAULT_USER_PWD "user"; +#define DEFAULT_ADMIN_LOGIN "admin" +#define DEFAULT_USER_LOGIN "user" +#define ADMIN_PWD_ENTRY "ADMIN_PWD" +#define USER_PWD_ENTRY "USER_PWD" +#define AUTH_ENTRY_NB 20 +#define MAX_LOCAL_PASSWORD_LENGTH 16 +#define MIN_LOCAL_PASSWORD_LENGTH 1 +#endif + +//Default 404 +const char PAGE_404 [] = "\n\nRedirecting... \n\n\n\n\n\n\n"; +const char PAGE_CAPTIVE [] = "\n\nCaptive Portal \n\n\n
Captive Portal page : $QUERY$- you will be redirected...\n

\nif not redirected, click here\n

\n\n\n\n
\n\n\n\n"; + + +Web_Server web_server; +bool Web_Server::_setupdone = false; +uint16_t Web_Server::_port = 0; +String Web_Server::_hostname = ""; +uint16_t Web_Server::_data_port = 0; +long Web_Server::_id_connection = 0; +uint8_t Web_Server::_upload_status = UPLOAD_STATUS_NONE; +WebServer * Web_Server::_webserver = NULL; +WebSocketsServer * Web_Server::_socket_server = NULL; +#ifdef ENABLE_AUTHENTICATION +auth_ip * Web_Server::_head = NULL; +uint8_t Web_Server::_nb_ip = 0; +#define MAX_AUTH_IP 10 +#endif +Web_Server::Web_Server(){ + +} +Web_Server::~Web_Server(){ + end(); +} + +long Web_Server::get_client_ID() { + return _id_connection; +} + +bool Web_Server::begin(){ + + bool no_error = true; + _setupdone = false; + Preferences prefs; + prefs.begin(NAMESPACE, true); + int8_t penabled = prefs.getChar(HTTP_ENABLE_ENTRY, DEFAULT_HTTP_STATE); + //Get http port + _port = prefs.getUShort(HTTP_PORT_ENTRY, DEFAULT_WEBSERVER_PORT); + //Get telnet port + _data_port = prefs.getUShort(TELNET_PORT_ENTRY, DEFAULT_TELNETSERVER_PORT); + //Get hostname + String defV = DEFAULT_HOSTNAME; + _hostname = prefs.getString(HOSTNAME_ENTRY, defV); + prefs.end(); + if (penabled == 0) return false; + //create instance + _webserver= new WebServer(_port); +#ifdef ENABLE_AUTHENTICATION + //here the list of headers to be recorded + const char * headerkeys[] = {"Cookie"} ; + size_t headerkeyssize = sizeof (headerkeys) / sizeof (char*); + //ask server to track these headers + _webserver->collectHeaders (headerkeys, headerkeyssize ); +#endif + _socket_server = new WebSocketsServer(_port + 1); + _socket_server->begin(); + _socket_server->onEvent(handle_Websocket_Event); + + + //Websocket output + Serial2Socket.attachWS(_socket_server); + + //events functions + //_web_events->onConnect(handle_onevent_connect); + //events management + // _webserver->addHandler(_web_events); + + //Websocket function + //_web_socket->onEvent(handle_Websocket_Event); + //Websocket management + //_webserver->addHandler(_web_socket); + + //Web server handlers + //trick to catch command line on "/" before file being processed + _webserver->on("/",HTTP_ANY, handle_root); + + //Page not found handler + _webserver->onNotFound (handle_not_found); + + //need to be there even no authentication to say to UI no authentication + _webserver->on("/login", HTTP_ANY, handle_login); + + //web commands + _webserver->on ("/command", HTTP_ANY, handle_web_command); + _webserver->on ("/command_silent", HTTP_ANY, handle_web_command_silent); + + //SPIFFS + _webserver->on ("/files", HTTP_ANY, handleFileList, SPIFFSFileupload); + + //web update + _webserver->on ("/updatefw", HTTP_ANY, handleUpdate, WebUpdateUpload); + +#ifdef ENABLE_SD_CARD + //Direct SD management + _webserver->on("/upload", HTTP_ANY, handle_direct_SDFileList,SDFile_direct_upload); + //_webserver->on("/SD", HTTP_ANY, handle_SDCARD); +#endif + +#ifdef ENABLE_CAPTIVE_PORTAL + if(WiFi.getMode() != WIFI_STA){ + // if DNSServer is started with "*" for domain name, it will reply with + // provided IP to all DNS request + dnsServer.start(DNS_PORT, "*", WiFi.softAPIP()); + grbl_send(CLIENT_ALL,"[MSG:Captive Portal Started]\r\n"); + _webserver->on ("/generate_204", HTTP_ANY, handle_root); + _webserver->on ("/gconnectivitycheck.gstatic.com", HTTP_ANY, handle_root); + //do not forget the / at the end + _webserver->on ("/fwlink/", HTTP_ANY, handle_root); + } +#endif + +#ifdef ENABLE_SSDP + //SSDP service presentation + if(WiFi.getMode() == WIFI_STA){ + _webserver->on ("/description.xml", HTTP_GET, handle_SSDP); + //Add specific for SSDP + SSDP.setSchemaURL ("description.xml"); + SSDP.setHTTPPort (_port); + SSDP.setName (_hostname); + SSDP.setURL ("/"); + SSDP.setDeviceType ("upnp:rootdevice"); + /*Any customization could be here + SSDP.setModelName (ESP32_MODEL_NAME); + SSDP.setModelURL (ESP32_MODEL_URL); + SSDP.setModelNumber (ESP_MODEL_NUMBER); + SSDP.setManufacturer (ESP_MANUFACTURER_NAME); + SSDP.setManufacturerURL (ESP_MANUFACTURER_URL); + */ + + //Start SSDP + grbl_send(CLIENT_ALL,"[MSG:SSDP Started]\r\n"); + SSDP.begin(); + } +#endif + grbl_send(CLIENT_ALL,"[MSG:HTTP Started]\r\n"); + //start webserver + _webserver->begin(); +#ifdef ENABLE_MDNS + //add mDNS + if(WiFi.getMode() == WIFI_STA){ + MDNS.addService("http","tcp",_port); + } +#endif + _setupdone = true; + return no_error; +} + +void Web_Server::end(){ + _setupdone = false; +#ifdef ENABLE_MDNS + //remove mDNS + mdns_service_remove("_http", "_tcp"); +#endif + if (_socket_server) { + delete _socket_server; + _socket_server = NULL; + } + if (_webserver) { + delete _webserver; + _webserver = NULL; + } +#ifdef ENABLE_AUTHENTICATION + while (_head) { + auth_ip * current = _head; + _head = _head->_next; + delete current; + } + _nb_ip = 0; +#endif +} + +//Root of Webserver///////////////////////////////////////////////////// + +void Web_Server::handle_root() +{ + String path = "/index.html"; + String contentType = getContentType(path); + String pathWithGz = path + ".gz"; + //if have a index.html or gzip version this is default root page + if((SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)) && !_webserver->hasArg("forcefallback") && _webserver->arg("forcefallback")!="yes") { + if(SPIFFS.exists(pathWithGz)) { + path = pathWithGz; + } + File file = SPIFFS.open(path, FILE_READ); + _webserver->streamFile(file, contentType); + file.close(); + return; + } + //if no lets launch the default content + _webserver->sendHeader("Content-Encoding", "gzip"); + _webserver->send_P(200,"text/html",PAGE_NOFILES,PAGE_NOFILES_SIZE); +} + +//Handle not registred path on SPIFFS neither SD /////////////////////// +void Web_Server:: handle_not_found() +{ + if (is_authenticated() == LEVEL_GUEST) { + _webserver->sendContent_P("HTTP/1.1 301 OK\r\nLocation: /\r\nCache-Control: no-cache\r\n\r\n"); + //_webserver->client().stop(); + return; + } + bool page_not_found = false; + String path = _webserver->urlDecode(_webserver->uri()); + String contentType = getContentType(path); + String pathWithGz = path + ".gz"; + +#ifdef ENABLE_SD_CARD + if ((path.substring(0,4) == "/SD/")) { + //remove /SD + path = path.substring(3); + if(SD.exists((char *)pathWithGz.c_str()) || SD.exists((char *)path.c_str())) { + if(SD.exists((char *)pathWithGz.c_str())) { + path = pathWithGz; + } + File datafile = SD.open((char *)path.c_str()); + if (datafile) { + if( _webserver->streamFile(datafile, contentType) == datafile.size()) { + datafile.close(); + wifi_config.wait(0); + return; + } else{ + datafile.close(); + } + } + } + String content = "cannot find "; + content+=path; + _webserver->send(404,"text/plain",content.c_str()); + return; + } else +#endif + if(SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)) { + if(SPIFFS.exists(pathWithGz)) { + path = pathWithGz; + } + File file = SPIFFS.open(path, FILE_READ); + _webserver->streamFile(file, contentType); + file.close(); + return; + } else { + page_not_found = true; + } + + if (page_not_found ) { +#ifdef ENABLE_CAPTIVE_PORTAL + if (WiFi.getMode()!=WIFI_STA ) { + String contentType= PAGE_CAPTIVE; + String stmp = WiFi.softAPIP().toString(); + //Web address = ip + port + String KEY_IP = "$WEB_ADDRESS$"; + String KEY_QUERY = "$QUERY$"; + if (_port != 80) { + stmp+=":"; + stmp+=String(_port); + } + contentType.replace(KEY_IP,stmp); + contentType.replace(KEY_IP,stmp); + contentType.replace(KEY_QUERY,_webserver->uri()); + _webserver->send(200,"text/html",contentType); + //_webserver->sendContent_P(NOT_AUTH_NF); + //_webserver->client().stop(); + return; + } +#endif + path = "/404.htm"; + contentType = getContentType(path); + pathWithGz = path + ".gz"; + if(SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)) { + if(SPIFFS.exists(pathWithGz)) { + path = pathWithGz; + } + File file = SPIFFS.open(path, FILE_READ); + _webserver->streamFile(file, contentType); + file.close(); + + } else { + //if not template use default page + contentType = PAGE_404; + String stmp; + if (WiFi.getMode()==WIFI_STA ) { + stmp=WiFi.localIP().toString(); + } else { + stmp=WiFi.softAPIP().toString(); + } + //Web address = ip + port + String KEY_IP = "$WEB_ADDRESS$"; + String KEY_QUERY = "$QUERY$"; + if ( _port != 80) { + stmp+=":"; + stmp+=String(_port); + } + contentType.replace(KEY_IP,stmp); + contentType.replace(KEY_QUERY,_webserver->uri()); + _webserver->send(200,"text/html",contentType); + } + } +} + +//http SSDP xml presentation +void Web_Server::handle_SSDP () +{ + StreamString sschema ; + if (sschema.reserve (1024) ) { + String templ = "" + "" + "" + "1" + "0" + "" + "http://%s:%u/" + "" + "upnp:rootdevice" + "%s" + "/" + "%s" + "ESP32" + "Marlin" + "http://espressif.com/en/products/hardware/esp-wroom-32/overview" + "Espressif Systems" + "http://espressif.com" + "uuid:%s" + "" + "\r\n" + "\r\n"; + char uuid[37]; + String sip = WiFi.localIP().toString(); + uint32_t chipId = (uint16_t) (ESP.getEfuseMac() >> 32); + sprintf (uuid, "38323636-4558-4dda-9188-cda0e6%02x%02x%02x", + (uint16_t) ( (chipId >> 16) & 0xff), + (uint16_t) ( (chipId >> 8) & 0xff), + (uint16_t) chipId & 0xff ); + String serialNumber = String (chipId); + sschema.printf (templ.c_str(), + sip.c_str(), + _port, + _hostname.c_str(), + serialNumber.c_str(), + uuid); + _webserver->send (200, "text/xml", (String) sschema); + } else { + _webserver->send (500); + } +} + +bool Web_Server::is_realtime_cmd(char c){ + if (c == CMD_STATUS_REPORT) return true; + if (c == CMD_CYCLE_START) return true; + if (c == CMD_RESET) return true; + if (c == CMD_FEED_HOLD) return true; + if (c == CMD_SAFETY_DOOR) return true; + if (c == CMD_JOG_CANCEL) return true; + if (c == CMD_DEBUG_REPORT) return true; + if (c == CMD_FEED_OVR_RESET) return true; + if (c == CMD_FEED_OVR_COARSE_PLUS) return true; + if (c == CMD_FEED_OVR_COARSE_MINUS) return true; + if (c == CMD_FEED_OVR_FINE_PLUS) return true; + if (c == CMD_FEED_OVR_FINE_MINUS) return true; + if (c == CMD_RAPID_OVR_RESET) return true; + if (c == CMD_RAPID_OVR_MEDIUM) return true; + if (c == CMD_RAPID_OVR_LOW) return true; + if (c == CMD_SPINDLE_OVR_COARSE_PLUS) return true; + if (c == CMD_SPINDLE_OVR_COARSE_MINUS) return true; + if (c == CMD_SPINDLE_OVR_FINE_PLUS) return true; + if (c == CMD_SPINDLE_OVR_FINE_MINUS) return true; + if (c == CMD_SPINDLE_OVR_STOP) return true; + if (c == CMD_COOLANT_FLOOD_OVR_TOGGLE) return true; + if (c == CMD_COOLANT_MIST_OVR_TOGGLE) return true; + return false; +} + +//Handle web command query and send answer////////////////////////////// +void Web_Server::handle_web_command () +{ + //to save time if already disconnected + //if (_webserver->hasArg ("PAGEID") ) { + // if (_webserver->arg ("PAGEID").length() > 0 ) { + // if (_webserver->arg ("PAGEID").toInt() != _id_connection) { + // _webserver->send (200, "text/plain", "Invalid command"); + // return; + // } + // } + //} + level_authenticate_type auth_level = is_authenticated(); + String cmd = ""; + if (_webserver->hasArg ("plain") || _webserver->hasArg ("commandText") ) { + if (_webserver->hasArg ("plain") ) { + cmd = _webserver->arg ("plain"); + } else { + cmd = _webserver->arg ("commandText"); + } + } else { + _webserver->send (200, "text/plain", "Invalid command"); + return; + } + //if it is internal command [ESPXXX] + cmd.trim(); + int ESPpos = cmd.indexOf ("[ESP"); + if (ESPpos > -1) { + //is there the second part? + int ESPpos2 = cmd.indexOf ("]", ESPpos); + if (ESPpos2 > -1) { + //Split in command and parameters + String cmd_part1 = cmd.substring (ESPpos + 4, ESPpos2); + String cmd_part2 = ""; + //only [ESP800] is allowed login free if authentication is enabled + if ( (auth_level == LEVEL_GUEST) && (cmd_part1.toInt() != 800) ) { + _webserver->send (401, "text/plain", "Authentication failed!\n"); + return; + } + //is there space for parameters? + if (ESPpos2 < cmd.length() ) { + cmd_part2 = cmd.substring (ESPpos2 + 1); + } + //if command is a valid number then execute command + if (cmd_part1.toInt() != 0) { + ESPResponseStream espresponse(_webserver); + //commmand is web only + execute_internal_command (cmd_part1.toInt(), cmd_part2, auth_level, &espresponse); + //flush + espresponse.flush(); + } + //if not is not a valid [ESPXXX] command + } + } else { //execute GCODE + if (auth_level == LEVEL_GUEST) { + _webserver->send (401, "text/plain", "Authentication failed!\n"); + return; + } + //Instead of send several commands one by one by web / send full set and split here + String scmd; + String res = "Ok"; + uint8_t sindex = 0; + scmd = get_Splited_Value(cmd,'\n', sindex); + while ( scmd != "" ){ + if (scmd.length() > 1)scmd += "\n"; + else if (!is_realtime_cmd(scmd[0]) )scmd += "\n"; + if (!Serial2Socket.push(scmd.c_str()))res = "Error"; + sindex++; + scmd = get_Splited_Value(cmd,'\n', sindex); + } + _webserver->send (200, "text/plain", res.c_str()); + } +} +//Handle web command query and send answer////////////////////////////// +void Web_Server::handle_web_command_silent () +{ + //to save time if already disconnected + //if (_webserver->hasArg ("PAGEID") ) { + // if (_webserver->arg ("PAGEID").length() > 0 ) { + // if (_webserver->arg ("PAGEID").toInt() != _id_connection) { + // _webserver->send (200, "text/plain", "Invalid command"); + // return; + // } + // } + //} + level_authenticate_type auth_level = is_authenticated(); + String cmd = ""; + if (_webserver->hasArg ("plain") || _webserver->hasArg ("commandText") ) { + if (_webserver->hasArg ("plain") ) { + cmd = _webserver->arg ("plain"); + } else { + cmd = _webserver->arg ("commandText"); + } + } else { + _webserver->send (200, "text/plain", "Invalid command"); + return; + } + //if it is internal command [ESPXXX] + cmd.trim(); + int ESPpos = cmd.indexOf ("[ESP"); + if (ESPpos > -1) { + //is there the second part? + int ESPpos2 = cmd.indexOf ("]", ESPpos); + if (ESPpos2 > -1) { + //Split in command and parameters + String cmd_part1 = cmd.substring (ESPpos + 4, ESPpos2); + String cmd_part2 = ""; + //only [ESP800] is allowed login free if authentication is enabled + if ( (auth_level == LEVEL_GUEST) && (cmd_part1.toInt() != 800) ) { + _webserver->send (401, "text/plain", "Authentication failed!\n"); + return; + } + //is there space for parameters? + if (ESPpos2 < cmd.length() ) { + cmd_part2 = cmd.substring (ESPpos2 + 1); + } + //if command is a valid number then execute command + if (cmd_part1.toInt() != 0) { + //commmand is web only + if(execute_internal_command (cmd_part1.toInt(), cmd_part2, auth_level, NULL)) _webserver->send (200, "text/plain", "ok"); + else _webserver->send (200, "text/plain", "error"); + } + //if not is not a valid [ESPXXX] command + } + } else { //execute GCODE + if (auth_level == LEVEL_GUEST) { + _webserver->send (401, "text/plain", "Authentication failed!\n"); + return; + } + //Instead of send several commands one by one by web / send full set and split here + String scmd; + uint8_t sindex = 0; + scmd = get_Splited_Value(cmd,'\n', sindex); + String res = "Ok"; + while ( scmd != "" ){ + if (scmd.length() > 1)scmd+="\n"; + else if (!is_realtime_cmd(scmd[0]) )scmd+="\n"; + if (!Serial2Socket.push(scmd.c_str()))res = "Error"; + sindex++; + scmd = get_Splited_Value(cmd,'\n', sindex); + } + _webserver->send (200, "text/plain", res.c_str()); + } +} + + +bool Web_Server::execute_internal_command (int cmd, String cmd_params, level_authenticate_type auth_level, ESPResponseStream *espresponse) +{ + bool response = true; + level_authenticate_type auth_type = auth_level; + + //manage parameters + String parameter; + switch (cmd) { + //Get SD Card Status + //[ESP200] + case 200: + { + if (!espresponse) return false; + String resp = "No SD card"; +#ifdef ENABLE_SD_CARD + + int8_t state = get_sd_state(true); + if (state == SDCARD_IDLE)resp="SD card detected"; + else if (state == SDCARD_NOT_PRESENT)resp="No SD card"; + else resp="Busy"; +#endif + espresponse->println (resp.c_str()); + } + break; + //Get full ESP32 wifi settings content + //[ESP400] + case 400: + { + String v; + String defV; + Preferences prefs; + if (!espresponse) return false; +#ifdef ENABLE_AUTHENTICATION + if (auth_type == LEVEL_GUEST) return false; +#endif + int8_t vi; + espresponse->print("{\"EEPROM\":["); + prefs.begin(NAMESPACE, true); + //1 - Hostname + espresponse->print ("{\"F\":\"network\",\"P\":\""); + espresponse->print (HOSTNAME_ENTRY); + espresponse->print ("\",\"T\":\"S\",\"V\":\""); + espresponse->print (_hostname.c_str()); + espresponse->print ("\",\"H\":\"Hostname\" ,\"S\":\""); + espresponse->print (String(MAX_HOSTNAME_LENGTH).c_str()); + espresponse->print ("\", \"M\":\""); + espresponse->print (String(MIN_HOSTNAME_LENGTH).c_str()); + espresponse->print ("\"}"); + espresponse->print (","); + + //2 - http protocol mode + espresponse->print ("{\"F\":\"network\",\"P\":\""); + espresponse->print (HTTP_ENABLE_ENTRY); + espresponse->print ("\",\"T\":\"B\",\"V\":\""); + vi = prefs.getChar(HTTP_ENABLE_ENTRY, 1); + espresponse->print (String(vi).c_str()); + espresponse->print ("\",\"H\":\"HTTP protocol\",\"O\":[{\"Enabled\":\"1\"},{\"Disabled\":\"0\"}]}"); + espresponse->print (","); + + //3 - http port + espresponse->print ("{\"F\":\"network\",\"P\":\""); + espresponse->print (HTTP_PORT_ENTRY); + espresponse->print ("\",\"T\":\"I\",\"V\":\""); + espresponse->print (String(_port).c_str()); + espresponse->print ("\",\"H\":\"HTTP Port\",\"S\":\""); + espresponse->print (String(MAX_HTTP_PORT).c_str()); + espresponse->print ("\",\"M\":\""); + espresponse->print (String(MIN_HTTP_PORT).c_str()); + espresponse->print ("\"}"); + espresponse->print (","); + +#ifdef ENABLE_TELNET + //4 - telnet protocol mode + espresponse->print ("{\"F\":\"network\",\"P\":\""); + espresponse->print (TELNET_ENABLE_ENTRY); + espresponse->print ("\",\"T\":\"B\",\"V\":\""); + vi = prefs.getChar(TELNET_ENABLE_ENTRY, 0); + espresponse->print (String(vi).c_str()); + espresponse->print ("\",\"H\":\"Telnet protocol\",\"O\":[{\"Enabled\":\"1\"},{\"Disabled\":\"0\"}]}"); + espresponse->print (","); + + //5 - telnet Port + espresponse->print ("{\"F\":\"network\",\"P\":\""); + espresponse->print (TELNET_PORT_ENTRY); + espresponse->print ("\",\"T\":\"I\",\"V\":\""); + espresponse->print (String(_data_port).c_str()); + espresponse->print ("\",\"H\":\"Telnet Port\",\"S\":\""); + espresponse->print (String(MAX_TELNET_PORT).c_str()); + espresponse->print ("\",\"M\":\""); + espresponse->print (String(MIN_TELNET_PORT).c_str()); + espresponse->print ("\"}"); + espresponse->print (","); +#endif + //6 - wifi mode + espresponse->print ("{\"F\":\"network\",\"P\":\""); + espresponse->print (ESP_WIFI_MODE); + espresponse->print ("\",\"T\":\"B\",\"V\":\""); + vi = prefs.getChar(ESP_WIFI_MODE, ESP_WIFI_OFF); + espresponse->print (String(vi).c_str()); + espresponse->print ("\",\"H\":\"Wifi mode\",\"O\":[{\"STA\":\"1\"},{\"AP\":\"2\"},{\"None\":\"0\"}]}"); + espresponse->print (","); + + //7 - STA SSID + espresponse->print ("{\"F\":\"network\",\"P\":\""); + espresponse->print (STA_SSID_ENTRY); + espresponse->print ("\",\"T\":\"S\",\"V\":\""); + defV = DEFAULT_STA_SSID; + espresponse->print (prefs.getString(STA_SSID_ENTRY, defV).c_str()); + espresponse->print ("\",\"S\":\""); + espresponse->print (String(MAX_SSID_LENGTH).c_str()); + espresponse->print ("\",\"H\":\"Station SSID\",\"M\":\""); + espresponse->print (String(MIN_SSID_LENGTH).c_str()); + espresponse->print ("\"}"); + espresponse->print (","); + + //8 - STA password + espresponse->print ("{\"F\":\"network\",\"P\":\""); + espresponse->print (STA_PWD_ENTRY); + espresponse->print ("\",\"T\":\"S\",\"V\":\""); + espresponse->print (HIDDEN_PASSWORD); + espresponse->print ("\",\"S\":\""); + espresponse->print (String(MAX_PASSWORD_LENGTH).c_str()); + espresponse->print ("\",\"H\":\"Station Password\",\"M\":\""); + espresponse->print (String(MIN_PASSWORD_LENGTH).c_str()); + espresponse->print ("\"}"); + espresponse->print (","); + + // 9 - STA IP mode + espresponse->print ("{\"F\":\"network\",\"P\":\""); + espresponse->print (STA_IP_MODE_ENTRY); + espresponse->print ("\",\"T\":\"B\",\"V\":\""); + espresponse->print (String(prefs.getChar(STA_IP_MODE_ENTRY, DHCP_MODE)).c_str()); + espresponse->print ("\",\"H\":\"Station IP Mode\",\"O\":[{\"DHCP\":\"0\"},{\"Static\":\"1\"}]}"); + espresponse->print (","); + + //10-STA static IP + espresponse->print ("{\"F\":\"network\",\"P\":\""); + espresponse->print (STA_IP_ENTRY); + espresponse->print ("\",\"T\":\"A\",\"V\":\""); + espresponse->print (wifi_config.IP_string_from_int(prefs.getInt(STA_IP_ENTRY, 0)).c_str()); + espresponse->print ("\",\"H\":\"Station Static IP\"}"); + espresponse->print (","); + + //11-STA static Gateway + espresponse->print ("{\"F\":\"network\",\"P\":\""); + espresponse->print (STA_GW_ENTRY); + espresponse->print ("\",\"T\":\"A\",\"V\":\""); + espresponse->print (wifi_config.IP_string_from_int(prefs.getInt(STA_GW_ENTRY, 0)).c_str()); + espresponse->print ("\",\"H\":\"Station Static Gateway\"}"); + espresponse->print (","); + + //12-STA static Mask + espresponse->print ("{\"F\":\"network\",\"P\":\""); + espresponse->print (STA_MK_ENTRY); + espresponse->print ("\",\"T\":\"A\",\"V\":\""); + espresponse->print (wifi_config.IP_string_from_int(prefs.getInt(STA_MK_ENTRY, 0)).c_str()); + espresponse->print ("\",\"H\":\"Station Static Mask\"}"); + espresponse->print (","); + + //13 - AP SSID + espresponse->print ("{\"F\":\"network\",\"P\":\""); + espresponse->print (AP_SSID_ENTRY); + espresponse->print ("\",\"T\":\"S\",\"V\":\""); + defV = DEFAULT_AP_SSID; + espresponse->print (prefs.getString(AP_SSID_ENTRY, defV).c_str()); + espresponse->print ("\",\"S\":\""); + espresponse->print (String(MAX_SSID_LENGTH).c_str()); + espresponse->print ("\",\"H\":\"AP SSID\",\"M\":\""); + espresponse->print (String(MIN_SSID_LENGTH).c_str()); + espresponse->print ("\"}"); + espresponse->print (","); + + //14 - AP password + espresponse->print ("{\"F\":\"network\",\"P\":\""); + espresponse->print (AP_PWD_ENTRY); + espresponse->print ("\",\"T\":\"S\",\"V\":\""); + espresponse->print (HIDDEN_PASSWORD); + espresponse->print ("\",\"S\":\""); + espresponse->print (String(MAX_PASSWORD_LENGTH).c_str()); + espresponse->print ("\",\"H\":\"AP Password\",\"M\":\""); + espresponse->print (String(MIN_PASSWORD_LENGTH).c_str()); + espresponse->print ("\"}"); + espresponse->print (","); + + //15 - AP static IP + espresponse->print ("{\"F\":\"network\",\"P\":\""); + espresponse->print (AP_IP_ENTRY); + espresponse->print ("\",\"T\":\"A\",\"V\":\""); + defV = DEFAULT_AP_IP; + espresponse->print (wifi_config.IP_string_from_int(prefs.getInt(AP_IP_ENTRY, wifi_config.IP_int_from_string(defV))).c_str()); + espresponse->print ("\",\"H\":\"AP Static IP\"}"); + espresponse->print (","); + + //16 - AP Channel + espresponse->print ("{\"F\":\"network\",\"P\":\""); + espresponse->print (AP_CHANNEL_ENTRY); + espresponse->print ("\",\"T\":\"B\",\"V\":\""); + espresponse->print (String(prefs.getChar(AP_CHANNEL_ENTRY, DEFAULT_AP_CHANNEL)).c_str()); + espresponse->print ("\",\"H\":\"AP Channel\",\"O\":["); + for (int i = MIN_CHANNEL; i <= MAX_CHANNEL ; i++) { + espresponse->print ("{\""); + espresponse->print (String(i).c_str()); + espresponse->print ("\":\""); + espresponse->print (String(i).c_str()); + espresponse->print ("\"}"); + if (i < MAX_CHANNEL) { + espresponse->print (","); + } + } + espresponse->print ("]}"); + + espresponse->print ("]}"); + prefs.end(); + } + break; + //Set EEPROM setting + //[ESP401]P= T= V= pwd= + case 401: + { +#ifdef ENABLE_AUTHENTICATION + if (auth_type != LEVEL_ADMIN) return false; +#endif + //check validity of parameters + String spos = get_param (cmd_params, "P=", false); + String styp = get_param (cmd_params, "T=", false); + String sval = get_param (cmd_params, "V=", true); + spos.trim(); + sval.trim(); + if (spos.length() == 0) { + response = false; + } + if (! (styp == "B" || styp == "S" || styp == "A" || styp == "I" || styp == "F") ) { + response = false; + } + if (sval.length() == 0) { + response = false; + } + + if (response) { + Preferences prefs; + prefs.begin(NAMESPACE, false); + //Byte value + if ((styp == "B") || (styp == "F")){ + int8_t bbuf = sval.toInt(); + if (prefs.putChar(spos.c_str(), bbuf) ==0 ) { + response = false; + } else { + //dynamique refresh is better than restart the board + if (spos == ESP_WIFI_MODE){ + //TODO + } + if (spos == AP_CHANNEL_ENTRY) { + //TODO + } + if (spos == HTTP_ENABLE_ENTRY) { + //TODO + } + if (spos == TELNET_ENABLE_ENTRY) { + //TODO + } + } + } + //Integer value + if (styp == "I") { + int16_t ibuf = sval.toInt(); + if (prefs.putUShort(spos.c_str(), ibuf) == 0) { + response = false; + } else { + if (spos == HTTP_PORT_ENTRY){ + //TODO + } + if (spos == TELNET_PORT_ENTRY){ + //TODO + //Serial.println(ibuf); + } + } + + } + //String value + if (styp == "S") { + if (prefs.putString(spos.c_str(), sval) == 0) { + response = false; + } else { + if (spos == HOSTNAME_ENTRY){ + //TODO + } + if (spos == STA_SSID_ENTRY){ + //TODO + } + if (spos == STA_PWD_ENTRY){ + //TODO + } + if (spos == AP_SSID_ENTRY){ + //TODO + } + if (spos == AP_PWD_ENTRY){ + //TODO + } + } + + } + //IP address + if (styp == "A") { + if (prefs.putInt(spos.c_str(), wifi_config.IP_int_from_string(sval)) == 0) { + response = false; + } else { + if (spos == STA_IP_ENTRY){ + //TODO + } + if (spos == STA_GW_ENTRY){ + //TODO + } + if (spos == STA_MK_ENTRY){ + //TODO + } + if (spos == AP_IP_ENTRY){ + //TODO + } + } + } + prefs.end(); + } + if (!response) { + if (espresponse) espresponse->println ("Error: Incorrect Command"); + } else { + if (espresponse) espresponse->println ("ok"); + } + + } + break; + //Get available AP list (limited to 30) + //output is JSON + //[ESP410] + case 410: { + if (!espresponse)return false; +#ifdef ENABLE_AUTHENTICATION + if (auth_type == LEVEL_GUEST) return false; +#endif + espresponse->print("{\"AP_LIST\":["); + int n = WiFi.scanComplete(); + if (n == -2) { + WiFi.scanNetworks (true); + } else if (n) { + for (int i = 0; i < n; ++i) { + if (i > 0) { + espresponse->print (","); + } + espresponse->print ("{\"SSID\":\""); + espresponse->print (WiFi.SSID (i).c_str()); + espresponse->print ("\",\"SIGNAL\":\""); + espresponse->print (String(wifi_config.getSignal (WiFi.RSSI (i) )).c_str()); + espresponse->print ("\",\"IS_PROTECTED\":\""); + + if (WiFi.encryptionType (i) == WIFI_AUTH_OPEN) { + espresponse->print ("0"); + } else { + espresponse->print ("1"); + } + espresponse->print ("\"}"); + } + } + WiFi.scanDelete(); + if (WiFi.scanComplete() == -2) { + WiFi.scanNetworks (true); + } + espresponse->print ("]}"); + } + break; + //Get ESP current status + case 420: + { +#ifdef ENABLE_AUTHENTICATION + if (auth_type == LEVEL_GUEST) return false; +#endif + if (!espresponse)return false; + espresponse->print ("Chip ID: "); + espresponse->print (String ( (uint16_t) (ESP.getEfuseMac() >> 32) ).c_str()); + espresponse->print ("\n"); + espresponse->print ("CPU Frequency: "); + espresponse->print (String (ESP.getCpuFreqMHz() ).c_str()); + espresponse->print ("Mhz"); + espresponse->print ("\n"); + espresponse->print ("CPU Temperature: "); + espresponse->print (String (temperatureRead(), 1).c_str()); + espresponse->print ("°C"); + espresponse->print ("\n"); + espresponse->print ("Free memory: "); + espresponse->print (formatBytes (ESP.getFreeHeap()).c_str()); + espresponse->print ("\n"); + espresponse->print ("SDK: "); + espresponse->print (ESP.getSdkVersion()); + espresponse->print ("\n"); + espresponse->print ("Flash Size: "); + espresponse->print (formatBytes (ESP.getFlashChipSize()).c_str()); + espresponse->print ("\n"); + espresponse->print ("Available Size for update: "); + //Not OTA on 2Mb board per spec + if (ESP.getFlashChipSize() > 0x20000) { + espresponse->print (formatBytes (0x140000).c_str()); + } else { + espresponse->print (formatBytes (0x0).c_str()); + } + espresponse->print ("\n"); + espresponse->print ("Available Size for SPIFFS: "); + espresponse->print (formatBytes (SPIFFS.totalBytes()).c_str()); + espresponse->print ("\n"); + espresponse->print ("Baud rate: "); + long br = Serial.baudRate(); + //workaround for ESP32 + if (br == 115201) { + br = 115200; + } + if (br == 230423) { + br = 230400; + } + espresponse->print (String(br).c_str()); + espresponse->print ("\n"); + espresponse->print ("Sleep mode: "); + if (WiFi.getSleep())espresponse->print ("Modem"); + else espresponse->print ("None"); + espresponse->print ("\n"); + espresponse->print ("Web port: "); + espresponse->print (String(_port).c_str()); + espresponse->print ("\n"); + espresponse->print ("Data port: "); + if (_data_port!=0)espresponse->print (String(_data_port).c_str()); + else espresponse->print ("Disabled"); + espresponse->print ("\n"); + espresponse->print ("Hostname: "); + espresponse->print ( _hostname.c_str()); + espresponse->print ("\n"); + espresponse->print ("Active Mode: "); + if (WiFi.getMode() == WIFI_STA) { + espresponse->print ("STA ("); + espresponse->print ( WiFi.macAddress().c_str()); + espresponse->print (")"); + espresponse->print ("\n"); + espresponse->print ("Connected to: "); + if (WiFi.isConnected()){ //in theory no need but ... + espresponse->print (WiFi.SSID().c_str()); + espresponse->print ("\n"); + espresponse->print ("Signal: "); + espresponse->print ( String(wifi_config.getSignal (WiFi.RSSI())).c_str()); + espresponse->print ("%"); + espresponse->print ("\n"); + uint8_t PhyMode; + esp_wifi_get_protocol (ESP_IF_WIFI_STA, &PhyMode); + espresponse->print ("Phy Mode: "); + if (PhyMode == (WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N)) espresponse->print ("11n"); + else if (PhyMode == (WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G)) espresponse->print ("11g"); + else if (PhyMode == (WIFI_PROTOCOL_11B )) espresponse->print ("11b"); + else espresponse->print ("???"); + espresponse->print ("\n"); + espresponse->print ("Channel: "); + espresponse->print (String (WiFi.channel()).c_str()); + espresponse->print ("\n"); + espresponse->print ("IP Mode: "); + tcpip_adapter_dhcp_status_t dhcp_status; + tcpip_adapter_dhcpc_get_status (TCPIP_ADAPTER_IF_STA, &dhcp_status); + if (dhcp_status == TCPIP_ADAPTER_DHCP_STARTED)espresponse->print ("DHCP"); + else espresponse->print ("Static"); + espresponse->print ("\n"); + espresponse->print ("IP: "); + espresponse->print (WiFi.localIP().toString().c_str()); + espresponse->print ("\n"); + espresponse->print ("Gateway: "); + espresponse->print (WiFi.gatewayIP().toString().c_str()); + espresponse->print ("\n"); + espresponse->print ("Mask: "); + espresponse->print (WiFi.subnetMask().toString().c_str()); + espresponse->print ("\n"); + espresponse->print ("DNS: "); + espresponse->print (WiFi.dnsIP().toString().c_str()); + espresponse->print ("\n"); + } //this is web command so connection => no command + espresponse->print ("Disabled Mode: "); + espresponse->print ("AP ("); + espresponse->print (WiFi.softAPmacAddress().c_str()); + espresponse->print (")"); + espresponse->print ("\n"); + } else if (WiFi.getMode() == WIFI_AP) { + espresponse->print ("AP ("); + espresponse->print (WiFi.softAPmacAddress().c_str()); + espresponse->print (")"); + espresponse->print ("\n"); + wifi_config_t conf; + esp_wifi_get_config (ESP_IF_WIFI_AP, &conf); + espresponse->print ("SSID: "); + espresponse->print ((const char*) conf.ap.ssid); + espresponse->print ("\n"); + espresponse->print ("Visible: "); + espresponse->print ( (conf.ap.ssid_hidden == 0) ? "Yes" : "No"); + espresponse->print ("\n"); + espresponse->print ("Authentication: "); + if (conf.ap.authmode == WIFI_AUTH_OPEN) { + espresponse->print ("None"); + } else if (conf.ap.authmode == WIFI_AUTH_WEP) { + espresponse->print ("WEP"); + } else if (conf.ap.authmode == WIFI_AUTH_WPA_PSK) { + espresponse->print ("WPA"); + } else if (conf.ap.authmode == WIFI_AUTH_WPA2_PSK) { + espresponse->print ("WPA2"); + } else { + espresponse->print ("WPA/WPA2"); + } + espresponse->print ("\n"); + espresponse->print ("Max Connections: "); + espresponse->print (String(conf.ap.max_connection).c_str()); + espresponse->print ("\n"); + espresponse->print ("DHCP Server: "); + tcpip_adapter_dhcp_status_t dhcp_status; + tcpip_adapter_dhcps_get_status (TCPIP_ADAPTER_IF_AP, &dhcp_status); + if (dhcp_status == TCPIP_ADAPTER_DHCP_STARTED)espresponse->print ("Started"); + else espresponse->print ("Stopped"); + espresponse->print ("\n"); + espresponse->print ("IP: "); + espresponse->print (WiFi.softAPIP().toString().c_str()); + espresponse->print ("\n"); + tcpip_adapter_ip_info_t ip_AP; + tcpip_adapter_get_ip_info (TCPIP_ADAPTER_IF_AP, &ip_AP); + espresponse->print ("Gateway: "); + espresponse->print (IPAddress (ip_AP.gw.addr).toString().c_str()); + espresponse->print ("\n"); + espresponse->print ("Mask: "); + espresponse->print (IPAddress (ip_AP.netmask.addr).toString().c_str()); + espresponse->print ("\n"); + espresponse->print ("Connected clients: "); + wifi_sta_list_t station; + tcpip_adapter_sta_list_t tcpip_sta_list; + esp_wifi_ap_get_sta_list (&station); + tcpip_adapter_get_sta_list (&station, &tcpip_sta_list); + espresponse->print (String(station.num).c_str()); + espresponse->print ("\n"); + for (int i = 0; i < station.num; i++) { + espresponse->print (mac2str(tcpip_sta_list.sta[i].mac)); + espresponse->print (" "); + espresponse->print ( IPAddress (tcpip_sta_list.sta[i].ip.addr).toString().c_str()); + espresponse->print ("\n"); + } + espresponse->print ("Disabled Mode: "); + espresponse->print ("STA ("); + espresponse->print (WiFi.macAddress().c_str()); + espresponse->print (")"); + espresponse->print ("\n"); + } else if (WiFi.getMode() == WIFI_AP_STA) //we should not be in this state but just in case .... + { + espresponse->print ("Mixed"); + espresponse->print ("\n"); + espresponse->print ("STA ("); + espresponse->print (WiFi.macAddress().c_str()); + espresponse->print (")"); + espresponse->print ("\n"); + espresponse->print ("AP ("); + espresponse->print (WiFi.softAPmacAddress().c_str()); + espresponse->print (")"); + espresponse->print ("\n"); + + } else { //we should not be there if no wifi .... + espresponse->print ("Wifi Off"); + espresponse->print ("\n"); + } + //TODO to complete + espresponse->print ("FW version: "); + espresponse->print (GRBL_VERSION); + espresponse->print (" ("); + espresponse->print (GRBL_VERSION_BUILD); + espresponse->print (") (ESP32)"); + } + break; + //Set ESP mode + //cmd is RESTART + //[ESP444] + case 444: + parameter = get_param(cmd_params,"", true); +#ifdef ENABLE_AUTHENTICATION + if (auth_type != LEVEL_ADMIN) { + response = false; + } else +#endif + { + if (parameter=="RESTART") { + grbl_send(CLIENT_ALL,"[MSG:Restart ongoing]\r\n"); + wifi_config.restart_ESP(); + } else response = false; + } + if (!response) { + if (espresponse)espresponse->println ("Error: Incorrect Command"); + } else { + if (espresponse)espresponse->println ("ok"); + } + break; +#ifdef ENABLE_AUTHENTICATION + //Change / Reset user password + //[ESP555] + case 555: { + if (auth_type == LEVEL_ADMIN) { + parameter = get_param (cmd_params, "", true); + if (parameter.length() == 0) { + Preferences prefs; + parameter = DEFAULT_USER_PWD; + prefs.begin(NAMESPACE, false); + if (prefs.putString(USER_PWD_ENTRY, parameter) != parameter.length()){ + response = false; + espresponse->println ("error"); + } else espresponse->println ("ok"); + prefs.end(); + + } else { + if (isLocalPasswordValid (parameter.c_str() ) ) { + Preferences prefs; + prefs.begin(NAMESPACE, false); + if (prefs.putString(USER_PWD_ENTRY, parameter) != parameter.length()) { + response = false; + espresponse->println ("error"); + } else espresponse->println ("ok"); + prefs.end(); + } else { + espresponse->println ("error"); + response = false; + } + } + } else { + espresponse->println ("error"); + response = false; + } + break; + } +#endif + //[ESP700] + case 700: { //read local file +#ifdef ENABLE_AUTHENTICATION + if (auth_type == LEVEL_GUEST) return false; +#endif + cmd_params.trim() ; + if ( (cmd_params.length() > 0) && (cmd_params[0] != '/') ) { + cmd_params = "/" + cmd_params; + } + File currentfile = SPIFFS.open (cmd_params, FILE_READ); + if (currentfile) {//if file open success + //until no line in file + while (currentfile.available()) { + String currentline = currentfile.readStringUntil('\n'); + currentline.replace("\n",""); + currentline.replace("\r",""); + if (currentline.length() > 0) { + int ESPpos = currentline.indexOf ("[ESP"); + if (ESPpos > -1) { + //is there the second part? + int ESPpos2 = currentline.indexOf ("]", ESPpos); + if (ESPpos2 > -1) { + //Split in command and parameters + String cmd_part1 = currentline.substring (ESPpos + 4, ESPpos2); + String cmd_part2 = ""; + //is there space for parameters? + if (ESPpos2 < currentline.length() ) { + cmd_part2 = currentline.substring (ESPpos2 + 1); + } + //if command is a valid number then execute command + if(cmd_part1.toInt()!=0) { + if (!execute_internal_command(cmd_part1.toInt(),cmd_part2, auth_type, espresponse)) response = false; + } + //if not is not a valid [ESPXXX] command ignore it + } + } else { + //preprocess line + String processedline = ""; + char c; + uint8_t index = 0; + uint8_t line_flags = 0; + for (uint16_t index=0; index < currentline.length(); index++){ + c = currentline[index]; + if (c == '\r' || c == ' ' || c == '\n') { + // ignore these whitespace items + } + else if (c == '(') { + line_flags |= LINE_FLAG_COMMENT_PARENTHESES; + } + else if (c == ')') { + // End of '()' comment. Resume line allowed. + if (line_flags & LINE_FLAG_COMMENT_PARENTHESES) { line_flags &= ~(LINE_FLAG_COMMENT_PARENTHESES); } + } + else if (c == ';') { + // NOTE: ';' comment to EOL is a LinuxCNC definition. Not NIST. + if (!(line_flags & LINE_FLAG_COMMENT_PARENTHESES)) // semi colon inside parentheses do not mean anything + line_flags |= LINE_FLAG_COMMENT_SEMICOLON; + } + + else { // add characters to the line + if (!line_flags) { + c = toupper(c); // make upper case + processedline += c; + } + } + } + if (processedline.length() > 0)gc_execute_line((char *)processedline.c_str(), CLIENT_WEBUI); + wifi_config.wait (1); + } + wifi_config.wait (1); + } + } + currentfile.close(); + if (espresponse)espresponse->println ("ok"); + } else { + if (espresponse)espresponse->println ("error"); + response = false; + } + break; + } + //Format SPIFFS + //[ESP710]FORMAT pwd= + case 710: +#ifdef ENABLE_AUTHENTICATION + if (auth_type != LEVEL_ADMIN) return false; +#endif + parameter = get_param (cmd_params, "", true); +#ifdef ENABLE_AUTHENTICATION + if (auth_type != LEVEL_ADMIN) { + espresponse->println ("error"); + response = false; + break; + } else +#endif + { + if (parameter == "FORMAT") { + if (espresponse)espresponse->print ("Formating"); + SPIFFS.format(); + if (espresponse)espresponse->println ("...Done"); + } else { + if (espresponse)espresponse->println ("error"); + response = false; + } + } + break; + //get fw version / fw target / hostname / authentication + //[ESP800] + case 800: + { + if (!espresponse)return false; + String resp; + resp = "FW version:"; + resp += GRBL_VERSION; + resp += " # FW target:grbl-embedded # FW HW:"; + #ifdef ENABLE_SD_CARD + resp += "Direct SD"; + #else + resp += "No SD"; + #endif + resp += " # primary sd:/sd # secondary sd:none # authentication:"; + #ifdef ENABLE_AUTHENTICATION + resp += "yes"; + #else + resp += "no"; + #endif + resp += " # webcommunication: Sync: "; + resp += String(_port + 1); + resp += "# hostname:"; + resp += _hostname; + if (WiFi.getMode() == WIFI_AP)resp += "(AP mode)"; + if (espresponse)espresponse->println (resp.c_str()); + } + break; + default: + if (espresponse)espresponse->println ("Error: Incorrect Command"); + response = false; + break; + } + return response; +} + +//login status check +void Web_Server::handle_login() +{ +#ifdef ENABLE_AUTHENTICATION + String smsg; + String sUser,sPassword; + String auths; + int code = 200; + bool msg_alert_error=false; + //disconnect can be done anytime no need to check credential + if (_webserver->hasArg("DISCONNECT")) { + String cookie = _webserver->header("Cookie"); + int pos = cookie.indexOf("ESPSESSIONID="); + String sessionID; + if (pos!= -1) { + int pos2 = cookie.indexOf(";",pos); + sessionID = cookie.substring(pos+strlen("ESPSESSIONID="),pos2); + } + ClearAuthIP(_webserver->client().remoteIP(), sessionID.c_str()); + _webserver->sendHeader("Set-Cookie","ESPSESSIONID=0"); + _webserver->sendHeader("Cache-Control","no-cache"); + String buffer2send = "{\"status\":\"Ok\",\"authentication_lvl\":\"guest\"}"; + _webserver->send(code, "application/json", buffer2send); + //_webserver->client().stop(); + return; + } + + level_authenticate_type auth_level = is_authenticated(); + if (auth_level == LEVEL_GUEST) auths = "guest"; + else if (auth_level == LEVEL_USER) auths = "user"; + else if (auth_level == LEVEL_ADMIN) auths = "admin"; + else auths = "???"; + + //check is it is a submission or a query + if (_webserver->hasArg("SUBMIT")) { + //is there a correct list of query? + if ( _webserver->hasArg("PASSWORD") && _webserver->hasArg("USER")) { + //USER + sUser = _webserver->arg("USER"); + if ( !((sUser == DEFAULT_ADMIN_LOGIN) || (sUser == DEFAULT_USER_LOGIN))) { + msg_alert_error=true; + smsg = "Error : Incorrect User"; + code = 401; + } + if (msg_alert_error == false) { + //Password + sPassword = _webserver->arg("PASSWORD"); + String sadminPassword; + + Preferences prefs; + prefs.begin(NAMESPACE, true); + String defV = DEFAULT_ADMIN_PWD; + sadminPassword = prefs.getString(ADMIN_PWD_ENTRY, defV); + String suserPassword; + defV = DEFAULT_USER_PWD; + suserPassword = prefs.getString(USER_PWD_ENTRY, defV); + prefs.end(); + + if(!(((sUser == DEFAULT_ADMIN_LOGIN) && (strcmp(sPassword.c_str(),sadminPassword.c_str()) == 0)) || + ((sUser == DEFAULT_USER_LOGIN) && (strcmp(sPassword.c_str(),suserPassword.c_str()) == 0)))) { + msg_alert_error=true; + smsg = "Error: Incorrect password"; + code = 401; + } + } + } else { + msg_alert_error=true; + smsg = "Error: Missing data"; + code = 500; + } + //change password + if (_webserver->hasArg("PASSWORD") && _webserver->hasArg("USER") && _webserver->hasArg("NEWPASSWORD") && (msg_alert_error==false) ) { + String newpassword = _webserver->arg("NEWPASSWORD"); + if (isLocalPasswordValid(newpassword.c_str())) { + String spos; + if(sUser == DEFAULT_ADMIN_LOGIN) spos = ADMIN_PWD_ENTRY; + else spos = USER_PWD_ENTRY; + + Preferences prefs; + prefs.begin(NAMESPACE, false); + if (prefs.putString(spos.c_str(), newpassword) != newpassword.length()) { + msg_alert_error = true; + smsg = "Error: Cannot apply changes"; + code = 500; + } + prefs.end(); + } else { + msg_alert_error=true; + smsg = "Error: Incorrect password"; + code = 500; + } + } + if ((code == 200) || (code == 500)) { + level_authenticate_type current_auth_level; + if(sUser == DEFAULT_ADMIN_LOGIN) { + current_auth_level = LEVEL_ADMIN; + } else if(sUser == DEFAULT_USER_LOGIN){ + current_auth_level = LEVEL_USER; + } else { + current_auth_level = LEVEL_GUEST; + } + //create Session + if ((current_auth_level != auth_level) || (auth_level== LEVEL_GUEST)) { + auth_ip * current_auth = new auth_ip; + current_auth->level = current_auth_level; + current_auth->ip=_webserver->client().remoteIP(); + strcpy(current_auth->sessionID,create_session_ID()); + strcpy(current_auth->userID,sUser.c_str()); + current_auth->last_time=millis(); + if (AddAuthIP(current_auth)) { + String tmps ="ESPSESSIONID="; + tmps+=current_auth->sessionID; + _webserver->sendHeader("Set-Cookie",tmps); + _webserver->sendHeader("Cache-Control","no-cache"); + switch(current_auth->level) { + case LEVEL_ADMIN: + auths = "admin"; + break; + case LEVEL_USER: + auths = "user"; + break; + default: + auths = "guest"; + break; + } + } else { + delete current_auth; + msg_alert_error=true; + code = 500; + smsg = "Error: Too many connections"; + } + } + } + if (code == 200) smsg = "Ok"; + + //build JSON + String buffer2send = "{\"status\":\"" + smsg + "\",\"authentication_lvl\":\""; + buffer2send += auths; + buffer2send += "\"}"; + _webserver->send(code, "application/json", buffer2send); + } else { + if (auth_level != LEVEL_GUEST) { + String cookie = _webserver->header("Cookie"); + int pos = cookie.indexOf("ESPSESSIONID="); + String sessionID; + if (pos!= -1) { + int pos2 = cookie.indexOf(";",pos); + sessionID = cookie.substring(pos+strlen("ESPSESSIONID="),pos2); + auth_ip * current_auth_info = GetAuth(_webserver->client().remoteIP(), sessionID.c_str()); + if (current_auth_info != NULL){ + sUser = current_auth_info->userID; + } + } + } + String buffer2send = "{\"status\":\"200\",\"authentication_lvl\":\""; + buffer2send += auths; + buffer2send += "\",\"user\":\""; + buffer2send += sUser; + buffer2send +="\"}"; + _webserver->send(code, "application/json", buffer2send); + } +#else + _webserver->sendHeader("Cache-Control","no-cache"); + _webserver->send(200, "application/json", "{\"status\":\"Ok\",\"authentication_lvl\":\"admin\"}"); +#endif +} +//SPIFFS +//SPIFFS files list and file commands +void Web_Server::handleFileList () +{ + level_authenticate_type auth_level = is_authenticated(); + if (auth_level == LEVEL_GUEST) { + _upload_status = UPLOAD_STATUS_NONE; + _webserver->send (401, "text/plain", "Authentication failed!\n"); + return; + } + String path ; + String status = "Ok"; + if ( (_upload_status == UPLOAD_STATUS_FAILED) || (_upload_status == UPLOAD_STATUS_CANCELLED) ) { + status = "Upload failed"; + } + //be sure root is correct according authentication + if (auth_level == LEVEL_ADMIN) { + path = "/"; + } else { + path = "/user"; + } + //get current path + if (_webserver->hasArg ("path") ) { + path += _webserver->arg ("path") ; + } + //to have a clean path + path.trim(); + path.replace ("//", "/"); + if (path[path.length() - 1] != '/') { + path += "/"; + } + //check if query need some action + if (_webserver->hasArg ("action") ) { + //delete a file + if (_webserver->arg ("action") == "delete" && _webserver->hasArg ("filename") ) { + String filename; + String shortname = _webserver->arg ("filename"); + shortname.replace ("/", ""); + filename = path + _webserver->arg ("filename"); + filename.replace ("//", "/"); + if (!SPIFFS.exists (filename) ) { + status = shortname + " does not exists!"; + } else { + if (SPIFFS.remove (filename) ) { + status = shortname + " deleted"; + //what happen if no "/." and no other subfiles ? + String ptmp = path; + if ( (path != "/") && (path[path.length() - 1] = '/') ) { + ptmp = path.substring (0, path.length() - 1); + } + File dir = SPIFFS.open (ptmp); + File dircontent = dir.openNextFile(); + if (!dircontent) { + //keep directory alive even empty + File r = SPIFFS.open (path + "/.", FILE_WRITE); + if (r) { + r.close(); + } + } + } else { + status = "Cannot deleted " ; + status += shortname ; + } + } + } + //delete a directory + if (_webserver->arg ("action") == "deletedir" && _webserver->hasArg ("filename") ) { + String filename; + String shortname = _webserver->arg ("filename"); + shortname.replace ("/", ""); + filename = path + _webserver->arg ("filename"); + filename += "/"; + filename.replace ("//", "/"); + if (filename != "/") { + bool delete_error = false; + File dir = SPIFFS.open (path + shortname); + { + File file2deleted = dir.openNextFile(); + while (file2deleted) { + String fullpath = file2deleted.name(); + if (!SPIFFS.remove (fullpath) ) { + delete_error = true; + status = "Cannot deleted " ; + status += fullpath; + } + file2deleted = dir.openNextFile(); + } + } + if (!delete_error) { + status = shortname ; + status += " deleted"; + } + } + } + //create a directory + if (_webserver->arg ("action") == "createdir" && _webserver->hasArg ("filename") ) { + String filename; + filename = path + _webserver->arg ("filename") + "/."; + String shortname = _webserver->arg ("filename"); + shortname.replace ("/", ""); + filename.replace ("//", "/"); + if (SPIFFS.exists (filename) ) { + status = shortname + " already exists!"; + } else { + File r = SPIFFS.open (filename, FILE_WRITE); + if (!r) { + status = "Cannot create "; + status += shortname ; + } else { + r.close(); + status = shortname + " created"; + } + } + } + } + String jsonfile = "{"; + String ptmp = path; + if ( (path != "/") && (path[path.length() - 1] = '/') ) { + ptmp = path.substring (0, path.length() - 1); + } + File dir = SPIFFS.open (ptmp); + jsonfile += "\"files\":["; + bool firstentry = true; + String subdirlist = ""; + File fileparsed = dir.openNextFile(); + while (fileparsed) { + String filename = fileparsed.name(); + String size = ""; + bool addtolist = true; + //remove path from name + filename = filename.substring (path.length(), filename.length() ); + //check if file or subfile + if (filename.indexOf ("/") > -1) { + //Do not rely on "/." to define directory as SPIFFS upload won't create it but directly files + //and no need to overload SPIFFS if not necessary to create "/." if no need + //it will reduce SPIFFS available space so limit it to creation + filename = filename.substring (0, filename.indexOf ("/") ); + String tag = "*"; + tag += filename + "*"; + if (subdirlist.indexOf (tag) > -1 || filename.length() == 0) { //already in list + addtolist = false; //no need to add + } else { + size = "-1"; //it is subfile so display only directory, size will be -1 to describe it is directory + if (subdirlist.length() == 0) { + subdirlist += "*"; + } + subdirlist += filename + "*"; //add to list + } + } else { + //do not add "." file + if (! ( (filename == ".") || (filename == "") ) ) { + size = formatBytes (fileparsed.size() ); + } else { + addtolist = false; + } + } + if (addtolist) { + if (!firstentry) { + jsonfile += ","; + } else { + firstentry = false; + } + jsonfile += "{"; + jsonfile += "\"name\":\""; + jsonfile += filename; + jsonfile += "\",\"size\":\""; + jsonfile += size; + jsonfile += "\""; + jsonfile += "}"; + } + fileparsed = dir.openNextFile(); + } + jsonfile += "],"; + jsonfile += "\"path\":\"" + path + "\","; + jsonfile += "\"status\":\"" + status + "\","; + size_t totalBytes; + size_t usedBytes; + totalBytes = SPIFFS.totalBytes(); + usedBytes = SPIFFS.usedBytes(); + jsonfile += "\"total\":\"" + formatBytes (totalBytes) + "\","; + jsonfile += "\"used\":\"" + formatBytes (usedBytes) + "\","; + jsonfile.concat (F ("\"occupation\":\"") ); + jsonfile += String (100 * usedBytes / totalBytes); + jsonfile += "\""; + jsonfile += "}"; + path = ""; + _webserver->sendHeader("Cache-Control", "no-cache"); + _webserver->send(200, "application/json", jsonfile); + _upload_status = UPLOAD_STATUS_NONE; +} + +//SPIFFS files uploader handle +void Web_Server::SPIFFSFileupload () +{ + //get authentication status + level_authenticate_type auth_level= is_authenticated(); + //Guest cannot upload - only admin + if (auth_level == LEVEL_GUEST) { + _upload_status = UPLOAD_STATUS_CANCELLED; + grbl_send(CLIENT_ALL,"[MSG:Upload rejected]\r\n"); + _webserver->client().stop(); + return; + } + static String filename; + static File fsUploadFile = (File)0; + + HTTPUpload& upload = _webserver->upload(); + //Upload start + //************** + if(upload.status == UPLOAD_FILE_START) { + String upload_filename = upload.filename; + if (upload_filename[0] != '/') filename = "/" + upload_filename; + else filename = upload.filename; + //according User or Admin the root is different as user is isolate to /user when admin has full access + if(auth_level != LEVEL_ADMIN) { + upload_filename = filename; + filename = "/user" + upload_filename; + } + + if (SPIFFS.exists (filename) ) { + SPIFFS.remove (filename); + } + if (fsUploadFile ) { + fsUploadFile.close(); + } + //create file + fsUploadFile = SPIFFS.open(filename, FILE_WRITE); + //check If creation succeed + if (fsUploadFile) { + //if yes upload is started + _upload_status= UPLOAD_STATUS_ONGOING; + } else { + //if no set cancel flag + _upload_status=UPLOAD_STATUS_CANCELLED; + grbl_send(CLIENT_ALL,"[MSG:Upload error]\r\n"); + _webserver->client().stop(); + } + //Upload write + //************** + } else if(upload.status == UPLOAD_FILE_WRITE) { + //check if file is available and no error + if(fsUploadFile && _upload_status == UPLOAD_STATUS_ONGOING) { + //no error so write post date + fsUploadFile.write(upload.buf, upload.currentSize); + } else { + //we have a problem set flag UPLOAD_STATUS_CANCELLED + _upload_status=UPLOAD_STATUS_CANCELLED; + fsUploadFile.close(); + if (SPIFFS.exists (filename) ) { + SPIFFS.remove (filename); + } + _webserver->client().stop(); + grbl_send(CLIENT_ALL,"[MSG:Upload error]\r\n"); + } + //Upload end + //************** + } else if(upload.status == UPLOAD_FILE_END) { + //check if file is still open + if(fsUploadFile) { + //close it + fsUploadFile.close(); + //check size + String sizeargname = upload.filename + "S"; + fsUploadFile = SPIFFS.open (filename, FILE_READ); + uint32_t filesize = fsUploadFile.size(); + fsUploadFile.close(); + if (_webserver->hasArg (sizeargname.c_str()) ) { + if (_webserver->arg (sizeargname.c_str()) != String(filesize)) { + _upload_status = UPLOAD_STATUS_FAILED; + SPIFFS.remove (filename); + } + } + if (_upload_status == UPLOAD_STATUS_ONGOING) { + _upload_status = UPLOAD_STATUS_SUCCESSFUL; + } else grbl_send(CLIENT_ALL,"[MSG:Upload error]\r\n"); + } else { + //we have a problem set flag UPLOAD_STATUS_CANCELLED + _upload_status=UPLOAD_STATUS_CANCELLED; + _webserver->client().stop(); + if (SPIFFS.exists (filename) ) { + SPIFFS.remove (filename); + } + grbl_send(CLIENT_ALL,"[MSG:Upload error]\r\n"); + + } + //Upload cancelled + //************** + } else { + if (_upload_status == UPLOAD_STATUS_ONGOING) { + _upload_status = UPLOAD_STATUS_CANCELLED; + } + if(fsUploadFile)fsUploadFile.close(); + if (SPIFFS.exists (filename) ) { + SPIFFS.remove (filename); + } + grbl_send(CLIENT_ALL,"[MSG:Upload error]\r\n"); + } + wifi_config.wait(0); +} + +//Web Update handler +void Web_Server::handleUpdate () +{ + level_authenticate_type auth_level = is_authenticated(); + if (auth_level != LEVEL_ADMIN) { + _upload_status = UPLOAD_STATUS_NONE; + _webserver->send (403, "text/plain", "Not allowed, log in first!\n"); + return; + } + String jsonfile = "{\"status\":\"" ; + jsonfile += String(_upload_status); + jsonfile += "\"}"; + //send status + _webserver->sendHeader("Cache-Control", "no-cache"); + _webserver->send(200, "application/json", jsonfile); + //if success restart + if (_upload_status == UPLOAD_STATUS_SUCCESSFUL) { + wifi_config.wait(1000); + wifi_config.restart_ESP(); + } else { + _upload_status = UPLOAD_STATUS_NONE; + } +} + +//File upload for Web update +void Web_Server::WebUpdateUpload () +{ + static size_t last_upload_update; + static uint32_t maxSketchSpace ; + //only admin can update FW + if (is_authenticated() != LEVEL_ADMIN) { + _upload_status = UPLOAD_STATUS_CANCELLED; + _webserver->client().stop(); + grbl_send(CLIENT_ALL,"[MSG:Upload rejected]\r\n"); + return; + } + + //get current file ID + HTTPUpload& upload = _webserver->upload(); + //Upload start + //************** + if(upload.status == UPLOAD_FILE_START) { + grbl_send(CLIENT_ALL,"[MSG:Update Firmware]\r\n"); + _upload_status= UPLOAD_STATUS_ONGOING; + + //Not sure can do OTA on 2Mb board + maxSketchSpace = (ESP.getFlashChipSize() > 0x20000) ? 0x140000 : 0x140000 / 2; + last_upload_update = 0; + if(!Update.begin(maxSketchSpace)) { //start with max available size + _upload_status=UPLOAD_STATUS_CANCELLED; + grbl_send(CLIENT_ALL,"[MSG:Update cancelled]\r\n"); + _webserver->client().stop(); + return; + } else { + grbl_send(CLIENT_ALL,"\n[MSG:Update 0%]\r\n"); + } + //Upload write + //************** + } else if(upload.status == UPLOAD_FILE_WRITE) { + //check if no error + if (_upload_status == UPLOAD_STATUS_ONGOING) { + //we do not know the total file size yet but we know the available space so let's use it + if ( ((100 * upload.totalSize) / maxSketchSpace) !=last_upload_update) { + last_upload_update = (100 * upload.totalSize) / maxSketchSpace; + String s = "Update "; + s+= String(last_upload_update); + s+="%"; + grbl_sendf(CLIENT_ALL,"[MSG:%s]\r\n", s.c_str()); + } + if(Update.write(upload.buf, upload.currentSize) != upload.currentSize) { + _upload_status=UPLOAD_STATUS_CANCELLED; + } + } + //Upload end + //************** + } else if(upload.status == UPLOAD_FILE_END) { + if(Update.end(true)) { //true to set the size to the current progress + //Now Reboot + grbl_send(CLIENT_ALL,"[MSG:Update 100%]\r\n"); + _upload_status=UPLOAD_STATUS_SUCCESSFUL; + } + } else if(upload.status == UPLOAD_FILE_ABORTED) { + grbl_send(CLIENT_ALL,"[MSG:Update failed]\r\n"); + Update.end(); + _upload_status=UPLOAD_STATUS_CANCELLED; + } + wifi_config.wait(0); +} + + +#ifdef ENABLE_SD_CARD + +//Function to delete not empty directory on SD card +bool Web_Server::deleteRecursive(String path) +{ + bool result = true; + File file = SD.open((char *)path.c_str()); + //failed + if (!file) { + return false; + } + if(!file.isDirectory()) { + file.close(); + //return if success or not + return SD.remove((char *)path.c_str()); + } + file.rewindDirectory(); + while(true) { + File entry = file.openNextFile(); + if (!entry) { + break; + } + String entryPath = entry.name(); + if(entry.isDirectory()) { + entry.close(); + if(!deleteRecursive(entryPath)) { + result = false; + } + } else { + entry.close(); + if (!SD.remove((char *)entryPath.c_str())) { + result = false; + break; + } + } + wifi_config.wait(0); //wdtFeed + } + file.close(); + if (result) return SD.rmdir((char *)path.c_str()); + else return false; +} + +//direct SD files list////////////////////////////////////////////////// +void Web_Server::handle_direct_SDFileList() +{ + //this is only for admin and user + if (is_authenticated() == LEVEL_GUEST) { + _upload_status=UPLOAD_STATUS_NONE; + _webserver->send(401, "application/json", "{\"status\":\"Authentication failed!\"}"); + return; + } + + String path="/"; + String sstatus="Ok"; + if ((_upload_status == UPLOAD_STATUS_FAILED) || (_upload_status == UPLOAD_STATUS_CANCELLED)) { + sstatus = "Upload failed"; + _upload_status = UPLOAD_STATUS_NONE; + } + bool list_files = true; + uint32_t totalspace = 0; + uint32_t usedspace = 0; + if (get_sd_state(true) != SDCARD_IDLE) { + _webserver->sendHeader("Cache-Control","no-cache"); + _webserver->send(200, "application/json", "{\"status\":\"No SD Card\"}"); + return; + } + set_sd_state(SDCARD_BUSY_PARSING); + //get current path + if(_webserver->hasArg("path")) { + path += _webserver->arg("path") ; + } + //to have a clean path + path.trim(); + path.replace("//","/"); + if (path[path.length()-1] !='/') { + path +="/"; + } + //check if query need some action + if(_webserver->hasArg("action")) { + //delete a file + if(_webserver->arg("action") == "delete" && _webserver->hasArg("filename")) { + String filename; + String shortname = _webserver->arg("filename"); + filename = path + shortname; + shortname.replace("/",""); + filename.replace("//","/"); + if(!SD.exists((char *)filename.c_str())) { + sstatus = shortname + " does not exist!"; + } else { + if (SD.remove((char *)filename.c_str())) { + sstatus = shortname + " deleted"; + } else { + sstatus = "Cannot deleted " ; + sstatus+=shortname ; + } + } + } + //delete a directory + if( _webserver->arg("action") == "deletedir" && _webserver->hasArg("filename")) { + String filename; + String shortname = _webserver->arg("filename"); + shortname.replace("/",""); + filename = path + "/" + shortname; + filename.replace("//","/"); + if (filename != "/") { + if(!SD.exists((char *)filename.c_str())) { + sstatus = shortname + " does not exist!"; + } else { + if (!deleteRecursive(filename)) { + sstatus ="Error deleting: "; + sstatus += shortname ; + } else { + sstatus = shortname ; + sstatus+=" deleted"; + } + } + } else { + sstatus ="Cannot delete root"; + } + } + //create a directory + if( _webserver->arg("action")=="createdir" && _webserver->hasArg("filename")) { + String filename; + String shortname = _webserver->arg("filename"); + filename = path + shortname; + shortname.replace("/",""); + filename.replace("//","/"); + if(SD.exists((char *)filename.c_str())) { + sstatus = shortname + " already exists!"; + } else { + if (!SD.mkdir((char *)filename.c_str())) { + sstatus = "Cannot create "; + sstatus += shortname ; + } else { + sstatus = shortname + " created"; + } + } + } + } + //check if no need build file list + if( _webserver->hasArg("dontlist")) { + if( _webserver->arg("dontlist") == "yes") { + list_files = false; + } + } + String jsonfile = "{" ; + jsonfile+="\"files\":["; + + if (path!="/")path = path.substring(0,path.length()-1); + if (path!="/" && !SD.exists((char *)path.c_str())) { + + String s = "{\"status\":\" "; + s += path; + s+= " does not exist on SD Card\"}"; + _webserver->send(200, "application/json", s.c_str()); + return; + } + if (list_files) { + File dir = SD.open((char *)path.c_str()); + if (!dir) { + } + if(!dir.isDirectory()) { + dir.close(); + } + dir.rewindDirectory(); + File entry = dir.openNextFile(); + int i = 0; + while(entry) { + wifi_config.wait (1); + if (i>0) { + jsonfile+=","; + } + jsonfile+="{\"name\":\""; + String tmpname = entry.name(); + int pos = tmpname.lastIndexOf("/"); + tmpname = tmpname.substring(pos+1); + jsonfile+=tmpname; + jsonfile+="\",\"shortname\":\""; //No need here + jsonfile+=tmpname; + jsonfile+="\",\"size\":\""; + if (entry.isDirectory()) { + jsonfile+="-1"; + } else { + // files have sizes, directories do not + jsonfile+=formatBytes(entry.size()); + } + jsonfile+="\",\"datetime\":\""; + //TODO - can be done later + jsonfile+="\"}"; + i++; + entry.close(); + entry = dir.openNextFile(); + } + dir.close(); + } + jsonfile+="],\"path\":\""; + jsonfile+=path + "\","; + static uint32_t volTotal = 1; + static uint32_t volFree = 0; + jsonfile+="\"total\":\""; + String stotalspace,susedspace; + //SDCard are in GB or MB but no less + totalspace = SD.totalBytes(); + usedspace = SD.usedBytes(); + stotalspace = formatBytes(totalspace); + susedspace = formatBytes(usedspace); + + uint32_t occupedspace = (volFree/volTotal)*100; + //minimum if even one byte is used is 1% + if ( (occupedspace <= 1) && (volTotal!=volFree)) { + occupedspace=1; + } + if (totalspace) { + jsonfile+= stotalspace ; + } else { + jsonfile+= "-1"; + } + jsonfile+="\",\"used\":\""; + jsonfile+= susedspace ; + jsonfile+="\",\"occupation\":\""; + if (totalspace) { + jsonfile+= String(occupedspace); + } else { + jsonfile+= "-1"; + } + jsonfile+= "\","; + jsonfile+= "\"mode\":\"direct\","; + jsonfile+= "\"status\":\""; + jsonfile+=sstatus + "\""; + jsonfile+= "}"; + _webserver->sendHeader("Cache-Control","no-cache"); + _webserver->send (200, "application/json", jsonfile.c_str()); + _upload_status=UPLOAD_STATUS_NONE; + set_sd_state(SDCARD_IDLE); +} + +//SD File upload with direct access to SD/////////////////////////////// +void Web_Server::SDFile_direct_upload() +{ + static String filename ; + static File sdUploadFile; + //this is only for admin and user + if (is_authenticated() == LEVEL_GUEST) { + _upload_status=UPLOAD_STATUS_NONE; + _webserver->send(401, "application/json", "{\"status\":\"Authentication failed!\"}"); + return; + } + //retrieve current file id + HTTPUpload& upload = _webserver->upload(); + //Upload start + //************** + if(upload.status == UPLOAD_FILE_START) { + filename= upload.filename; + //on SD need to add / if not present + if (filename[0]!='/') { + filename= "/"+upload.filename; + } + //check if SD Card is available + if ( get_sd_state(true) != SDCARD_IDLE) { + _upload_status=UPLOAD_STATUS_CANCELLED; + grbl_send(CLIENT_ALL,"[MSG:Upload cancelled]\r\n"); + _webserver->client().stop(); + return; + } + set_sd_state(SDCARD_BUSY_UPLOADING); + //delete file on SD Card if already present + if(SD.exists((char *)filename.c_str())) { + SD.remove((char *)filename.c_str()); + } + //Create file for writing + sdUploadFile = SD.open((char *)filename.c_str(), FILE_WRITE); + //check if creation succeed + if (!sdUploadFile) { + //if creation failed + _upload_status=UPLOAD_STATUS_FAILED; + set_sd_state(SDCARD_IDLE); + grbl_send(CLIENT_ALL,"[MSG:Upload failed]\r\n"); + _webserver->client().stop(); + } + //if creation succeed set flag UPLOAD_STATUS_ONGOING + else { + _upload_status= UPLOAD_STATUS_ONGOING; + } + //Upload write + //************** + } else if(upload.status == UPLOAD_FILE_WRITE) { + if(sdUploadFile && (_upload_status == UPLOAD_STATUS_ONGOING) && (get_sd_state(false) == SDCARD_BUSY_UPLOADING)) { + //no error write post data + sdUploadFile.write(upload.buf, upload.currentSize); + } else { //if error set flag UPLOAD_STATUS_FAILED + _upload_status = UPLOAD_STATUS_FAILED; + set_sd_state(SDCARD_IDLE); + grbl_send(CLIENT_ALL,"[MSG:Upload failed]\r\n"); + _webserver->client().stop(); + } + //Upload end + //************** + } else if(upload.status == UPLOAD_FILE_END) { + //if file is open close it + if(sdUploadFile) { + sdUploadFile.close(); + //TODO Check size + String sizeargname = upload.filename + "S"; + if (_webserver->hasArg (sizeargname.c_str()) ) { + uint32_t filesize = 0; + sdUploadFile = SD.open (filename.c_str(), FILE_READ); + filesize = sdUploadFile.size(); + sdUploadFile.close(); + if (_webserver->arg (sizeargname.c_str()) != String(filesize)) { + _upload_status = UPLOAD_STATUS_FAILED; + SD.remove (filename.c_str()); + } + } + } else { + _upload_status = UPLOAD_STATUS_FAILED; + set_sd_state(SDCARD_IDLE); + grbl_send(CLIENT_ALL,"[MSG:Upload failed]\r\n"); + } + if (_upload_status == UPLOAD_STATUS_ONGOING) { + _upload_status = UPLOAD_STATUS_SUCCESSFUL; + } + set_sd_state(SDCARD_IDLE); + } else {//Upload cancelled + _upload_status=UPLOAD_STATUS_FAILED; + set_sd_state(SDCARD_IDLE); + grbl_send(CLIENT_ALL,"[MSG:Upload failed]\r\n"); + _webserver->client().stop(); + if(sdUploadFile) { + sdUploadFile.close(); + } + } + wifi_config.wait(0); +} +#endif + +void Web_Server::handle(){ +static uint32_t timeout = millis(); +#ifdef ENABLE_CAPTIVE_PORTAL + if(WiFi.getMode() != WIFI_STA){ + dnsServer.processNextRequest(); + } +#endif + if (_webserver)_webserver->handleClient(); + if (_socket_server && _setupdone)_socket_server->loop(); + if ((millis() - timeout) > 10000) { + if (_socket_server){ + String s = "PING:"; + s+=String(_id_connection); + _socket_server->broadcastTXT(s); + timeout=millis(); + } + } +} + + +void Web_Server::handle_Websocket_Event(uint8_t num, uint8_t type, uint8_t * payload, size_t length) { + + switch(type) { + case WStype_DISCONNECTED: + //USE_SERIAL.printf("[%u] Disconnected!\n", num); + break; + case WStype_CONNECTED: + { + IPAddress ip = _socket_server->remoteIP(num); + //USE_SERIAL.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload); + String s = "CURRENT_ID:" + String(num); + // send message to client + _id_connection = num; + _socket_server->sendTXT(_id_connection, s); + s = "ACTIVE_ID:" + String(_id_connection); + _socket_server->broadcastTXT(s); + } + break; + case WStype_TEXT: + //USE_SERIAL.printf("[%u] get Text: %s\n", num, payload); + + // send message to client + // webSocket.sendTXT(num, "message here"); + + // send data to all connected clients + // webSocket.broadcastTXT("message here"); + break; + case WStype_BIN: + //USE_SERIAL.printf("[%u] get binary length: %u\n", num, length); + //hexdump(payload, length); + + // send message to client + // webSocket.sendBIN(num, payload, length); + break; + default: + break; + } + +} + +//just simple helper to convert mac address to string +char * Web_Server::mac2str (uint8_t mac [8]) +{ + static char macstr [18]; + if (0 > sprintf (macstr, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]) ) { + strcpy (macstr, "00:00:00:00:00:00"); + } + return macstr; +} + +String Web_Server::get_Splited_Value(String data, char separator, int index) +{ + int found = 0; + int strIndex[] = {0, -1}; + int maxIndex = data.length()-1; + + for(int i=0; i<=maxIndex && found<=index; i++){ + if(data.charAt(i)==separator || i==maxIndex){ + found++; + strIndex[0] = strIndex[1]+1; + strIndex[1] = (i == maxIndex) ? i+1 : i; + } + } + + return found>index ? data.substring(strIndex[0], strIndex[1]) : ""; +} + + +//helper to format size to readable string +String Web_Server::formatBytes (uint32_t bytes) +{ + if (bytes < 1024) { + return String (bytes) + " B"; + } else if (bytes < (1024 * 1024) ) { + return String (bytes / 1024.0) + " KB"; + } else if (bytes < (1024 * 1024 * 1024) ) { + return String (bytes / 1024.0 / 1024.0) + " MB"; + } else { + return String (bytes / 1024.0 / 1024.0 / 1024.0) + " GB"; + } +} + +//helper to extract content type from file extension +//Check what is the content tye according extension file +String Web_Server::getContentType (String filename) +{ + String file_name = filename; + file_name.toLowerCase(); + if (filename.endsWith (".htm") ) { + return "text/html"; + } else if (file_name.endsWith (".html") ) { + return "text/html"; + } else if (file_name.endsWith (".css") ) { + return "text/css"; + } else if (file_name.endsWith (".js") ) { + return "application/javascript"; + } else if (file_name.endsWith (".png") ) { + return "image/png"; + } else if (file_name.endsWith (".gif") ) { + return "image/gif"; + } else if (file_name.endsWith (".jpeg") ) { + return "image/jpeg"; + } else if (file_name.endsWith (".jpg") ) { + return "image/jpeg"; + } else if (file_name.endsWith (".ico") ) { + return "image/x-icon"; + } else if (file_name.endsWith (".xml") ) { + return "text/xml"; + } else if (file_name.endsWith (".pdf") ) { + return "application/x-pdf"; + } else if (file_name.endsWith (".zip") ) { + return "application/x-zip"; + } else if (file_name.endsWith (".gz") ) { + return "application/x-gzip"; + } else if (file_name.endsWith (".txt") ) { + return "text/plain"; + } + return "application/octet-stream"; +} + +//check authentification +level_authenticate_type Web_Server::is_authenticated() +{ +#ifdef ENABLE_AUTHENTICATION + if (_webserver->hasHeader ("Cookie") ) { + String cookie = _webserver->header ("Cookie"); + int pos = cookie.indexOf ("ESPSESSIONID="); + if (pos != -1) { + int pos2 = cookie.indexOf (";", pos); + String sessionID = cookie.substring (pos + strlen ("ESPSESSIONID="), pos2); + IPAddress ip = _webserver->client().remoteIP(); + //check if cookie can be reset and clean table in same time + return ResetAuthIP (ip, sessionID.c_str() ); + } + } + return LEVEL_GUEST; +#else + return LEVEL_ADMIN; +#endif +} + +#ifdef ENABLE_AUTHENTICATION + +bool Web_Server::isLocalPasswordValid (const char * password) +{ + char c; + //limited size + if ( (strlen (password) > MAX_LOCAL_PASSWORD_LENGTH) || (strlen (password) < MIN_LOCAL_PASSWORD_LENGTH) ) { + return false; + } + //no space allowed + for (int i = 0; i < strlen (password); i++) { + c = password[i]; + if (c == ' ') { + return false; + } + } + return true; +} + +//add the information in the linked list if possible +bool Web_Server::AddAuthIP (auth_ip * item) +{ + if (_nb_ip > MAX_AUTH_IP) { + return false; + } + item->_next = _head; + _head = item; + _nb_ip++; + return true; +} + +//Session ID based on IP and time using 16 char +char * Web_Server::create_session_ID() +{ + static char sessionID[17]; +//reset SESSIONID + for (int i = 0; i < 17; i++) { + sessionID[i] = '\0'; + } +//get time + uint32_t now = millis(); +//get remote IP + IPAddress remoteIP = _webserver->client().remoteIP(); +//generate SESSIONID + if (0 > sprintf (sessionID, "%02X%02X%02X%02X%02X%02X%02X%02X", remoteIP[0], remoteIP[1], remoteIP[2], remoteIP[3], (uint8_t) ( (now >> 0) & 0xff), (uint8_t) ( (now >> 8) & 0xff), (uint8_t) ( (now >> 16) & 0xff), (uint8_t) ( (now >> 24) & 0xff) ) ) { + strcpy (sessionID, "NONE"); + } + return sessionID; +} + + +bool Web_Server::ClearAuthIP (IPAddress ip, const char * sessionID) +{ + auth_ip * current = _head; + auth_ip * previous = NULL; + bool done = false; + while (current) { + if ( (ip == current->ip) && (strcmp (sessionID, current->sessionID) == 0) ) { + //remove + done = true; + if (current == _head) { + _head = current->_next; + _nb_ip--; + delete current; + current = _head; + } else { + previous->_next = current->_next; + _nb_ip--; + delete current; + current = previous->_next; + } + } else { + previous = current; + current = current->_next; + } + } + return done; +} + +//Get info +auth_ip * Web_Server::GetAuth (IPAddress ip, const char * sessionID) +{ + auth_ip * current = _head; + auth_ip * previous = NULL; + //get time + //uint32_t now = millis(); + while (current) { + if (ip == current->ip) { + if (strcmp (sessionID, current->sessionID) == 0) { + //found + return current; + } + } + previous = current; + current = current->_next; + } + return NULL; +} + +//Review all IP to reset timers +level_authenticate_type Web_Server::ResetAuthIP (IPAddress ip, const char * sessionID) +{ + auth_ip * current = _head; + auth_ip * previous = NULL; + //get time + //uint32_t now = millis(); + while (current) { + if ( (millis() - current->last_time) > 360000) { + //remove + if (current == _head) { + _head = current->_next; + _nb_ip--; + delete current; + current = _head; + } else { + previous->_next = current->_next; + _nb_ip--; + delete current; + current = previous->_next; + } + } else { + if (ip == current->ip) { + if (strcmp (sessionID, current->sessionID) == 0) { + //reset time + current->last_time = millis(); + return (level_authenticate_type) current->level; + } + } + previous = current; + current = current->_next; + } + } + return LEVEL_GUEST; +} +#endif + +String Web_Server::get_param (String & cmd_params, const char * id, bool withspace) +{ + static String parameter; + String sid = id; + int start; + int end = -1; + parameter = ""; + //if no id it means it is first part of cmd + if (strlen (id) == 0) { + start = 0; + } + //else find id position + else { + start = cmd_params.indexOf (id); + } + //if no id found and not first part leave + if (start == -1 ) { + return parameter; + } + //password and SSID can have space so handle it + //if no space expected use space as delimiter + if (!withspace) { + end = cmd_params.indexOf (" ", start); + } + //if no end found - take all + if (end == -1) { + end = cmd_params.length(); + } + //extract parameter + parameter = cmd_params.substring (start + strlen (id), end); + //be sure no extra space + parameter.trim(); + return parameter; +} + +ESPResponseStream::ESPResponseStream(WebServer * webserver){ + _header_sent=false; + _webserver = webserver; +} + +void ESPResponseStream::println(const char *data){ + print(data); + print("\n"); +} + +void ESPResponseStream::print(const char *data){ + if (!_header_sent) { + _webserver->setContentLength(CONTENT_LENGTH_UNKNOWN); + _webserver->sendHeader("Content-Type","text/html"); + _webserver->sendHeader("Cache-Control","no-cache"); + _webserver->send(200); + _header_sent = true; + } + _buffer+=data; + if (_buffer.length() > 1200) { + //send data + _webserver->sendContent(_buffer); + //reset buffer + _buffer = ""; + } + +} + +void ESPResponseStream::flush(){ + if(_header_sent) { + //send data + if(_buffer.length() > 0)_webserver->sendContent(_buffer); + //close connection + _webserver->sendContent(""); + } + _header_sent = false; + _buffer = ""; + +} + + +#endif // Enable HTTP && ENABLE_WIFI + +#endif // ARDUINO_ARCH_ESP32 diff --git a/Grbl_Esp32/web_server.h b/Grbl_Esp32/web_server.h new file mode 100644 index 00000000..c1869044 --- /dev/null +++ b/Grbl_Esp32/web_server.h @@ -0,0 +1,122 @@ +/* + web_server.h - wifi services functions class + + Copyright (c) 2014 Luc Lebosse. All rights reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + + +#ifndef _WEB_SERVER_H +#define _WEB_SERVER_H + + +#include "config.h" +class WebSocketsServer; +class WebServer; + +//Authentication level +typedef enum { + LEVEL_GUEST = 0, + LEVEL_USER = 1, + LEVEL_ADMIN = 2 +} level_authenticate_type; + +#ifdef ENABLE_AUTHENTICATION +struct auth_ip { + IPAddress ip; + level_authenticate_type level; + char userID[17]; + char sessionID[17]; + uint32_t last_time; + auth_ip * _next; +}; + +#endif + + + + +class ESPResponseStream{ + public: + void print(const char *data); + void println(const char *data); + void flush(); + ESPResponseStream(WebServer * webserver); + private: + bool _header_sent; + WebServer * _webserver; + String _buffer; +}; + +class Web_Server { + public: + Web_Server(); + ~Web_Server(); + bool begin(); + void end(); + void handle(); + static long get_client_ID(); + private: + static bool _setupdone; + static WebServer * _webserver; + static long _id_connection; + static WebSocketsServer * _socket_server; + static uint16_t _port; + static uint16_t _data_port; + static String _hostname; + static uint8_t _upload_status; + static char * mac2str (uint8_t mac [8]); + static String formatBytes (uint32_t bytes); + static String getContentType (String filename); + static String get_Splited_Value(String data, char separator, int index); + static level_authenticate_type is_authenticated(); +#ifdef ENABLE_AUTHENTICATION + static auth_ip * _head; + static uint8_t _nb_ip; + static bool AddAuthIP (auth_ip * item); + static char * create_session_ID(); + static bool ClearAuthIP (IPAddress ip, const char * sessionID); + static auth_ip * GetAuth (IPAddress ip, const char * sessionID); + static level_authenticate_type ResetAuthIP (IPAddress ip, const char * sessionID); + static bool isLocalPasswordValid (const char * password); +#endif + static String get_param (String & cmd_params, const char * id, bool withspace); + static bool execute_internal_command (int cmd, String cmd_params, level_authenticate_type auth_level, ESPResponseStream *espresponse); +#ifdef ENABLE_SSDP + static void handle_SSDP (); +#endif + static void handle_root(); + static void handle_login(); + static void handle_not_found (); + static void handle_web_command (); + static void handle_web_command_silent (); + static void handle_Websocket_Event(uint8_t num, uint8_t type, uint8_t * payload, size_t length); + static void SPIFFSFileupload (); + static void handleFileList (); + static void handleUpdate (); + static void WebUpdateUpload (); + static bool is_realtime_cmd(char c); +#ifdef ENABLE_SD_CARD + static void handle_direct_SDFileList(); + static void SDFile_direct_upload(); + static bool deleteRecursive(String path); +#endif +}; + +extern Web_Server web_server; + +#endif + diff --git a/Grbl_Esp32/wificonfig.cpp b/Grbl_Esp32/wificonfig.cpp new file mode 100644 index 00000000..1aae5ff4 --- /dev/null +++ b/Grbl_Esp32/wificonfig.cpp @@ -0,0 +1,550 @@ +/* + wificonfig.cpp - wifi functions class + + Copyright (c) 2014 Luc Lebosse. All rights reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifdef ARDUINO_ARCH_ESP32 + +//#include "grbl.h" +#include "config.h" + +#ifdef ENABLE_WIFI + +#include +#include +#include +#include +#include +#include +#include "wificonfig.h" +#include "wifiservices.h" +//#include "http_ESP32.h" +#include "report.h" + +#ifdef __cplusplus +extern "C" { +#endif +esp_err_t esp_task_wdt_reset(); +#ifdef __cplusplus +} +#endif + +WiFiConfig wifi_config; + +bool WiFiConfig::restart_ESP_module = false; + +WiFiConfig::WiFiConfig(){ +} + +WiFiConfig::~WiFiConfig(){ + end(); +} + +const char *WiFiConfig::info(){ + static String result; + String tmp; + result = "[MSG:"; + if((WiFi.getMode() == WIFI_MODE_STA ) || (WiFi.getMode() == WIFI_MODE_APSTA )) { + result += "Mode=STA:SSID="; + result += WiFi.SSID(); + result += ":Status="; + result += (WiFi.status()==WL_CONNECTED)?"Connected":"Not connected"; + result += ":IP="; + result += WiFi.localIP().toString(); + result += ":MAC="; + tmp = WiFi.macAddress(); + tmp.replace(":","-"); + result += tmp; + + } + if((WiFi.getMode() == WIFI_MODE_AP ) || (WiFi.getMode() == WIFI_MODE_APSTA )) { + if(WiFi.getMode() == WIFI_MODE_APSTA ) { + result+= "]\r\n[MSG:"; + } + result+="Mode=AP:SSDI="; + wifi_config_t conf; + esp_wifi_get_config (ESP_IF_WIFI_AP, &conf); + result+= (const char*)conf.ap.ssid; + result+=":IP="; + result+=WiFi.softAPIP().toString(); + result+=":MAC="; + tmp = WiFi.softAPmacAddress(); + tmp.replace(":","-"); + result += tmp; + } + if(WiFi.getMode() == WIFI_MODE_NULL)result+="No Wifi"; + result+= "]\r\n"; + return result.c_str(); +} + +/** + * Helper to convert IP string to int + */ + +uint32_t WiFiConfig::IP_int_from_string(String & s){ + uint32_t ip_int = 0; + IPAddress ipaddr; + if (ipaddr.fromString(s)) ip_int = ipaddr; + return ip_int; +} + +/** + * Helper to convert int to IP string + */ + +String WiFiConfig::IP_string_from_int(uint32_t ip_int){ + IPAddress ipaddr(ip_int); + return ipaddr.toString(); +} + +/** + * Check if Hostname string is valid + */ + +bool WiFiConfig::isHostnameValid (const char * hostname) +{ + //limited size + char c; + if (strlen (hostname) > MAX_HOSTNAME_LENGTH || strlen (hostname) < MIN_HOSTNAME_LENGTH) { + return false; + } + //only letter and digit + for (int i = 0; i < strlen (hostname); i++) { + c = hostname[i]; + if (! (isdigit (c) || isalpha (c) || c == '_') ) { + return false; + } + if (c == ' ') { + return false; + } + } + return true; +} + +/** + * Check if SSID string is valid + */ + +bool WiFiConfig::isSSIDValid (const char * ssid) +{ + //limited size + //char c; + if (strlen (ssid) > MAX_SSID_LENGTH || strlen (ssid) < MIN_SSID_LENGTH) { + return false; + } + //only printable + for (int i = 0; i < strlen (ssid); i++) { + if (!isPrintable (ssid[i]) ) { + return false; + } + } + return true; +} + +/** + * Check if password string is valid + */ + +bool WiFiConfig::isPasswordValid (const char * password) +{ + if (strlen (password) == 0) return true; //open network + //limited size + if ((strlen (password) > MAX_PASSWORD_LENGTH) || (strlen (password) < MIN_PASSWORD_LENGTH)) { + return false; + } + //no space allowed ? + /* for (int i = 0; i < strlen (password); i++) + if (password[i] == ' ') { + return false; + }*/ + return true; +} + +/** + * Check if IP string is valid + */ +bool WiFiConfig::isValidIP(const char * string){ + IPAddress ip; + return ip.fromString(string); +} + +/* + * delay is to avoid with asyncwebserver and may need to wait sometimes + */ +void WiFiConfig::wait(uint32_t milliseconds){ + uint32_t timeout = millis(); + esp_task_wdt_reset(); //for a wait 0; + //wait feeding WDT + while ( (millis() - timeout) < milliseconds) { + esp_task_wdt_reset(); + } +} + +/** + * WiFi events + * SYSTEM_EVENT_WIFI_READY < ESP32 WiFi ready + * SYSTEM_EVENT_SCAN_DONE < ESP32 finish scanning AP + * SYSTEM_EVENT_STA_START < ESP32 station start + * SYSTEM_EVENT_STA_STOP < ESP32 station stop + * SYSTEM_EVENT_STA_CONNECTED < ESP32 station connected to AP + * SYSTEM_EVENT_STA_DISCONNECTED < ESP32 station disconnected from AP + * SYSTEM_EVENT_STA_AUTHMODE_CHANGE < the auth mode of AP connected by ESP32 station changed + * SYSTEM_EVENT_STA_GOT_IP < ESP32 station got IP from connected AP + * SYSTEM_EVENT_STA_LOST_IP < ESP32 station lost IP and the IP is reset to 0 + * SYSTEM_EVENT_STA_WPS_ER_SUCCESS < ESP32 station wps succeeds in enrollee mode + * SYSTEM_EVENT_STA_WPS_ER_FAILED < ESP32 station wps fails in enrollee mode + * SYSTEM_EVENT_STA_WPS_ER_TIMEOUT < ESP32 station wps timeout in enrollee mode + * SYSTEM_EVENT_STA_WPS_ER_PIN < ESP32 station wps pin code in enrollee mode + * SYSTEM_EVENT_AP_START < ESP32 soft-AP start + * SYSTEM_EVENT_AP_STOP < ESP32 soft-AP stop + * SYSTEM_EVENT_AP_STACONNECTED < a station connected to ESP32 soft-AP + * SYSTEM_EVENT_AP_STADISCONNECTED < a station disconnected from ESP32 soft-AP + * SYSTEM_EVENT_AP_PROBEREQRECVED < Receive probe request packet in soft-AP interface + * SYSTEM_EVENT_GOT_IP6 < ESP32 station or ap or ethernet interface v6IP addr is preferred + * SYSTEM_EVENT_ETH_START < ESP32 ethernet start + * SYSTEM_EVENT_ETH_STOP < ESP32 ethernet stop + * SYSTEM_EVENT_ETH_CONNECTED < ESP32 ethernet phy link up + * SYSTEM_EVENT_ETH_DISCONNECTED < ESP32 ethernet phy link down + * SYSTEM_EVENT_ETH_GOT_IP < ESP32 ethernet got IP from connected AP + * SYSTEM_EVENT_MAX + */ + +void WiFiConfig::WiFiEvent(WiFiEvent_t event) +{ + switch (event) + { + case SYSTEM_EVENT_STA_GOT_IP: + grbl_sendf(CLIENT_ALL,"[MSG:Connected with %s]\r\n",WiFi.localIP().toString().c_str()); + break; + case SYSTEM_EVENT_STA_DISCONNECTED: + grbl_send(CLIENT_ALL,"[MSG:Disconnected]\r\n"); + break; + default: + break; + } +} + +/* + * Get WiFi signal strength + */ +int32_t WiFiConfig::getSignal (int32_t RSSI) +{ + if (RSSI <= -100) { + return 0; + } + if (RSSI >= -50) { + return 100; + } + return (2 * (RSSI + 100) ); +} + +/* + * Connect client to AP + */ + +bool WiFiConfig::ConnectSTA2AP(){ + String msg, msg_out; + uint8_t count = 0; + uint8_t dot = 0; + wl_status_t status = WiFi.status(); + while (status != WL_CONNECTED && count < 40) { + + switch (status) { + case WL_NO_SSID_AVAIL: + msg="No SSID"; + break; + case WL_CONNECT_FAILED: + msg="Connection failed"; + break; + case WL_CONNECTED: + break; + default: + if ((dot>3) || (dot==0) ){ + dot=0; + msg_out = "Connecting"; + } + msg_out+="."; + msg= msg_out; + dot++; + break; + } + grbl_sendf(CLIENT_ALL,"[MSG:%s]\r\n",msg.c_str()); + wait (500); + count++; + status = WiFi.status(); + } + return (status == WL_CONNECTED); +} + +/* + * Start client mode (Station) + */ + +bool WiFiConfig::StartSTA(){ + String defV; + Preferences prefs; + //stop active service + wifi_services.end(); + //Sanity check + if((WiFi.getMode() == WIFI_STA) || (WiFi.getMode() == WIFI_AP_STA))WiFi.disconnect(); + if((WiFi.getMode() == WIFI_AP) || (WiFi.getMode() == WIFI_AP_STA))WiFi.softAPdisconnect(); + WiFi.enableAP (false); + WiFi.mode(WIFI_STA); + //Get parameters for STA + prefs.begin(NAMESPACE, true); + defV = DEFAULT_HOSTNAME; + String h = prefs.getString(HOSTNAME_ENTRY, defV); + WiFi.setHostname(h.c_str()); + //SSID + defV = DEFAULT_STA_SSID; + String SSID = prefs.getString(STA_SSID_ENTRY, defV); + if (SSID.length() == 0)SSID = DEFAULT_STA_SSID; + //password + defV = DEFAULT_STA_PWD; + String password = prefs.getString(STA_PWD_ENTRY, defV); + int8_t IP_mode = prefs.getChar(STA_IP_MODE_ENTRY, DHCP_MODE); + //IP + defV = DEFAULT_STA_IP; + int32_t IP = prefs.getInt(STA_IP_ENTRY, IP_int_from_string(defV)); + //GW + defV = DEFAULT_STA_GW; + int32_t GW = prefs.getInt(STA_GW_ENTRY, IP_int_from_string(defV)); + //MK + defV = DEFAULT_STA_MK; + int32_t MK = prefs.getInt(STA_MK_ENTRY, IP_int_from_string(defV)); + prefs.end(); + //if not DHCP + if (IP_mode != DHCP_MODE) { + IPAddress ip(IP), mask(MK), gateway(GW); + WiFi.config(ip, gateway,mask); + } + if (WiFi.begin(SSID.c_str(), (password.length() > 0)?password.c_str():NULL)){ + grbl_send(CLIENT_ALL,"\n[MSG:Client Started]\r\n"); + grbl_sendf(CLIENT_ALL,"[MSG:Connecting %s]\r\n", SSID.c_str()); + return ConnectSTA2AP(); + } else { + grbl_send(CLIENT_ALL,"[MSG:Starting client failed]\r\n"); + return false; + } +} + +/** + * Setup and start Access point + */ + +bool WiFiConfig::StartAP(){ + String defV; + Preferences prefs; + //stop active services + wifi_services.end(); + //Sanity check + if((WiFi.getMode() == WIFI_STA) || (WiFi.getMode() == WIFI_AP_STA))WiFi.disconnect(); + if((WiFi.getMode() == WIFI_AP) || (WiFi.getMode() == WIFI_AP_STA))WiFi.softAPdisconnect(); + WiFi.enableSTA (false); + WiFi.mode(WIFI_AP); + //Get parameters for AP + prefs.begin(NAMESPACE, true); + //SSID + defV = DEFAULT_AP_SSID; + String SSID = prefs.getString(AP_SSID_ENTRY, defV); + if (SSID.length() == 0)SSID = DEFAULT_AP_SSID; + //password + defV = DEFAULT_AP_PWD; + String password = prefs.getString(AP_PWD_ENTRY, defV); + //channel + int8_t channel = prefs.getChar(AP_CHANNEL_ENTRY, DEFAULT_AP_CHANNEL); + if (channel == 0)channel = DEFAULT_AP_CHANNEL; + //IP + defV = DEFAULT_AP_IP; + int32_t IP = prefs.getInt(AP_IP_ENTRY, IP_int_from_string(defV)); + if (IP==0){ + IP = IP_int_from_string(defV); + } + prefs.end(); + IPAddress ip(IP); + IPAddress mask; + mask.fromString(DEFAULT_AP_MK); + //Set static IP + WiFi.softAPConfig(ip, ip, mask); + //Start AP + if(WiFi.softAP(SSID.c_str(), (password.length() > 0)?password.c_str():NULL, channel)) { + grbl_sendf(CLIENT_ALL,"\n[MSG:AP Started %s]\r\n", WiFi.softAPIP().toString().c_str()); + return true; + } else { + grbl_send(CLIENT_ALL,"[MSG:Starting AP failed]\r\n"); + return false; + } +} + +/** + * Stop WiFi + */ + +void WiFiConfig::StopWiFi(){ + //Sanity check + if((WiFi.getMode() == WIFI_STA) || (WiFi.getMode() == WIFI_AP_STA))WiFi.disconnect(true); + if((WiFi.getMode() == WIFI_AP) || (WiFi.getMode() == WIFI_AP_STA))WiFi.softAPdisconnect(true); + wifi_services.end(); + WiFi.mode(WIFI_OFF); + grbl_send(CLIENT_ALL,"\n[MSG:WiFi Off]\r\n"); +} + +/** + * begin WiFi setup + */ +void WiFiConfig::begin() { + Preferences prefs; + //stop active services + wifi_services.end(); + //setup events + WiFi.onEvent(WiFiConfig::WiFiEvent); + //open preferences as read-only + prefs.begin(NAMESPACE, true); + int8_t wifiMode = prefs.getChar(ESP_WIFI_MODE, DEFAULT_WIFI_MODE); + prefs.end(); + if (wifiMode == ESP_WIFI_AP) { + StartAP(); + //start services + wifi_services.begin(); + } else if (wifiMode == ESP_WIFI_STA){ + if(!StartSTA()){ + grbl_send(CLIENT_ALL,"[MSG:Cannot connect to AP]\r\n"); + StartAP(); + } + //start services + wifi_services.begin(); + }else WiFi.mode(WIFI_OFF); +} + +/** + * End WiFi + */ +void WiFiConfig::end() { + StopWiFi(); +} + +/** + * Restart ESP + */ +void WiFiConfig::restart_ESP(){ + restart_ESP_module=true; +} + +/** + * Restart ESP + */ +void WiFiConfig::reset_ESP(){ + Preferences prefs; + prefs.begin(NAMESPACE, false); + String sval; + int8_t bbuf; + int16_t ibuf; + bool error = false; + sval = DEFAULT_HOSTNAME; + if (prefs.putString(HOSTNAME_ENTRY, sval) == 0){ + error = true; + } + sval = DEFAULT_STA_SSID; + if (prefs.putString(STA_SSID_ENTRY, sval) == 0){ + error = true; + } + sval = DEFAULT_STA_PWD; + if (prefs.putString(STA_PWD_ENTRY, sval) == 0){ + error = true; + } + sval = DEFAULT_AP_SSID; + if (prefs.putString(AP_SSID_ENTRY, sval) == 0){ + error = true; + } + sval = DEFAULT_AP_PWD; + if (prefs.putString(AP_PWD_ENTRY, sval) == 0){ + error = true; + } + + bbuf = DEFAULT_AP_CHANNEL; + if (prefs.putChar(AP_CHANNEL_ENTRY, bbuf) ==0 ) { + error = true; + } + bbuf = DEFAULT_STA_IP_MODE; + if (prefs.putChar(STA_IP_MODE_ENTRY, bbuf) ==0 ) { + error = true; + } + bbuf = DEFAULT_HTTP_STATE; + if (prefs.putChar(HTTP_ENABLE_ENTRY, bbuf) ==0 ) { + error = true; + } + bbuf = DEFAULT_TELNET_STATE; + if (prefs.putChar(TELNET_ENABLE_ENTRY, bbuf) ==0 ) { + error = true; + } + bbuf = DEFAULT_WIFI_MODE; + if (prefs.putChar(ESP_WIFI_MODE, bbuf) ==0 ) { + error = true; + } + ibuf = DEFAULT_WEBSERVER_PORT; + if (prefs.putUShort(HTTP_PORT_ENTRY, ibuf) == 0) { + error = true; + } + ibuf = DEFAULT_TELNETSERVER_PORT; + if (prefs.putUShort(TELNET_PORT_ENTRY, ibuf) == 0) { + error = true; + } + sval = DEFAULT_STA_IP; + if (prefs.putInt(STA_IP_ENTRY, wifi_config.IP_int_from_string(sval)) == 0) { + error = true; + } + sval = DEFAULT_STA_GW; + if (prefs.putInt(STA_GW_ENTRY, wifi_config.IP_int_from_string(sval)) == 0) { + error = true; + } + sval = DEFAULT_STA_MK; + if (prefs.putInt(STA_MK_ENTRY, wifi_config.IP_int_from_string(sval)) == 0) { + error = true; + } + sval = DEFAULT_AP_IP; + if (prefs.putInt(AP_IP_ENTRY, wifi_config.IP_int_from_string(sval)) == 0) { + error = true; + } + prefs.end(); + if (error) { + grbl_send(CLIENT_ALL,"[MSG:WiFi reset error]\r\n"); + } else { + grbl_send(CLIENT_ALL,"[MSG:WiFi reset done]\r\n"); + } +} + + +/** + * Handle not critical actions that must be done in sync environement + */ +void WiFiConfig::handle() { + //in case of restart requested + if (restart_ESP_module) { + end(); + ESP.restart(); + while (1) {}; + } + + //Services + wifi_services.handle(); +} + + +#endif // ENABLE_WIFI + +#endif // ARDUINO_ARCH_ESP32 diff --git a/Grbl_Esp32/wificonfig.h b/Grbl_Esp32/wificonfig.h new file mode 100644 index 00000000..2b967038 --- /dev/null +++ b/Grbl_Esp32/wificonfig.h @@ -0,0 +1,123 @@ +/* + wificonfig.h - wifi functions class + + Copyright (c) 2014 Luc Lebosse. All rights reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +//Preferences entries +#define NAMESPACE "GRBL" +#define HOSTNAME_ENTRY "ESP_HOSTNAME" +#define STA_SSID_ENTRY "STA_SSID" +#define STA_PWD_ENTRY "STA_PWD" +#define STA_IP_ENTRY "STA_IP" +#define STA_GW_ENTRY "STA_GW" +#define STA_MK_ENTRY "STA_MK" +#define ESP_WIFI_MODE "WIFI_MODE" +#define AP_SSID_ENTRY "AP_SSID" +#define AP_PWD_ENTRY "AP_PWD" +#define AP_IP_ENTRY "AP_IP" +#define AP_CHANNEL_ENTRY "AP_CHANNEL" +#define HTTP_ENABLE_ENTRY "HTTP_ON" +#define HTTP_PORT_ENTRY "HTTP_PORT" +#define TELNET_ENABLE_ENTRY "TELNET_ON" +#define TELNET_PORT_ENTRY "TELNET_PORT" +#define STA_IP_MODE_ENTRY "STA_IP_MODE" + +//Wifi Mode +#define ESP_WIFI_OFF 0 +#define ESP_WIFI_STA 1 +#define ESP_WIFI_AP 2 + +#define DHCP_MODE 0 +#define STATIC_MODE 0 + +//Switch +#define ESP_SAVE_ONLY 0 +#define ESP_APPLY_NOW 1 + +//defaults values +#define DEFAULT_HOSTNAME "grblesp" +#define DEFAULT_STA_SSID "GRBL_ESP" +#define DEFAULT_STA_PWD "12345678" +#define DEFAULT_STA_IP "0.0.0.0" +#define DEFAULT_STA_GW "0.0.0.0" +#define DEFAULT_STA_MK "0.0.0.0" +#define DEFAULT_WIFI_MODE ESP_WIFI_AP +#define DEFAULT_AP_SSID "GRBL_ESP" +#define DEFAULT_AP_PWD "12345678" +#define DEFAULT_AP_IP "192.168.0.1" +#define DEFAULT_AP_MK "255.255.255.0" +#define DEFAULT_AP_CHANNEL 1 +#define DEFAULT_WEBSERVER_PORT 80 +#define DEFAULT_HTTP_STATE 1 +#define DEFAULT_TELNETSERVER_PORT 23 +#define DEFAULT_TELNET_STATE 1 +#define DEFAULT_STA_IP_MODE DHCP_MODE +#define HIDDEN_PASSWORD "********" + +//boundaries +#define MAX_SSID_LENGTH 32 +#define MIN_SSID_LENGTH 1 +#define MAX_PASSWORD_LENGTH 64 +//min size of password is 0 or upper than 8 char +//so let set min is 8 +#define MIN_PASSWORD_LENGTH 8 +#define MAX_HOSTNAME_LENGTH 32 +#define MIN_HOSTNAME_LENGTH 1 +#define MAX_HTTP_PORT 65001 +#define MIN_HTTP_PORT 1 +#define MAX_TELNET_PORT 65001 +#define MIN_TELNET_PORT 1 +#define MIN_CHANNEL 1 +#define MAX_CHANNEL 14 + + +#ifndef _WIFI_CONFIG_H +#define _WIFI_CONFIG_H +#include "WiFi.h" + +class WiFiConfig { +public: + WiFiConfig(); + ~WiFiConfig(); + static const char *info(); + static void wait(uint32_t milliseconds); + static bool isValidIP(const char * string); + static bool isPasswordValid (const char * password); + static bool isSSIDValid (const char * ssid); + static bool isHostnameValid (const char * hostname); + static uint32_t IP_int_from_string(String & s); + static String IP_string_from_int(uint32_t ip_int); + + static bool StartAP(); + static bool StartSTA(); + static void StopWiFi(); + static int32_t getSignal (int32_t RSSI); + static void begin(); + static void end(); + static void handle(); + static void restart_ESP(); + static void reset_ESP(); + private : + static bool ConnectSTA2AP(); + static void WiFiEvent(WiFiEvent_t event); + static bool restart_ESP_module; +}; + +extern WiFiConfig wifi_config; + +#endif diff --git a/Grbl_Esp32/wifiservices.cpp b/Grbl_Esp32/wifiservices.cpp new file mode 100644 index 00000000..a6cc5fe3 --- /dev/null +++ b/Grbl_Esp32/wifiservices.cpp @@ -0,0 +1,154 @@ +/* + wifiservices.cpp - wifi services functions class + + Copyright (c) 2014 Luc Lebosse. All rights reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifdef ARDUINO_ARCH_ESP32 + +#include "config.h" + +#ifdef ENABLE_WIFI + +#include +#include +#include +#include +#include "report.h" +#include "wificonfig.h" +#include "wifiservices.h" +#ifdef ENABLE_MDNS +#include +#endif +#ifdef ENABLE_OTA +#include +#endif +#ifdef ENABLE_HTTP +#include "web_server.h" +#endif +#ifdef ENABLE_TELNET +#include "telnet_server.h" +#endif + +WiFiServices wifi_services; + +WiFiServices::WiFiServices(){ +} +WiFiServices::~WiFiServices(){ + end(); +} + +bool WiFiServices::begin(){ + bool no_error = true; + //Sanity check + if(WiFi.getMode() == WIFI_OFF) return false; + String h; + Preferences prefs; + //Get hostname + String defV = DEFAULT_HOSTNAME; + prefs.begin(NAMESPACE, true); + h = prefs.getString(HOSTNAME_ENTRY, defV); + prefs.end(); + WiFi.scanNetworks (true); + //Start SPIFFS + SPIFFS.begin(true); +#ifdef ENABLE_MDNS + //no need in AP mode + if(WiFi.getMode() == WIFI_STA){ + //start mDns + if (!MDNS.begin(h.c_str())) { + grbl_send(CLIENT_ALL,"[MSG:Cannot start mDNS]\r\n"); + no_error = false; + } else { + grbl_sendf(CLIENT_ALL,"[MSG:Start mDNS with hostname:%s]\r\n",h.c_str()); + } + } +#endif +#ifdef ENABLE_OTA + ArduinoOTA + .onStart([]() { + String type; + if (ArduinoOTA.getCommand() == U_FLASH) + type = "sketch"; + else {// U_SPIFFS + // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() + type = "filesystem"; + SPIFFS.end(); + } + grbl_sendf(CLIENT_ALL,"[MSG:Start OTA updating %s]\r\n", type.c_str()); + }) + .onEnd([]() { + grbl_sendf(CLIENT_ALL,"[MSG:End OTA]\r\n"); + + }) + .onProgress([](unsigned int progress, unsigned int total) { + grbl_sendf(CLIENT_ALL,"[MSG:OTA Progress: %u%%]\r\n", (progress / (total / 100))); + }) + .onError([](ota_error_t error) { + grbl_sendf(CLIENT_ALL,"[MSG:OTA Error(%u):]\r\n", error); + if (error == OTA_AUTH_ERROR) grbl_send(CLIENT_ALL,"[MSG:Auth Failed]\r\n"); + else if (error == OTA_BEGIN_ERROR) grbl_send(CLIENT_ALL,"[MSG:Begin Failed]\r\n"); + else if (error == OTA_CONNECT_ERROR) grbl_send(CLIENT_ALL,"[MSG:Connect Failed]\r\n"); + else if (error == OTA_RECEIVE_ERROR) grbl_send(CLIENT_ALL,"[MSG:Receive Failed]\r\n"); + else if (error == OTA_END_ERROR) grbl_send(CLIENT_ALL,"[MSG:End Failed]\r\n"); + }); + ArduinoOTA.begin(); +#endif +#ifdef ENABLE_HTTP + web_server.begin(); +#endif +#ifdef ENABLE_TELNET + telnet_server.begin(); +#endif + return no_error; +} +void WiFiServices::end(){ +#ifdef ENABLE_TELNET + telnet_server.end(); +#endif + +#ifdef ENABLE_HTTP + web_server.end(); +#endif + //stop OTA +#ifdef ENABLE_OTA + ArduinoOTA.end(); +#endif + //Stop SPIFFS + SPIFFS.end(); + +#ifdef ENABLE_MDNS + //Stop mDNS + //MDNS.end(); +#endif +} + +void WiFiServices::handle(){ +#ifdef ENABLE_OTA + ArduinoOTA.handle(); +#endif +#ifdef ENABLE_HTTP + web_server.handle(); +#endif +#ifdef ENABLE_TELNET + telnet_server.handle(); +#endif +} + +#endif // ENABLE_WIFI + +#endif // ARDUINO_ARCH_ESP32 diff --git a/Grbl_Esp32/wifiservices.h b/Grbl_Esp32/wifiservices.h new file mode 100644 index 00000000..790fd676 --- /dev/null +++ b/Grbl_Esp32/wifiservices.h @@ -0,0 +1,39 @@ +/* + wifiservices.h - wifi services functions class + + Copyright (c) 2014 Luc Lebosse. All rights reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + + + +#ifndef _WIFI_SERVICES_H +#define _WIFI_SERVICES_H + + +class WiFiServices { + public: + WiFiServices(); + ~WiFiServices(); + static bool begin(); + static void end(); + static void handle(); +}; + +extern WiFiServices wifi_services; + +#endif + diff --git a/libraries/ESP32SSDP/ESP32SSDP.cpp b/libraries/ESP32SSDP/ESP32SSDP.cpp new file mode 100644 index 00000000..866cce57 --- /dev/null +++ b/libraries/ESP32SSDP/ESP32SSDP.cpp @@ -0,0 +1,442 @@ +/* +ESP32 Simple Service Discovery +Copyright (c) 2015 Hristo Gochkov + +Original (Arduino) version by Filippo Sallemi, July 23, 2014. +Can be found at: https://github.com/nomadnt/uSSDP + +License (MIT license): + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + +#include +#include "ESP32SSDP.h" +#include "WiFiUdp.h" +#include + +//#define DEBUG_SSDP Serial + +#define SSDP_INTERVAL 1200 +#define SSDP_PORT 1900 +#define SSDP_METHOD_SIZE 10 +#define SSDP_URI_SIZE 2 +#define SSDP_BUFFER_SIZE 64 +#define SSDP_MULTICAST_TTL 2 +static const IPAddress SSDP_MULTICAST_ADDR(239, 255, 255, 250); + + + +static const char _ssdp_response_template[] PROGMEM = + "HTTP/1.1 200 OK\r\n" + "EXT:\r\n"; + +static const char _ssdp_notify_template[] PROGMEM = + "NOTIFY * HTTP/1.1\r\n" + "HOST: 239.255.255.250:1900\r\n" + "NTS: ssdp:alive\r\n"; + +static const char _ssdp_packet_template[] PROGMEM = + "%s" // _ssdp_response_template / _ssdp_notify_template + "CACHE-CONTROL: max-age=%u\r\n" // SSDP_INTERVAL + "SERVER: Arduino/1.0 UPNP/1.1 %s/%s\r\n" // _modelName, _modelNumber + "USN: uuid:%s\r\n" // _uuid + "%s: %s\r\n" // "NT" or "ST", _deviceType + "LOCATION: http://%u.%u.%u.%u:%u/%s\r\n" // WiFi.localIP(), _port, _schemaURL + "\r\n"; + +static const char _ssdp_schema_template[] PROGMEM = + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/xml\r\n" + "Connection: close\r\n" + "Access-Control-Allow-Origin: *\r\n" + "\r\n" + "" + "" + "" + "1" + "0" + "" + "http://%u.%u.%u.%u:%u/" // WiFi.localIP(), _port + "" + "%s" + "%s" + "%s" + "%s" + "%s" + "%s" + "%s" + "%s" + "%s" + "uuid:%s" + "" +// "" +// "" +// "image/png" +// "48" +// "48" +// "24" +// "icon48.png" +// "" +// "" +// "image/png" +// "120" +// "120" +// "24" +// "icon120.png" +// "" +// "" + "\r\n" + "\r\n"; + +struct SSDPTimer { + ETSTimer timer; +}; + +SSDPClass::SSDPClass() : +_server(0), +_timer(new SSDPTimer), +_port(80), +_ttl(SSDP_MULTICAST_TTL), +_respondToPort(0), +_pending(false), +_delay(0), +_process_time(0), +_notify_time(0) +{ + _uuid[0] = '\0'; + _modelNumber[0] = '\0'; + sprintf(_deviceType, "urn:schemas-upnp-org:device:Basic:1"); + _friendlyName[0] = '\0'; + _presentationURL[0] = '\0'; + _serialNumber[0] = '\0'; + _modelName[0] = '\0'; + _modelURL[0] = '\0'; + _manufacturer[0] = '\0'; + _manufacturerURL[0] = '\0'; + sprintf(_schemaURL, "ssdp/schema.xml"); +} + +SSDPClass::~SSDPClass(){ + delete _timer; +} + + +bool SSDPClass::begin(){ + _pending = false; + + uint32_t chipId = ((uint16_t) (ESP.getEfuseMac() >> 32)); + sprintf(_uuid, "38323636-4558-4dda-9188-cda0e6%02x%02x%02x", + (uint16_t) ((chipId >> 16) & 0xff), + (uint16_t) ((chipId >> 8) & 0xff), + (uint16_t) chipId & 0xff ); + +#ifdef DEBUG_SSDP + DEBUG_SSDP.printf("SSDP UUID: %s\n", (char *)_uuid); +#endif + + if (_server) { + delete (_server); + _server = 0; + } + + _server = new WiFiUDP; + + if (!(_server->beginMulticast(IPAddress(SSDP_MULTICAST_ADDR), SSDP_PORT))) { +#ifdef DEBUG_SSDP + DEBUG_SSDP.println("Error begin"); +#endif + return false; + } + + _startTimer(); + + return true; +} + +void SSDPClass::_send(ssdp_method_t method){ + char buffer[1460]; + IPAddress ip = WiFi.localIP(); + + char valueBuffer[strlen_P(_ssdp_notify_template)+1]; + strcpy_P(valueBuffer, (method == NONE)?_ssdp_response_template:_ssdp_notify_template); + + int len = snprintf_P(buffer, sizeof(buffer), + _ssdp_packet_template, + valueBuffer, + SSDP_INTERVAL, + _modelName, _modelNumber, + _uuid, + (method == NONE)?"ST":"NT", + _deviceType, + ip[0], ip[1], ip[2], ip[3], _port, _schemaURL + ); + + IPAddress remoteAddr; + uint16_t remotePort; + if(method == NONE) { + remoteAddr = _respondToAddr; + remotePort = _respondToPort; +#ifdef DEBUG_SSDP + DEBUG_SSDP.print("Sending Response to "); +#endif + } else { + remoteAddr = IPAddress(SSDP_MULTICAST_ADDR); + remotePort = SSDP_PORT; +#ifdef DEBUG_SSDP + DEBUG_SSDP.println("Sending Notify to "); +#endif + } +#ifdef DEBUG_SSDP + DEBUG_SSDP.print(remoteAddr); + DEBUG_SSDP.print(":"); + DEBUG_SSDP.println(remotePort); +#endif + _server->beginPacket(remoteAddr, remotePort); + _server->println(buffer); + _server->endPacket(); +} + +void SSDPClass::schema(WiFiClient client){ + IPAddress ip = WiFi.localIP(); + char buffer[strlen_P(_ssdp_schema_template)+1]; + strcpy_P(buffer, _ssdp_schema_template); + client.printf(buffer, + ip[0], ip[1], ip[2], ip[3], _port, + _deviceType, + _friendlyName, + _presentationURL, + _serialNumber, + _modelName, + _modelNumber, + _modelURL, + _manufacturer, + _manufacturerURL, + _uuid + ); +} + +void SSDPClass::_update(){ + int nbBytes =0; + char * packetBuffer = NULL; + + if(!_pending && _server) { + ssdp_method_t method = NONE; + nbBytes= _server->parsePacket(); + typedef enum {METHOD, URI, PROTO, KEY, VALUE, ABORT} states; + states state = METHOD; + typedef enum {START, MAN, ST, MX} headers; + headers header = START; + + uint8_t cursor = 0; + uint8_t cr = 0; + + char buffer[SSDP_BUFFER_SIZE] = {0}; + packetBuffer = new char[nbBytes +1]; + int message_size=_server->read(packetBuffer,nbBytes); + int process_pos = 0; + packetBuffer[message_size]='\0'; + _respondToAddr = _server->remoteIP(); + _respondToPort = _server->remotePort(); +#ifdef DEBUG_SSDP + if (message_size) { + DEBUG_SSDP.println("****************************************************"); + DEBUG_SSDP.println(_server->remoteIP()); + DEBUG_SSDP.println(packetBuffer); + DEBUG_SSDP.println("****************************************************"); + } +#endif + while(process_pos < message_size){ + + char c = packetBuffer[process_pos]; + process_pos++; + (c == '\r' || c == '\n') ? cr++ : cr = 0; +#ifdef DEBUG_SSDP + if ((c == '\r' || c == '\n') && (cr < 2)) DEBUG_SSDP.println(buffer); +#endif + switch(state){ + case METHOD: + if(c == ' '){ + if(strcmp(buffer, "M-SEARCH") == 0) method = SEARCH; + + if(method == NONE) state = ABORT; + else state = URI; + cursor = 0; + + } else if(cursor < SSDP_METHOD_SIZE - 1){ buffer[cursor++] = c; buffer[cursor] = '\0'; } + break; + case URI: + if(c == ' '){ + if(strcmp(buffer, "*")) state = ABORT; + else state = PROTO; + cursor = 0; + } else if(cursor < SSDP_URI_SIZE - 1){ buffer[cursor++] = c; buffer[cursor] = '\0'; } + break; + case PROTO: + if(cr == 2){ state = KEY; cursor = 0; } + break; + case KEY: + if(cr == 4){ _pending = true; _process_time = millis(); } + else if(c == ' '){ cursor = 0; state = VALUE; } + else if(c != '\r' && c != '\n' && c != ':' && cursor < SSDP_BUFFER_SIZE - 1){ buffer[cursor++] = c; buffer[cursor] = '\0'; } + break; + case VALUE: + if(cr == 2){ + switch(header){ + case START: + break; + case MAN: +#ifdef DEBUG_SSDP + DEBUG_SSDP.printf("MAN: %s\n", (char *)buffer); +#endif + break; + case ST: + if(strcmp(buffer, "ssdp:all")){ + state = ABORT; +#ifdef DEBUG_SSDP + DEBUG_SSDP.printf("REJECT: %s\n", (char *)buffer); +#endif + } + // if the search type matches our type, we should respond instead of ABORT + if(strcasecmp(buffer, _deviceType) == 0){ + _pending = true; + _process_time = 0; +#ifdef DEBUG_SSDP + DEBUG_SSDP.println("the search type matches our type"); +#endif + state = KEY; + } + break; + case MX: + _delay = random(0, atoi(buffer)) * 1000L; + break; + } + + if(state != ABORT){ state = KEY; header = START; cursor = 0; } + } else if(c != '\r' && c != '\n'){ + if(header == START){ + if(strncmp(buffer, "MA", 2) == 0) header = MAN; + else if(strcmp(buffer, "ST") == 0) header = ST; + else if(strcmp(buffer, "MX") == 0) header = MX; + } + + if(cursor < SSDP_BUFFER_SIZE - 1){ buffer[cursor++] = c; buffer[cursor] = '\0'; } + } + break; + case ABORT: + _pending = false; _delay = 0; + break; + } + } + } + if(packetBuffer) delete packetBuffer; + if(_pending && (millis() - _process_time) > _delay){ + _pending = false; _delay = 0; +#ifdef DEBUG_SSDP + DEBUG_SSDP.println("Send None"); +#endif + _send(NONE); + } else if(_notify_time == 0 || (millis() - _notify_time) > (SSDP_INTERVAL * 1000L)){ + _notify_time = millis(); + #ifdef DEBUG_SSDP + DEBUG_SSDP.println("Send Notify"); +#endif + _send(NOTIFY); + } else { +#ifdef DEBUG_SSDP + DEBUG_SSDP.println("Do not sent"); +#endif + } + + if (_pending) { + _server->flush(); + } + +} + +void SSDPClass::setSchemaURL(const char *url){ + strlcpy(_schemaURL, url, sizeof(_schemaURL)); +} + +void SSDPClass::setHTTPPort(uint16_t port){ + _port = port; +} + +void SSDPClass::setDeviceType(const char *deviceType){ + strlcpy(_deviceType, deviceType, sizeof(_deviceType)); +} + +void SSDPClass::setName(const char *name){ + strlcpy(_friendlyName, name, sizeof(_friendlyName)); +} + +void SSDPClass::setURL(const char *url){ + strlcpy(_presentationURL, url, sizeof(_presentationURL)); +} + +void SSDPClass::setSerialNumber(const char *serialNumber){ + strlcpy(_serialNumber, serialNumber, sizeof(_serialNumber)); +} + +void SSDPClass::setSerialNumber(const uint32_t serialNumber){ + snprintf(_serialNumber, sizeof(uint32_t)*2+1, "%08X", serialNumber); +} + +void SSDPClass::setModelName(const char *name){ + strlcpy(_modelName, name, sizeof(_modelName)); +} + +void SSDPClass::setModelNumber(const char *num){ + strlcpy(_modelNumber, num, sizeof(_modelNumber)); +} + +void SSDPClass::setModelURL(const char *url){ + strlcpy(_modelURL, url, sizeof(_modelURL)); +} + +void SSDPClass::setManufacturer(const char *name){ + strlcpy(_manufacturer, name, sizeof(_manufacturer)); +} + +void SSDPClass::setManufacturerURL(const char *url){ + strlcpy(_manufacturerURL, url, sizeof(_manufacturerURL)); +} + +void SSDPClass::setTTL(const uint8_t ttl){ + _ttl = ttl; +} + +void SSDPClass::_onTimerStatic(SSDPClass* self) { +#ifdef DEBUG_SSDP + DEBUG_SSDP.println("Update"); +#endif + self->_update(); +} + +void SSDPClass::_startTimer() { + ETSTimer* tm = &(_timer->timer); + const int interval = 1000; + ets_timer_disarm(tm); + ets_timer_setfn(tm, reinterpret_cast(&SSDPClass::_onTimerStatic), reinterpret_cast(this)); + ets_timer_arm(tm, interval, 1 /* repeat */); +} + +#if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_SSDP) +SSDPClass SSDP; +#endif diff --git a/libraries/ESP32SSDP/ESP32SSDP.h b/libraries/ESP32SSDP/ESP32SSDP.h new file mode 100644 index 00000000..e9017baa --- /dev/null +++ b/libraries/ESP32SSDP/ESP32SSDP.h @@ -0,0 +1,126 @@ +/* +ESP32 Simple Service Discovery +Copyright (c) 2015 Hristo Gochkov + +Original (Arduino) version by Filippo Sallemi, July 23, 2014. +Can be found at: https://github.com/nomadnt/uSSDP + +License (MIT license): + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + +#ifndef ESP32SSDP_H +#define ESP32SSDP_H + +#include +#include +#include + +#define SSDP_UUID_SIZE 37 +#define SSDP_SCHEMA_URL_SIZE 64 +#define SSDP_DEVICE_TYPE_SIZE 64 +#define SSDP_FRIENDLY_NAME_SIZE 64 +#define SSDP_SERIAL_NUMBER_SIZE 32 +#define SSDP_PRESENTATION_URL_SIZE 128 +#define SSDP_MODEL_NAME_SIZE 64 +#define SSDP_MODEL_URL_SIZE 128 +#define SSDP_MODEL_VERSION_SIZE 32 +#define SSDP_MANUFACTURER_SIZE 64 +#define SSDP_MANUFACTURER_URL_SIZE 128 + +typedef enum { + NONE, + SEARCH, + NOTIFY +} ssdp_method_t; + + +struct SSDPTimer; + +class SSDPClass{ + public: + SSDPClass(); + ~SSDPClass(); + + bool begin(); + + void schema(WiFiClient client); + + void setDeviceType(const String& deviceType) { setDeviceType(deviceType.c_str()); } + void setDeviceType(const char *deviceType); + void setName(const String& name) { setName(name.c_str()); } + void setName(const char *name); + void setURL(const String& url) { setURL(url.c_str()); } + void setURL(const char *url); + void setSchemaURL(const String& url) { setSchemaURL(url.c_str()); } + void setSchemaURL(const char *url); + void setSerialNumber(const String& serialNumber) { setSerialNumber(serialNumber.c_str()); } + void setSerialNumber(const char *serialNumber); + void setSerialNumber(const uint32_t serialNumber); + void setModelName(const String& name) { setModelName(name.c_str()); } + void setModelName(const char *name); + void setModelNumber(const String& num) { setModelNumber(num.c_str()); } + void setModelNumber(const char *num); + void setModelURL(const String& url) { setModelURL(url.c_str()); } + void setModelURL(const char *url); + void setManufacturer(const String& name) { setManufacturer(name.c_str()); } + void setManufacturer(const char *name); + void setManufacturerURL(const String& url) { setManufacturerURL(url.c_str()); } + void setManufacturerURL(const char *url); + void setHTTPPort(uint16_t port); + void setTTL(uint8_t ttl); + + protected: + void _send(ssdp_method_t method); + void _update(); + void _startTimer(); + static void _onTimerStatic(SSDPClass* self); + + WiFiUDP *_server; + SSDPTimer* _timer; + uint16_t _port; + uint8_t _ttl; + + IPAddress _respondToAddr; + uint16_t _respondToPort; + + bool _pending; + unsigned short _delay; + unsigned long _process_time; + unsigned long _notify_time; + + char _schemaURL[SSDP_SCHEMA_URL_SIZE]; + char _uuid[SSDP_UUID_SIZE]; + char _deviceType[SSDP_DEVICE_TYPE_SIZE]; + char _friendlyName[SSDP_FRIENDLY_NAME_SIZE]; + char _serialNumber[SSDP_SERIAL_NUMBER_SIZE]; + char _presentationURL[SSDP_PRESENTATION_URL_SIZE]; + char _manufacturer[SSDP_MANUFACTURER_SIZE]; + char _manufacturerURL[SSDP_MANUFACTURER_URL_SIZE]; + char _modelName[SSDP_MODEL_NAME_SIZE]; + char _modelURL[SSDP_MODEL_URL_SIZE]; + char _modelNumber[SSDP_MODEL_VERSION_SIZE]; +}; + +#if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_SSDP) +extern SSDPClass SSDP; +#endif + +#endif diff --git a/libraries/ESP32SSDP/README.rst b/libraries/ESP32SSDP/README.rst new file mode 100644 index 00000000..b2c92c75 --- /dev/null +++ b/libraries/ESP32SSDP/README.rst @@ -0,0 +1,22 @@ +ESP32 Simple Service Discovery Copyright (c) 2015 Hristo Gochkov +Original (Arduino) version by Filippo Sallemi, July 23, 2014. Can be +found at: https://github.com/nomadnt/uSSDP + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/libraries/ESP32SSDP/examples/SSDP/SSDP.ino b/libraries/ESP32SSDP/examples/SSDP/SSDP.ino new file mode 100644 index 00000000..8014a3a5 --- /dev/null +++ b/libraries/ESP32SSDP/examples/SSDP/SSDP.ino @@ -0,0 +1,51 @@ +#include +#include +#include + +const char* ssid = "********"; +const char* password = "********"; + +WebServer HTTP(80); + +void setup() { + Serial.begin(115200); + Serial.println(); + Serial.println("Starting WiFi..."); + + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + if(WiFi.waitForConnectResult() == WL_CONNECTED){ + + Serial.printf("Starting HTTP...\n"); + HTTP.on("/index.html", HTTP_GET, [](){ + HTTP.send(200, "text/plain", "Hello World!"); + }); + HTTP.on("/description.xml", HTTP_GET, [](){ + SSDP.schema(HTTP.client()); + }); + HTTP.begin(); + + Serial.printf("Starting SSDP...\n"); + SSDP.setSchemaURL("description.xml"); + SSDP.setHTTPPort(80); + SSDP.setName("Philips hue clone"); + SSDP.setSerialNumber("001788102201"); + SSDP.setURL("index.html"); + SSDP.setModelName("Philips hue bridge 2012"); + SSDP.setModelNumber("929000226503"); + SSDP.setModelURL("http://www.meethue.com"); + SSDP.setManufacturer("Royal Philips Electronics"); + SSDP.setManufacturerURL("http://www.philips.com"); + SSDP.begin(); + + Serial.printf("Ready!\n"); + } else { + Serial.printf("WiFi Failed\n"); + while(1) delay(100); + } +} + +void loop() { + HTTP.handleClient(); + delay(1); +} diff --git a/libraries/ESP32SSDP/keywords.txt b/libraries/ESP32SSDP/keywords.txt new file mode 100644 index 00000000..241d3414 --- /dev/null +++ b/libraries/ESP32SSDP/keywords.txt @@ -0,0 +1,53 @@ +####################################### +# Syntax Coloring Map For Ultrasound +####################################### + +####################################### +# Datatypes (KEYWORD1) +####################################### + +ESP8266SSDP KEYWORD1 +SSDP KEYWORD1 + +####################################### +# Methods and Functions (KEYWORD2) +####################################### + +begin KEYWORD2 +schema KEYWORD2 +setName KEYWORD2 +setURL KEYWORD2 +setHTTPPort KEYWORD2 +setSchemaURL KEYWORD2 +setSerialNumber KEYWORD2 +setModelName KEYWORD2 +setModelNumber KEYWORD2 +setModelURL KEYWORD2 +setManufacturer KEYWORD2 +setManufacturerURL KEYWORD2 + +####################################### +# Constants (LITERAL1) +####################################### +SSDP_INTERVAL LITERAL1 +SSDP_PORT LITERAL1 +SSDP_METHOD_SIZE LITERAL1 +SSDP_URI_SIZE LITERAL1 +SSDP_BUFFER_SIZE LITERAL1 +SSDP_BASE_SIZE LITERAL1 +SSDP_FRIENDLY_NAME_SIZE LITERAL1 +SSDP_SERIAL_NUMBER_SIZE LITERAL1 +SSDP_PRESENTATION_URL_SIZE LITERAL1 +SSDP_MODEL_NAME_SIZE LITERAL1 +SSDP_MODEL_URL_SIZE LITERAL1 +SSDP_MODEL_VERSION_SIZE LITERAL1 +SSDP_MANUFACTURER_SIZE LITERAL1 +SSDP_MANUFACTURER_URL_SIZE LITERAL1 +SEARCH LITERAL1 +NOTIFY LITERAL1 +BASIC LITERAL1 +MANAGEABLE LITERAL1 +SOLARPROTECTIONBLIND LITERAL1 +DIGITALSECURITYCAMERA LITERAL1 +HVAC LITERAL1 +LIGHTINGCONTROL LITERAL1 diff --git a/libraries/ESP32SSDP/library.properties b/libraries/ESP32SSDP/library.properties new file mode 100644 index 00000000..e37cd254 --- /dev/null +++ b/libraries/ESP32SSDP/library.properties @@ -0,0 +1,9 @@ +name=ESP32SSPD +version=1.0 +author=Me-No-Dev +maintainer=Me-No-Dev +sentence=Simple SSDP library for ESP32 +paragraph=Only for ESP32 +category=Communication +url= +architectures=esp32 diff --git a/libraries/arduinoWebSockets/.gitignore b/libraries/arduinoWebSockets/.gitignore new file mode 100644 index 00000000..44b2c85f --- /dev/null +++ b/libraries/arduinoWebSockets/.gitignore @@ -0,0 +1,29 @@ +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app +/tests/webSocketServer/node_modules diff --git a/libraries/arduinoWebSockets/.travis.yml b/libraries/arduinoWebSockets/.travis.yml new file mode 100644 index 00000000..14693dd8 --- /dev/null +++ b/libraries/arduinoWebSockets/.travis.yml @@ -0,0 +1,40 @@ +sudo: false +language: bash +os: + - linux +env: + matrix: + - CPU="esp8266" BOARD="esp8266com:esp8266:generic:CpuFrequency=80" IDE_VERSION=1.6.5 + - CPU="esp8266" BOARD="esp8266com:esp8266:generic:CpuFrequency=80,FlashSize=1M0,FlashMode=qio,FlashFreq=80" IDE_VERSION=1.8.5 + - CPU="esp8266" BOARD="esp8266com:esp8266:generic:CpuFrequency=80,Debug=Serial1" IDE_VERSION=1.6.5 + - CPU="esp32" BOARD="espressif:esp32:esp32:FlashFreq=80" IDE_VERSION=1.6.5 + - CPU="esp32" BOARD="espressif:esp32:esp32:FlashFreq=80" IDE_VERSION=1.8.5 + +script: + - /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_1.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :1 -ac -screen 0 1280x1024x16 + - sleep 3 + - export DISPLAY=:1.0 + - wget http://downloads.arduino.cc/arduino-$IDE_VERSION-linux64.tar.xz + - tar xf arduino-$IDE_VERSION-linux64.tar.xz + - mv arduino-$IDE_VERSION $HOME/arduino_ide + - export PATH="$HOME/arduino_ide:$PATH" + - which arduino + - mkdir -p $HOME/Arduino/libraries + - cp -r $TRAVIS_BUILD_DIR $HOME/Arduino/libraries/arduinoWebSockets + - source $TRAVIS_BUILD_DIR/travis/common.sh + - get_core $CPU + - cd $TRAVIS_BUILD_DIR + - arduino --board $BOARD --save-prefs + - arduino --get-pref sketchbook.path + - build_sketches arduino $HOME/Arduino/libraries/arduinoWebSockets/examples/$CPU $CPU + +notifications: + email: + on_success: change + on_failure: change + webhooks: + urls: + - https://webhooks.gitter.im/e/1aa78fbe15080b0c2e37 + on_success: change # options: [always|never|change] default: always + on_failure: always # options: [always|never|change] default: always + on_start: false # default: false diff --git a/libraries/arduinoWebSockets/LICENSE b/libraries/arduinoWebSockets/LICENSE new file mode 100644 index 00000000..f166cc57 --- /dev/null +++ b/libraries/arduinoWebSockets/LICENSE @@ -0,0 +1,502 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! \ No newline at end of file diff --git a/libraries/arduinoWebSockets/README.md b/libraries/arduinoWebSockets/README.md new file mode 100644 index 00000000..63eef3e2 --- /dev/null +++ b/libraries/arduinoWebSockets/README.md @@ -0,0 +1,98 @@ +WebSocket Server and Client for Arduino [![Build Status](https://travis-ci.org/Links2004/arduinoWebSockets.svg?branch=master)](https://travis-ci.org/Links2004/arduinoWebSockets) +=========================================== + +a WebSocket Server and Client for Arduino based on RFC6455. + + +##### Supported features of RFC6455 ##### + - text frame + - binary frame + - connection close + - ping + - pong + - continuation frame + +##### Limitations ##### + - max input length is limited to the ram size and the ```WEBSOCKETS_MAX_DATA_SIZE``` define + - max output length has no limit (the hardware is the limit) + - Client send big frames with mask 0x00000000 (on AVR all frames) + - continuation frame reassembly need to be handled in the application code + + ##### Limitations for Async ##### + - Functions called from within the context of the websocket event might not honor `yield()` and/or `delay()`. See [this issue](https://github.com/Links2004/arduinoWebSockets/issues/58#issuecomment-192376395) for more info and a potential workaround. + - wss / SSL is not possible. + +##### Supported Hardware ##### + - ESP8266 [Arduino for ESP8266](https://github.com/esp8266/Arduino/) + - ESP32 [Arduino for ESP32](https://github.com/espressif/arduino-esp32) + - ESP31B + - Particle with STM32 ARM Cortex M3 + - ATmega328 with Ethernet Shield (ATmega branch) + - ATmega328 with enc28j60 (ATmega branch) + - ATmega2560 with Ethernet Shield (ATmega branch) + - ATmega2560 with enc28j60 (ATmega branch) + +###### Note: ###### + + version 2.0 and up is not compatible with AVR/ATmega, check ATmega branch. + + Arduino for AVR not supports std namespace of c++. + +### wss / SSL ### + supported for: + - wss client on the ESP8266 + - wss / SSL is not natively supported in WebSocketsServer however it is possible to achieve secure websockets + by running the device behind an SSL proxy. See [Nginx](examples/Nginx/esp8266.ssl.reverse.proxy.conf) for a + sample Nginx server configuration file to enable this. + +### ESP Async TCP ### + +This libary can run in Async TCP mode on the ESP. + +The mode can be activated in the ```WebSockets.h``` (see WEBSOCKETS_NETWORK_TYPE define). + +[ESPAsyncTCP](https://github.com/me-no-dev/ESPAsyncTCP) libary is required. + + +### High Level Client API ### + + - `begin` : Initiate connection sequence to the websocket host. +``` +void begin(const char *host, uint16_t port, const char * url = "/", const char * protocol = "arduino"); +void begin(String host, uint16_t port, String url = "/", String protocol = "arduino"); + ``` + - `onEvent`: Callback to handle for websocket events + + ``` + void onEvent(WebSocketClientEvent cbEvent); + ``` + + - `WebSocketClientEvent`: Handler for websocket events + ``` + void (*WebSocketClientEvent)(WStype_t type, uint8_t * payload, size_t length) + ``` +Where `WStype_t type` is defined as: + ``` + typedef enum { + WStype_ERROR, + WStype_DISCONNECTED, + WStype_CONNECTED, + WStype_TEXT, + WStype_BIN, + WStype_FRAGMENT_TEXT_START, + WStype_FRAGMENT_BIN_START, + WStype_FRAGMENT, + WStype_FRAGMENT_FIN, + } WStype_t; + ``` + +### Issues ### +Submit issues to: https://github.com/Links2004/arduinoWebSockets/issues + +[![Join the chat at https://gitter.im/Links2004/arduinoWebSockets](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Links2004/arduinoWebSockets?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +### License and credits ### + +The library is licensed under [LGPLv2.1](https://github.com/Links2004/arduinoWebSockets/blob/master/LICENSE) + +[libb64](http://libb64.sourceforge.net/) written by Chris Venter. It is distributed under Public Domain see [LICENSE](https://github.com/Links2004/arduinoWebSockets/blob/master/src/libb64/LICENSE). diff --git a/libraries/arduinoWebSockets/examples/Nginx/esp8266.ssl.reverse.proxy.conf b/libraries/arduinoWebSockets/examples/Nginx/esp8266.ssl.reverse.proxy.conf new file mode 100644 index 00000000..ec5aa89f --- /dev/null +++ b/libraries/arduinoWebSockets/examples/Nginx/esp8266.ssl.reverse.proxy.conf @@ -0,0 +1,83 @@ +# ESP8266 nginx SSL reverse proxy configuration file (tested and working on nginx v1.10.0) + +# proxy cache location +proxy_cache_path /opt/etc/nginx/cache levels=1:2 keys_zone=ESP8266_cache:10m max_size=10g inactive=5m use_temp_path=off; + +# webserver proxy +server { + + # general server parameters + listen 50080; + server_name myDomain.net; + access_log /opt/var/log/nginx/myDomain.net.access.log; + + # SSL configuration + ssl on; + ssl_certificate /usr/builtin/etc/certificate/lets-encrypt/myDomain.net/fullchain.pem; + ssl_certificate_key /usr/builtin/etc/certificate/lets-encrypt/myDomain.net/privkey.pem; + ssl_session_cache builtin:1000 shared:SSL:10m; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4; + ssl_prefer_server_ciphers on; + + location / { + + # proxy caching configuration + proxy_cache ESP8266_cache; + proxy_cache_revalidate on; + proxy_cache_min_uses 1; + proxy_cache_use_stale off; + proxy_cache_lock on; + # proxy_cache_bypass $http_cache_control; + # include the sessionId cookie value as part of the cache key - keeps the cache per user + # proxy_cache_key $proxy_host$request_uri$cookie_sessionId; + + # header pass through configuration + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # ESP8266 custom headers which identify to the device that it's running through an SSL proxy + proxy_set_header X-SSL On; + proxy_set_header X-SSL-WebserverPort 50080; + proxy_set_header X-SSL-WebsocketPort 50081; + + # extra debug headers + add_header X-Proxy-Cache $upstream_cache_status; + add_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # actual proxying configuration + proxy_ssl_session_reuse on; + # target the IP address of the device with proxy_pass + proxy_pass http://192.168.0.20; + proxy_read_timeout 90; + } + } + +# websocket proxy +server { + + # general server parameters + listen 50081; + server_name myDomain.net; + access_log /opt/var/log/nginx/myDomain.net.wss.access.log; + + # SSL configuration + ssl on; + ssl_certificate /usr/builtin/etc/certificate/lets-encrypt/myDomain.net/fullchain.pem; + ssl_certificate_key /usr/builtin/etc/certificate/lets-encrypt/myDomain.net/privkey.pem; + ssl_session_cache builtin:1000 shared:SSL:10m; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4; + ssl_prefer_server_ciphers on; + + location / { + + # websocket upgrade tunnel configuration + proxy_pass http://192.168.0.20:81; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_read_timeout 86400; + } + } diff --git a/libraries/arduinoWebSockets/examples/avr/WebSocketClientAVR/WebSocketClientAVR.ino b/libraries/arduinoWebSockets/examples/avr/WebSocketClientAVR/WebSocketClientAVR.ino new file mode 100644 index 00000000..9d49d149 --- /dev/null +++ b/libraries/arduinoWebSockets/examples/avr/WebSocketClientAVR/WebSocketClientAVR.ino @@ -0,0 +1,84 @@ +/* + * WebSocketClientAVR.ino + * + * Created on: 10.12.2015 + * + */ + +#include + +#include +#include + +#include + + + +// Enter a MAC address for your controller below. +// Newer Ethernet shields have a MAC address printed on a sticker on the shield +byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; + +// Set the static IP address to use if the DHCP fails to assign +IPAddress ip(192, 168, 0, 177); + +WebSocketsClient webSocket; + + + +void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) { + + + switch(type) { + case WStype_DISCONNECTED: + Serial.println("[WSc] Disconnected!\n"); + break; + case WStype_CONNECTED: + { + Serial.print("[WSc] Connected to url: "); + Serial.println((char *)payload); + // send message to server when Connected + webSocket.sendTXT("Connected"); + } + break; + case WStype_TEXT: + Serial.print("[WSc] get text: "); + Serial.println((char *)payload); + // send message to server + // webSocket.sendTXT("message here"); + break; + case WStype_BIN: + Serial.print("[WSc] get binary length: "); + Serial.println(length); + // hexdump(payload, length); + + // send data to server + // webSocket.sendBIN(payload, length); + break; + } + +} + +void setup() +{ + // Open serial communications and wait for port to open: + Serial.begin(115200); + while (!Serial) {} + + // start the Ethernet connection: + if (Ethernet.begin(mac) == 0) { + Serial.println("Failed to configure Ethernet using DHCP"); + // no point in carrying on, so do nothing forevermore: + // try to congifure using IP address instead of DHCP: + Ethernet.begin(mac, ip); + } + + webSocket.begin("192.168.0.123", 8011); + webSocket.onEvent(webSocketEvent); + +} + + +void loop() +{ + webSocket.loop(); +} diff --git a/libraries/arduinoWebSockets/examples/esp32/WebSocketClient/WebSocketClient.ino b/libraries/arduinoWebSockets/examples/esp32/WebSocketClient/WebSocketClient.ino new file mode 100644 index 00000000..5e5ead46 --- /dev/null +++ b/libraries/arduinoWebSockets/examples/esp32/WebSocketClient/WebSocketClient.ino @@ -0,0 +1,110 @@ +/* + * WebSocketClient.ino + * + * Created on: 24.05.2015 + * + */ + +#include + +#include +#include +#include + +#include + + +WiFiMulti WiFiMulti; +WebSocketsClient webSocket; + +#define USE_SERIAL Serial1 + +void hexdump(const void *mem, uint32_t len, uint8_t cols = 16) { + const uint8_t* src = (const uint8_t*) mem; + USE_SERIAL.printf("\n[HEXDUMP] Address: 0x%08X len: 0x%X (%d)", (ptrdiff_t)src, len, len); + for(uint32_t i = 0; i < len; i++) { + if(i % cols == 0) { + USE_SERIAL.printf("\n[0x%08X] 0x%08X: ", (ptrdiff_t)src, i); + } + USE_SERIAL.printf("%02X ", *src); + src++; + } + USE_SERIAL.printf("\n"); +} + +void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) { + + switch(type) { + case WStype_DISCONNECTED: + USE_SERIAL.printf("[WSc] Disconnected!\n"); + break; + case WStype_CONNECTED: + USE_SERIAL.printf("[WSc] Connected to url: %s\n", payload); + + // send message to server when Connected + webSocket.sendTXT("Connected"); + break; + case WStype_TEXT: + USE_SERIAL.printf("[WSc] get text: %s\n", payload); + + // send message to server + // webSocket.sendTXT("message here"); + break; + case WStype_BIN: + USE_SERIAL.printf("[WSc] get binary length: %u\n", length); + hexdump(payload, length); + + // send data to server + // webSocket.sendBIN(payload, length); + break; + case WStype_ERROR: + case WStype_FRAGMENT_TEXT_START: + case WStype_FRAGMENT_BIN_START: + case WStype_FRAGMENT: + case WStype_FRAGMENT_FIN: + break; + } + +} + +void setup() { + // USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + //Serial.setDebugOutput(true); + USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + USE_SERIAL.println(); + USE_SERIAL.println(); + + for(uint8_t t = 4; t > 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + WiFiMulti.addAP("SSID", "passpasspass"); + + //WiFi.disconnect(); + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + // server address, port and URL + webSocket.begin("192.168.0.123", 81, "/"); + + // event handler + webSocket.onEvent(webSocketEvent); + + // use HTTP Basic Authorization this is optional remove if not needed + webSocket.setAuthorization("user", "Password"); + + // try ever 5000 again if connection has failed + webSocket.setReconnectInterval(5000); + +} + +void loop() { + webSocket.loop(); +} diff --git a/libraries/arduinoWebSockets/examples/esp32/WebSocketClientSSL/WebSocketClientSSL.ino b/libraries/arduinoWebSockets/examples/esp32/WebSocketClientSSL/WebSocketClientSSL.ino new file mode 100644 index 00000000..9d722427 --- /dev/null +++ b/libraries/arduinoWebSockets/examples/esp32/WebSocketClientSSL/WebSocketClientSSL.ino @@ -0,0 +1,106 @@ +/* + * WebSocketClientSSL.ino + * + * Created on: 10.12.2015 + * + * note SSL is only possible with the ESP8266 + * + */ + +#include + +#include +#include +#include + +#include + + +WiFiMulti WiFiMulti; +WebSocketsClient webSocket; + +#define USE_SERIAL Serial1 + +void hexdump(const void *mem, uint32_t len, uint8_t cols = 16) { + const uint8_t* src = (const uint8_t*) mem; + USE_SERIAL.printf("\n[HEXDUMP] Address: 0x%08X len: 0x%X (%d)", (ptrdiff_t)src, len, len); + for(uint32_t i = 0; i < len; i++) { + if(i % cols == 0) { + USE_SERIAL.printf("\n[0x%08X] 0x%08X: ", (ptrdiff_t)src, i); + } + USE_SERIAL.printf("%02X ", *src); + src++; + } + USE_SERIAL.printf("\n"); +} + +void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) { + + + switch(type) { + case WStype_DISCONNECTED: + USE_SERIAL.printf("[WSc] Disconnected!\n"); + break; + case WStype_CONNECTED: + { + USE_SERIAL.printf("[WSc] Connected to url: %s\n", payload); + + // send message to server when Connected + webSocket.sendTXT("Connected"); + } + break; + case WStype_TEXT: + USE_SERIAL.printf("[WSc] get text: %s\n", payload); + + // send message to server + // webSocket.sendTXT("message here"); + break; + case WStype_BIN: + USE_SERIAL.printf("[WSc] get binary length: %u\n", length); + hexdump(payload, length); + + // send data to server + // webSocket.sendBIN(payload, length); + break; + case WStype_ERROR: + case WStype_FRAGMENT_TEXT_START: + case WStype_FRAGMENT_BIN_START: + case WStype_FRAGMENT: + case WStype_FRAGMENT_FIN: + break; + } + +} + +void setup() { + // USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + //Serial.setDebugOutput(true); + USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + USE_SERIAL.println(); + USE_SERIAL.println(); + + for(uint8_t t = 4; t > 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + WiFiMulti.addAP("SSID", "passpasspass"); + + //WiFi.disconnect(); + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + webSocket.beginSSL("192.168.0.123", 81); + webSocket.onEvent(webSocketEvent); + +} + +void loop() { + webSocket.loop(); +} diff --git a/libraries/arduinoWebSockets/examples/esp32/WebSocketServer/WebSocketServer.ino b/libraries/arduinoWebSockets/examples/esp32/WebSocketServer/WebSocketServer.ino new file mode 100644 index 00000000..3e0d4f5b --- /dev/null +++ b/libraries/arduinoWebSockets/examples/esp32/WebSocketServer/WebSocketServer.ino @@ -0,0 +1,104 @@ +/* + * WebSocketServer.ino + * + * Created on: 22.05.2015 + * + */ + +#include + +#include +#include +#include + +#include + +WiFiMulti WiFiMulti; +WebSocketsServer webSocket = WebSocketsServer(81); + +#define USE_SERIAL Serial1 + +void hexdump(const void *mem, uint32_t len, uint8_t cols = 16) { + const uint8_t* src = (const uint8_t*) mem; + USE_SERIAL.printf("\n[HEXDUMP] Address: 0x%08X len: 0x%X (%d)", (ptrdiff_t)src, len, len); + for(uint32_t i = 0; i < len; i++) { + if(i % cols == 0) { + USE_SERIAL.printf("\n[0x%08X] 0x%08X: ", (ptrdiff_t)src, i); + } + USE_SERIAL.printf("%02X ", *src); + src++; + } + USE_SERIAL.printf("\n"); +} + +void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { + + switch(type) { + case WStype_DISCONNECTED: + USE_SERIAL.printf("[%u] Disconnected!\n", num); + break; + case WStype_CONNECTED: + { + IPAddress ip = webSocket.remoteIP(num); + USE_SERIAL.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload); + + // send message to client + webSocket.sendTXT(num, "Connected"); + } + break; + case WStype_TEXT: + USE_SERIAL.printf("[%u] get Text: %s\n", num, payload); + + // send message to client + // webSocket.sendTXT(num, "message here"); + + // send data to all connected clients + // webSocket.broadcastTXT("message here"); + break; + case WStype_BIN: + USE_SERIAL.printf("[%u] get binary length: %u\n", num, length); + hexdump(payload, length); + + // send message to client + // webSocket.sendBIN(num, payload, length); + break; + case WStype_ERROR: + case WStype_FRAGMENT_TEXT_START: + case WStype_FRAGMENT_BIN_START: + case WStype_FRAGMENT: + case WStype_FRAGMENT_FIN: + break; + } + +} + +void setup() { + // USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + //Serial.setDebugOutput(true); + USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + USE_SERIAL.println(); + USE_SERIAL.println(); + + for(uint8_t t = 4; t > 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + WiFiMulti.addAP("SSID", "passpasspass"); + + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + webSocket.begin(); + webSocket.onEvent(webSocketEvent); +} + +void loop() { + webSocket.loop(); +} diff --git a/libraries/arduinoWebSockets/examples/esp8266/WebSocketClient/WebSocketClient.ino b/libraries/arduinoWebSockets/examples/esp8266/WebSocketClient/WebSocketClient.ino new file mode 100644 index 00000000..b990c13a --- /dev/null +++ b/libraries/arduinoWebSockets/examples/esp8266/WebSocketClient/WebSocketClient.ino @@ -0,0 +1,92 @@ +/* + * WebSocketClient.ino + * + * Created on: 24.05.2015 + * + */ + +#include + +#include +#include + +#include + +#include + +ESP8266WiFiMulti WiFiMulti; +WebSocketsClient webSocket; + +#define USE_SERIAL Serial1 + +void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) { + + switch(type) { + case WStype_DISCONNECTED: + USE_SERIAL.printf("[WSc] Disconnected!\n"); + break; + case WStype_CONNECTED: { + USE_SERIAL.printf("[WSc] Connected to url: %s\n", payload); + + // send message to server when Connected + webSocket.sendTXT("Connected"); + } + break; + case WStype_TEXT: + USE_SERIAL.printf("[WSc] get text: %s\n", payload); + + // send message to server + // webSocket.sendTXT("message here"); + break; + case WStype_BIN: + USE_SERIAL.printf("[WSc] get binary length: %u\n", length); + hexdump(payload, length); + + // send data to server + // webSocket.sendBIN(payload, length); + break; + } + +} + +void setup() { + // USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + //Serial.setDebugOutput(true); + USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + USE_SERIAL.println(); + USE_SERIAL.println(); + + for(uint8_t t = 4; t > 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + WiFiMulti.addAP("SSID", "passpasspass"); + + //WiFi.disconnect(); + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + // server address, port and URL + webSocket.begin("192.168.0.123", 81, "/"); + + // event handler + webSocket.onEvent(webSocketEvent); + + // use HTTP Basic Authorization this is optional remove if not needed + webSocket.setAuthorization("user", "Password"); + + // try ever 5000 again if connection has failed + webSocket.setReconnectInterval(5000); + +} + +void loop() { + webSocket.loop(); +} diff --git a/libraries/arduinoWebSockets/examples/esp8266/WebSocketClientSSL/WebSocketClientSSL.ino b/libraries/arduinoWebSockets/examples/esp8266/WebSocketClientSSL/WebSocketClientSSL.ino new file mode 100644 index 00000000..d45060e9 --- /dev/null +++ b/libraries/arduinoWebSockets/examples/esp8266/WebSocketClientSSL/WebSocketClientSSL.ino @@ -0,0 +1,88 @@ +/* + * WebSocketClientSSL.ino + * + * Created on: 10.12.2015 + * + * note SSL is only possible with the ESP8266 + * + */ + +#include + +#include +#include + +#include + +#include + +ESP8266WiFiMulti WiFiMulti; +WebSocketsClient webSocket; + + +#define USE_SERIAL Serial1 + +void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) { + + + switch(type) { + case WStype_DISCONNECTED: + USE_SERIAL.printf("[WSc] Disconnected!\n"); + break; + case WStype_CONNECTED: + { + USE_SERIAL.printf("[WSc] Connected to url: %s\n", payload); + + // send message to server when Connected + webSocket.sendTXT("Connected"); + } + break; + case WStype_TEXT: + USE_SERIAL.printf("[WSc] get text: %s\n", payload); + + // send message to server + // webSocket.sendTXT("message here"); + break; + case WStype_BIN: + USE_SERIAL.printf("[WSc] get binary length: %u\n", length); + hexdump(payload, length); + + // send data to server + // webSocket.sendBIN(payload, length); + break; + } + +} + +void setup() { + // USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + //Serial.setDebugOutput(true); + USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + USE_SERIAL.println(); + USE_SERIAL.println(); + + for(uint8_t t = 4; t > 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + WiFiMulti.addAP("SSID", "passpasspass"); + + //WiFi.disconnect(); + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + webSocket.beginSSL("192.168.0.123", 81); + webSocket.onEvent(webSocketEvent); + +} + +void loop() { + webSocket.loop(); +} diff --git a/libraries/arduinoWebSockets/examples/esp8266/WebSocketClientSocketIO/WebSocketClientSocketIO.ino b/libraries/arduinoWebSockets/examples/esp8266/WebSocketClientSocketIO/WebSocketClientSocketIO.ino new file mode 100644 index 00000000..40e343e2 --- /dev/null +++ b/libraries/arduinoWebSockets/examples/esp8266/WebSocketClientSocketIO/WebSocketClientSocketIO.ino @@ -0,0 +1,113 @@ +/* + * WebSocketClientSocketIO.ino + * + * Created on: 06.06.2016 + * + */ + +#include + +#include +#include + +#include + +#include + +ESP8266WiFiMulti WiFiMulti; +WebSocketsClient webSocket; + + +#define USE_SERIAL Serial1 + +#define MESSAGE_INTERVAL 30000 +#define HEARTBEAT_INTERVAL 25000 + +uint64_t messageTimestamp = 0; +uint64_t heartbeatTimestamp = 0; +bool isConnected = false; + +void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) { + + + switch(type) { + case WStype_DISCONNECTED: + USE_SERIAL.printf("[WSc] Disconnected!\n"); + isConnected = false; + break; + case WStype_CONNECTED: + { + USE_SERIAL.printf("[WSc] Connected to url: %s\n", payload); + isConnected = true; + + // send message to server when Connected + // socket.io upgrade confirmation message (required) + webSocket.sendTXT("5"); + } + break; + case WStype_TEXT: + USE_SERIAL.printf("[WSc] get text: %s\n", payload); + + // send message to server + // webSocket.sendTXT("message here"); + break; + case WStype_BIN: + USE_SERIAL.printf("[WSc] get binary length: %u\n", length); + hexdump(payload, length); + + // send data to server + // webSocket.sendBIN(payload, length); + break; + } + +} + +void setup() { + // USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + //Serial.setDebugOutput(true); + USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + USE_SERIAL.println(); + USE_SERIAL.println(); + + for(uint8_t t = 4; t > 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + WiFiMulti.addAP("SSID", "passpasspass"); + + //WiFi.disconnect(); + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + webSocket.beginSocketIO("192.168.0.123", 81); + //webSocket.setAuthorization("user", "Password"); // HTTP Basic Authorization + webSocket.onEvent(webSocketEvent); + +} + +void loop() { + webSocket.loop(); + + if(isConnected) { + + uint64_t now = millis(); + + if(now - messageTimestamp > MESSAGE_INTERVAL) { + messageTimestamp = now; + // example socket.io message with type "messageType" and JSON payload + webSocket.sendTXT("42[\"messageType\",{\"greeting\":\"hello\"}]"); + } + if((now - heartbeatTimestamp) > HEARTBEAT_INTERVAL) { + heartbeatTimestamp = now; + // socket.io heartbeat message + webSocket.sendTXT("2"); + } + } +} diff --git a/libraries/arduinoWebSockets/examples/esp8266/WebSocketClientStomp/WebSocketClientStomp.ino b/libraries/arduinoWebSockets/examples/esp8266/WebSocketClientStomp/WebSocketClientStomp.ino new file mode 100644 index 00000000..a0eb011f --- /dev/null +++ b/libraries/arduinoWebSockets/examples/esp8266/WebSocketClientStomp/WebSocketClientStomp.ino @@ -0,0 +1,149 @@ +/* + WebSocketClientStomp.ino + + Example for connecting and maintining a connection with a STOMP websocket connection. + In this example, we connect to a Spring application (see https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html). + + Created on: 25.09.2017 + Author: Martin Becker , Contact: becker@informatik.uni-wuerzburg.de +*/ + +// PRE + +#define USE_SERIAL Serial + + +// LIBRARIES + +#include +#include + +#include +#include + + +// SETTINGS + +const char* wlan_ssid = "yourssid"; +const char* wlan_password = "somepassword"; + +const char* ws_host = "the.host.net"; +const int ws_port = 80; + +// URL for STOMP endpoint. +// For the default config of Spring's STOMP support, the default URL is "/socketentry/websocket". +const char* stompUrl = "/socketentry/websocket"; // don't forget the leading "/" !!! + + +// VARIABLES + +WebSocketsClient webSocket; + + +// FUNCTIONS + +/** + * STOMP messages need to be NULL-terminated (i.e., \0 or \u0000). + * However, when we send a String or a char[] array without specifying + * a length, the size of the message payload is derived by strlen() internally, + * thus dropping any NULL values appended to the "msg"-String. + * + * To solve this, we first convert the String to a NULL terminated char[] array + * via "c_str" and set the length of the payload to include the NULL value. + */ +void sendMessage(String & msg) { + webSocket.sendTXT(msg.c_str(), msg.length() + 1); +} + +void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) { + + switch (type) { + case WStype_DISCONNECTED: + USE_SERIAL.printf("[WSc] Disconnected!\n"); + break; + case WStype_CONNECTED: + { + USE_SERIAL.printf("[WSc] Connected to url: %s\n", payload); + + String msg = "CONNECT\r\naccept-version:1.1,1.0\r\nheart-beat:10000,10000\r\n\r\n"; + sendMessage(msg); + } + break; + case WStype_TEXT: + { + // ##################### + // handle STOMP protocol + // ##################### + + String text = (char*) payload; + USE_SERIAL.printf("[WSc] get text: %s\n", payload); + + if (text.startsWith("CONNECTED")) { + + // subscribe to some channels + + String msg = "SUBSCRIBE\nid:sub-0\ndestination:/user/queue/messages\n\n"; + sendMessage(msg); + delay(1000); + + // and send a message + + msg = "SEND\ndestination:/app/message\n\n{\"user\":\"esp\",\"message\":\"Hello!\"}"; + sendMessage(msg); + delay(1000); + + } else { + + // do something with messages + + } + + break; + } + case WStype_BIN: + USE_SERIAL.printf("[WSc] get binary length: %u\n", length); + hexdump(payload, length); + + // send data to server + // webSocket.sendBIN(payload, length); + break; + } + +} + +void setup() { + + // setup serial + + // USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + // USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + + + // connect to WiFi + + USE_SERIAL.print("Logging into WLAN: "); Serial.print(wlan_ssid); Serial.print(" ..."); + WiFi.mode(WIFI_STA); + WiFi.begin(wlan_ssid, wlan_password); + + while (WiFi.status() != WL_CONNECTED) { + delay(500); + USE_SERIAL.print("."); + } + USE_SERIAL.println(" success."); + USE_SERIAL.print("IP: "); USE_SERIAL.println(WiFi.localIP()); + + + // connect to websocket + webSocket.begin(ws_host, ws_port, stompUrl); + webSocket.setExtraHeaders(); // remove "Origin: file://" header because it breaks the connection with Spring's default websocket config + // webSocket.setExtraHeaders("foo: I am so funny\r\nbar: not"); // some headers, in case you feel funny + webSocket.onEvent(webSocketEvent); +} + +void loop() { + webSocket.loop(); +} diff --git a/libraries/arduinoWebSockets/examples/esp8266/WebSocketClientStompOverSockJs/WebSocketClientStompOverSockJs.ino b/libraries/arduinoWebSockets/examples/esp8266/WebSocketClientStompOverSockJs/WebSocketClientStompOverSockJs.ino new file mode 100644 index 00000000..cb0c45be --- /dev/null +++ b/libraries/arduinoWebSockets/examples/esp8266/WebSocketClientStompOverSockJs/WebSocketClientStompOverSockJs.ino @@ -0,0 +1,150 @@ +/* + WebSocketClientStompOverSockJs.ino + + Example for connecting and maintining a connection with a SockJS+STOMP websocket connection. + In this example, we connect to a Spring application (see https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html). + + Created on: 18.07.2017 + Author: Martin Becker , Contact: becker@informatik.uni-wuerzburg.de +*/ + +// PRE + +#define USE_SERIAL Serial + + +// LIBRARIES + +#include +#include + +#include +#include + + +// SETTINGS + +const char* wlan_ssid = "yourssid"; +const char* wlan_password = "somepassword"; + +const char* ws_host = "the.host.net"; +const int ws_port = 80; + +// base URL for SockJS (websocket) connection +// The complete URL will look something like this(cf. http://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html#section-36): +// ws://://<3digits>//websocket +// For the default config of Spring's SockJS/STOMP support, the default base URL is "/socketentry/". +const char* ws_baseurl = "/socketentry/"; // don't forget leading and trailing "/" !!! + + +// VARIABLES + +WebSocketsClient webSocket; + + +// FUNCTIONS + +void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) { + + switch (type) { + case WStype_DISCONNECTED: + USE_SERIAL.printf("[WSc] Disconnected!\n"); + break; + case WStype_CONNECTED: + { + USE_SERIAL.printf("[WSc] Connected to url: %s\n", payload); + } + break; + case WStype_TEXT: + { + // ##################### + // handle SockJs+STOMP protocol + // ##################### + + String text = (char*) payload; + + USE_SERIAL.printf("[WSc] get text: %s\n", payload); + + if (payload[0] == 'h') { + + USE_SERIAL.println("Heartbeat!"); + + } else if (payload[0] == 'o') { + + // on open connection + char *msg = "[\"CONNECT\\naccept-version:1.1,1.0\\nheart-beat:10000,10000\\n\\n\\u0000\"]"; + webSocket.sendTXT(msg); + + } else if (text.startsWith("a[\"CONNECTED")) { + + // subscribe to some channels + + char *msg = "[\"SUBSCRIBE\\nid:sub-0\\ndestination:/user/queue/messages\\n\\n\\u0000\"]"; + webSocket.sendTXT(msg); + delay(1000); + + // and send a message + + msg = "[\"SEND\\ndestination:/app/message\\n\\n{\\\"user\\\":\\\"esp\\\",\\\"message\\\":\\\"Hello!\\\"}\\u0000\"]"; + webSocket.sendTXT(msg); + delay(1000); + } + + break; + } + case WStype_BIN: + USE_SERIAL.printf("[WSc] get binary length: %u\n", length); + hexdump(payload, length); + + // send data to server + // webSocket.sendBIN(payload, length); + break; + } + +} + +void setup() { + + // setup serial + + // USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + // USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + + + // connect to WiFi + + USE_SERIAL.print("Logging into WLAN: "); Serial.print(wlan_ssid); Serial.print(" ..."); + WiFi.mode(WIFI_STA); + WiFi.begin(wlan_ssid, wlan_password); + + while (WiFi.status() != WL_CONNECTED) { + delay(500); + USE_SERIAL.print("."); + } + USE_SERIAL.println(" success."); + USE_SERIAL.print("IP: "); USE_SERIAL.println(WiFi.localIP()); + + + // ##################### + // create socket url according to SockJS protocol (cf. http://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html#section-36) + // ##################### + String socketUrl = ws_baseurl; + socketUrl += random(0, 999); + socketUrl += "/"; + socketUrl += random(0, 999999); // should be a random string, but this works (see ) + socketUrl += "/websocket"; + + // connect to websocket + webSocket.begin(ws_host, ws_port, socketUrl); + webSocket.setExtraHeaders(); // remove "Origin: file://" header because it breaks the connection with Spring's default websocket config + // webSocket.setExtraHeaders("foo: I am so funny\r\nbar: not"); // some headers, in case you feel funny + webSocket.onEvent(webSocketEvent); +} + +void loop() { + webSocket.loop(); +} diff --git a/libraries/arduinoWebSockets/examples/esp8266/WebSocketServer/WebSocketServer.ino b/libraries/arduinoWebSockets/examples/esp8266/WebSocketServer/WebSocketServer.ino new file mode 100644 index 00000000..1ac3002d --- /dev/null +++ b/libraries/arduinoWebSockets/examples/esp8266/WebSocketServer/WebSocketServer.ino @@ -0,0 +1,86 @@ +/* + * WebSocketServer.ino + * + * Created on: 22.05.2015 + * + */ + +#include + +#include +#include +#include +#include + +ESP8266WiFiMulti WiFiMulti; + +WebSocketsServer webSocket = WebSocketsServer(81); + +#define USE_SERIAL Serial1 + +void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { + + switch(type) { + case WStype_DISCONNECTED: + USE_SERIAL.printf("[%u] Disconnected!\n", num); + break; + case WStype_CONNECTED: + { + IPAddress ip = webSocket.remoteIP(num); + USE_SERIAL.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload); + + // send message to client + webSocket.sendTXT(num, "Connected"); + } + break; + case WStype_TEXT: + USE_SERIAL.printf("[%u] get Text: %s\n", num, payload); + + // send message to client + // webSocket.sendTXT(num, "message here"); + + // send data to all connected clients + // webSocket.broadcastTXT("message here"); + break; + case WStype_BIN: + USE_SERIAL.printf("[%u] get binary length: %u\n", num, length); + hexdump(payload, length); + + // send message to client + // webSocket.sendBIN(num, payload, length); + break; + } + +} + +void setup() { + // USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + //Serial.setDebugOutput(true); + USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + USE_SERIAL.println(); + USE_SERIAL.println(); + + for(uint8_t t = 4; t > 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + WiFiMulti.addAP("SSID", "passpasspass"); + + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + webSocket.begin(); + webSocket.onEvent(webSocketEvent); +} + +void loop() { + webSocket.loop(); +} + diff --git a/libraries/arduinoWebSockets/examples/esp8266/WebSocketServerAllFunctionsDemo/WebSocketServerAllFunctionsDemo.ino b/libraries/arduinoWebSockets/examples/esp8266/WebSocketServerAllFunctionsDemo/WebSocketServerAllFunctionsDemo.ino new file mode 100644 index 00000000..5fed1a95 --- /dev/null +++ b/libraries/arduinoWebSockets/examples/esp8266/WebSocketServerAllFunctionsDemo/WebSocketServerAllFunctionsDemo.ino @@ -0,0 +1,132 @@ +/* + * WebSocketServerAllFunctionsDemo.ino + * + * Created on: 10.05.2018 + * + */ + +#include + +#include +#include +#include +#include +#include +#include + +#define LED_RED 15 +#define LED_GREEN 12 +#define LED_BLUE 13 + +#define USE_SERIAL Serial + +ESP8266WiFiMulti WiFiMulti; + +ESP8266WebServer server(80); +WebSocketsServer webSocket = WebSocketsServer(81); + +void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { + + switch(type) { + case WStype_DISCONNECTED: + USE_SERIAL.printf("[%u] Disconnected!\n", num); + break; + case WStype_CONNECTED: { + IPAddress ip = webSocket.remoteIP(num); + USE_SERIAL.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload); + + // send message to client + webSocket.sendTXT(num, "Connected"); + } + break; + case WStype_TEXT: + USE_SERIAL.printf("[%u] get Text: %s\n", num, payload); + + if(payload[0] == '#') { + // we get RGB data + + // decode rgb data + uint32_t rgb = (uint32_t) strtol((const char *) &payload[1], NULL, 16); + + analogWrite(LED_RED, ((rgb >> 16) & 0xFF)); + analogWrite(LED_GREEN, ((rgb >> 8) & 0xFF)); + analogWrite(LED_BLUE, ((rgb >> 0) & 0xFF)); + } + + break; + } + +} + +void setup() { + //USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + //USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + USE_SERIAL.println(); + USE_SERIAL.println(); + + for(uint8_t t = 4; t > 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + pinMode(LED_RED, OUTPUT); + pinMode(LED_GREEN, OUTPUT); + pinMode(LED_BLUE, OUTPUT); + + digitalWrite(LED_RED, 1); + digitalWrite(LED_GREEN, 1); + digitalWrite(LED_BLUE, 1); + + WiFiMulti.addAP("SSID", "passpasspass"); + + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + // start webSocket server + webSocket.begin(); + webSocket.onEvent(webSocketEvent); + + if(MDNS.begin("esp8266")) { + USE_SERIAL.println("MDNS responder started"); + } + + // handle index + server.on("/", []() { + // send index.html + server.send(200, "text/html", "LED Control:

R:
G:
B:
"); + }); + + server.begin(); + + // Add service to MDNS + MDNS.addService("http", "tcp", 80); + MDNS.addService("ws", "tcp", 81); + + digitalWrite(LED_RED, 0); + digitalWrite(LED_GREEN, 0); + digitalWrite(LED_BLUE, 0); + +} + +unsigned long last_10sec = 0; +unsigned int counter = 0; + +void loop() { + unsigned long t = millis(); + webSocket.loop(); + server.handleClient(); + + if((t - last_10sec) > 10 * 1000) { + counter++; + bool ping = (counter % 2); + int i = webSocket.connectedClients(ping); + USE_SERIAL.printf("%d Connected websocket clients ping: %d\n", i, ping); + last_10sec = millis(); + } +} diff --git a/libraries/arduinoWebSockets/examples/esp8266/WebSocketServerFragmentation/WebSocketServerFragmentation.ino b/libraries/arduinoWebSockets/examples/esp8266/WebSocketServerFragmentation/WebSocketServerFragmentation.ino new file mode 100644 index 00000000..84c9775d --- /dev/null +++ b/libraries/arduinoWebSockets/examples/esp8266/WebSocketServerFragmentation/WebSocketServerFragmentation.ino @@ -0,0 +1,94 @@ +/* + * WebSocketServer.ino + * + * Created on: 22.05.2015 + * + */ + +#include + +#include +#include +#include +#include + +ESP8266WiFiMulti WiFiMulti; + +WebSocketsServer webSocket = WebSocketsServer(81); + +#define USE_SERIAL Serial + +String fragmentBuffer = ""; + +void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { + + switch(type) { + case WStype_DISCONNECTED: + USE_SERIAL.printf("[%u] Disconnected!\n", num); + break; + case WStype_CONNECTED: { + IPAddress ip = webSocket.remoteIP(num); + USE_SERIAL.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload); + + // send message to client + webSocket.sendTXT(num, "Connected"); + } + break; + case WStype_TEXT: + USE_SERIAL.printf("[%u] get Text: %s\n", num, payload); + break; + case WStype_BIN: + USE_SERIAL.printf("[%u] get binary length: %u\n", num, length); + hexdump(payload, length); + break; + + // Fragmentation / continuation opcode handling + // case WStype_FRAGMENT_BIN_START: + case WStype_FRAGMENT_TEXT_START: + fragmentBuffer = (char*)payload; + USE_SERIAL.printf("[%u] get start start of Textfragment: %s\n", num, payload); + break; + case WStype_FRAGMENT: + fragmentBuffer += (char*)payload; + USE_SERIAL.printf("[%u] get Textfragment : %s\n", num, payload); + break; + case WStype_FRAGMENT_FIN: + fragmentBuffer += (char*)payload; + USE_SERIAL.printf("[%u] get end of Textfragment: %s\n", num, payload); + USE_SERIAL.printf("[%u] full frame: %s\n", num, fragmentBuffer.c_str()); + break; + } + +} + +void setup() { + // USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + //Serial.setDebugOutput(true); + USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + USE_SERIAL.println(); + USE_SERIAL.println(); + + for(uint8_t t = 4; t > 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + WiFiMulti.addAP("SSID", "passpasspass"); + + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + webSocket.begin(); + webSocket.onEvent(webSocketEvent); +} + +void loop() { + webSocket.loop(); +} + diff --git a/libraries/arduinoWebSockets/examples/esp8266/WebSocketServerHttpHeaderValidation/WebSocketServerHttpHeaderValidation.ino b/libraries/arduinoWebSockets/examples/esp8266/WebSocketServerHttpHeaderValidation/WebSocketServerHttpHeaderValidation.ino new file mode 100644 index 00000000..8bc646c4 --- /dev/null +++ b/libraries/arduinoWebSockets/examples/esp8266/WebSocketServerHttpHeaderValidation/WebSocketServerHttpHeaderValidation.ino @@ -0,0 +1,86 @@ +/* + * WebSocketServerHttpHeaderValidation.ino + * + * Created on: 08.06.2016 + * + */ + +#include + +#include +#include +#include +#include + +ESP8266WiFiMulti WiFiMulti; + +WebSocketsServer webSocket = WebSocketsServer(81); + +#define USE_SERIAL Serial1 + +const unsigned long int validSessionId = 12345; //some arbitrary value to act as a valid sessionId + +/* + * Returns a bool value as an indicator to describe whether a user is allowed to initiate a websocket upgrade + * based on the value of a cookie. This function expects the rawCookieHeaderValue to look like this "sessionId=|" + */ +bool isCookieValid(String rawCookieHeaderValue) { + + if (rawCookieHeaderValue.indexOf("sessionId") != -1) { + String sessionIdStr = rawCookieHeaderValue.substring(rawCookieHeaderValue.indexOf("sessionId=") + 10, rawCookieHeaderValue.indexOf("|")); + unsigned long int sessionId = strtoul(sessionIdStr.c_str(), NULL, 10); + return sessionId == validSessionId; + } + return false; +} + +/* + * The WebSocketServerHttpHeaderValFunc delegate passed to webSocket.onValidateHttpHeader + */ +bool validateHttpHeader(String headerName, String headerValue) { + + //assume a true response for any headers not handled by this validator + bool valid = true; + + if(headerName.equalsIgnoreCase("Cookie")) { + //if the header passed is the Cookie header, validate it according to the rules in 'isCookieValid' function + valid = isCookieValid(headerValue); + } + + return valid; +} + +void setup() { + // USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + //Serial.setDebugOutput(true); + USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + USE_SERIAL.println(); + USE_SERIAL.println(); + + for(uint8_t t = 4; t > 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + WiFiMulti.addAP("SSID", "passpasspass"); + + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + //connecting clients must supply a valid session cookie at websocket upgrade handshake negotiation time + const char * headerkeys[] = { "Cookie" }; + size_t headerKeyCount = sizeof(headerkeys) / sizeof(char*); + webSocket.onValidateHttpHeader(validateHttpHeader, headerkeys, headerKeyCount); + webSocket.begin(); +} + +void loop() { + webSocket.loop(); +} + diff --git a/libraries/arduinoWebSockets/examples/esp8266/WebSocketServer_LEDcontrol/WebSocketServer_LEDcontrol.ino b/libraries/arduinoWebSockets/examples/esp8266/WebSocketServer_LEDcontrol/WebSocketServer_LEDcontrol.ino new file mode 100644 index 00000000..8f32e753 --- /dev/null +++ b/libraries/arduinoWebSockets/examples/esp8266/WebSocketServer_LEDcontrol/WebSocketServer_LEDcontrol.ino @@ -0,0 +1,121 @@ +/* + * WebSocketServer_LEDcontrol.ino + * + * Created on: 26.11.2015 + * + */ + +#include + +#include +#include +#include +#include +#include +#include + +#define LED_RED 15 +#define LED_GREEN 12 +#define LED_BLUE 13 + +#define USE_SERIAL Serial + + +ESP8266WiFiMulti WiFiMulti; + +ESP8266WebServer server(80); +WebSocketsServer webSocket = WebSocketsServer(81); + +void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { + + switch(type) { + case WStype_DISCONNECTED: + USE_SERIAL.printf("[%u] Disconnected!\n", num); + break; + case WStype_CONNECTED: { + IPAddress ip = webSocket.remoteIP(num); + USE_SERIAL.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload); + + // send message to client + webSocket.sendTXT(num, "Connected"); + } + break; + case WStype_TEXT: + USE_SERIAL.printf("[%u] get Text: %s\n", num, payload); + + if(payload[0] == '#') { + // we get RGB data + + // decode rgb data + uint32_t rgb = (uint32_t) strtol((const char *) &payload[1], NULL, 16); + + analogWrite(LED_RED, ((rgb >> 16) & 0xFF)); + analogWrite(LED_GREEN, ((rgb >> 8) & 0xFF)); + analogWrite(LED_BLUE, ((rgb >> 0) & 0xFF)); + } + + break; + } + +} + +void setup() { + //USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + //USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + USE_SERIAL.println(); + USE_SERIAL.println(); + + for(uint8_t t = 4; t > 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + pinMode(LED_RED, OUTPUT); + pinMode(LED_GREEN, OUTPUT); + pinMode(LED_BLUE, OUTPUT); + + digitalWrite(LED_RED, 1); + digitalWrite(LED_GREEN, 1); + digitalWrite(LED_BLUE, 1); + + WiFiMulti.addAP("SSID", "passpasspass"); + + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + // start webSocket server + webSocket.begin(); + webSocket.onEvent(webSocketEvent); + + if(MDNS.begin("esp8266")) { + USE_SERIAL.println("MDNS responder started"); + } + + // handle index + server.on("/", []() { + // send index.html + server.send(200, "text/html", "LED Control:

R:
G:
B:
"); + }); + + server.begin(); + + // Add service to MDNS + MDNS.addService("http", "tcp", 80); + MDNS.addService("ws", "tcp", 81); + + digitalWrite(LED_RED, 0); + digitalWrite(LED_GREEN, 0); + digitalWrite(LED_BLUE, 0); + +} + +void loop() { + webSocket.loop(); + server.handleClient(); +} diff --git a/libraries/arduinoWebSockets/examples/particle/ParticleWebSocketClient/application.cpp b/libraries/arduinoWebSockets/examples/particle/ParticleWebSocketClient/application.cpp new file mode 100644 index 00000000..461228f3 --- /dev/null +++ b/libraries/arduinoWebSockets/examples/particle/ParticleWebSocketClient/application.cpp @@ -0,0 +1,46 @@ +/* To compile using make CLI, create a folder under \firmware\user\applications and copy application.cpp there. +* Then, copy src files under particleWebSocket folder. +*/ + +#include "application.h" +#include "particleWebSocket/WebSocketsClient.h" + +WebSocketsClient webSocket; + +void webSocketEvent(WStype_t type, uint8_t* payload, size_t length) +{ + switch (type) + { + case WStype_DISCONNECTED: + Serial.printlnf("[WSc] Disconnected!"); + break; + case WStype_CONNECTED: + Serial.printlnf("[WSc] Connected to URL: %s", payload); + webSocket.sendTXT("Connected\r\n"); + break; + case WStype_TEXT: + Serial.printlnf("[WSc] get text: %s", payload); + break; + case WStype_BIN: + Serial.printlnf("[WSc] get binary length: %u", length); + break; + } +} + +void setup() +{ + Serial.begin(9600); + + WiFi.setCredentials("[SSID]", "[PASSWORD]", WPA2, WLAN_CIPHER_AES_TKIP); + WiFi.connect(); + + webSocket.begin("192.168.1.153", 85, "/ClientService/?variable=Test1212"); + webSocket.onEvent(webSocketEvent); +} + +void loop() +{ + webSocket.sendTXT("Hello world!"); + delay(500); + webSocket.loop(); +} diff --git a/libraries/arduinoWebSockets/library.json b/libraries/arduinoWebSockets/library.json new file mode 100644 index 00000000..f1a58923 --- /dev/null +++ b/libraries/arduinoWebSockets/library.json @@ -0,0 +1,25 @@ +{ + "name": "WebSockets", + "description": "WebSocket Server and Client for Arduino based on RFC6455", + "keywords": "wifi, http, web, server, client, websocket", + "authors": [ + { + "name": "Markus Sattler", + "url": "https://github.com/Links2004", + "maintainer": true + } + ], + "repository": { + "type": "git", + "url": "https://github.com/Links2004/arduinoWebSockets.git" + }, + "version": "2.1.2", + "license": "LGPL-2.1", + "export": { + "exclude": [ + "tests" + ] + }, + "frameworks": "arduino", + "platforms": "atmelavr, espressif8266, espressif32" +} diff --git a/libraries/arduinoWebSockets/library.properties b/libraries/arduinoWebSockets/library.properties new file mode 100644 index 00000000..00d09455 --- /dev/null +++ b/libraries/arduinoWebSockets/library.properties @@ -0,0 +1,9 @@ +name=WebSockets +version=2.1.2 +author=Markus Sattler +maintainer=Markus Sattler +sentence=WebSockets for Arduino (Server + Client) +paragraph=use 2.x.x for ESP and 1.3 for AVR +category=Communication +url=https://github.com/Links2004/arduinoWebSockets +architectures=* diff --git a/libraries/arduinoWebSockets/src/WebSockets.cpp b/libraries/arduinoWebSockets/src/WebSockets.cpp new file mode 100644 index 00000000..727e4726 --- /dev/null +++ b/libraries/arduinoWebSockets/src/WebSockets.cpp @@ -0,0 +1,655 @@ +/** + * @file WebSockets.cpp + * @date 20.05.2015 + * @author Markus Sattler + * + * Copyright (c) 2015 Markus Sattler. All rights reserved. + * This file is part of the WebSockets for Arduino. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "WebSockets.h" + +#ifdef ESP8266 +#include +#endif + +extern "C" { +#ifdef CORE_HAS_LIBB64 +#include +#else +#include "libb64/cencode_inc.h" +#endif +} + +#ifdef ESP8266 +#include +#elif defined(ESP32) +#include +#else + +extern "C" { +#include "libsha1/libsha1.h" +} + +#endif + + +/** + * + * @param client WSclient_t * ptr to the client struct + * @param code uint16_t see RFC + * @param reason ptr to the disconnect reason message + * @param reasonLen length of the disconnect reason message + */ +void WebSockets::clientDisconnect(WSclient_t * client, uint16_t code, char * reason, size_t reasonLen) { + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] clientDisconnect code: %u\n", client->num, code); + if(client->status == WSC_CONNECTED && code) { + if(reason) { + sendFrame(client, WSop_close, (uint8_t *) reason, reasonLen); + } else { + uint8_t buffer[2]; + buffer[0] = ((code >> 8) & 0xFF); + buffer[1] = (code & 0xFF); + sendFrame(client, WSop_close, &buffer[0], 2); + } + } + clientDisconnect(client); +} + +/** + * + * @param client WSclient_t * ptr to the client struct + * @param opcode WSopcode_t + * @param payload uint8_t * ptr to the payload + * @param length size_t length of the payload + * @param mask bool add dummy mask to the frame (needed for web browser) + * @param fin bool can be used to send data in more then one frame (set fin on the last frame) + * @param headerToPayload bool set true if the payload has reserved 14 Byte at the beginning to dynamically add the Header (payload neet to be in RAM!) + * @return true if ok + */ +bool WebSockets::sendFrame(WSclient_t * client, WSopcode_t opcode, uint8_t * payload, size_t length, bool mask, bool fin, bool headerToPayload) { + + if(client->tcp && !client->tcp->connected()) { + DEBUG_WEBSOCKETS("[WS][%d][sendFrame] not Connected!?\n", client->num); + return false; + } + + if(client->status != WSC_CONNECTED) { + DEBUG_WEBSOCKETS("[WS][%d][sendFrame] not in WSC_CONNECTED state!?\n", client->num); + return false; + } + + DEBUG_WEBSOCKETS("[WS][%d][sendFrame] ------- send message frame -------\n", client->num); + DEBUG_WEBSOCKETS("[WS][%d][sendFrame] fin: %u opCode: %u mask: %u length: %u headerToPayload: %u\n", client->num, fin, opcode, mask, length, headerToPayload); + + if(opcode == WSop_text) { + DEBUG_WEBSOCKETS("[WS][%d][sendFrame] text: %s\n", client->num, (payload + (headerToPayload ? 14 : 0))); + } + + uint8_t maskKey[4] = { 0x00, 0x00, 0x00, 0x00 }; + uint8_t buffer[WEBSOCKETS_MAX_HEADER_SIZE] = { 0 }; + + uint8_t headerSize; + uint8_t * headerPtr; + uint8_t * payloadPtr = payload; + bool useInternBuffer = false; + bool ret = true; + + // calculate header Size + if(length < 126) { + headerSize = 2; + } else if(length < 0xFFFF) { + headerSize = 4; + } else { + headerSize = 10; + } + + if(mask) { + headerSize += 4; + } + +#ifdef WEBSOCKETS_USE_BIG_MEM + // only for ESP since AVR has less HEAP + // try to send data in one TCP package (only if some free Heap is there) + if(!headerToPayload && ((length > 0) && (length < 1400)) && (GET_FREE_HEAP > 6000)) { + DEBUG_WEBSOCKETS("[WS][%d][sendFrame] pack to one TCP package...\n", client->num); + uint8_t * dataPtr = (uint8_t *) malloc(length + WEBSOCKETS_MAX_HEADER_SIZE); + if(dataPtr) { + memcpy((dataPtr + WEBSOCKETS_MAX_HEADER_SIZE), payload, length); + headerToPayload = true; + useInternBuffer = true; + payloadPtr = dataPtr; + } + } +#endif + + // set Header Pointer + if(headerToPayload) { + // calculate offset in payload + headerPtr = (payloadPtr + (WEBSOCKETS_MAX_HEADER_SIZE - headerSize)); + } else { + headerPtr = &buffer[0]; + } + + // create header + + // byte 0 + *headerPtr = 0x00; + if(fin) { + *headerPtr |= bit(7); ///< set Fin + } + *headerPtr |= opcode; ///< set opcode + headerPtr++; + + // byte 1 + *headerPtr = 0x00; + if(mask) { + *headerPtr |= bit(7); ///< set mask + } + + if(length < 126) { + *headerPtr |= length; + headerPtr++; + } else if(length < 0xFFFF) { + *headerPtr |= 126; + headerPtr++; + *headerPtr = ((length >> 8) & 0xFF); + headerPtr++; + *headerPtr = (length & 0xFF); + headerPtr++; + } else { + // Normally we never get here (to less memory) + *headerPtr |= 127; + headerPtr++; + *headerPtr = 0x00; + headerPtr++; + *headerPtr = 0x00; + headerPtr++; + *headerPtr = 0x00; + headerPtr++; + *headerPtr = 0x00; + headerPtr++; + *headerPtr = ((length >> 24) & 0xFF); + headerPtr++; + *headerPtr = ((length >> 16) & 0xFF); + headerPtr++; + *headerPtr = ((length >> 8) & 0xFF); + headerPtr++; + *headerPtr = (length & 0xFF); + headerPtr++; + } + + if(mask) { + if(useInternBuffer) { + // if we use a Intern Buffer we can modify the data + // by this fact its possible the do the masking + for(uint8_t x = 0; x < sizeof(maskKey); x++) { + maskKey[x] = random(0xFF); + *headerPtr = maskKey[x]; + headerPtr++; + } + + uint8_t * dataMaskPtr; + + if(headerToPayload) { + dataMaskPtr = (payloadPtr + WEBSOCKETS_MAX_HEADER_SIZE); + } else { + dataMaskPtr = payloadPtr; + } + + for(size_t x = 0; x < length; x++) { + dataMaskPtr[x] = (dataMaskPtr[x] ^ maskKey[x % 4]); + } + + } else { + *headerPtr = maskKey[0]; + headerPtr++; + *headerPtr = maskKey[1]; + headerPtr++; + *headerPtr = maskKey[2]; + headerPtr++; + *headerPtr = maskKey[3]; + headerPtr++; + } + } + +#ifndef NODEBUG_WEBSOCKETS + unsigned long start = micros(); +#endif + + if(headerToPayload) { + // header has be added to payload + // payload is forced to reserved 14 Byte but we may not need all based on the length and mask settings + // offset in payload is calculatetd 14 - headerSize + if(write(client, &payloadPtr[(WEBSOCKETS_MAX_HEADER_SIZE - headerSize)], (length + headerSize)) != (length + headerSize)) { + ret = false; + } + } else { + // send header + if(write(client, &buffer[0], headerSize) != headerSize) { + ret = false; + } + + if(payloadPtr && length > 0) { + // send payload + if(write(client, &payloadPtr[0], length) != length) { + ret = false; + } + } + } + + DEBUG_WEBSOCKETS("[WS][%d][sendFrame] sending Frame Done (%luus).\n", client->num, (micros() - start)); + +#ifdef WEBSOCKETS_USE_BIG_MEM + if(useInternBuffer && payloadPtr) { + free(payloadPtr); + } +#endif + + return ret; +} + +/** + * callen when HTTP header is done + * @param client WSclient_t * ptr to the client struct + */ +void WebSockets::headerDone(WSclient_t * client) { + client->status = WSC_CONNECTED; + client->cWsRXsize = 0; + DEBUG_WEBSOCKETS("[WS][%d][headerDone] Header Handling Done.\n", client->num); +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + client->cHttpLine = ""; + handleWebsocket(client); +#endif +} + +/** + * handle the WebSocket stream + * @param client WSclient_t * ptr to the client struct + */ +void WebSockets::handleWebsocket(WSclient_t * client) { + if(client->cWsRXsize == 0) { + handleWebsocketCb(client); + } +} + +/** + * wait for + * @param client + * @param size + */ +bool WebSockets::handleWebsocketWaitFor(WSclient_t * client, size_t size) { + if(!client->tcp || !client->tcp->connected()) { + return false; + } + + if(size > WEBSOCKETS_MAX_HEADER_SIZE) { + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocketWaitFor] size: %d too big!\n", client->num, size); + return false; + } + + if(client->cWsRXsize >= size) { + return true; + } + + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocketWaitFor] size: %d cWsRXsize: %d\n", client->num, size, client->cWsRXsize); + readCb(client, &client->cWsHeader[client->cWsRXsize], (size - client->cWsRXsize), std::bind([](WebSockets * server, size_t size, WSclient_t * client, bool ok) { + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocketWaitFor][readCb] size: %d ok: %d\n", client->num, size, ok); + if(ok) { + client->cWsRXsize = size; + server->handleWebsocketCb(client); + } else { + DEBUG_WEBSOCKETS("[WS][%d][readCb] failed.\n", client->num); + client->cWsRXsize = 0; + // timeout or error + server->clientDisconnect(client, 1002); + } + }, this, size, std::placeholders::_1, std::placeholders::_2)); + return false; +} + +void WebSockets::handleWebsocketCb(WSclient_t * client) { + + if(!client->tcp || !client->tcp->connected()) { + return; + } + + uint8_t * buffer = client->cWsHeader; + + WSMessageHeader_t * header = &client->cWsHeaderDecode; + uint8_t * payload = NULL; + + uint8_t headerLen = 2; + + if(!handleWebsocketWaitFor(client, headerLen)) { + return; + } + + // split first 2 bytes in the data + header->fin = ((*buffer >> 7) & 0x01); + header->rsv1 = ((*buffer >> 6) & 0x01); + header->rsv2 = ((*buffer >> 5) & 0x01); + header->rsv3 = ((*buffer >> 4) & 0x01); + header->opCode = (WSopcode_t) (*buffer & 0x0F); + buffer++; + + header->mask = ((*buffer >> 7) & 0x01); + header->payloadLen = (WSopcode_t) (*buffer & 0x7F); + buffer++; + + if(header->payloadLen == 126) { + headerLen += 2; + if(!handleWebsocketWaitFor(client, headerLen)) { + return; + } + header->payloadLen = buffer[0] << 8 | buffer[1]; + buffer += 2; + } else if(header->payloadLen == 127) { + headerLen += 8; + // read 64bit integer as length + if(!handleWebsocketWaitFor(client, headerLen)) { + return; + } + + if(buffer[0] != 0 || buffer[1] != 0 || buffer[2] != 0 || buffer[3] != 0) { + // really too big! + header->payloadLen = 0xFFFFFFFF; + } else { + header->payloadLen = buffer[4] << 24 | buffer[5] << 16 | buffer[6] << 8 | buffer[7]; + } + buffer += 8; + } + + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] ------- read massage frame -------\n", client->num); + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] fin: %u rsv1: %u rsv2: %u rsv3 %u opCode: %u\n", client->num, header->fin, header->rsv1, header->rsv2, header->rsv3, header->opCode); + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] mask: %u payloadLen: %u\n", client->num, header->mask, header->payloadLen); + + if(header->payloadLen > WEBSOCKETS_MAX_DATA_SIZE) { + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] payload too big! (%u)\n", client->num, header->payloadLen); + clientDisconnect(client, 1009); + return; + } + + if(header->mask) { + headerLen += 4; + if(!handleWebsocketWaitFor(client, headerLen)) { + return; + } + header->maskKey = buffer; + buffer += 4; + } + + if(header->payloadLen > 0) { + // if text data we need one more + payload = (uint8_t *) malloc(header->payloadLen + 1); + + if(!payload) { + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] to less memory to handle payload %d!\n", client->num, header->payloadLen); + clientDisconnect(client, 1011); + return; + } + readCb(client, payload, header->payloadLen, std::bind(&WebSockets::handleWebsocketPayloadCb, this, std::placeholders::_1, std::placeholders::_2, payload)); + } else { + handleWebsocketPayloadCb(client, true, NULL); + } +} + +void WebSockets::handleWebsocketPayloadCb(WSclient_t * client, bool ok, uint8_t * payload) { + + WSMessageHeader_t * header = &client->cWsHeaderDecode; + if(ok) { + if(header->payloadLen > 0) { + payload[header->payloadLen] = 0x00; + + if(header->mask) { + //decode XOR + for(size_t i = 0; i < header->payloadLen; i++) { + payload[i] = (payload[i] ^ header->maskKey[i % 4]); + } + } + } + + switch(header->opCode) { + case WSop_text: + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] text: %s\n", client->num, payload); + // no break here! + case WSop_binary: + case WSop_continuation: + messageReceived(client, header->opCode, payload, header->payloadLen, header->fin); + break; + case WSop_ping: + // send pong back + sendFrame(client, WSop_pong, payload, header->payloadLen, true); + break; + case WSop_pong: + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] get pong (%s)\n", client->num, payload ? (const char*)payload : ""); + break; + case WSop_close: { + #ifndef NODEBUG_WEBSOCKETS + uint16_t reasonCode = 1000; + if(header->payloadLen >= 2) { + reasonCode = payload[0] << 8 | payload[1]; + } + #endif + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] get ask for close. Code: %d", client->num, reasonCode); + if(header->payloadLen > 2) { + DEBUG_WEBSOCKETS(" (%s)\n", (payload + 2)); + } else { + DEBUG_WEBSOCKETS("\n"); + } + clientDisconnect(client, 1000); + } + break; + default: + clientDisconnect(client, 1002); + break; + } + + if(payload) { + free(payload); + } + + // reset input + client->cWsRXsize = 0; +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + //register callback for next message + handleWebsocketWaitFor(client, 2); +#endif + + } else { + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] missing data!\n", client->num); + free(payload); + clientDisconnect(client, 1002); + } +} + +/** + * generate the key for Sec-WebSocket-Accept + * @param clientKey String + * @return String Accept Key + */ +String WebSockets::acceptKey(String & clientKey) { + uint8_t sha1HashBin[20] = { 0 }; +#ifdef ESP8266 + sha1(clientKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", &sha1HashBin[0]); +#elif defined(ESP32) + String data = clientKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + esp_sha(SHA1, (unsigned char*)data.c_str(), data.length(), &sha1HashBin[0]); +#else + clientKey += "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + SHA1_CTX ctx; + SHA1Init(&ctx); + SHA1Update(&ctx, (const unsigned char*)clientKey.c_str(), clientKey.length()); + SHA1Final(&sha1HashBin[0], &ctx); +#endif + + String key = base64_encode(sha1HashBin, 20); + key.trim(); + + return key; +} + +/** + * base64_encode + * @param data uint8_t * + * @param length size_t + * @return base64 encoded String + */ +String WebSockets::base64_encode(uint8_t * data, size_t length) { + size_t size = ((length * 1.6f) + 1); + char * buffer = (char *) malloc(size); + if(buffer) { + base64_encodestate _state; + base64_init_encodestate(&_state); + int len = base64_encode_block((const char *) &data[0], length, &buffer[0], &_state); + len = base64_encode_blockend((buffer + len), &_state); + + String base64 = String(buffer); + free(buffer); + return base64; + } + return String("-FAIL-"); +} + +/** + * read x byte from tcp or get timeout + * @param client WSclient_t * + * @param out uint8_t * data buffer + * @param n size_t byte count + * @return true if ok + */ +bool WebSockets::readCb(WSclient_t * client, uint8_t * out, size_t n, WSreadWaitCb cb) { +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + if(!client->tcp || !client->tcp->connected()) { + return false; + } + + client->tcp->readBytes(out, n, std::bind([](WSclient_t * client, bool ok, WSreadWaitCb cb) { + if(cb) { + cb(client, ok); + } + }, client, std::placeholders::_1, cb)); + +#else + unsigned long t = millis(); + size_t len; + DEBUG_WEBSOCKETS("[readCb] n: %zu t: %lu\n", n, t); + while(n > 0) { + if(client->tcp == NULL) { + DEBUG_WEBSOCKETS("[readCb] tcp is null!\n"); + if(cb) { + cb(client, false); + } + return false; + } + + if(!client->tcp->connected()) { + DEBUG_WEBSOCKETS("[readCb] not connected!\n"); + if(cb) { + cb(client, false); + } + return false; + } + + if((millis() - t) > WEBSOCKETS_TCP_TIMEOUT) { + DEBUG_WEBSOCKETS("[readCb] receive TIMEOUT! %lu\n", (millis() - t)); + if(cb) { + cb(client, false); + } + return false; + } + + if(!client->tcp->available()) { +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) + delay(0); +#endif + continue; + } + + len = client->tcp->read((uint8_t*) out, n); + if(len) { + t = millis(); + out += len; + n -= len; + //DEBUG_WEBSOCKETS("Receive %d left %d!\n", len, n); + } else { + //DEBUG_WEBSOCKETS("Receive %d left %d!\n", len, n); + } +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) + delay(0); +#endif + } + if(cb) { + cb(client, true); + } +#endif + return true; +} + +/** + * write x byte to tcp or get timeout + * @param client WSclient_t * + * @param out uint8_t * data buffer + * @param n size_t byte count + * @return bytes send + */ +size_t WebSockets::write(WSclient_t * client, uint8_t *out, size_t n) { + if(out == NULL) return 0; + if(client == NULL) return 0; + unsigned long t = millis(); + size_t len = 0; + size_t total = 0; + DEBUG_WEBSOCKETS("[write] n: %zu t: %lu\n", n, t); + while(n > 0) { + if(client->tcp == NULL) { + DEBUG_WEBSOCKETS("[write] tcp is null!\n"); + break; + } + + if(!client->tcp->connected()) { + DEBUG_WEBSOCKETS("[write] not connected!\n"); + break; + } + + if((millis() - t) > WEBSOCKETS_TCP_TIMEOUT) { + DEBUG_WEBSOCKETS("[write] write TIMEOUT! %lu\n", (millis() - t)); + break; + } + + len = client->tcp->write((const uint8_t*)out, n); + if(len) { + t = millis(); + out += len; + n -= len; + total += len; + //DEBUG_WEBSOCKETS("write %d left %d!\n", len, n); + } else { + //DEBUG_WEBSOCKETS("write %d failed left %d!\n", len, n); + } +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) + delay(0); +#endif + } + return total; +} + +size_t WebSockets::write(WSclient_t * client, const char *out) { + if(client == NULL) return 0; + if(out == NULL) return 0; + return write(client, (uint8_t*)out, strlen(out)); +} diff --git a/libraries/arduinoWebSockets/src/WebSockets.h b/libraries/arduinoWebSockets/src/WebSockets.h new file mode 100644 index 00000000..dee63977 --- /dev/null +++ b/libraries/arduinoWebSockets/src/WebSockets.h @@ -0,0 +1,311 @@ +/** + * @file WebSockets.h + * @date 20.05.2015 + * @author Markus Sattler + * + * Copyright (c) 2015 Markus Sattler. All rights reserved. + * This file is part of the WebSockets for Arduino. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef WEBSOCKETS_H_ +#define WEBSOCKETS_H_ + +#ifdef STM32_DEVICE +#include +#define bit(b) (1UL << (b)) // Taken directly from Arduino.h +#else +#include +#include +#endif + +#ifdef ARDUINO_ARCH_AVR +#error Version 2.x.x currently does not support Arduino with AVR since there is no support for std namespace of c++. +#error Use Version 1.x.x. (ATmega branch) +#else +#include +#endif + + +#ifndef NODEBUG_WEBSOCKETS +#ifdef DEBUG_ESP_PORT +#define DEBUG_WEBSOCKETS(...) DEBUG_ESP_PORT.printf( __VA_ARGS__ ) +#else +//#define DEBUG_WEBSOCKETS(...) os_printf( __VA_ARGS__ ) +#endif +#endif + + +#ifndef DEBUG_WEBSOCKETS +#define DEBUG_WEBSOCKETS(...) +#define NODEBUG_WEBSOCKETS +#endif + +#if defined(ESP8266) || defined(ESP32) + +#define WEBSOCKETS_MAX_DATA_SIZE (15*1024) +#define WEBSOCKETS_USE_BIG_MEM +#define GET_FREE_HEAP ESP.getFreeHeap() +// moves all Header strings to Flash (~300 Byte) +//#define WEBSOCKETS_SAVE_RAM + +#elif defined(STM32_DEVICE) + +#define WEBSOCKETS_MAX_DATA_SIZE (15*1024) +#define WEBSOCKETS_USE_BIG_MEM +#define GET_FREE_HEAP System.freeMemory() + +#else + +//atmega328p has only 2KB ram! +#define WEBSOCKETS_MAX_DATA_SIZE (1024) +// moves all Header strings to Flash +#define WEBSOCKETS_SAVE_RAM + +#endif + + +#define WEBSOCKETS_TCP_TIMEOUT (2000) + +#define NETWORK_ESP8266_ASYNC (0) +#define NETWORK_ESP8266 (1) +#define NETWORK_W5100 (2) +#define NETWORK_ENC28J60 (3) +#define NETWORK_ESP32 (4) + +// max size of the WS Message Header +#define WEBSOCKETS_MAX_HEADER_SIZE (14) + +#if !defined(WEBSOCKETS_NETWORK_TYPE) +// select Network type based +#if defined(ESP8266) || defined(ESP31B) +#define WEBSOCKETS_NETWORK_TYPE NETWORK_ESP8266 +//#define WEBSOCKETS_NETWORK_TYPE NETWORK_ESP8266_ASYNC +//#define WEBSOCKETS_NETWORK_TYPE NETWORK_W5100 + +#elif defined(ESP32) +#define WEBSOCKETS_NETWORK_TYPE NETWORK_ESP32 + +#else +#define WEBSOCKETS_NETWORK_TYPE NETWORK_W5100 + +#endif +#endif + +// Includes and defined based on Network Type +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + +// Note: +// No SSL/WSS support for client in Async mode +// TLS lib need a sync interface! + + +#if defined(ESP8266) +#include +#elif defined(ESP32) +#include +#include +#elif defined(ESP31B) +#include +#else +#error "network type ESP8266 ASYNC only possible on the ESP mcu!" +#endif + +#include +#include +#define WEBSOCKETS_NETWORK_CLASS AsyncTCPbuffer +#define WEBSOCKETS_NETWORK_SERVER_CLASS AsyncServer + +#elif (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) + +#if !defined(ESP8266) && !defined(ESP31B) +#error "network type ESP8266 only possible on the ESP mcu!" +#endif + +#ifdef ESP8266 +#include +#else +#include +#endif +#define WEBSOCKETS_NETWORK_CLASS WiFiClient +#define WEBSOCKETS_NETWORK_SERVER_CLASS WiFiServer + +#elif (WEBSOCKETS_NETWORK_TYPE == NETWORK_W5100) + +#ifdef STM32_DEVICE +#define WEBSOCKETS_NETWORK_CLASS TCPClient +#define WEBSOCKETS_NETWORK_SERVER_CLASS TCPServer +#else +#include +#include +#define WEBSOCKETS_NETWORK_CLASS EthernetClient +#define WEBSOCKETS_NETWORK_SERVER_CLASS EthernetServer +#endif + +#elif (WEBSOCKETS_NETWORK_TYPE == NETWORK_ENC28J60) + +#include +#define WEBSOCKETS_NETWORK_CLASS UIPClient +#define WEBSOCKETS_NETWORK_SERVER_CLASS UIPServer + +#elif (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + +#include +#include +#define WEBSOCKETS_NETWORK_CLASS WiFiClient +#define WEBSOCKETS_NETWORK_SERVER_CLASS WiFiServer + +#else +#error "no network type selected!" +#endif + +// moves all Header strings to Flash (~300 Byte) +#ifdef WEBSOCKETS_SAVE_RAM +#define WEBSOCKETS_STRING(var) F(var) +#else +#define WEBSOCKETS_STRING(var) var +#endif + +typedef enum { + WSC_NOT_CONNECTED, + WSC_HEADER, + WSC_CONNECTED +} WSclientsStatus_t; + +typedef enum { + WStype_ERROR, + WStype_DISCONNECTED, + WStype_CONNECTED, + WStype_TEXT, + WStype_BIN, + WStype_FRAGMENT_TEXT_START, + WStype_FRAGMENT_BIN_START, + WStype_FRAGMENT, + WStype_FRAGMENT_FIN, +} WStype_t; + +typedef enum { + WSop_continuation = 0x00, ///< %x0 denotes a continuation frame + WSop_text = 0x01, ///< %x1 denotes a text frame + WSop_binary = 0x02, ///< %x2 denotes a binary frame + ///< %x3-7 are reserved for further non-control frames + WSop_close = 0x08, ///< %x8 denotes a connection close + WSop_ping = 0x09, ///< %x9 denotes a ping + WSop_pong = 0x0A ///< %xA denotes a pong + ///< %xB-F are reserved for further control frames +} WSopcode_t; + +typedef struct { + + bool fin; + bool rsv1; + bool rsv2; + bool rsv3; + + WSopcode_t opCode; + bool mask; + + size_t payloadLen; + + uint8_t * maskKey; +} WSMessageHeader_t; + +typedef struct { + uint8_t num; ///< connection number + + WSclientsStatus_t status; + + WEBSOCKETS_NETWORK_CLASS * tcp; + + bool isSocketIO; ///< client for socket.io server + +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + bool isSSL; ///< run in ssl mode + WiFiClientSecure * ssl; +#endif + + String cUrl; ///< http url + uint16_t cCode; ///< http code + + bool cIsUpgrade; ///< Connection == Upgrade + bool cIsWebsocket; ///< Upgrade == websocket + + String cSessionId; ///< client Set-Cookie (session id) + String cKey; ///< client Sec-WebSocket-Key + String cAccept; ///< client Sec-WebSocket-Accept + String cProtocol; ///< client Sec-WebSocket-Protocol + String cExtensions; ///< client Sec-WebSocket-Extensions + uint16_t cVersion; ///< client Sec-WebSocket-Version + + uint8_t cWsRXsize; ///< State of the RX + uint8_t cWsHeader[WEBSOCKETS_MAX_HEADER_SIZE]; ///< RX WS Message buffer + WSMessageHeader_t cWsHeaderDecode; + + String base64Authorization; ///< Base64 encoded Auth request + String plainAuthorization; ///< Base64 encoded Auth request + + String extraHeaders; + + bool cHttpHeadersValid; ///< non-websocket http header validity indicator + size_t cMandatoryHeadersCount; ///< non-websocket mandatory http headers present count + +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + String cHttpLine; ///< HTTP header lines +#endif + +} WSclient_t; + + + +class WebSockets { + protected: +#ifdef __AVR__ + typedef void (*WSreadWaitCb)(WSclient_t * client, bool ok); +#else + typedef std::function WSreadWaitCb; +#endif + + virtual void clientDisconnect(WSclient_t * client) = 0; + virtual bool clientIsConnected(WSclient_t * client) = 0; + + virtual void messageReceived(WSclient_t * client, WSopcode_t opcode, uint8_t * payload, size_t length, bool fin) = 0; + + void clientDisconnect(WSclient_t * client, uint16_t code, char * reason = NULL, size_t reasonLen = 0); + bool sendFrame(WSclient_t * client, WSopcode_t opcode, uint8_t * payload = NULL, size_t length = 0, bool mask = false, bool fin = true, bool headerToPayload = false); + + void headerDone(WSclient_t * client); + + void handleWebsocket(WSclient_t * client); + + bool handleWebsocketWaitFor(WSclient_t * client, size_t size); + void handleWebsocketCb(WSclient_t * client); + void handleWebsocketPayloadCb(WSclient_t * client, bool ok, uint8_t * payload); + + String acceptKey(String & clientKey); + String base64_encode(uint8_t * data, size_t length); + + bool readCb(WSclient_t * client, uint8_t *out, size_t n, WSreadWaitCb cb); + virtual size_t write(WSclient_t * client, uint8_t *out, size_t n); + size_t write(WSclient_t * client, const char *out); + + +}; + +#ifndef UNUSED +#define UNUSED(var) (void)(var) +#endif +#endif /* WEBSOCKETS_H_ */ diff --git a/libraries/arduinoWebSockets/src/WebSocketsClient.cpp b/libraries/arduinoWebSockets/src/WebSocketsClient.cpp new file mode 100644 index 00000000..f98822a8 --- /dev/null +++ b/libraries/arduinoWebSockets/src/WebSocketsClient.cpp @@ -0,0 +1,762 @@ +/** + * @file WebSocketsClient.cpp + * @date 20.05.2015 + * @author Markus Sattler + * + * Copyright (c) 2015 Markus Sattler. All rights reserved. + * This file is part of the WebSockets for Arduino. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "WebSockets.h" +#include "WebSocketsClient.h" + +WebSocketsClient::WebSocketsClient() { + _cbEvent = NULL; + _client.num = 0; + _client.extraHeaders = WEBSOCKETS_STRING("Origin: file://"); +} + +WebSocketsClient::~WebSocketsClient() { + disconnect(); +} + +/** + * calles to init the Websockets server + */ +void WebSocketsClient::begin(const char *host, uint16_t port, const char * url, const char * protocol) { + _host = host; + _port = port; +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + _fingerprint = ""; +#endif + + _client.num = 0; + _client.status = WSC_NOT_CONNECTED; + _client.tcp = NULL; +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + _client.isSSL = false; + _client.ssl = NULL; +#endif + _client.cUrl = url; + _client.cCode = 0; + _client.cIsUpgrade = false; + _client.cIsWebsocket = true; + _client.cKey = ""; + _client.cAccept = ""; + _client.cProtocol = protocol; + _client.cExtensions = ""; + _client.cVersion = 0; + _client.base64Authorization = ""; + _client.plainAuthorization = ""; + _client.isSocketIO = false; + +#ifdef ESP8266 + randomSeed(RANDOM_REG32); +#else + // todo find better seed + randomSeed(millis()); +#endif +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + asyncConnect(); +#endif + + _lastConnectionFail = 0; + _reconnectInterval = 500; +} + +void WebSocketsClient::begin(String host, uint16_t port, String url, String protocol) { + begin(host.c_str(), port, url.c_str(), protocol.c_str()); +} + +void WebSocketsClient::begin(IPAddress host, uint16_t port, const char * url, const char * protocol) { + return begin(host.toString().c_str(), port, url, protocol); +} + +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) +void WebSocketsClient::beginSSL(const char *host, uint16_t port, const char * url, const char * fingerprint, const char * protocol) { + begin(host, port, url, protocol); + _client.isSSL = true; + _fingerprint = fingerprint; +} + +void WebSocketsClient::beginSSL(String host, uint16_t port, String url, String fingerprint, String protocol) { + beginSSL(host.c_str(), port, url.c_str(), fingerprint.c_str(), protocol.c_str()); +} +#endif + +void WebSocketsClient::beginSocketIO(const char *host, uint16_t port, const char * url, const char * protocol) { + begin(host, port, url, protocol); + _client.isSocketIO = true; +} + +void WebSocketsClient::beginSocketIO(String host, uint16_t port, String url, String protocol) { + beginSocketIO(host.c_str(), port, url.c_str(), protocol.c_str()); +} + +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) +void WebSocketsClient::beginSocketIOSSL(const char *host, uint16_t port, const char * url, const char * protocol) { + begin(host, port, url, protocol); + _client.isSocketIO = true; + _client.isSSL = true; + _fingerprint = ""; +} + +void WebSocketsClient::beginSocketIOSSL(String host, uint16_t port, String url, String protocol) { + beginSocketIOSSL(host.c_str(), port, url.c_str(), protocol.c_str()); +} +#endif + +#if (WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) +/** + * called in arduino loop + */ +void WebSocketsClient::loop(void) { + if(!clientIsConnected(&_client)) { + // do not flood the server + if((millis() - _lastConnectionFail) < _reconnectInterval) { + return; + } + +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + if(_client.isSSL) { + DEBUG_WEBSOCKETS("[WS-Client] connect wss...\n"); + if(_client.ssl) { + delete _client.ssl; + _client.ssl = NULL; + _client.tcp = NULL; + } + _client.ssl = new WiFiClientSecure(); + _client.tcp = _client.ssl; + } else { + DEBUG_WEBSOCKETS("[WS-Client] connect ws...\n"); + if(_client.tcp) { + delete _client.tcp; + _client.tcp = NULL; + } + _client.tcp = new WiFiClient(); + } +#else + _client.tcp = new WEBSOCKETS_NETWORK_CLASS(); +#endif + + if(!_client.tcp) { + DEBUG_WEBSOCKETS("[WS-Client] creating Network class failed!"); + return; + } + + if(_client.tcp->connect(_host.c_str(), _port)) { + connectedCb(); + _lastConnectionFail = 0; + } else { + connectFailedCb(); + _lastConnectionFail = millis(); + + } + } else { + handleClientData(); + } +} +#endif + +/** + * set callback function + * @param cbEvent WebSocketServerEvent + */ +void WebSocketsClient::onEvent(WebSocketClientEvent cbEvent) { + _cbEvent = cbEvent; +} + +/** + * send text data to client + * @param num uint8_t client id + * @param payload uint8_t * + * @param length size_t + * @param headerToPayload bool (see sendFrame for more details) + * @return true if ok + */ +bool WebSocketsClient::sendTXT(uint8_t * payload, size_t length, bool headerToPayload) { + if(length == 0) { + length = strlen((const char *) payload); + } + if(clientIsConnected(&_client)) { + return sendFrame(&_client, WSop_text, payload, length, true, true, headerToPayload); + } + return false; +} + +bool WebSocketsClient::sendTXT(const uint8_t * payload, size_t length) { + return sendTXT((uint8_t *) payload, length); +} + +bool WebSocketsClient::sendTXT(char * payload, size_t length, bool headerToPayload) { + return sendTXT((uint8_t *) payload, length, headerToPayload); +} + +bool WebSocketsClient::sendTXT(const char * payload, size_t length) { + return sendTXT((uint8_t *) payload, length); +} + +bool WebSocketsClient::sendTXT(String & payload) { + return sendTXT((uint8_t *) payload.c_str(), payload.length()); +} + +/** + * send binary data to client + * @param num uint8_t client id + * @param payload uint8_t * + * @param length size_t + * @param headerToPayload bool (see sendFrame for more details) + * @return true if ok + */ +bool WebSocketsClient::sendBIN(uint8_t * payload, size_t length, bool headerToPayload) { + if(clientIsConnected(&_client)) { + return sendFrame(&_client, WSop_binary, payload, length, true, true, headerToPayload); + } + return false; +} + +bool WebSocketsClient::sendBIN(const uint8_t * payload, size_t length) { + return sendBIN((uint8_t *) payload, length); +} + +/** + * sends a WS ping to Server + * @param payload uint8_t * + * @param length size_t + * @return true if ping is send out + */ +bool WebSocketsClient::sendPing(uint8_t * payload, size_t length) { + if(clientIsConnected(&_client)) { + return sendFrame(&_client, WSop_ping, payload, length, true); + } + return false; +} + +bool WebSocketsClient::sendPing(String & payload) { + return sendPing((uint8_t *) payload.c_str(), payload.length()); +} + +/** + * disconnect one client + * @param num uint8_t client id + */ +void WebSocketsClient::disconnect(void) { + if(clientIsConnected(&_client)) { + WebSockets::clientDisconnect(&_client, 1000); + } +} + +/** + * set the Authorizatio for the http request + * @param user const char * + * @param password const char * + */ +void WebSocketsClient::setAuthorization(const char * user, const char * password) { + if(user && password) { + String auth = user; + auth += ":"; + auth += password; + _client.base64Authorization = base64_encode((uint8_t *) auth.c_str(), auth.length()); + } +} + +/** + * set the Authorizatio for the http request + * @param auth const char * base64 + */ +void WebSocketsClient::setAuthorization(const char * auth) { + if(auth) { + //_client.base64Authorization = auth; + _client.plainAuthorization = auth; + } +} + +/** + * set extra headers for the http request; + * separate headers by "\r\n" + * @param extraHeaders const char * extraHeaders + */ +void WebSocketsClient::setExtraHeaders(const char * extraHeaders) { + _client.extraHeaders = extraHeaders; +} + +/** + * set the reconnect Interval + * how long to wait after a connection initiate failed + * @param time in ms + */ +void WebSocketsClient::setReconnectInterval(unsigned long time) { + _reconnectInterval = time; +} + +//################################################################################# +//################################################################################# +//################################################################################# + +/** + * + * @param client WSclient_t * ptr to the client struct + * @param opcode WSopcode_t + * @param payload uint8_t * + * @param length size_t + */ +void WebSocketsClient::messageReceived(WSclient_t * client, WSopcode_t opcode, uint8_t * payload, size_t length, bool fin) { + WStype_t type = WStype_ERROR; + + UNUSED(client); + + switch(opcode) { + case WSop_text: + type = fin ? WStype_TEXT : WStype_FRAGMENT_TEXT_START; + break; + case WSop_binary: + type = fin ? WStype_BIN : WStype_FRAGMENT_BIN_START; + break; + case WSop_continuation: + type = fin ? WStype_FRAGMENT_FIN : WStype_FRAGMENT; + break; + case WSop_close: + case WSop_ping: + case WSop_pong: + default: + break; + } + + runCbEvent(type, payload, length); + +} + +/** + * Disconnect an client + * @param client WSclient_t * ptr to the client struct + */ +void WebSocketsClient::clientDisconnect(WSclient_t * client) { + + bool event = false; + +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + if(client->isSSL && client->ssl) { + if(client->ssl->connected()) { + client->ssl->flush(); + client->ssl->stop(); + } + event = true; + delete client->ssl; + client->ssl = NULL; + client->tcp = NULL; + } +#endif + + if(client->tcp) { + if(client->tcp->connected()) { +#if (WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) + client->tcp->flush(); +#endif + client->tcp->stop(); + } + event = true; +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + client->status = WSC_NOT_CONNECTED; +#else + delete client->tcp; +#endif + client->tcp = NULL; + } + + client->cCode = 0; + client->cKey = ""; + client->cAccept = ""; + client->cVersion = 0; + client->cIsUpgrade = false; + client->cIsWebsocket = false; + client->cSessionId = ""; + + client->status = WSC_NOT_CONNECTED; + + DEBUG_WEBSOCKETS("[WS-Client] client disconnected.\n"); + if(event) { + runCbEvent(WStype_DISCONNECTED, NULL, 0); + } +} + +/** + * get client state + * @param client WSclient_t * ptr to the client struct + * @return true = conneted + */ +bool WebSocketsClient::clientIsConnected(WSclient_t * client) { + + if(!client->tcp) { + return false; + } + + if(client->tcp->connected()) { + if(client->status != WSC_NOT_CONNECTED) { + return true; + } + } else { + // client lost + if(client->status != WSC_NOT_CONNECTED) { + DEBUG_WEBSOCKETS("[WS-Client] connection lost.\n"); + // do cleanup + clientDisconnect(client); + } + } + + if(client->tcp) { + // do cleanup + clientDisconnect(client); + } + + return false; +} +#if (WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) +/** + * Handel incomming data from Client + */ +void WebSocketsClient::handleClientData(void) { + int len = _client.tcp->available(); + if(len > 0) { + switch(_client.status) { + case WSC_HEADER: { + String headerLine = _client.tcp->readStringUntil('\n'); + handleHeader(&_client, &headerLine); + } + break; + case WSC_CONNECTED: + WebSockets::handleWebsocket(&_client); + break; + default: + WebSockets::clientDisconnect(&_client, 1002); + break; + } + } +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + delay(0); +#endif +} +#endif + +/** + * send the WebSocket header to Server + * @param client WSclient_t * ptr to the client struct + */ +void WebSocketsClient::sendHeader(WSclient_t * client) { + + static const char * NEW_LINE = "\r\n"; + + DEBUG_WEBSOCKETS("[WS-Client][sendHeader] sending header...\n"); + + uint8_t randomKey[16] = { 0 }; + + for(uint8_t i = 0; i < sizeof(randomKey); i++) { + randomKey[i] = random(0xFF); + } + + client->cKey = base64_encode(&randomKey[0], 16); + +#ifndef NODEBUG_WEBSOCKETS + unsigned long start = micros(); +#endif + + String handshake; + bool ws_header = true; + String url = client->cUrl; + + if(client->isSocketIO) { + if(client->cSessionId.length() == 0) { + url += WEBSOCKETS_STRING("&transport=polling"); + ws_header = false; + } else { + url += WEBSOCKETS_STRING("&transport=websocket&sid="); + url += client->cSessionId; + } + } + + handshake = WEBSOCKETS_STRING("GET "); + handshake += url + WEBSOCKETS_STRING(" HTTP/1.1\r\n" + "Host: "); + handshake += _host + ":" + _port + NEW_LINE; + + if(ws_header) { + handshake += WEBSOCKETS_STRING("Connection: Upgrade\r\n" + "Upgrade: websocket\r\n" + "Sec-WebSocket-Version: 13\r\n" + "Sec-WebSocket-Key: "); + handshake += client->cKey + NEW_LINE; + + if(client->cProtocol.length() > 0) { + handshake += WEBSOCKETS_STRING("Sec-WebSocket-Protocol: "); + handshake += client->cProtocol + NEW_LINE; + } + + if(client->cExtensions.length() > 0) { + handshake += WEBSOCKETS_STRING("Sec-WebSocket-Extensions: "); + handshake += client->cExtensions + NEW_LINE; + } + } else { + handshake += WEBSOCKETS_STRING("Connection: keep-alive\r\n"); + } + + // add extra headers; by default this includes "Origin: file://" + if(client->extraHeaders) { + handshake += client->extraHeaders + NEW_LINE; + } + + handshake += WEBSOCKETS_STRING("User-Agent: arduino-WebSocket-Client\r\n"); + + if(client->base64Authorization.length() > 0) { + handshake += WEBSOCKETS_STRING("Authorization: Basic "); + handshake += client->base64Authorization + NEW_LINE; + } + + if(client->plainAuthorization.length() > 0) { + handshake += WEBSOCKETS_STRING("Authorization: "); + handshake += client->plainAuthorization + NEW_LINE; + } + + handshake += NEW_LINE; + + DEBUG_WEBSOCKETS("[WS-Client][sendHeader] handshake %s", (uint8_t* )handshake.c_str()); + write(client, (uint8_t*) handshake.c_str(), handshake.length()); + +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + client->tcp->readStringUntil('\n', &(client->cHttpLine), std::bind(&WebSocketsClient::handleHeader, this, client, &(client->cHttpLine))); +#endif + + DEBUG_WEBSOCKETS("[WS-Client][sendHeader] sending header... Done (%luus).\n", (micros() - start)); + +} + +/** + * handle the WebSocket header reading + * @param client WSclient_t * ptr to the client struct + */ +void WebSocketsClient::handleHeader(WSclient_t * client, String * headerLine) { + + headerLine->trim(); // remove \r + + if(headerLine->length() > 0) { + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] RX: %s\n", headerLine->c_str()); + + if(headerLine->startsWith(WEBSOCKETS_STRING("HTTP/1."))) { + // "HTTP/1.1 101 Switching Protocols" + client->cCode = headerLine->substring(9, headerLine->indexOf(' ', 9)).toInt(); + } else if(headerLine->indexOf(':')) { + String headerName = headerLine->substring(0, headerLine->indexOf(':')); + String headerValue = headerLine->substring(headerLine->indexOf(':') + 1); + + // remove space in the beginning (RFC2616) + if(headerValue[0] == ' ') { + headerValue.remove(0, 1); + } + + if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Connection"))) { + if(headerValue.equalsIgnoreCase(WEBSOCKETS_STRING("upgrade"))) { + client->cIsUpgrade = true; + } + } else if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Upgrade"))) { + if(headerValue.equalsIgnoreCase(WEBSOCKETS_STRING("websocket"))) { + client->cIsWebsocket = true; + } + } else if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Sec-WebSocket-Accept"))) { + client->cAccept = headerValue; + client->cAccept.trim(); // see rfc6455 + } else if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Sec-WebSocket-Protocol"))) { + client->cProtocol = headerValue; + } else if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Sec-WebSocket-Extensions"))) { + client->cExtensions = headerValue; + } else if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Sec-WebSocket-Version"))) { + client->cVersion = headerValue.toInt(); + } else if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Set-Cookie"))) { + if(headerValue.indexOf(WEBSOCKETS_STRING("HttpOnly")) > -1) { + client->cSessionId = headerValue.substring(headerValue.indexOf('=') + 1, headerValue.indexOf(";")); + } else { + client->cSessionId = headerValue.substring(headerValue.indexOf('=') + 1); + } + } + } else { + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] Header error (%s)\n", headerLine->c_str()); + } + + (*headerLine) = ""; +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + client->tcp->readStringUntil('\n', &(client->cHttpLine), std::bind(&WebSocketsClient::handleHeader, this, client, &(client->cHttpLine))); +#endif + + } else { + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] Header read fin.\n"); + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] Client settings:\n"); + + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] - cURL: %s\n", client->cUrl.c_str()); + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] - cKey: %s\n", client->cKey.c_str()); + + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] Server header:\n"); + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] - cCode: %d\n", client->cCode); + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] - cIsUpgrade: %d\n", client->cIsUpgrade); + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] - cIsWebsocket: %d\n", client->cIsWebsocket); + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] - cAccept: %s\n", client->cAccept.c_str()); + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] - cProtocol: %s\n", client->cProtocol.c_str()); + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] - cExtensions: %s\n", client->cExtensions.c_str()); + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] - cVersion: %d\n", client->cVersion); + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] - cSessionId: %s\n", client->cSessionId.c_str()); + + bool ok = (client->cIsUpgrade && client->cIsWebsocket); + + if(ok) { + switch(client->cCode) { + case 101: ///< Switching Protocols + + break; + case 200: + if(client->isSocketIO) { + break; + } + case 403: ///< Forbidden + // todo handle login + default: ///< Server dont unterstand requrst + ok = false; + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] serverCode is not 101 (%d)\n", client->cCode); + clientDisconnect(client); + _lastConnectionFail = millis(); + break; + } + } + + if(ok) { + + if(client->cAccept.length() == 0) { + ok = false; + } else { + // generate Sec-WebSocket-Accept key for check + String sKey = acceptKey(client->cKey); + if(sKey != client->cAccept) { + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] Sec-WebSocket-Accept is wrong\n"); + ok = false; + } + } + } + + if(ok) { + + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] Websocket connection init done.\n"); + headerDone(client); + + runCbEvent(WStype_CONNECTED, (uint8_t *) client->cUrl.c_str(), client->cUrl.length()); + + } else if(clientIsConnected(client) && client->isSocketIO && client->cSessionId.length() > 0) { + sendHeader(client); + } else { + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] no Websocket connection close.\n"); + _lastConnectionFail = millis(); + if(clientIsConnected(client)) { + write(client, "This is a webSocket client!"); + } + clientDisconnect(client); + } + } +} + +void WebSocketsClient::connectedCb() { + + DEBUG_WEBSOCKETS("[WS-Client] connected to %s:%u.\n", _host.c_str(), _port); + +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + _client.tcp->onDisconnect(std::bind([](WebSocketsClient * c, AsyncTCPbuffer * obj, WSclient_t * client) -> bool { + DEBUG_WEBSOCKETS("[WS-Server][%d] Disconnect client\n", client->num); + client->status = WSC_NOT_CONNECTED; + client->tcp = NULL; + + // reconnect + c->asyncConnect(); + + return true; + }, this, std::placeholders::_1, &_client)); +#endif + + _client.status = WSC_HEADER; + +#if (WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) + // set Timeout for readBytesUntil and readStringUntil + _client.tcp->setTimeout(WEBSOCKETS_TCP_TIMEOUT); +#endif + +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) + _client.tcp->setNoDelay(true); + + if(_client.isSSL && _fingerprint.length()) { + if(!_client.ssl->verify(_fingerprint.c_str(), _host.c_str())) { + DEBUG_WEBSOCKETS("[WS-Client] certificate mismatch\n"); + WebSockets::clientDisconnect(&_client, 1000); + return; + } + } +#endif + + // send Header to Server + sendHeader(&_client); + +} + +void WebSocketsClient::connectFailedCb() { + DEBUG_WEBSOCKETS("[WS-Client] connection to %s:%u Faild\n", _host.c_str(), _port); +} + +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + +void WebSocketsClient::asyncConnect() { + + DEBUG_WEBSOCKETS("[WS-Client] asyncConnect...\n"); + + AsyncClient * tcpclient = new AsyncClient(); + + if(!tcpclient) { + DEBUG_WEBSOCKETS("[WS-Client] creating AsyncClient class failed!\n"); + return; + } + + tcpclient->onDisconnect([](void *obj, AsyncClient* c) { + c->free(); + delete c; + }); + + tcpclient->onConnect(std::bind([](WebSocketsClient * ws , AsyncClient * tcp) { + ws->_client.tcp = new AsyncTCPbuffer(tcp); + if(!ws->_client.tcp) { + DEBUG_WEBSOCKETS("[WS-Client] creating Network class failed!\n"); + ws->connectFailedCb(); + return; + } + ws->connectedCb(); + }, this, std::placeholders::_2)); + + tcpclient->onError(std::bind([](WebSocketsClient * ws , AsyncClient * tcp) { + ws->connectFailedCb(); + + // reconnect + ws->asyncConnect(); + }, this, std::placeholders::_2)); + + if(!tcpclient->connect(_host.c_str(), _port)) { + connectFailedCb(); + delete tcpclient; + } + +} + +#endif diff --git a/libraries/arduinoWebSockets/src/WebSocketsClient.h b/libraries/arduinoWebSockets/src/WebSocketsClient.h new file mode 100644 index 00000000..61b8ea2a --- /dev/null +++ b/libraries/arduinoWebSockets/src/WebSocketsClient.h @@ -0,0 +1,136 @@ +/** + * @file WebSocketsClient.h + * @date 20.05.2015 + * @author Markus Sattler + * + * Copyright (c) 2015 Markus Sattler. All rights reserved. + * This file is part of the WebSockets for Arduino. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef WEBSOCKETSCLIENT_H_ +#define WEBSOCKETSCLIENT_H_ + +#include "WebSockets.h" + +class WebSocketsClient: private WebSockets { + public: +#ifdef __AVR__ + typedef void (*WebSocketClientEvent)(WStype_t type, uint8_t * payload, size_t length); +#else + typedef std::function WebSocketClientEvent; +#endif + + + WebSocketsClient(void); + virtual ~WebSocketsClient(void); + + void begin(const char *host, uint16_t port, const char * url = "/", const char * protocol = "arduino"); + void begin(String host, uint16_t port, String url = "/", String protocol = "arduino"); + void begin(IPAddress host, uint16_t port, const char * url = "/", const char * protocol = "arduino"); + +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + void beginSSL(const char *host, uint16_t port, const char * url = "/", const char * = "", const char * protocol = "arduino"); + void beginSSL(String host, uint16_t port, String url = "/", String fingerprint = "", String protocol = "arduino"); +#endif + + void beginSocketIO(const char *host, uint16_t port, const char * url = "/socket.io/?EIO=3", const char * protocol = "arduino"); + void beginSocketIO(String host, uint16_t port, String url = "/socket.io/?EIO=3", String protocol = "arduino"); + +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + void beginSocketIOSSL(const char *host, uint16_t port, const char * url = "/socket.io/?EIO=3", const char * protocol = "arduino"); + void beginSocketIOSSL(String host, uint16_t port, String url = "/socket.io/?EIO=3", String protocol = "arduino"); +#endif + +#if (WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) + void loop(void); +#else + // Async interface not need a loop call + void loop(void) __attribute__ ((deprecated)) {} +#endif + + void onEvent(WebSocketClientEvent cbEvent); + + bool sendTXT(uint8_t * payload, size_t length = 0, bool headerToPayload = false); + bool sendTXT(const uint8_t * payload, size_t length = 0); + bool sendTXT(char * payload, size_t length = 0, bool headerToPayload = false); + bool sendTXT(const char * payload, size_t length = 0); + bool sendTXT(String & payload); + + bool sendBIN(uint8_t * payload, size_t length, bool headerToPayload = false); + bool sendBIN(const uint8_t * payload, size_t length); + + bool sendPing(uint8_t * payload = NULL, size_t length = 0); + bool sendPing(String & payload); + + void disconnect(void); + + void setAuthorization(const char * user, const char * password); + void setAuthorization(const char * auth); + + void setExtraHeaders(const char * extraHeaders = NULL); + + void setReconnectInterval(unsigned long time); + + protected: + String _host; + uint16_t _port; + +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + String _fingerprint; +#endif + WSclient_t _client; + + WebSocketClientEvent _cbEvent; + + unsigned long _lastConnectionFail; + unsigned long _reconnectInterval; + + void messageReceived(WSclient_t * client, WSopcode_t opcode, uint8_t * payload, size_t length, bool fin); + + void clientDisconnect(WSclient_t * client); + bool clientIsConnected(WSclient_t * client); + +#if (WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) + void handleClientData(void); +#endif + + void sendHeader(WSclient_t * client); + void handleHeader(WSclient_t * client, String * headerLine); + + void connectedCb(); + void connectFailedCb(); + +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + void asyncConnect(); +#endif + + /** + * called for sending a Event to the app + * @param type WStype_t + * @param payload uint8_t * + * @param length size_t + */ + virtual void runCbEvent(WStype_t type, uint8_t * payload, size_t length) { + if(_cbEvent) { + _cbEvent(type, payload, length); + } + } + +}; + +#endif /* WEBSOCKETSCLIENT_H_ */ diff --git a/libraries/arduinoWebSockets/src/WebSocketsServer.cpp b/libraries/arduinoWebSockets/src/WebSocketsServer.cpp new file mode 100644 index 00000000..b6f950f4 --- /dev/null +++ b/libraries/arduinoWebSockets/src/WebSocketsServer.cpp @@ -0,0 +1,873 @@ +/** + * @file WebSocketsServer.cpp + * @date 20.05.2015 + * @author Markus Sattler + * + * Copyright (c) 2015 Markus Sattler. All rights reserved. + * This file is part of the WebSockets for Arduino. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "WebSockets.h" +#include "WebSocketsServer.h" + +WebSocketsServer::WebSocketsServer(uint16_t port, String origin, String protocol) { + _port = port; + _origin = origin; + _protocol = protocol; + _runnning = false; + + _server = new WEBSOCKETS_NETWORK_SERVER_CLASS(port); + +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + _server->onClient([](void *s, AsyncClient* c){ + ((WebSocketsServer*)s)->newClient(new AsyncTCPbuffer(c)); + }, this); +#endif + + _cbEvent = NULL; + + _httpHeaderValidationFunc = NULL; + _mandatoryHttpHeaders = NULL; + _mandatoryHttpHeaderCount = 0; + + memset(&_clients[0], 0x00, (sizeof(WSclient_t) * WEBSOCKETS_SERVER_CLIENT_MAX)); +} + + +WebSocketsServer::~WebSocketsServer() { + // disconnect all clients + close(); + + if (_mandatoryHttpHeaders) + delete[] _mandatoryHttpHeaders; + + _mandatoryHttpHeaderCount = 0; +} + +/** + * called to initialize the Websocket server + */ +void WebSocketsServer::begin(void) { + WSclient_t * client; + + // init client storage + for(uint8_t i = 0; i < WEBSOCKETS_SERVER_CLIENT_MAX; i++) { + client = &_clients[i]; + + client->num = i; + client->status = WSC_NOT_CONNECTED; + client->tcp = NULL; +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + client->isSSL = false; + client->ssl = NULL; +#endif + client->cUrl = ""; + client->cCode = 0; + client->cKey = ""; + client->cProtocol = ""; + client->cVersion = 0; + client->cIsUpgrade = false; + client->cIsWebsocket = false; + + client->base64Authorization = ""; + + client->cWsRXsize = 0; + +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + client->cHttpLine = ""; +#endif + } + +#ifdef ESP8266 + randomSeed(RANDOM_REG32); +#elif defined(ESP32) + #define DR_REG_RNG_BASE 0x3ff75144 + randomSeed(READ_PERI_REG(DR_REG_RNG_BASE)); +#else + // TODO find better seed + randomSeed(millis()); +#endif + + _runnning = true; + _server->begin(); + + DEBUG_WEBSOCKETS("[WS-Server] Server Started.\n"); +} + +void WebSocketsServer::close(void) { + _runnning = false; + disconnect(); + +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) + _server->close(); +#elif (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + _server->end(); +#else + // TODO how to close server? +#endif + +} + +#if (WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) +/** + * called in arduino loop + */ +void WebSocketsServer::loop(void) { + if(_runnning) { + handleNewClients(); + handleClientData(); + } +} +#endif + +/** + * set callback function + * @param cbEvent WebSocketServerEvent + */ +void WebSocketsServer::onEvent(WebSocketServerEvent cbEvent) { + _cbEvent = cbEvent; +} + +/* + * Sets the custom http header validator function + * @param httpHeaderValidationFunc WebSocketServerHttpHeaderValFunc ///< pointer to the custom http header validation function + * @param mandatoryHttpHeaders[] const char* ///< the array of named http headers considered to be mandatory / must be present in order for websocket upgrade to succeed + * @param mandatoryHttpHeaderCount size_t ///< the number of items in the mandatoryHttpHeaders array + */ +void WebSocketsServer::onValidateHttpHeader( + WebSocketServerHttpHeaderValFunc validationFunc, + const char* mandatoryHttpHeaders[], + size_t mandatoryHttpHeaderCount) +{ + _httpHeaderValidationFunc = validationFunc; + + if (_mandatoryHttpHeaders) + delete[] _mandatoryHttpHeaders; + + _mandatoryHttpHeaderCount = mandatoryHttpHeaderCount; + _mandatoryHttpHeaders = new String[_mandatoryHttpHeaderCount]; + + for (size_t i = 0; i < _mandatoryHttpHeaderCount; i++) { + _mandatoryHttpHeaders[i] = mandatoryHttpHeaders[i]; + } +} + +/* + * send text data to client + * @param num uint8_t client id + * @param payload uint8_t * + * @param length size_t + * @param headerToPayload bool (see sendFrame for more details) + * @return true if ok + */ +bool WebSocketsServer::sendTXT(uint8_t num, uint8_t * payload, size_t length, bool headerToPayload) { + if(num >= WEBSOCKETS_SERVER_CLIENT_MAX) { + return false; + } + if(length == 0) { + length = strlen((const char *) payload); + } + WSclient_t * client = &_clients[num]; + if(clientIsConnected(client)) { + return sendFrame(client, WSop_text, payload, length, false, true, headerToPayload); + } + return false; +} + +bool WebSocketsServer::sendTXT(uint8_t num, const uint8_t * payload, size_t length) { + return sendTXT(num, (uint8_t *) payload, length); +} + +bool WebSocketsServer::sendTXT(uint8_t num, char * payload, size_t length, bool headerToPayload) { + return sendTXT(num, (uint8_t *) payload, length, headerToPayload); +} + +bool WebSocketsServer::sendTXT(uint8_t num, const char * payload, size_t length) { + return sendTXT(num, (uint8_t *) payload, length); +} + +bool WebSocketsServer::sendTXT(uint8_t num, String & payload) { + return sendTXT(num, (uint8_t *) payload.c_str(), payload.length()); +} + +/** + * send text data to client all + * @param payload uint8_t * + * @param length size_t + * @param headerToPayload bool (see sendFrame for more details) + * @return true if ok + */ +bool WebSocketsServer::broadcastTXT(uint8_t * payload, size_t length, bool headerToPayload) { + WSclient_t * client; + bool ret = true; + if(length == 0) { + length = strlen((const char *) payload); + } + + for(uint8_t i = 0; i < WEBSOCKETS_SERVER_CLIENT_MAX; i++) { + client = &_clients[i]; + if(clientIsConnected(client)) { + if(!sendFrame(client, WSop_text, payload, length, false, true, headerToPayload)) { + ret = false; + } + } +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) + delay(0); +#endif + } + return ret; +} + +bool WebSocketsServer::broadcastTXT(const uint8_t * payload, size_t length) { + return broadcastTXT((uint8_t *) payload, length); +} + +bool WebSocketsServer::broadcastTXT(char * payload, size_t length, bool headerToPayload) { + return broadcastTXT((uint8_t *) payload, length, headerToPayload); +} + +bool WebSocketsServer::broadcastTXT(const char * payload, size_t length) { + return broadcastTXT((uint8_t *) payload, length); +} + +bool WebSocketsServer::broadcastTXT(String & payload) { + return broadcastTXT((uint8_t *) payload.c_str(), payload.length()); +} + +/** + * send binary data to client + * @param num uint8_t client id + * @param payload uint8_t * + * @param length size_t + * @param headerToPayload bool (see sendFrame for more details) + * @return true if ok + */ +bool WebSocketsServer::sendBIN(uint8_t num, uint8_t * payload, size_t length, bool headerToPayload) { + if(num >= WEBSOCKETS_SERVER_CLIENT_MAX) { + return false; + } + WSclient_t * client = &_clients[num]; + if(clientIsConnected(client)) { + return sendFrame(client, WSop_binary, payload, length, false, true, headerToPayload); + } + return false; +} + +bool WebSocketsServer::sendBIN(uint8_t num, const uint8_t * payload, size_t length) { + return sendBIN(num, (uint8_t *) payload, length); +} + +/** + * send binary data to client all + * @param payload uint8_t * + * @param length size_t + * @param headerToPayload bool (see sendFrame for more details) + * @return true if ok + */ +bool WebSocketsServer::broadcastBIN(uint8_t * payload, size_t length, bool headerToPayload) { + WSclient_t * client; + bool ret = true; + for(uint8_t i = 0; i < WEBSOCKETS_SERVER_CLIENT_MAX; i++) { + client = &_clients[i]; + if(clientIsConnected(client)) { + if(!sendFrame(client, WSop_binary, payload, length, false, true, headerToPayload)) { + ret = false; + } + } +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) + delay(0); +#endif + } + return ret; +} + +bool WebSocketsServer::broadcastBIN(const uint8_t * payload, size_t length) { + return broadcastBIN((uint8_t *) payload, length); +} + + +/** + * sends a WS ping to Client + * @param num uint8_t client id + * @param payload uint8_t * + * @param length size_t + * @return true if ping is send out + */ +bool WebSocketsServer::sendPing(uint8_t num, uint8_t * payload, size_t length) { + if(num >= WEBSOCKETS_SERVER_CLIENT_MAX) { + return false; + } + WSclient_t * client = &_clients[num]; + if(clientIsConnected(client)) { + return sendFrame(client, WSop_ping, payload, length); + } + return false; +} + +bool WebSocketsServer::sendPing(uint8_t num, String & payload) { + return sendPing(num, (uint8_t *) payload.c_str(), payload.length()); +} + +/** + * sends a WS ping to all Client + * @param payload uint8_t * + * @param length size_t + * @return true if ping is send out + */ +bool WebSocketsServer::broadcastPing(uint8_t * payload, size_t length) { + WSclient_t * client; + bool ret = true; + for(uint8_t i = 0; i < WEBSOCKETS_SERVER_CLIENT_MAX; i++) { + client = &_clients[i]; + if(clientIsConnected(client)) { + if(!sendFrame(client, WSop_ping, payload, length)) { + ret = false; + } + } +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) + delay(0); +#endif + } + return ret; +} + +bool WebSocketsServer::broadcastPing(String & payload) { + return broadcastPing((uint8_t *) payload.c_str(), payload.length()); +} + + +/** + * disconnect all clients + */ +void WebSocketsServer::disconnect(void) { + WSclient_t * client; + for(uint8_t i = 0; i < WEBSOCKETS_SERVER_CLIENT_MAX; i++) { + client = &_clients[i]; + if(clientIsConnected(client)) { + WebSockets::clientDisconnect(client, 1000); + } + } +} + +/** + * disconnect one client + * @param num uint8_t client id + */ +void WebSocketsServer::disconnect(uint8_t num) { + if(num >= WEBSOCKETS_SERVER_CLIENT_MAX) { + return; + } + WSclient_t * client = &_clients[num]; + if(clientIsConnected(client)) { + WebSockets::clientDisconnect(client, 1000); + } +} + + +/* + * set the Authorization for the http request + * @param user const char * + * @param password const char * + */ +void WebSocketsServer::setAuthorization(const char * user, const char * password) { + if(user && password) { + String auth = user; + auth += ":"; + auth += password; + _base64Authorization = base64_encode((uint8_t *)auth.c_str(), auth.length()); + } +} + +/** + * set the Authorizatio for the http request + * @param auth const char * base64 + */ +void WebSocketsServer::setAuthorization(const char * auth) { + if(auth) { + _base64Authorization = auth; + } +} + +/** + * count the connected clients (optional ping them) + * @param ping bool ping the connected clients + */ +int WebSocketsServer::connectedClients(bool ping) { + WSclient_t * client; + int count = 0; + for(uint8_t i = 0; i < WEBSOCKETS_SERVER_CLIENT_MAX; i++) { + client = &_clients[i]; + if(client->status == WSC_CONNECTED) { + if(ping != true || sendPing(i)) { + count++; + } + } + } + return count; +} + +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) +/** + * get an IP for a client + * @param num uint8_t client id + * @return IPAddress + */ +IPAddress WebSocketsServer::remoteIP(uint8_t num) { + if(num < WEBSOCKETS_SERVER_CLIENT_MAX) { + WSclient_t * client = &_clients[num]; + if(clientIsConnected(client)) { + return client->tcp->remoteIP(); + } + } + + return IPAddress(); +} +#endif + +//################################################################################# +//################################################################################# +//################################################################################# + +/** + * handle new client connection + * @param client + */ +bool WebSocketsServer::newClient(WEBSOCKETS_NETWORK_CLASS * TCPclient) { + WSclient_t * client; + // search free list entry for client + for(uint8_t i = 0; i < WEBSOCKETS_SERVER_CLIENT_MAX; i++) { + client = &_clients[i]; + + // state is not connected or tcp connection is lost + if(!clientIsConnected(client)) { + + client->tcp = TCPclient; + +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + client->isSSL = false; + client->tcp->setNoDelay(true); +#endif +#if (WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) + // set Timeout for readBytesUntil and readStringUntil + client->tcp->setTimeout(WEBSOCKETS_TCP_TIMEOUT); +#endif + client->status = WSC_HEADER; +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + IPAddress ip = client->tcp->remoteIP(); + DEBUG_WEBSOCKETS("[WS-Server][%d] new client from %d.%d.%d.%d\n", client->num, ip[0], ip[1], ip[2], ip[3]); +#else + DEBUG_WEBSOCKETS("[WS-Server][%d] new client\n", client->num); +#endif + + +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + client->tcp->onDisconnect(std::bind([](WebSocketsServer * server, AsyncTCPbuffer * obj, WSclient_t * client) -> bool { + DEBUG_WEBSOCKETS("[WS-Server][%d] Disconnect client\n", client->num); + + AsyncTCPbuffer ** sl = &server->_clients[client->num].tcp; + if(*sl == obj) { + client->status = WSC_NOT_CONNECTED; + *sl = NULL; + } + return true; + }, this, std::placeholders::_1, client)); + + + client->tcp->readStringUntil('\n', &(client->cHttpLine), std::bind(&WebSocketsServer::handleHeader, this, client, &(client->cHttpLine))); +#endif + + return true; + break; + } + } + return false; +} + +/** + * + * @param client WSclient_t * ptr to the client struct + * @param opcode WSopcode_t + * @param payload uint8_t * + * @param length size_t + */ +void WebSocketsServer::messageReceived(WSclient_t * client, WSopcode_t opcode, uint8_t * payload, size_t length, bool fin) { + WStype_t type = WStype_ERROR; + + switch(opcode) { + case WSop_text: + type = fin ? WStype_TEXT : WStype_FRAGMENT_TEXT_START; + break; + case WSop_binary: + type = fin ? WStype_BIN : WStype_FRAGMENT_BIN_START; + break; + case WSop_continuation: + type = fin ? WStype_FRAGMENT_FIN : WStype_FRAGMENT; + break; + case WSop_close: + case WSop_ping: + case WSop_pong: + default: + break; + } + + runCbEvent(client->num, type, payload, length); + +} + +/** + * Disconnect an client + * @param client WSclient_t * ptr to the client struct + */ +void WebSocketsServer::clientDisconnect(WSclient_t * client) { + + +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + if(client->isSSL && client->ssl) { + if(client->ssl->connected()) { + client->ssl->flush(); + client->ssl->stop(); + } + delete client->ssl; + client->ssl = NULL; + client->tcp = NULL; + } +#endif + + if(client->tcp) { + if(client->tcp->connected()) { +#if (WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) + client->tcp->flush(); +#endif + client->tcp->stop(); + } +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + client->status = WSC_NOT_CONNECTED; +#else + delete client->tcp; +#endif + client->tcp = NULL; + } + + client->cUrl = ""; + client->cKey = ""; + client->cProtocol = ""; + client->cVersion = 0; + client->cIsUpgrade = false; + client->cIsWebsocket = false; + + client->cWsRXsize = 0; + +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + client->cHttpLine = ""; +#endif + + client->status = WSC_NOT_CONNECTED; + + DEBUG_WEBSOCKETS("[WS-Server][%d] client disconnected.\n", client->num); + + runCbEvent(client->num, WStype_DISCONNECTED, NULL, 0); + +} + +/** + * get client state + * @param client WSclient_t * ptr to the client struct + * @return true = connected + */ +bool WebSocketsServer::clientIsConnected(WSclient_t * client) { + + if(!client->tcp) { + return false; + } + + if(client->tcp->connected()) { + if(client->status != WSC_NOT_CONNECTED) { + return true; + } + } else { + // client lost + if(client->status != WSC_NOT_CONNECTED) { + DEBUG_WEBSOCKETS("[WS-Server][%d] client connection lost.\n", client->num); + // do cleanup + clientDisconnect(client); + } + } + + if(client->tcp) { + // do cleanup + DEBUG_WEBSOCKETS("[WS-Server][%d] client list cleanup.\n", client->num); + clientDisconnect(client); + } + + return false; +} +#if (WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) +/** + * Handle incoming Connection Request + */ +void WebSocketsServer::handleNewClients(void) { + +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + while(_server->hasClient()) { +#endif + bool ok = false; + +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + // store new connection + WEBSOCKETS_NETWORK_CLASS * tcpClient = new WEBSOCKETS_NETWORK_CLASS(_server->available()); +#else + WEBSOCKETS_NETWORK_CLASS * tcpClient = new WEBSOCKETS_NETWORK_CLASS(_server->available()); +#endif + + if(!tcpClient) { + DEBUG_WEBSOCKETS("[WS-Client] creating Network class failed!"); + return; + } + + ok = newClient(tcpClient); + + if(!ok) { + // no free space to handle client +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + IPAddress ip = tcpClient->remoteIP(); + DEBUG_WEBSOCKETS("[WS-Server] no free space new client from %d.%d.%d.%d\n", ip[0], ip[1], ip[2], ip[3]); +#else + DEBUG_WEBSOCKETS("[WS-Server] no free space new client\n"); +#endif + tcpClient->stop(); + } + +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + delay(0); + } +#endif + +} + + +/** + * Handel incomming data from Client + */ +void WebSocketsServer::handleClientData(void) { + + WSclient_t * client; + for(uint8_t i = 0; i < WEBSOCKETS_SERVER_CLIENT_MAX; i++) { + client = &_clients[i]; + if(clientIsConnected(client)) { + int len = client->tcp->available(); + if(len > 0) { + //DEBUG_WEBSOCKETS("[WS-Server][%d][handleClientData] len: %d\n", client->num, len); + switch(client->status) { + case WSC_HEADER: + { + String headerLine = client->tcp->readStringUntil('\n'); + handleHeader(client, &headerLine); + } + break; + case WSC_CONNECTED: + WebSockets::handleWebsocket(client); + break; + default: + WebSockets::clientDisconnect(client, 1002); + break; + } + } + } +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) + delay(0); +#endif + } +} +#endif + +/* + * returns an indicator whether the given named header exists in the configured _mandatoryHttpHeaders collection + * @param headerName String ///< the name of the header being checked + */ +bool WebSocketsServer::hasMandatoryHeader(String headerName) { + for (size_t i = 0; i < _mandatoryHttpHeaderCount; i++) { + if (_mandatoryHttpHeaders[i].equalsIgnoreCase(headerName)) + return true; + } + return false; +} + + +/** + * handles http header reading for WebSocket upgrade + * @param client WSclient_t * ///< pointer to the client struct + * @param headerLine String ///< the header being read / processed + */ +void WebSocketsServer::handleHeader(WSclient_t * client, String * headerLine) { + + static const char * NEW_LINE = "\r\n"; + + headerLine->trim(); // remove \r + + if(headerLine->length() > 0) { + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] RX: %s\n", client->num, headerLine->c_str()); + + // websocket requests always start with GET see rfc6455 + if(headerLine->startsWith("GET ")) { + + // cut URL out + client->cUrl = headerLine->substring(4, headerLine->indexOf(' ', 4)); + + //reset non-websocket http header validation state for this client + client->cHttpHeadersValid = true; + client->cMandatoryHeadersCount = 0; + + } else if(headerLine->indexOf(':')) { + String headerName = headerLine->substring(0, headerLine->indexOf(':')); + String headerValue = headerLine->substring(headerLine->indexOf(':') + 1); + + // remove space in the beginning (RFC2616) + if(headerValue[0] == ' ') { + headerValue.remove(0, 1); + } + + if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Connection"))) { + headerValue.toLowerCase(); + if(headerValue.indexOf(WEBSOCKETS_STRING("upgrade")) >= 0) { + client->cIsUpgrade = true; + } + } else if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Upgrade"))) { + if(headerValue.equalsIgnoreCase(WEBSOCKETS_STRING("websocket"))) { + client->cIsWebsocket = true; + } + } else if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Sec-WebSocket-Version"))) { + client->cVersion = headerValue.toInt(); + } else if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Sec-WebSocket-Key"))) { + client->cKey = headerValue; + client->cKey.trim(); // see rfc6455 + } else if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Sec-WebSocket-Protocol"))) { + client->cProtocol = headerValue; + } else if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Sec-WebSocket-Extensions"))) { + client->cExtensions = headerValue; + } else if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Authorization"))) { + client->base64Authorization = headerValue; + } else { + client->cHttpHeadersValid &= execHttpHeaderValidation(headerName, headerValue); + if(_mandatoryHttpHeaderCount > 0 && hasMandatoryHeader(headerName)) { + client->cMandatoryHeadersCount++; + } + } + + } else { + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] Header error (%s)\n", headerLine->c_str()); + } + + (*headerLine) = ""; +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + client->tcp->readStringUntil('\n', &(client->cHttpLine), std::bind(&WebSocketsServer::handleHeader, this, client, &(client->cHttpLine))); +#endif + } else { + + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] Header read fin.\n", client->num); + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] - cURL: %s\n", client->num, client->cUrl.c_str()); + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] - cIsUpgrade: %d\n", client->num, client->cIsUpgrade); + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] - cIsWebsocket: %d\n", client->num, client->cIsWebsocket); + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] - cKey: %s\n", client->num, client->cKey.c_str()); + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] - cProtocol: %s\n", client->num, client->cProtocol.c_str()); + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] - cExtensions: %s\n", client->num, client->cExtensions.c_str()); + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] - cVersion: %d\n", client->num, client->cVersion); + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] - base64Authorization: %s\n", client->num, client->base64Authorization.c_str()); + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] - cHttpHeadersValid: %d\n", client->num, client->cHttpHeadersValid); + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] - cMandatoryHeadersCount: %d\n", client->num, client->cMandatoryHeadersCount); + + bool ok = (client->cIsUpgrade && client->cIsWebsocket); + + if(ok) { + if(client->cUrl.length() == 0) { + ok = false; + } + if(client->cKey.length() == 0) { + ok = false; + } + if(client->cVersion != 13) { + ok = false; + } + if(!client->cHttpHeadersValid) { + ok = false; + } + if (client->cMandatoryHeadersCount != _mandatoryHttpHeaderCount) { + ok = false; + } + } + + if(_base64Authorization.length() > 0) { + String auth = WEBSOCKETS_STRING("Basic "); + auth += _base64Authorization; + if(auth != client->base64Authorization) { + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] HTTP Authorization failed!\n", client->num); + handleAuthorizationFailed(client); + return; + } + } + + if(ok) { + + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] Websocket connection incoming.\n", client->num); + + // generate Sec-WebSocket-Accept key + String sKey = acceptKey(client->cKey); + + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] - sKey: %s\n", client->num, sKey.c_str()); + + client->status = WSC_CONNECTED; + + String handshake = WEBSOCKETS_STRING("HTTP/1.1 101 Switching Protocols\r\n" + "Server: arduino-WebSocketsServer\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Version: 13\r\n" + "Sec-WebSocket-Accept: "); + handshake += sKey + NEW_LINE; + + if(_origin.length() > 0) { + handshake += WEBSOCKETS_STRING("Access-Control-Allow-Origin: "); + handshake +=_origin + NEW_LINE; + } + + if(client->cProtocol.length() > 0) { + handshake += WEBSOCKETS_STRING("Sec-WebSocket-Protocol: "); + handshake +=_protocol + NEW_LINE; + } + + // header end + handshake += NEW_LINE; + + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] handshake %s", client->num, (uint8_t*)handshake.c_str()); + + write(client, (uint8_t*)handshake.c_str(), handshake.length()); + + headerDone(client); + + // send ping + WebSockets::sendFrame(client, WSop_ping); + + runCbEvent(client->num, WStype_CONNECTED, (uint8_t *) client->cUrl.c_str(), client->cUrl.length()); + + } else { + handleNonWebsocketConnection(client); + } + } +} + + + diff --git a/libraries/arduinoWebSockets/src/WebSocketsServer.h b/libraries/arduinoWebSockets/src/WebSocketsServer.h new file mode 100644 index 00000000..db945a6f --- /dev/null +++ b/libraries/arduinoWebSockets/src/WebSocketsServer.h @@ -0,0 +1,212 @@ +/** + * @file WebSocketsServer.h + * @date 20.05.2015 + * @author Markus Sattler + * + * Copyright (c) 2015 Markus Sattler. All rights reserved. + * This file is part of the WebSockets for Arduino. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef WEBSOCKETSSERVER_H_ +#define WEBSOCKETSSERVER_H_ + +#include "WebSockets.h" + +#ifndef WEBSOCKETS_SERVER_CLIENT_MAX +#define WEBSOCKETS_SERVER_CLIENT_MAX (5) +#endif + + + + +class WebSocketsServer: protected WebSockets { +public: + +#ifdef __AVR__ + typedef void (*WebSocketServerEvent)(uint8_t num, WStype_t type, uint8_t * payload, size_t length); + typedef bool (*WebSocketServerHttpHeaderValFunc)(String headerName, String headerValue); +#else + typedef std::function WebSocketServerEvent; + typedef std::function WebSocketServerHttpHeaderValFunc; +#endif + + WebSocketsServer(uint16_t port, String origin = "", String protocol = "arduino"); + virtual ~WebSocketsServer(void); + + void begin(void); + void close(void); + +#if (WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) + void loop(void); +#else + // Async interface not need a loop call + void loop(void) __attribute__ ((deprecated)) {} +#endif + + void onEvent(WebSocketServerEvent cbEvent); + void onValidateHttpHeader( + WebSocketServerHttpHeaderValFunc validationFunc, + const char* mandatoryHttpHeaders[], + size_t mandatoryHttpHeaderCount); + + + bool sendTXT(uint8_t num, uint8_t * payload, size_t length = 0, bool headerToPayload = false); + bool sendTXT(uint8_t num, const uint8_t * payload, size_t length = 0); + bool sendTXT(uint8_t num, char * payload, size_t length = 0, bool headerToPayload = false); + bool sendTXT(uint8_t num, const char * payload, size_t length = 0); + bool sendTXT(uint8_t num, String & payload); + + bool broadcastTXT(uint8_t * payload, size_t length = 0, bool headerToPayload = false); + bool broadcastTXT(const uint8_t * payload, size_t length = 0); + bool broadcastTXT(char * payload, size_t length = 0, bool headerToPayload = false); + bool broadcastTXT(const char * payload, size_t length = 0); + bool broadcastTXT(String & payload); + + bool sendBIN(uint8_t num, uint8_t * payload, size_t length, bool headerToPayload = false); + bool sendBIN(uint8_t num, const uint8_t * payload, size_t length); + + bool broadcastBIN(uint8_t * payload, size_t length, bool headerToPayload = false); + bool broadcastBIN(const uint8_t * payload, size_t length); + + bool sendPing(uint8_t num, uint8_t * payload = NULL, size_t length = 0); + bool sendPing(uint8_t num, String & payload); + + bool broadcastPing(uint8_t * payload = NULL, size_t length = 0); + bool broadcastPing(String & payload); + + void disconnect(void); + void disconnect(uint8_t num); + + void setAuthorization(const char * user, const char * password); + void setAuthorization(const char * auth); + + int connectedClients(bool ping = false); + +#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + IPAddress remoteIP(uint8_t num); +#endif + +protected: + uint16_t _port; + String _origin; + String _protocol; + String _base64Authorization; ///< Base64 encoded Auth request + String * _mandatoryHttpHeaders; + size_t _mandatoryHttpHeaderCount; + + WEBSOCKETS_NETWORK_SERVER_CLASS * _server; + + WSclient_t _clients[WEBSOCKETS_SERVER_CLIENT_MAX]; + + WebSocketServerEvent _cbEvent; + WebSocketServerHttpHeaderValFunc _httpHeaderValidationFunc; + + bool _runnning; + + bool newClient(WEBSOCKETS_NETWORK_CLASS * TCPclient); + + void messageReceived(WSclient_t * client, WSopcode_t opcode, uint8_t * payload, size_t length, bool fin); + + void clientDisconnect(WSclient_t * client); + bool clientIsConnected(WSclient_t * client); + +#if (WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) + void handleNewClients(void); + void handleClientData(void); +#endif + + void handleHeader(WSclient_t * client, String * headerLine); + + /** + * called if a non Websocket connection is coming in. + * Note: can be override + * @param client WSclient_t * ptr to the client struct + */ + virtual void handleNonWebsocketConnection(WSclient_t * client) { + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] no Websocket connection close.\n", client->num); + client->tcp->write("HTTP/1.1 400 Bad Request\r\n" + "Server: arduino-WebSocket-Server\r\n" + "Content-Type: text/plain\r\n" + "Content-Length: 32\r\n" + "Connection: close\r\n" + "Sec-WebSocket-Version: 13\r\n" + "\r\n" + "This is a Websocket server only!"); + clientDisconnect(client); + } + + /** + * called if a non Authorization connection is coming in. + * Note: can be override + * @param client WSclient_t * ptr to the client struct + */ + virtual void handleAuthorizationFailed(WSclient_t *client) { + client->tcp->write("HTTP/1.1 401 Unauthorized\r\n" + "Server: arduino-WebSocket-Server\r\n" + "Content-Type: text/plain\r\n" + "Content-Length: 45\r\n" + "Connection: close\r\n" + "Sec-WebSocket-Version: 13\r\n" + "WWW-Authenticate: Basic realm=\"WebSocket Server\"" + "\r\n" + "This Websocket server requires Authorization!"); + clientDisconnect(client); + } + + /** + * called for sending a Event to the app + * @param num uint8_t + * @param type WStype_t + * @param payload uint8_t * + * @param length size_t + */ + virtual void runCbEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { + if(_cbEvent) { + _cbEvent(num, type, payload, length); + } + } + + /* + * Called at client socket connect handshake negotiation time for each http header that is not + * a websocket specific http header (not Connection, Upgrade, Sec-WebSocket-*) + * If the custom httpHeaderValidationFunc returns false for any headerName / headerValue passed, the + * socket negotiation is considered invalid and the upgrade to websockets request is denied / rejected + * This mechanism can be used to enable custom authentication schemes e.g. test the value + * of a session cookie to determine if a user is logged on / authenticated + */ + virtual bool execHttpHeaderValidation(String headerName, String headerValue) { + if(_httpHeaderValidationFunc) { + //return the value of the custom http header validation function + return _httpHeaderValidationFunc(headerName, headerValue); + } + //no custom http header validation so just assume all is good + return true; + } + +private: + /* + * returns an indicator whether the given named header exists in the configured _mandatoryHttpHeaders collection + * @param headerName String ///< the name of the header being checked + */ + bool hasMandatoryHeader(String headerName); + +}; + + + +#endif /* WEBSOCKETSSERVER_H_ */ diff --git a/libraries/arduinoWebSockets/src/libb64/AUTHORS b/libraries/arduinoWebSockets/src/libb64/AUTHORS new file mode 100644 index 00000000..af687375 --- /dev/null +++ b/libraries/arduinoWebSockets/src/libb64/AUTHORS @@ -0,0 +1,7 @@ +libb64: Base64 Encoding/Decoding Routines +====================================== + +Authors: +------- + +Chris Venter chris.venter@gmail.com http://rocketpod.blogspot.com diff --git a/libraries/arduinoWebSockets/src/libb64/LICENSE b/libraries/arduinoWebSockets/src/libb64/LICENSE new file mode 100644 index 00000000..a6b56069 --- /dev/null +++ b/libraries/arduinoWebSockets/src/libb64/LICENSE @@ -0,0 +1,29 @@ +Copyright-Only Dedication (based on United States law) +or Public Domain Certification + +The person or persons who have associated work with this document (the +"Dedicator" or "Certifier") hereby either (a) certifies that, to the best of +his knowledge, the work of authorship identified is in the public domain of the +country from which the work is published, or (b) hereby dedicates whatever +copyright the dedicators holds in the work of authorship identified below (the +"Work") to the public domain. A certifier, moreover, dedicates any copyright +interest he may have in the associated work, and for these purposes, is +described as a "dedicator" below. + +A certifier has taken reasonable steps to verify the copyright status of this +work. Certifier recognizes that his good faith efforts may not shield him from +liability if in fact the work certified is not in the public domain. + +Dedicator makes this dedication for the benefit of the public at large and to +the detriment of the Dedicator's heirs and successors. Dedicator intends this +dedication to be an overt act of relinquishment in perpetuity of all present +and future rights under copyright law, whether vested or contingent, in the +Work. Dedicator understands that such relinquishment of all rights includes +the relinquishment of all rights to enforce (by lawsuit or otherwise) those +copyrights in the Work. + +Dedicator recognizes that, once placed in the public domain, the Work may be +freely reproduced, distributed, transmitted, used, modified, built upon, or +otherwise exploited by anyone for any purpose, commercial or non-commercial, +and in any way, including by methods that have not yet been invented or +conceived. \ No newline at end of file diff --git a/libraries/arduinoWebSockets/src/libb64/cdecode.c b/libraries/arduinoWebSockets/src/libb64/cdecode.c new file mode 100644 index 00000000..e135da24 --- /dev/null +++ b/libraries/arduinoWebSockets/src/libb64/cdecode.c @@ -0,0 +1,98 @@ +/* +cdecoder.c - c source to a base64 decoding algorithm implementation + +This is part of the libb64 project, and has been placed in the public domain. +For details, see http://sourceforge.net/projects/libb64 +*/ + +#ifdef ESP8266 +#include +#endif + +#if defined(ESP32) +#define CORE_HAS_LIBB64 +#endif + +#ifndef CORE_HAS_LIBB64 +#include "cdecode_inc.h" + +int base64_decode_value(char value_in) +{ + static const char decoding[] = {62,-1,-1,-1,63,52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-2,-1,-1,-1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,-1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51}; + static const char decoding_size = sizeof(decoding); + value_in -= 43; + if (value_in < 0 || value_in > decoding_size) return -1; + return decoding[(int)value_in]; +} + +void base64_init_decodestate(base64_decodestate* state_in) +{ + state_in->step = step_a; + state_in->plainchar = 0; +} + +int base64_decode_block(const char* code_in, const int length_in, char* plaintext_out, base64_decodestate* state_in) +{ + const char* codechar = code_in; + char* plainchar = plaintext_out; + char fragment; + + *plainchar = state_in->plainchar; + + switch (state_in->step) + { + while (1) + { + case step_a: + do { + if (codechar == code_in+length_in) + { + state_in->step = step_a; + state_in->plainchar = *plainchar; + return plainchar - plaintext_out; + } + fragment = (char)base64_decode_value(*codechar++); + } while (fragment < 0); + *plainchar = (fragment & 0x03f) << 2; + case step_b: + do { + if (codechar == code_in+length_in) + { + state_in->step = step_b; + state_in->plainchar = *plainchar; + return plainchar - plaintext_out; + } + fragment = (char)base64_decode_value(*codechar++); + } while (fragment < 0); + *plainchar++ |= (fragment & 0x030) >> 4; + *plainchar = (fragment & 0x00f) << 4; + case step_c: + do { + if (codechar == code_in+length_in) + { + state_in->step = step_c; + state_in->plainchar = *plainchar; + return plainchar - plaintext_out; + } + fragment = (char)base64_decode_value(*codechar++); + } while (fragment < 0); + *plainchar++ |= (fragment & 0x03c) >> 2; + *plainchar = (fragment & 0x003) << 6; + case step_d: + do { + if (codechar == code_in+length_in) + { + state_in->step = step_d; + state_in->plainchar = *plainchar; + return plainchar - plaintext_out; + } + fragment = (char)base64_decode_value(*codechar++); + } while (fragment < 0); + *plainchar++ |= (fragment & 0x03f); + } + } + /* control should not reach here */ + return plainchar - plaintext_out; +} + +#endif diff --git a/libraries/arduinoWebSockets/src/libb64/cdecode_inc.h b/libraries/arduinoWebSockets/src/libb64/cdecode_inc.h new file mode 100644 index 00000000..d0d7f489 --- /dev/null +++ b/libraries/arduinoWebSockets/src/libb64/cdecode_inc.h @@ -0,0 +1,28 @@ +/* +cdecode.h - c header for a base64 decoding algorithm + +This is part of the libb64 project, and has been placed in the public domain. +For details, see http://sourceforge.net/projects/libb64 +*/ + +#ifndef BASE64_CDECODE_H +#define BASE64_CDECODE_H + +typedef enum +{ + step_a, step_b, step_c, step_d +} base64_decodestep; + +typedef struct +{ + base64_decodestep step; + char plainchar; +} base64_decodestate; + +void base64_init_decodestate(base64_decodestate* state_in); + +int base64_decode_value(char value_in); + +int base64_decode_block(const char* code_in, const int length_in, char* plaintext_out, base64_decodestate* state_in); + +#endif /* BASE64_CDECODE_H */ diff --git a/libraries/arduinoWebSockets/src/libb64/cencode.c b/libraries/arduinoWebSockets/src/libb64/cencode.c new file mode 100644 index 00000000..afe1463c --- /dev/null +++ b/libraries/arduinoWebSockets/src/libb64/cencode.c @@ -0,0 +1,119 @@ +/* +cencoder.c - c source to a base64 encoding algorithm implementation + +This is part of the libb64 project, and has been placed in the public domain. +For details, see http://sourceforge.net/projects/libb64 +*/ + +#ifdef ESP8266 +#include +#endif + +#if defined(ESP32) +#define CORE_HAS_LIBB64 +#endif + +#ifndef CORE_HAS_LIBB64 +#include "cencode_inc.h" + +const int CHARS_PER_LINE = 72; + +void base64_init_encodestate(base64_encodestate* state_in) +{ + state_in->step = step_A; + state_in->result = 0; + state_in->stepcount = 0; +} + +char base64_encode_value(char value_in) +{ + static const char* encoding = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + if (value_in > 63) return '='; + return encoding[(int)value_in]; +} + +int base64_encode_block(const char* plaintext_in, int length_in, char* code_out, base64_encodestate* state_in) +{ + const char* plainchar = plaintext_in; + const char* const plaintextend = plaintext_in + length_in; + char* codechar = code_out; + char result; + char fragment; + + result = state_in->result; + + switch (state_in->step) + { + while (1) + { + case step_A: + if (plainchar == plaintextend) + { + state_in->result = result; + state_in->step = step_A; + return codechar - code_out; + } + fragment = *plainchar++; + result = (fragment & 0x0fc) >> 2; + *codechar++ = base64_encode_value(result); + result = (fragment & 0x003) << 4; + case step_B: + if (plainchar == plaintextend) + { + state_in->result = result; + state_in->step = step_B; + return codechar - code_out; + } + fragment = *plainchar++; + result |= (fragment & 0x0f0) >> 4; + *codechar++ = base64_encode_value(result); + result = (fragment & 0x00f) << 2; + case step_C: + if (plainchar == plaintextend) + { + state_in->result = result; + state_in->step = step_C; + return codechar - code_out; + } + fragment = *plainchar++; + result |= (fragment & 0x0c0) >> 6; + *codechar++ = base64_encode_value(result); + result = (fragment & 0x03f) >> 0; + *codechar++ = base64_encode_value(result); + + ++(state_in->stepcount); + if (state_in->stepcount == CHARS_PER_LINE/4) + { + *codechar++ = '\n'; + state_in->stepcount = 0; + } + } + } + /* control should not reach here */ + return codechar - code_out; +} + +int base64_encode_blockend(char* code_out, base64_encodestate* state_in) +{ + char* codechar = code_out; + + switch (state_in->step) + { + case step_B: + *codechar++ = base64_encode_value(state_in->result); + *codechar++ = '='; + *codechar++ = '='; + break; + case step_C: + *codechar++ = base64_encode_value(state_in->result); + *codechar++ = '='; + break; + case step_A: + break; + } + *codechar++ = 0x00; + + return codechar - code_out; +} + +#endif diff --git a/libraries/arduinoWebSockets/src/libb64/cencode_inc.h b/libraries/arduinoWebSockets/src/libb64/cencode_inc.h new file mode 100644 index 00000000..c1e3464a --- /dev/null +++ b/libraries/arduinoWebSockets/src/libb64/cencode_inc.h @@ -0,0 +1,31 @@ +/* +cencode.h - c header for a base64 encoding algorithm + +This is part of the libb64 project, and has been placed in the public domain. +For details, see http://sourceforge.net/projects/libb64 +*/ + +#ifndef BASE64_CENCODE_H +#define BASE64_CENCODE_H + +typedef enum +{ + step_A, step_B, step_C +} base64_encodestep; + +typedef struct +{ + base64_encodestep step; + char result; + int stepcount; +} base64_encodestate; + +void base64_init_encodestate(base64_encodestate* state_in); + +char base64_encode_value(char value_in); + +int base64_encode_block(const char* plaintext_in, int length_in, char* code_out, base64_encodestate* state_in); + +int base64_encode_blockend(char* code_out, base64_encodestate* state_in); + +#endif /* BASE64_CENCODE_H */ diff --git a/libraries/arduinoWebSockets/src/libsha1/libsha1.c b/libraries/arduinoWebSockets/src/libsha1/libsha1.c new file mode 100644 index 00000000..48f4df5a --- /dev/null +++ b/libraries/arduinoWebSockets/src/libsha1/libsha1.c @@ -0,0 +1,202 @@ +/* from valgrind tests */ + +/* ================ sha1.c ================ */ +/* +SHA-1 in C +By Steve Reid +100% Public Domain + +Test Vectors (from FIPS PUB 180-1) +"abc" + A9993E36 4706816A BA3E2571 7850C26C 9CD0D89D +"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq" + 84983E44 1C3BD26E BAAE4AA1 F95129E5 E54670F1 +A million repetitions of "a" + 34AA973C D4C4DAA4 F61EEB2B DBAD2731 6534016F +*/ + +/* #define LITTLE_ENDIAN * This should be #define'd already, if true. */ +/* #define SHA1HANDSOFF * Copies data before messing with it. */ + +#if !defined(ESP8266) && !defined(ESP32) + +#define SHA1HANDSOFF + +#include +#include +#include + +#include "libsha1.h" + + +#define rol(value, bits) (((value) << (bits)) | ((value) >> (32 - (bits)))) + +/* blk0() and blk() perform the initial expand. */ +/* I got the idea of expanding during the round function from SSLeay */ +#if BYTE_ORDER == LITTLE_ENDIAN +#define blk0(i) (block->l[i] = (rol(block->l[i],24)&0xFF00FF00) \ + |(rol(block->l[i],8)&0x00FF00FF)) +#elif BYTE_ORDER == BIG_ENDIAN +#define blk0(i) block->l[i] +#else +#error "Endianness not defined!" +#endif +#define blk(i) (block->l[i&15] = rol(block->l[(i+13)&15]^block->l[(i+8)&15] \ + ^block->l[(i+2)&15]^block->l[i&15],1)) + +/* (R0+R1), R2, R3, R4 are the different operations used in SHA1 */ +#define R0(v,w,x,y,z,i) z+=((w&(x^y))^y)+blk0(i)+0x5A827999+rol(v,5);w=rol(w,30); +#define R1(v,w,x,y,z,i) z+=((w&(x^y))^y)+blk(i)+0x5A827999+rol(v,5);w=rol(w,30); +#define R2(v,w,x,y,z,i) z+=(w^x^y)+blk(i)+0x6ED9EBA1+rol(v,5);w=rol(w,30); +#define R3(v,w,x,y,z,i) z+=(((w|x)&y)|(w&x))+blk(i)+0x8F1BBCDC+rol(v,5);w=rol(w,30); +#define R4(v,w,x,y,z,i) z+=(w^x^y)+blk(i)+0xCA62C1D6+rol(v,5);w=rol(w,30); + + +/* Hash a single 512-bit block. This is the core of the algorithm. */ + +void SHA1Transform(uint32_t state[5], const unsigned char buffer[64]) +{ + uint32_t a, b, c, d, e; + typedef union { + unsigned char c[64]; + uint32_t l[16]; + } CHAR64LONG16; +#ifdef SHA1HANDSOFF + CHAR64LONG16 block[1]; /* use array to appear as a pointer */ + memcpy(block, buffer, 64); +#else + /* The following had better never be used because it causes the + * pointer-to-const buffer to be cast into a pointer to non-const. + * And the result is written through. I threw a "const" in, hoping + * this will cause a diagnostic. + */ + CHAR64LONG16* block = (const CHAR64LONG16*)buffer; +#endif + /* Copy context->state[] to working vars */ + a = state[0]; + b = state[1]; + c = state[2]; + d = state[3]; + e = state[4]; + /* 4 rounds of 20 operations each. Loop unrolled. */ + R0(a,b,c,d,e, 0); R0(e,a,b,c,d, 1); R0(d,e,a,b,c, 2); R0(c,d,e,a,b, 3); + R0(b,c,d,e,a, 4); R0(a,b,c,d,e, 5); R0(e,a,b,c,d, 6); R0(d,e,a,b,c, 7); + R0(c,d,e,a,b, 8); R0(b,c,d,e,a, 9); R0(a,b,c,d,e,10); R0(e,a,b,c,d,11); + R0(d,e,a,b,c,12); R0(c,d,e,a,b,13); R0(b,c,d,e,a,14); R0(a,b,c,d,e,15); + R1(e,a,b,c,d,16); R1(d,e,a,b,c,17); R1(c,d,e,a,b,18); R1(b,c,d,e,a,19); + R2(a,b,c,d,e,20); R2(e,a,b,c,d,21); R2(d,e,a,b,c,22); R2(c,d,e,a,b,23); + R2(b,c,d,e,a,24); R2(a,b,c,d,e,25); R2(e,a,b,c,d,26); R2(d,e,a,b,c,27); + R2(c,d,e,a,b,28); R2(b,c,d,e,a,29); R2(a,b,c,d,e,30); R2(e,a,b,c,d,31); + R2(d,e,a,b,c,32); R2(c,d,e,a,b,33); R2(b,c,d,e,a,34); R2(a,b,c,d,e,35); + R2(e,a,b,c,d,36); R2(d,e,a,b,c,37); R2(c,d,e,a,b,38); R2(b,c,d,e,a,39); + R3(a,b,c,d,e,40); R3(e,a,b,c,d,41); R3(d,e,a,b,c,42); R3(c,d,e,a,b,43); + R3(b,c,d,e,a,44); R3(a,b,c,d,e,45); R3(e,a,b,c,d,46); R3(d,e,a,b,c,47); + R3(c,d,e,a,b,48); R3(b,c,d,e,a,49); R3(a,b,c,d,e,50); R3(e,a,b,c,d,51); + R3(d,e,a,b,c,52); R3(c,d,e,a,b,53); R3(b,c,d,e,a,54); R3(a,b,c,d,e,55); + R3(e,a,b,c,d,56); R3(d,e,a,b,c,57); R3(c,d,e,a,b,58); R3(b,c,d,e,a,59); + R4(a,b,c,d,e,60); R4(e,a,b,c,d,61); R4(d,e,a,b,c,62); R4(c,d,e,a,b,63); + R4(b,c,d,e,a,64); R4(a,b,c,d,e,65); R4(e,a,b,c,d,66); R4(d,e,a,b,c,67); + R4(c,d,e,a,b,68); R4(b,c,d,e,a,69); R4(a,b,c,d,e,70); R4(e,a,b,c,d,71); + R4(d,e,a,b,c,72); R4(c,d,e,a,b,73); R4(b,c,d,e,a,74); R4(a,b,c,d,e,75); + R4(e,a,b,c,d,76); R4(d,e,a,b,c,77); R4(c,d,e,a,b,78); R4(b,c,d,e,a,79); + /* Add the working vars back into context.state[] */ + state[0] += a; + state[1] += b; + state[2] += c; + state[3] += d; + state[4] += e; + /* Wipe variables */ + a = b = c = d = e = 0; +#ifdef SHA1HANDSOFF + memset(block, '\0', sizeof(block)); +#endif +} + + +/* SHA1Init - Initialize new context */ + +void SHA1Init(SHA1_CTX* context) +{ + /* SHA1 initialization constants */ + context->state[0] = 0x67452301; + context->state[1] = 0xEFCDAB89; + context->state[2] = 0x98BADCFE; + context->state[3] = 0x10325476; + context->state[4] = 0xC3D2E1F0; + context->count[0] = context->count[1] = 0; +} + + +/* Run your data through this. */ + +void SHA1Update(SHA1_CTX* context, const unsigned char* data, uint32_t len) +{ + uint32_t i, j; + + j = context->count[0]; + if ((context->count[0] += len << 3) < j) + context->count[1]++; + context->count[1] += (len>>29); + j = (j >> 3) & 63; + if ((j + len) > 63) { + memcpy(&context->buffer[j], data, (i = 64-j)); + SHA1Transform(context->state, context->buffer); + for ( ; i + 63 < len; i += 64) { + SHA1Transform(context->state, &data[i]); + } + j = 0; + } + else i = 0; + memcpy(&context->buffer[j], &data[i], len - i); +} + + +/* Add padding and return the message digest. */ + +void SHA1Final(unsigned char digest[20], SHA1_CTX* context) +{ + unsigned i; + unsigned char finalcount[8]; + unsigned char c; + +#if 0 /* untested "improvement" by DHR */ + /* Convert context->count to a sequence of bytes + * in finalcount. Second element first, but + * big-endian order within element. + * But we do it all backwards. + */ + unsigned char *fcp = &finalcount[8]; + + for (i = 0; i < 2; i++) + { + uint32_t t = context->count[i]; + int j; + + for (j = 0; j < 4; t >>= 8, j++) + *--fcp = (unsigned char) t; + } +#else + for (i = 0; i < 8; i++) { + finalcount[i] = (unsigned char)((context->count[(i >= 4 ? 0 : 1)] + >> ((3-(i & 3)) * 8) ) & 255); /* Endian independent */ + } +#endif + c = 0200; + SHA1Update(context, &c, 1); + while ((context->count[0] & 504) != 448) { + c = 0000; + SHA1Update(context, &c, 1); + } + SHA1Update(context, finalcount, 8); /* Should cause a SHA1Transform() */ + for (i = 0; i < 20; i++) { + digest[i] = (unsigned char) + ((context->state[i>>2] >> ((3-(i & 3)) * 8) ) & 255); + } + /* Wipe variables */ + memset(context, '\0', sizeof(*context)); + memset(&finalcount, '\0', sizeof(finalcount)); +} +/* ================ end of sha1.c ================ */ + + +#endif diff --git a/libraries/arduinoWebSockets/src/libsha1/libsha1.h b/libraries/arduinoWebSockets/src/libsha1/libsha1.h new file mode 100644 index 00000000..ee3718e1 --- /dev/null +++ b/libraries/arduinoWebSockets/src/libsha1/libsha1.h @@ -0,0 +1,21 @@ +/* ================ sha1.h ================ */ +/* +SHA-1 in C +By Steve Reid +100% Public Domain +*/ + +#if !defined(ESP8266) && !defined(ESP32) + +typedef struct { + uint32_t state[5]; + uint32_t count[2]; + unsigned char buffer[64]; +} SHA1_CTX; + +void SHA1Transform(uint32_t state[5], const unsigned char buffer[64]); +void SHA1Init(SHA1_CTX* context); +void SHA1Update(SHA1_CTX* context, const unsigned char* data, uint32_t len); +void SHA1Final(unsigned char digest[20], SHA1_CTX* context); + +#endif diff --git a/libraries/arduinoWebSockets/tests/webSocket.html b/libraries/arduinoWebSockets/tests/webSocket.html new file mode 100644 index 00000000..66a27089 --- /dev/null +++ b/libraries/arduinoWebSockets/tests/webSocket.html @@ -0,0 +1,49 @@ + + + + + + + +LED Control:
+
+R:
+G:
+B:
+ + \ No newline at end of file diff --git a/libraries/arduinoWebSockets/tests/webSocketServer/index.js b/libraries/arduinoWebSockets/tests/webSocketServer/index.js new file mode 100644 index 00000000..389e1930 --- /dev/null +++ b/libraries/arduinoWebSockets/tests/webSocketServer/index.js @@ -0,0 +1,57 @@ +#!/usr/bin/env node +var WebSocketServer = require('websocket').server; +var http = require('http'); + +var server = http.createServer(function(request, response) { + console.log((new Date()) + ' Received request for ' + request.url); + response.writeHead(404); + response.end(); +}); +server.listen(8011, function() { + console.log((new Date()) + ' Server is listening on port 8011'); +}); + +wsServer = new WebSocketServer({ + httpServer: server, + // You should not use autoAcceptConnections for production + // applications, as it defeats all standard cross-origin protection + // facilities built into the protocol and the browser. You should + // *always* verify the connection's origin and decide whether or not + // to accept it. + autoAcceptConnections: false +}); + +function originIsAllowed(origin) { + // put logic here to detect whether the specified origin is allowed. + return true; +} + +wsServer.on('request', function(request) { + + if (!originIsAllowed(request.origin)) { + // Make sure we only accept requests from an allowed origin + request.reject(); + console.log((new Date()) + ' Connection from origin ' + request.origin + ' rejected.'); + return; + } + + var connection = request.accept('arduino', request.origin); + console.log((new Date()) + ' Connection accepted.'); + + connection.on('message', function(message) { + if (message.type === 'utf8') { + console.log('Received Message: ' + message.utf8Data); + // connection.sendUTF(message.utf8Data); + } + else if (message.type === 'binary') { + console.log('Received Binary Message of ' + message.binaryData.length + ' bytes'); + //connection.sendBytes(message.binaryData); + } + }); + + connection.on('close', function(reasonCode, description) { + console.log((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected.'); + }); + + connection.sendUTF("Hallo Client!"); +}); diff --git a/libraries/arduinoWebSockets/tests/webSocketServer/package.json b/libraries/arduinoWebSockets/tests/webSocketServer/package.json new file mode 100644 index 00000000..9538323e --- /dev/null +++ b/libraries/arduinoWebSockets/tests/webSocketServer/package.json @@ -0,0 +1,27 @@ +{ + "name": "webSocketServer", + "version": "1.0.0", + "description": "WebSocketServer for testing", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/Links2004/arduinoWebSockets" + }, + "keywords": [ + "esp8266", + "websocket", + "arduino" + ], + "author": "Markus Sattler", + "license": "LGPLv2", + "bugs": { + "url": "https://github.com/Links2004/arduinoWebSockets/issues" + }, + "homepage": "https://github.com/Links2004/arduinoWebSockets", + "dependencies": { + "websocket": "^1.0.18" + } +} diff --git a/libraries/arduinoWebSockets/travis/common.sh b/libraries/arduinoWebSockets/travis/common.sh new file mode 100644 index 00000000..be959faf --- /dev/null +++ b/libraries/arduinoWebSockets/travis/common.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +function build_sketches() +{ + local arduino=$1 + local srcpath=$2 + local platform=$3 + local sketches=$(find $srcpath -name *.ino) + for sketch in $sketches; do + local sketchdir=$(dirname $sketch) + if [[ -f "$sketchdir/.$platform.skip" ]]; then + echo -e "\n\n ------------ Skipping $sketch ------------ \n\n"; + continue + fi + echo -e "\n\n ------------ Building $sketch ------------ \n\n"; + $arduino --verify $sketch; + local result=$? + if [ $result -ne 0 ]; then + echo "Build failed ($sketch) build verbose..." + $arduino --verify --verbose --preserve-temp-files $sketch + result=$? + fi + if [ $result -ne 0 ]; then + echo "Build failed ($1) $sketch" + return $result + fi + done +} + + +function get_core() +{ + echo Setup core for $1 + + cd $HOME/arduino_ide/hardware + + if [ "$1" = "esp8266" ] ; then + mkdir esp8266com + cd esp8266com + git clone https://github.com/esp8266/Arduino.git esp8266 + cd esp8266/tools + python get.py + fi + + if [ "$1" = "esp32" ] ; then + mkdir espressif + cd espressif + git clone https://github.com/espressif/arduino-esp32.git esp32 + cd esp32/tools + python get.py + fi + +}
Unknown page : $QUERY$- you will be redirected...\n

\nif not redirected,
click here\n

\n\n\n\n