From 3f4f4f778008cae3779c31e16d75dbd0ea6cc16d Mon Sep 17 00:00:00 2001 From: Luc <8822552+luc-github@users.noreply.github.com> Date: Sun, 29 Dec 2019 19:04:08 +0100 Subject: [PATCH] initial commit not functional yet --- .gitignore | 5 + README.md | 83 + astyle.bat | 7 + examples/basicesp3d/basicesp3d.ino | 35 + images/ESP3D_social.png | Bin 0 -> 36138 bytes library.properties | 9 + src/esp3dlib.cpp | 61 + src/esp3dlib.h | 35 + src/nofile.h | 329 ++++ src/sd_ESP32.cpp | 300 ++++ src/sd_ESP32.h | 59 + src/serial2socket.cpp | 170 ++ src/serial2socket.h | 81 + src/web_server.cpp | 2565 ++++++++++++++++++++++++++++ src/web_server.h | 120 ++ src/wificonfig.cpp | 429 +++++ src/wificonfig.h | 133 ++ src/wifiservices.cpp | 142 ++ src/wifiservices.h | 39 + 19 files changed, 4602 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 astyle.bat create mode 100644 examples/basicesp3d/basicesp3d.ino create mode 100644 images/ESP3D_social.png create mode 100644 library.properties create mode 100644 src/esp3dlib.cpp create mode 100644 src/esp3dlib.h create mode 100644 src/nofile.h create mode 100644 src/sd_ESP32.cpp create mode 100644 src/sd_ESP32.h create mode 100644 src/serial2socket.cpp create mode 100644 src/serial2socket.h create mode 100644 src/web_server.cpp create mode 100644 src/web_server.h create mode 100644 src/wificonfig.cpp create mode 100644 src/wificonfig.h create mode 100644 src/wifiservices.cpp create mode 100644 src/wifiservices.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2a14318 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +Thumbs.db +.DS_Store +*.orig +embedded/node_modules +embedded/dist diff --git a/README.md b/README.md new file mode 100644 index 0000000..ae4476c --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# ESP3DLib 1.0 + + +Library for ESP32 used with 3D printer using [[ESP32 core version](https://github.com/espressif/arduino-esp32) + + +Firmware should work with any 3D printer using Marlin 2.0 + +The web interface files are present in data directory but UI has it's own repository [ESP3D-WEBUI](https://github.com/luc-github/ESP3D-WEBUI/tree/2.1). + +## Donate +Every support is welcome: [PayPal – The safer, easier way to pay online.](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=Y8FFE7NA4LJWQ) +Especially if need to buy new modules for testing. + +## Features +* Serial/Wifi bridge using configurable port 8888, here to enable/disable [TCP_IP_DATA_FEATURE](https://github.com/luc-github/ESP3D/blob/master/esp3d/config.h) +* Use GPIO2 to ground to reset all settings in hard way - 2-6 sec after boot / not before!! Set GPIO2 to ground before boot change boot mode and go to special boot that do not reach FW. Currently boot take 10 sec - giving 8 seconds to connect GPIO2 to GND and do an hard recovery for settings, here to enable/disable [RECOVERY_FEATURE](https://github.com/luc-github/ESP8266/blob/master/esp8266/config.h) +* Complete configuration by web browser (Station or Access point) or by Serial commands +* Authentication for sensitive pages, here to enable/disable [AUTHENTICATION_FEATURE](https://github.com/luc-github/ESP3D/blob/master/esp3d/config.h) +* Update firmware by web browser, here to enable/disable [WEB_UPDATE_FEATURE](https://github.com/luc-github/ESP3D/blob/master/esp3d/config.h) +* Control ESP module using commands on serial or data port, here to enable/disable [SERIAL_COMMAND_FEATURE](https://github.com/luc-github/ESP3D/blob/master/esp3d/config.h) +* Captive portal in Access point mode which redirect all unknow call to main page, here to enable/disable [CAPTIVE_PORTAL_FEATURE](https://github.com/luc-github/ESP3D/blob/master/esp3d/config.h) +* mDNS which allows to key the name defined in web browser and connect only with bonjour installed on computer, here to enable/disable [MDNS_FEATURE](https://github.com/luc-github/ESP3D/blob/master/esp3d/config.h) +* SSDP, this feature is a discovery protocol, supported on Windows out of the box, here to enable/disable [SSDP_FEATURE](https://github.com/luc-github/ESP3D/blob/master/esp3d/config.h) +* Fail safe mode (Access point)is enabled if cannot connect to defined station at boot. +* Choice of web server Async or Sync +* Websocket support +* OLED screen support +* The web ui add even more feature : https://github.com/luc-github/ESP3D-WEBUI/blob/master/README.md#features + + +## Web configuration +*Wifi Mode : Access point / Client station +*IP Generation: DHCP/Static IP +*IP/MASK/GATEWAY for static data +*Baud Rate for serial (supported : 9600, 19200, 38400, 57600, 115200, 230400, 250000) +*web port and data port + + +## Default Configuration +Default Settings: +AP:ESP8266 +PW:12345678 +Authentification: WPA +Mode: g (n is not supported by AP, just by STA) +channel: 11 +AP: visible +Sleep Mode: Modem +IP Mode: Static IP +IP: 192.168.0.1 +Mask: 255.255.255.0 +GW:192.168.0.1 +Baud rate: 115200 +Web port:80 +the websocket is web port + 1 => 80+1 : 81 +Data port: 8888 +Web Page refresh: 3 secondes +User: admin +Password: admin +User:user +Password: user + + + +## Direct commands: +Check wiki : https://github.com/luc-github/ESP3DLib/wiki/Direct-ESP3D-commands + +h your Printer fw with ESP connected on Serial - it bring troubles, at least on DaVinci, but no issue if you update using web UI + +## Contribution/customization +* To style the code before pushing PR please use [astyle --style=otbs *.h *.cpp *.ino](http://astyle.sourceforge.net/) +* The embedded page is created using nodejs then gulp to generate a compressed html page (tool.html.gz), all necessary modules can be installed using the install.bat file content, then it is included using bin2c (https://sourceforge.net/projects/bin2c/) to generate the h file used to create the file nofile.h, update the array and size according new out.h. +* The current UI is located [here](https://github.com/luc-github/ESP3D-WEBUI) + +# :question:Any question ? +Check [Wiki](https://github.com/luc-github/ESP3DLib/wiki) or [![Join the chat at https://gitter.im/luc-github/ESP3D](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/luc-github/ESP3D?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +## :exclamation:Any issue/feedback ? +Check [FAQ](https://github.com/luc-github/ESP3DLib/issues?utf8=%E2%9C%93&q=label%3AFAQ+) or [submit ticket](https://github.com/luc-github/ESP3DLib/issues) + + +## TODO/On going : +-- Put feature at same level as ESP3D V2.1 / V3.0 diff --git a/astyle.bat b/astyle.bat new file mode 100644 index 0000000..484dc7a --- /dev/null +++ b/astyle.bat @@ -0,0 +1,7 @@ + +cd %~dp0src +astyle --recursive --style=otbs *.h *.cpp *.ino +del /S *.ori +dir +cd .. +pause diff --git a/examples/basicesp3d/basicesp3d.ino b/examples/basicesp3d/basicesp3d.ino new file mode 100644 index 0000000..e2f54d8 --- /dev/null +++ b/examples/basicesp3d/basicesp3d.ino @@ -0,0 +1,35 @@ +/* + basic esp3dlib sample + + Copyright (c) 2019 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 +*/ +//library include +#include "esp3dLib.h" + +//global variable +Esp3DLib myesp3d; + +//Setup +void setup() +{ + myesp3d.init(); +} + +//main loop +void loop() +{ +} diff --git a/images/ESP3D_social.png b/images/ESP3D_social.png new file mode 100644 index 0000000000000000000000000000000000000000..4c4df6afd91b75283ad3005c76894f52d5b33cf5 GIT binary patch literal 36138 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 literal 0 HcmV?d00001 diff --git a/library.properties b/library.properties new file mode 100644 index 0000000..0630d2f --- /dev/null +++ b/library.properties @@ -0,0 +1,9 @@ +name=ESP3DLib +version=1.0.0 +author=Luc Lebosse +maintainer=Luc Lebosse, +sentence=A 3D printer front end for ESP boards. +paragraph=This library implements a 3D printer front end. +category=Communication +url=https://github.com/luc-github/ESP3DLib +architectures=esp32 diff --git a/src/esp3dlib.cpp b/src/esp3dlib.cpp new file mode 100644 index 0000000..249e52c --- /dev/null +++ b/src/esp3dlib.cpp @@ -0,0 +1,61 @@ +/* + This file is part of ESP3DLib library for 3D printer. + + ESP3D Firmware for 3D printer 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 + (at your option) any later version. + + ESP3D Firmware for 3D printer 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this Firmware. If not, see . + + This firmware is using the standard arduino IDE with module to support ESP8266/ESP32: + https://github.com/esp8266/Arduino + https://github.com/espressif/arduino-esp32 + + Latest version of the code and documentation can be found here : + https://github.com/luc-github/ESP3D + + Main author: luc lebosse + +*/ +#include "esp3dlib.h" + +void WiFiTaskfn( void * parameter ) +{ + WiFiConfig::begin(); + for(;;) { + wifi_config.handle(); + wifi_config.wait(0); // Yield to other tasks + } + vTaskDelete( NULL ); +} + + + + +//Contructor +Esp3DLib::Esp3DLib() +{ + +} + +//Begin which setup everything +void Esp3D::init() +{ + + xTaskCreatePinnedToCore( + WiFiTaskfn, /* Task function. */ + "WiFi Task", /* name of task. */ + 10000, /* Stack size of task */ + NULL, /* parameter of the task */ + 1, /* priority of the task */ + NULL, /* Task handle to keep track of created task */ + 0 /* Core to run the task */ + ); +} diff --git a/src/esp3dlib.h b/src/esp3dlib.h new file mode 100644 index 0000000..505b700 --- /dev/null +++ b/src/esp3dlib.h @@ -0,0 +1,35 @@ +/* + esp3dlib.h - esp3dlib class + + Copyright (c) 2019 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 ESP3DLIB_H +#define ESP3DLIB_H +//be sure correct IDE and settings are used for ESP8266 or ESP32 +#if !defined(ARDUINO_ARCH_ESP32) +#error Oops! Make sure you have 'ESP32' compatible board selected +#endif + +#include "Arduino.h" +class Esp3DLib +{ +public: + Esp3DLib(); + void init(); +}; +#endif diff --git a/src/nofile.h b/src/nofile.h new file mode 100644 index 0000000..f10f2d8 --- /dev/null +++ b/src/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 [] PROGMEM = { + 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/src/sd_ESP32.cpp b/src/sd_ESP32.cpp new file mode 100644 index 0000000..07e5984 --- /dev/null +++ b/src/sd_ESP32.cpp @@ -0,0 +1,300 @@ +/** + * Marlin 3D Printer Firmware + * Copyright (C) 2016 MarlinFirmware [https://github.com/MarlinFirmware/Marlin] + * + * Based on Sprinter and grbl. + * Copyright (C) 2011 Camiel Gubbels / Erik van der Zalm + * + * This program 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 + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifdef ARDUINO_ARCH_ESP32 +#include "../../inc/MarlinConfigPre.h" +#if ENABLED(SDSUPPORT) +#include "../../sd/cardreader.h" +#include "../../sd/SdVolume.h" +#include "../../sd/SdFatStructs.h" +#include "../../sd/SdFile.h" +#include "sd_ESP32.h" + + +//Cannot move to static variable due to conflict with ESP32 SD library +SdFile workDir; +dir_t dir_info; +SdVolume sd_volume; + + +ESP_SD::ESP_SD(){ + _size = 0; + _pos = 0; + _readonly=true; + //ugly Workaround to not expose SdFile class which conflict with Native ESP32 SD class + _sdfile = new SdFile; +} +ESP_SD::~ESP_SD(){ +if (((SdFile *)_sdfile)->isOpen())close(); +if (_sdfile) delete (SdFile *) _sdfile; +} + +bool ESP_SD::isopen(){ + return (((SdFile *)_sdfile)->isOpen()); +} + +int8_t ESP_SD::card_status(){ +if (!IS_SD_INSERTED() || !card.isMounted()) return 0; //No sd +if ( card.isPrinting() || card.isFileOpen() ) return -1; // busy +return 1; //ok +} + +bool ESP_SD::open(const char * path, bool readonly ){ + if (path == NULL) return false; + String fullpath=path; + String pathname = fullpath.substring(0,fullpath.lastIndexOf("/")); + String filename = makeshortname(fullpath.substring(fullpath.lastIndexOf("/")+1)); + if (pathname.length() == 0)pathname="/"; + if (!openDir(pathname))return false; + _pos = 0; + _readonly = readonly; + return ((SdFile *)_sdfile)->open(&workDir, filename.c_str(), readonly?O_READ:(O_CREAT | O_APPEND | O_WRITE | O_TRUNC)); +} + +uint32_t ESP_SD::size(){ + if(((SdFile *)_sdfile)->isOpen()) { + _size = ((SdFile *)_sdfile)->fileSize(); + } + return _size ; +} + +uint32_t ESP_SD::available(){ + if(!((SdFile *)_sdfile)->isOpen() || !_readonly) return 0; + _size = ((SdFile *)_sdfile)->fileSize(); + if (_size == 0) return 0; + + return _size - _pos ; +} + +void ESP_SD::close(){ + if(((SdFile *)_sdfile)->isOpen()) { + ((SdFile *)_sdfile)->sync(); + _size = ((SdFile *)_sdfile)->fileSize(); + ((SdFile *)_sdfile)->close(); + } +} + +int16_t ESP_SD::write(const uint8_t * data, uint16_t len){ + return ((SdFile *)_sdfile)->write(data, len); +} + +int16_t ESP_SD::write(const uint8_t byte){ + return ((SdFile *)_sdfile)->write(&byte, 1); +} + + +bool ESP_SD::exists(const char * path){ + bool response = open(path); + if (response) close(); + return response; +} + +bool ESP_SD::remove(const char * path){ + if (path == NULL) return false; + String fullpath=path; + String pathname = fullpath.substring(0,fullpath.lastIndexOf("/")); + String filename = makeshortname(fullpath.substring(fullpath.lastIndexOf("/")+1)); + if (pathname.length() == 0)pathname="/"; + if (!openDir(pathname))return false; + SdFile file; + return file.remove(&workDir, filename.c_str()); +} + +bool ESP_SD::dir_exists(const char * path){ + return openDir(path); +} + +bool ESP_SD::rmdir(const char * path){ + if (path == NULL) return false; + String fullpath=path; + if (fullpath=="/") return false; + if (!openDir(fullpath))return false; + return workDir.rmRfStar(); +} + +bool ESP_SD::mkdir(const char * path){ + if (path == NULL) return false; + String fullpath=path; + String pathname = fullpath.substring(0,fullpath.lastIndexOf("/")); + String filename = makeshortname(fullpath.substring(fullpath.lastIndexOf("/")+1)); + if (pathname.length() == 0)pathname="/"; + if (!openDir(pathname))return false; + SdFile file; + return file.mkdir(&workDir, filename.c_str()); +} + +int16_t ESP_SD::read(){ + if (!_readonly) return 0; + int16_t v = ((SdFile *)_sdfile)->read(); + if (v!=-1)_pos++; + return v; + +} + +uint16_t ESP_SD::read(uint8_t * buf, uint16_t nbyte){ + if (!_readonly) return 0; + int16_t v = ((SdFile *)_sdfile)->read(buf, nbyte); + if (v!=-1)_pos+=v; + return v; +} + +String ESP_SD::get_path_part(String data, int index){ + int found = 0; + int strIndex[] = {0, -1}; + int maxIndex; + String no_res; + String s = data; + s.trim(); + if (s.length() == 0) return no_res; + maxIndex = s.length()-1; + if ((s[0] == '/') && (s.length() > 1)){ + String s2 = &s[1]; + s = s2; + } + for(int i=0; i<=maxIndex && found<=index; i++){ + if(s.charAt(i)=='/' || i==maxIndex){ + found++; + strIndex[0] = strIndex[1]+1; + strIndex[1] = (i == maxIndex) ? i+1 : i; + } + } + + return found>index ? s.substring(strIndex[0], strIndex[1]) : no_res; +} + +String ESP_SD::makeshortname(String longname, uint8_t index){ + String s = longname; + String part_name; + String part_ext; + //Sanity check name is uppercase and no space + s.replace(" ",""); + s.toUpperCase(); + int pos = s.lastIndexOf("."); + //do we have extension ? + if (pos != -1) { + part_name = s.substring(0,pos); + if (part_name.lastIndexOf(".") !=-1) { + part_name.replace(".",""); + //trick for short name but force ~1 at the end + part_name+=" "; + } + part_ext = s.substring(pos+1,pos+4); + } else { + part_name = s; + } + //check is under 8 char + if (part_name.length() > 8) { + //if not cut and use index + //check file exists is not part of this function + part_name = part_name.substring(0,6); + part_name += "~" + String(index); + } + //remove the possible " " for the trick + part_name.replace(" ",""); + //create full short name + if (part_ext.length() > 0) part_name+="." + part_ext; + return part_name; +} + +String ESP_SD::makepath83(String longpath){ + String path; + String tmp; + int index = 0; + tmp = get_path_part(longpath,index); + while (tmp.length() > 0) { + path += "/"; + //TODO need to check short name index (~1) match actually the long name... + path += makeshortname (tmp); + index++; + tmp = get_path_part(longpath,index); + } + return path; +} + + +uint32_t ESP_SD::card_total_space(){ + + return (512.00) * (sd_volume.clusterCount()) * (sd_volume.blocksPerCluster()); +} +uint32_t ESP_SD::card_used_space(){ + return (512.00) * (sd_volume.clusterCount() - sd_volume.freeClusterCount() ) * (sd_volume.blocksPerCluster()); +} + +bool ESP_SD::openDir(String path){ + static SdFile root; + static String name; + int index = 0; + //SdFile *parent; + if(root.isOpen())root.close(); + if (!sd_volume.init(&(card.getSd2Card()))) { + return false; + } + if (!root.openRoot(&sd_volume)){ + return false; + } + root.rewind(); + workDir = root; + //parent = &workDir; + name = get_path_part(path,index); + while ((name.length() > 0) && (name!="/")) { + SdFile newDir; + if (!newDir.open(&root, name.c_str(), O_READ)) { + return false; + } + workDir=newDir; + //parent = &workDir; + index++; + if (index > MAX_DIR_DEPTH) { + return false; + } + name = get_path_part(path,index); + } + return true; +} +//TODO may be add date and use a struct for all info +bool ESP_SD::readDir(char name[13], uint32_t * size, bool * isFile){ + if ((name == NULL) || (size==NULL)) { + return false; + } + * size = 0; + name[0]= 0; + * isFile = false; + + if ((workDir.readDir(&dir_info, NULL)) > 0){ + workDir.dirName(dir_info,name); + * size = dir_info.fileSize; + if (DIR_IS_FILE(&dir_info))* isFile = true; + return true; + } + return false; +} + + +//TODO +/* +bool SD_file_timestamp(const char * path, uint8_t flag, uint16_t year, uint8_t month, uint8_t day, + uint8_t hour, uint8_t minute, uint8_t second){ +}**/ + +#endif + +#endif diff --git a/src/sd_ESP32.h b/src/sd_ESP32.h new file mode 100644 index 0000000..777852a --- /dev/null +++ b/src/sd_ESP32.h @@ -0,0 +1,59 @@ +/** + * Marlin 3D Printer Firmware + * Copyright (C) 2016 MarlinFirmware [https://github.com/MarlinFirmware/Marlin] + * + * Based on Sprinter and grbl. + * Copyright (C) 2011 Camiel Gubbels / Erik van der Zalm + * + * This program 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 + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef _ESP_SD_H_ +#define _ESP_SD_H_ +class ESP_SD{ + public: + ESP_SD(); + ~ESP_SD(); + int8_t card_status(); + uint32_t card_total_space(); + uint32_t card_used_space(); + bool open(const char * path, bool readonly = true ); + void close(); + int16_t write(const uint8_t * data, uint16_t len); + int16_t write(const uint8_t byte); + uint16_t read(uint8_t * buf, uint16_t nbyte); + int16_t read(); + uint32_t size(); + uint32_t available(); + bool exists(const char * path); + bool dir_exists(const char * path); + bool remove(const char * path); + bool rmdir(const char * path); + bool mkdir(const char * path); + bool isopen(); + String makepath83(String longpath); + String makeshortname(String longname, uint8_t index = 1); + bool openDir(String path); + bool readDir(char name[13], uint32_t * size, bool * isFile); + bool * isFile; + private: + void * _sdfile; + uint32_t _size; + uint32_t _pos; + bool _readonly; + String get_path_part(String data, int index); + +}; +#endif diff --git a/src/serial2socket.cpp b/src/serial2socket.cpp new file mode 100644 index 0000000..f5c603f --- /dev/null +++ b/src/serial2socket.cpp @@ -0,0 +1,170 @@ +/* + 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 "../../inc/MarlinConfigPre.h" + +#if ENABLED(WIFISUPPORT) +#include "serial2socket.h" +#include "wificonfig.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; + return true; +} + +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){ + log_i("[SOCKET]flush data, buffer size %d",_TXbufferSize); + ((WebSocketsServer *)_web_socket)->broadcastBIN(_TXbuffer,_TXbufferSize); + //refresh timout + _lastflush = millis(); + //reset buffer + _TXbufferSize = 0; + } +} + +#endif // ENABLE_WIFI + +#endif // ARDUINO_ARCH_ESP32 diff --git a/src/serial2socket.h b/src/serial2socket.h new file mode 100644 index 0000000..30a2cb0 --- /dev/null +++ b/src/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/src/web_server.cpp b/src/web_server.cpp new file mode 100644 index 0000000..148481c --- /dev/null +++ b/src/web_server.cpp @@ -0,0 +1,2565 @@ +/* + 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 "../../inc/MarlinConfigPre.h" + +#if ENABLED(WIFISUPPORT) + +#include "wificonfig.h" + +#if defined (ENABLE_HTTP) +#include "HAL.h" +#include "../../gcode/queue.h" +#include "wifiservices.h" +#include "serial2socket.h" +#include "web_server.h" +#include +#include "../../inc/Version.h" +#include +#include +#include +#if ENABLED(SDSUPPORT) +#include "sd_ESP32.h" +#endif +#include +#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" + +//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
Unknown page : $QUERY$- you will be redirected...\n

\nif not redirected, click here\n

\n\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 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); + + //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); + +#if ENABLED(SDSUPPORT) + //Direct SD management + _webserver->on("/upload", HTTP_ANY, handle_direct_SDFileList,SDFile_direct_upload); +#endif + +#ifdef ENABLE_CAPTIVE_PORTAL + if(WiFi.getMode() == WIFI_AP){ + // if DNSServer is started with "*" for domain name, it will reply with + // provided IP to all DNS request + dnsServer.start(DNS_PORT, "*", WiFi.softAPIP()); + _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 + MYSERIAL0.println("SSDP Started"); + SSDP.begin(); + } +#endif + MYSERIAL0.println("HTTP Started"); + //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_SSDP + SSDP.end(); +#endif +#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"; + +#if ENABLED(SDSUPPORT) + if ((path.substring(0,4) == "/SD/")) { + //remove /SD + path = path.substring(3); + ESP_SD SD_card; + if (SD_card.card_status() == 1) { + if (SD_card.exists(pathWithGz.c_str()) || SD_card.exists(path.c_str())) { + if (!SD_card.exists(path.c_str())) path = pathWithGz; + if(SD_card.open(path.c_str())) { + uint8_t buf[1200]; + _webserver->setContentLength(SD_card.size()); + _webserver->sendHeader("Cache-Control","no-cache"); + _webserver->send(200, "application/octet-stream", ""); + + WiFiClient c = _webserver->client(); + int16_t len = SD_card.read( buf, 1200); + while(len > 0) { + c.write(buf, len); + len = SD_card.read( buf, 1200); + wifi_config.wait(0); + handle(); + } + SD_card.close(); + return; + } + } + } + + 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 content=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); + } + content.replace(KEY_IP,stmp); + content.replace(KEY_IP,stmp); + content.replace(KEY_QUERY,_webserver->uri()); + _webserver->send(200,"text/html",content); + 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); + } + } +} +#ifdef ENABLE_SSDP +//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); + } +} + +#endif + +//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 != "" ){ + scmd+="\n"; + Serial2Socket.push(scmd.c_str()); + //GCodeQueue::enqueue_one_now(scmd.c_str()); + 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 != "" ){ + scmd+="\n"; + Serial2Socket.push(scmd.c_str()); + //GCodeQueue::enqueue_one_now(scmd.c_str()); + 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"; +#if ENABLED(SDSUPPORT) + ESP_SD card; + int8_t state = card.card_status(); + if (state == -1)resp="Busy"; + else if (state == 1)resp="SD card detected"; + else resp="No SD card"; +#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 (","); + + //TODO + //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 (",");*/ + + //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) && !((spos==AP_PWD_ENTRY) || (spos==STA_PWD_ENTRY))){ + 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: Marlin "); + espresponse->print (SHORT_BUILD_VERSION); + 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") { + MYSERIAL0.println("Restart ongoing"); +#if NUM_SERIAL > 1 + MYSERIAL1.println("Restart ongoing"); +#endif + 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 { + if (currentline.length() > 0){ + currentline+="\n"; + Serial2Socket.push(currentline.c_str()); + //GCodeQueue::enqueue_one_now(currentline.c_str()); + } + 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 += SHORT_BUILD_VERSION; + resp += " # FW target:marlin-embedded # FW HW:"; + #if ENABLED(SDSUPPORT) + 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; + MYSERIAL0.println("Upload rejected"); +#if NUM_SERIAL > 1 + MYSERIAL1.println("Upload rejected"); +#endif + _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; + MYSERIAL0.println("Upload error"); +#if NUM_SERIAL > 1 + MYSERIAL1.println("Upload error"); +#endif + _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(); + MYSERIAL0.println("Upload error"); +#if NUM_SERIAL > 1 + MYSERIAL1.println("Upload error"); +#endif + } + //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 { + MYSERIAL0.println("Upload error"); +#if NUM_SERIAL > 1 + MYSERIAL1.println("Upload error"); +#endif + } + } 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); + } + MYSERIAL0.println("Upload error"); +#if NUM_SERIAL > 1 + MYSERIAL1.println("Upload error"); +#endif + } + //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); + } + MYSERIAL0.println("Upload error"); +#if NUM_SERIAL > 1 + MYSERIAL1.println("Upload error"); +#endif + } + 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(); + MYSERIAL0.println("Upload rejected"); +#if NUM_SERIAL > 1 + MYSERIAL1.println("Upload rejected"); +#endif + return; + } + + //get current file ID + HTTPUpload& upload = _webserver->upload(); + //Upload start + //************** + if(upload.status == UPLOAD_FILE_START) { + MYSERIAL0.println("Update Firmware"); +#if NUM_SERIAL > 1 + MYSERIAL1.println("Update Firmware"); +#endif + _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; + MYSERIAL0.println("Update cancelled"); +#if NUM_SERIAL > 1 + MYSERIAL1.println("Update cancelled"); +#endif + _webserver->client().stop(); + return; + } else { + MYSERIAL0.println("Update 0%"); +#if NUM_SERIAL > 1 + MYSERIAL1.println("Update 0%"); +#endif + } + //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+="%"; + MYSERIAL0.println(s.c_str()); +#if NUM_SERIAL > 1 + MYSERIAL1.println(s.c_str()); +#endif + } + 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 + MYSERIAL0.println("Update 100%"); +#if NUM_SERIAL > 1 + MYSERIAL1.println("Update 100%"); +#endif + _upload_status=UPLOAD_STATUS_SUCCESSFUL; + } + } else if(upload.status == UPLOAD_FILE_ABORTED) { + MYSERIAL0.println("Update failed"); +#if NUM_SERIAL > 1 + MYSERIAL1.println("Update failed"); +#endif + Update.end(); + _upload_status=UPLOAD_STATUS_CANCELLED; + } + wifi_config.wait(0); +} + + +#if ENABLED(SDSUPPORT) + +//direct SD files list////////////////////////////////////////////////// +void Web_Server::handle_direct_SDFileList() +{ + //this is only for admin an 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; + ESP_SD card; + int8_t state = card.card_status(); + if (state != 1){ + _webserver->sendHeader("Cache-Control","no-cache"); + _webserver->send(200, "application/json", "{\"status\":\"No SD Card\"}"); + return; + } + + //get current path + if(_webserver->hasArg("path")) { + path += _webserver->arg("path") ; + } + //to have a clean path + path.trim(); + path.replace("//","/"); + + //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(!card.exists(filename.c_str())) { + sstatus = shortname + " does not exist!"; + } else { + if (card.remove(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(!card.dir_exists(filename.c_str())) { + sstatus = shortname + " does not exist!"; + } else { + if (!card.rmdir(filename.c_str())) { + 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(card.exists(filename.c_str())) { + sstatus = shortname + " already exists!"; + } else { + if (!card.mkdir(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 (!card.openDir(path)){ + String s = "{\"status\":\" "; + s += path; + s+= " does not exist on SD Card\"}"; + _webserver->sendHeader("Cache-Control","no-cache"); + _webserver->send(200, "application/json", s.c_str()); + return; + } + if (list_files) { + char name[13]; + uint32_t size; + bool isFile; + uint i = 0; + while (card.readDir(name,&size ,&isFile)) { + if (i>0) { + jsonfile+=","; + } + jsonfile+="{\"name\":\""; + jsonfile+=name; + jsonfile+="\",\"shortname\":\""; + jsonfile+=name; + jsonfile+="\",\"size\":\""; + if (isFile)jsonfile+=formatBytes(size); + else jsonfile+="-1"; + jsonfile+="\",\"datetime\":\""; + //TODO datatime + jsonfile+="\"}"; + i++; + wifi_config.wait(1); + } + jsonfile+="],\"path\":\""; + jsonfile+=path + "\","; + } + static uint32_t volTotal = card.card_total_space(); + static uint32_t volUsed = card.card_used_space();; + //TODO + //Get right values + uint32_t occupedspace = (volUsed/volTotal)*100; + jsonfile+="\"total\":\""; + if ( (occupedspace <= 1) && (volTotal!=volUsed)) { + occupedspace=1; + } + jsonfile+= formatBytes(volTotal); ; + + jsonfile+="\",\"used\":\""; + jsonfile+= formatBytes(volUsed); ; + jsonfile+="\",\"occupation\":\""; + jsonfile+=String(occupedspace); + 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; +} + +//SD File upload with direct access to SD/////////////////////////////// +void Web_Server::SDFile_direct_upload() +{ + static ESP_SD sdfile; + static String upload_filename; + //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) { + upload_filename = upload.filename; + if (upload_filename[0] != '/') { + upload_filename = "/" + upload.filename; + } + upload_filename= sdfile.makepath83(upload_filename); + if ( sdfile.card_status() != 1) { + _upload_status=UPLOAD_STATUS_CANCELLED; + MYSERIAL0.println("Upload cancelled"); +#if NUM_SERIAL > 1 + MYSERIAL1.println("Upload cancelled"); +#endif + _webserver->client().stop(); + return; + } + if (sdfile.exists (upload_filename.c_str()) ) { + sdfile.remove (upload_filename.c_str()); + } + + if (sdfile.isopen())sdfile.close(); + if (!sdfile.open (upload_filename.c_str(),false)) { + MYSERIAL0.println("Upload cancelled"); +#if NUM_SERIAL > 1 + MYSERIAL1.println("Upload cancelled"); +#endif + _webserver->client().stop(); + _upload_status = UPLOAD_STATUS_FAILED; + return ; + } else { + _upload_status = UPLOAD_STATUS_ONGOING; + } + //Upload write + //************** + } else if(upload.status == UPLOAD_FILE_WRITE) { + //we need to check SD is inside + if ( sdfile.card_status() != 1) { + sdfile.close(); + MYSERIAL0.println("Upload failed"); + #if NUM_SERIAL > 1 + MYSERIAL1.println("Upload failed"); + #endif + if (sdfile.exists (upload_filename.c_str()) ) { + sdfile.remove (upload_filename.c_str()); + } + _webserver->client().stop(); + return; + } + if (sdfile.isopen()) { + if ( (_upload_status = UPLOAD_STATUS_ONGOING) && (upload.currentSize > 0)) { + sdfile.write (upload.buf, upload.currentSize); + } + } + //Upload end + //************** + } else if(upload.status == UPLOAD_FILE_END) { + sdfile.close(); + uint32_t filesize = sdfile.size(); + String sizeargname = upload.filename + "S"; + if (_webserver->hasArg (sizeargname.c_str()) ) { + if (_webserver->arg (sizeargname.c_str()) != String(filesize)) { + MYSERIAL0.println("Upload failed"); + #if NUM_SERIAL > 1 + MYSERIAL1.println("Upload failed"); + #endif + _upload_status = UPLOAD_STATUS_FAILED; + } + } + if (_upload_status == UPLOAD_STATUS_ONGOING) { + _upload_status = UPLOAD_STATUS_SUCCESSFUL; + } + } else {//Upload cancelled + _upload_status=UPLOAD_STATUS_FAILED; + + MYSERIAL0.println("Upload failed"); +#if NUM_SERIAL > 1 + MYSERIAL1.println("Upload failed"); +#endif + _webserver->client().stop(); + sdfile.close(); + if (sdfile.exists (upload_filename.c_str()) ) { + sdfile.remove (upload_filename.c_str()); + } + } + wifi_config.wait(0); +} +#endif + +void Web_Server::handle(){ +static uint32_t timeout = millis(); +#ifdef ENABLE_CAPTIVE_PORTAL + if(WiFi.getMode() == WIFI_AP){ + dnsServer.processNextRequest(); + } +#endif + if (_webserver)_webserver->handleClient(); + if (_socket_server && _setupdone) { + Serial2Socket.handle_flush(); + _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 + +#endif // ENABLE_WIFI + +#endif // ARDUINO_ARCH_ESP32 diff --git a/src/web_server.h b/src/web_server.h new file mode 100644 index 0000000..d96e32e --- /dev/null +++ b/src/web_server.h @@ -0,0 +1,120 @@ +/* + 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 "wificonfig.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(); + static 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 (); +#if ENABLED(SDSUPPORT) + static void handle_direct_SDFileList(); + static void SDFile_direct_upload(); +#endif +}; + +extern Web_Server web_server; + +#endif + diff --git a/src/wificonfig.cpp b/src/wificonfig.cpp new file mode 100644 index 0000000..af0bfe7 --- /dev/null +++ b/src/wificonfig.cpp @@ -0,0 +1,429 @@ +/* + 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 "../../inc/MarlinConfigPre.h" + +#if ENABLED(WIFISUPPORT) +#include "HAL.h" +#include +#include +#include +#include +#include +#include +#include "wificonfig.h" +#include "wifiservices.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(); +} + +/** + * 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: + MYSERIAL0.println ("Connected"); + MYSERIAL0.println(WiFi.localIP()); + break; + case SYSTEM_EVENT_STA_DISCONNECTED: + MYSERIAL0.println("WiFi lost connection"); + 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; + } + MYSERIAL0.println(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)){ + MYSERIAL0.print("\nClient Started\nConnecting "); + MYSERIAL0.println(SSID.c_str()); + return ConnectSTA2AP(); + } else { + MYSERIAL0.println("\nStarting client failed"); + 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)) { + MYSERIAL0.print("\nAP Started "); + MYSERIAL0.println(WiFi.softAPIP().toString()); + return true; + } else { + MYSERIAL0.println("\nStarting AP failed"); + 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); + MYSERIAL0.println("\nWiFi Off"); +} + +/** + * 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()){ + MYSERIAL0.println("\nCannot connect to AP"); + 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; +} + +/** + * 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/src/wificonfig.h b/src/wificonfig.h new file mode 100644 index 0000000..3d31cab --- /dev/null +++ b/src/wificonfig.h @@ -0,0 +1,133 @@ +/* + 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 +*/ + +//Services that need to be used +#include "../../inc/MarlinConfigPre.h" +#define ENABLE_MDNS +#define ENABLE_OTA +#define ENABLE_HTTP +#define ENABLE_SSDP +#define ENABLE_CAPTIVE_PORTAL +//Authentication flag is now in Configuration_adv.h +//#define ENABLE_AUTHENTICATION +#define ENABLE_SERIAL2SOCKET_OUT + +#define ENABLE_SERIAL2SOCKET_IN + +//Preferences entries +#define NAMESPACE "MARLIN" +#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 "marlinesp" +#define DEFAULT_STA_SSID "MARLIN_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 "MARLIN_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 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 + +class WiFiConfig { +public: + WiFiConfig(); + ~WiFiConfig(); + 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(); + + private : + static bool ConnectSTA2AP(); + static void WiFiEvent(WiFiEvent_t event); + static bool restart_ESP_module; +}; + +extern WiFiConfig wifi_config; + +#endif diff --git a/src/wifiservices.cpp b/src/wifiservices.cpp new file mode 100644 index 0000000..59bb716 --- /dev/null +++ b/src/wifiservices.cpp @@ -0,0 +1,142 @@ +/* + 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 "../../inc/MarlinConfigPre.h" + +#if ENABLED(WIFISUPPORT) + +#include "HAL.h" +#include +#include +#include +#include +#include "wificonfig.h" +#include "wifiservices.h" +#ifdef ENABLE_MDNS +#include +#endif +#ifdef ENABLE_OTA +#include +#endif +#ifdef ENABLE_HTTP +#include "web_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_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(); + } + MYSERIAL0.printf("OTA:Start OTA updating %s]\r\n", type.c_str()); + }) + .onEnd([]() { + MYSERIAL0.println("OTA:End"); + + }) + .onProgress([](unsigned int progress, unsigned int total) { + MYSERIAL0.printf("OTA:OTA Progress: %u%%]\r\n", (progress / (total / 100))); + }) + .onError([](ota_error_t error) { + MYSERIAL0.printf("OTA: Error(%u)\r\n", error); + if (error == OTA_AUTH_ERROR) MYSERIAL0.println("OTA:Auth Failed]"); + else if (error == OTA_BEGIN_ERROR) MYSERIAL0.println("OTA:Begin Failed"); + else if (error == OTA_CONNECT_ERROR) MYSERIAL0.println("OTA:Connect Failed"); + else if (error == OTA_RECEIVE_ERROR) MYSERIAL0.println("OTA:Receive Failed"); + else if (error == OTA_END_ERROR) MYSERIAL0.println("OTA:End Failed]"); + }); + ArduinoOTA.begin(); +#endif +#ifdef ENABLE_MDNS + //no need in AP mode + if(WiFi.getMode() == WIFI_STA){ + //start mDns + if (!MDNS.begin(h.c_str())) { + MYSERIAL0.println("Cannot start mDNS"); + no_error = false; + } else { + MYSERIAL0.printf("Start mDNS with hostname:%s\r\n",h.c_str()); + } + } +#endif +#ifdef ENABLE_HTTP + web_server.begin(); +#endif + return no_error; +} +void WiFiServices::end(){ +#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 +} + +#endif // ENABLE_WIFI + +#endif // ARDUINO_ARCH_ESP32 diff --git a/src/wifiservices.h b/src/wifiservices.h new file mode 100644 index 0000000..790fd67 --- /dev/null +++ b/src/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 +