From 4f833f7555b7eeb7a6eba4f19e1b91463dd36779 Mon Sep 17 00:00:00 2001 From: jiang <1446935823@qq.com> Date: Wed, 10 Jun 2026 23:04:48 +0800 Subject: [PATCH] =?UTF-8?q?=E5=92=95=E5=92=95=E5=98=8E=E5=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/build.yaml | 40 + .gitignore | 5 + .vscode/extensions.json | 10 + .vscode/settings.json | 21 + Makefile | 17 + include/Http_Server.h | 14 + include/README | 38 + include/user.h | 23 + include/user_mqtt.h | 16 + lib/README | 45 + lib/WebServer/keywords.txt | 38 + lib/WebServer/library.properties | 9 + lib/WebServer/src/HTTP_Method.h | 9 + lib/WebServer/src/Parsing.cpp | 605 ++++++++++++ lib/WebServer/src/Uri.h | 29 + lib/WebServer/src/WebServer.cpp | 865 ++++++++++++++++++ lib/WebServer/src/WebServer.h | 303 ++++++ lib/WebServer/src/detail/RequestHandler.h | 91 ++ .../src/detail/RequestHandlersImpl.h | 263 ++++++ lib/WebServer/src/detail/mimetable.cpp | 33 + lib/WebServer/src/detail/mimetable.h | 41 + lib/WebServer/src/uri/UriBraces.h | 68 ++ lib/WebServer/src/uri/UriGlob.h | 22 + lib/WebServer/src/uri/UriRegex.h | 45 + platformio.ini | 19 + src/http_server.cpp | 765 ++++++++++++++++ src/main.cpp | 692 ++++++++++++++ src/user_mqtt.cpp | 350 +++++++ test/README | 10 + update.bat | 1 + web/index.html | 96 ++ web/wifi/index.html | 150 +++ 32 files changed, 4733 insertions(+) create mode 100644 .gitea/workflows/build.yaml create mode 100644 .gitignore create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 Makefile create mode 100644 include/Http_Server.h create mode 100644 include/README create mode 100644 include/user.h create mode 100644 include/user_mqtt.h create mode 100644 lib/README create mode 100644 lib/WebServer/keywords.txt create mode 100644 lib/WebServer/library.properties create mode 100644 lib/WebServer/src/HTTP_Method.h create mode 100644 lib/WebServer/src/Parsing.cpp create mode 100644 lib/WebServer/src/Uri.h create mode 100644 lib/WebServer/src/WebServer.cpp create mode 100644 lib/WebServer/src/WebServer.h create mode 100644 lib/WebServer/src/detail/RequestHandler.h create mode 100644 lib/WebServer/src/detail/RequestHandlersImpl.h create mode 100644 lib/WebServer/src/detail/mimetable.cpp create mode 100644 lib/WebServer/src/detail/mimetable.h create mode 100644 lib/WebServer/src/uri/UriBraces.h create mode 100644 lib/WebServer/src/uri/UriGlob.h create mode 100644 lib/WebServer/src/uri/UriRegex.h create mode 100644 platformio.ini create mode 100644 src/http_server.cpp create mode 100644 src/main.cpp create mode 100644 src/user_mqtt.cpp create mode 100644 test/README create mode 100644 update.bat create mode 100644 web/index.html create mode 100644 web/wifi/index.html diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml new file mode 100644 index 0000000..777baa2 --- /dev/null +++ b/.gitea/workflows/build.yaml @@ -0,0 +1,40 @@ +name: Build +run-name: Build bin +on: [ push ] + +env: + name: ${{ github.ref_name }}@${{ GITHUB_RUN_NUMBER }} + +jobs: + Build: + runs-on: ubuntu-latest + steps: + - run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event." + - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!" + - run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}" + - name: Get code + uses: actions/checkout@v4 + # uses: http://server.jiang1446.i234.me:3000/jiang/checkout@v4 + - run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner." + - run: echo "🖥️ The workflow is now ready to test your code on the runner." + - name: Build + run: | + cd ${{ gitea.workspace }} && make + - run: echo "🍏 This job's status is ${{ job.status }}." + - run: export name=6666 + - run: echo "🍏 ${{ github.ref_name }}@V${{ github.RUN_NUMBER }}." + - name: Use Go Action + id: use-go-action + uses: akkuman/gitea-release-action@v1 + # uses: http://server.jiang1446.i234.me:3000/jiang/gitea-release-action@v1 + env: + NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18 + with: + name: ${{ github.ref_name }}@V${{ github.RUN_NUMBER }} + tag_name: ${{ github.ref_name }}@V${{ github.RUN_NUMBER }} + # body: ${{ github.head_ref }} + # prerelease: true + md5sum: true + sha256sum: true + files: |- + .pio/build/esp32solo1/firmware.bin diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..bfecdc7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,21 @@ +{ + "files.associations": { + "string": "cpp", + "array": "cpp", + "memory": "cpp", + "functional": "cpp", + "tuple": "cpp", + "utility": "cpp", + "deque": "cpp", + "unordered_map": "cpp", + "vector": "cpp", + "string_view": "cpp", + "initializer_list": "cpp", + "thread": "cpp", + "compare": "cpp", + "ratio": "cpp", + "type_traits": "cpp", + "*.tcc": "cpp", + "istream": "cpp" + } +} \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..55454e0 --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +datasdir := $(HOME)/.make/ +pio := $(HOME)/.platformio/penv/bin/pio +ttydown := /dev/ttyUSB0 + +all : $(datasdir) + $(pio) run + +$(datasdir) : + sudo echo + curl -#fL https://raw.githubusercontent.com/platformio/platformio-core-installer/master/get-platformio.py -o get-platformio.py + sudo apt install -y python3-venv + python3 get-platformio.py + mkdir $(datasdir) + +clean : + $(pio) run --target clean + diff --git a/include/Http_Server.h b/include/Http_Server.h new file mode 100644 index 0000000..394a281 --- /dev/null +++ b/include/Http_Server.h @@ -0,0 +1,14 @@ +#ifndef _Http_Server_H_ +#define _Http_Server_H_ + +class Http_Server +{ + public: + void setup(); + void loop(); + +}; + +extern Http_Server httpserver; + +#endif diff --git a/include/README b/include/README new file mode 100644 index 0000000..2bfdc2e --- /dev/null +++ b/include/README @@ -0,0 +1,38 @@ +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/include/user.h b/include/user.h new file mode 100644 index 0000000..90f201d --- /dev/null +++ b/include/user.h @@ -0,0 +1,23 @@ +#ifndef _user_H_ +#define _user_H_ + +#include "HLW8012.h" + +extern char reboot_; +extern char user_; + +extern HLW8012 hlw8012; +extern unsigned int getVoltage; +extern double getCurrent; +extern unsigned int getActivePower; +extern unsigned int getApparentPower; +extern double getPowerFactor; + +extern String host; + +extern uint8_t if_mode; +extern int fan_mode; +extern int swing_mode; +extern int temp_mode; + +#endif \ No newline at end of file diff --git a/include/user_mqtt.h b/include/user_mqtt.h new file mode 100644 index 0000000..f728469 --- /dev/null +++ b/include/user_mqtt.h @@ -0,0 +1,16 @@ +#ifndef _USER_MQTT__H_ +#define _USER_MQTT__H_ + +class MQTT +{ + +public: + void setup(); + void loop(); + void pust(); + void irpost_(); +}; + +extern MQTT mqtt; + +#endif \ No newline at end of file diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..58415f5 --- /dev/null +++ b/lib/README @@ -0,0 +1,45 @@ +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/lib/WebServer/keywords.txt b/lib/WebServer/keywords.txt new file mode 100644 index 0000000..df20ff7 --- /dev/null +++ b/lib/WebServer/keywords.txt @@ -0,0 +1,38 @@ +####################################### +# Syntax Coloring Map For Ultrasound +####################################### + +####################################### +# Datatypes (KEYWORD1) +####################################### + +WebServer KEYWORD1 +WebServerSecure KEYWORD1 +HTTPMethod KEYWORD1 + +####################################### +# Methods and Functions (KEYWORD2) +####################################### + +begin KEYWORD2 +handleClient KEYWORD2 +on KEYWORD2 +addHandler KEYWORD2 +uri KEYWORD2 +method KEYWORD2 +client KEYWORD2 +send KEYWORD2 +arg KEYWORD2 +argName KEYWORD2 +args KEYWORD2 +hasArg KEYWORD2 +onNotFound KEYWORD2 + +####################################### +# Constants (LITERAL1) +####################################### + +HTTP_GET LITERAL1 +HTTP_POST LITERAL1 +HTTP_ANY LITERAL1 +CONTENT_LENGTH_UNKNOWN LITERAL1 diff --git a/lib/WebServer/library.properties b/lib/WebServer/library.properties new file mode 100644 index 0000000..1942ca4 --- /dev/null +++ b/lib/WebServer/library.properties @@ -0,0 +1,9 @@ +name=WebServer +version=2.0.0 +author=Ivan Grokhotkov +maintainer=Ivan Grokhtkov +sentence=Simple web server library +paragraph=The library supports HTTP GET and POST requests, provides argument parsing, handles one client at a time. +category=Communication +url= +architectures=esp32 diff --git a/lib/WebServer/src/HTTP_Method.h b/lib/WebServer/src/HTTP_Method.h new file mode 100644 index 0000000..66d53bf --- /dev/null +++ b/lib/WebServer/src/HTTP_Method.h @@ -0,0 +1,9 @@ +#ifndef _HTTP_Method_H_ +#define _HTTP_Method_H_ + +#include "http_parser.h" + +typedef enum http_method HTTPMethod; +#define HTTP_ANY (HTTPMethod)(255) + +#endif /* _HTTP_Method_H_ */ diff --git a/lib/WebServer/src/Parsing.cpp b/lib/WebServer/src/Parsing.cpp new file mode 100644 index 0000000..6e3ad62 --- /dev/null +++ b/lib/WebServer/src/Parsing.cpp @@ -0,0 +1,605 @@ +/* + Parsing.cpp - HTTP request parsing. + + Copyright (c) 2015 Ivan Grokhotkov. 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 + Modified 8 May 2015 by Hristo Gochkov (proper post and file upload handling) +*/ + +#include +#include +#include "NetworkServer.h" +#include "NetworkClient.h" +#include "WebServer.h" +#include "detail/mimetable.h" + +#ifndef WEBSERVER_MAX_POST_ARGS +#define WEBSERVER_MAX_POST_ARGS 32 +#endif + +#define __STR(a) #a +#define _STR(a) __STR(a) +static const char *_http_method_str[] = { +#define XX(num, name, string) _STR(name), + HTTP_METHOD_MAP(XX) +#undef XX +}; + +static const char Content_Type[] PROGMEM = "Content-Type"; +static const char filename[] PROGMEM = "filename"; + +static char *readBytesWithTimeout(NetworkClient &client, size_t maxLength, size_t &dataLength, int timeout_ms) { + char *buf = nullptr; + dataLength = 0; + while (dataLength < maxLength) { + int tries = timeout_ms; + size_t newLength; + while (!(newLength = client.available()) && tries--) { + delay(1); + } + if (!newLength) { + break; + } + if (!buf) { + buf = (char *)malloc(newLength + 1); + if (!buf) { + return nullptr; + } + } else { + char *newBuf = (char *)realloc(buf, dataLength + newLength + 1); + if (!newBuf) { + free(buf); + return nullptr; + } + buf = newBuf; + } + client.readBytes(buf + dataLength, newLength); + dataLength += newLength; + buf[dataLength] = '\0'; + } + return buf; +} + +bool WebServer::_parseRequest(NetworkClient &client) { + // Read the first line of HTTP request + String req = client.readStringUntil('\r'); + client.readStringUntil('\n'); + //reset header value + for (int i = 0; i < _headerKeysCount; ++i) { + _currentHeaders[i].value = String(); + } + + // First line of HTTP request looks like "GET /path HTTP/1.1" + // Retrieve the "/path" part by finding the spaces + int addr_start = req.indexOf(' '); + int addr_end = req.indexOf(' ', addr_start + 1); + if (addr_start == -1 || addr_end == -1) { + log_e("Invalid request: %s", req.c_str()); + return false; + } + + String methodStr = req.substring(0, addr_start); + String url = req.substring(addr_start + 1, addr_end); + String versionEnd = req.substring(addr_end + 8); + _currentVersion = atoi(versionEnd.c_str()); + String searchStr = ""; + int hasSearch = url.indexOf('?'); + if (hasSearch != -1) { + searchStr = url.substring(hasSearch + 1); + url = url.substring(0, hasSearch); + } + _currentUri = url; + _chunked = false; + _clientContentLength = 0; // not known yet, or invalid + + HTTPMethod method = HTTP_ANY; + size_t num_methods = sizeof(_http_method_str) / sizeof(const char *); + for (size_t i = 0; i < num_methods; i++) { + if (methodStr == _http_method_str[i]) { + method = (HTTPMethod)i; + break; + } + } + if (method == HTTP_ANY) { + log_e("Unknown HTTP Method: %s", methodStr.c_str()); + return false; + } + _currentMethod = method; + + log_v("method: %s url: %s search: %s", methodStr.c_str(), url.c_str(), searchStr.c_str()); + + //attach handler + RequestHandler *handler; + for (handler = _firstHandler; handler; handler = handler->next()) { + if (handler->canHandle(*this, _currentMethod, _currentUri)) { + break; + } + } + _currentHandler = handler; + + String formData; + // below is needed only when POST type request + if (method == HTTP_POST || method == HTTP_PUT || method == HTTP_PATCH || method == HTTP_DELETE) { + String boundaryStr; + String headerName; + String headerValue; + bool isForm = false; + bool isEncoded = false; + //parse headers + while (1) { + req = client.readStringUntil('\r'); + client.readStringUntil('\n'); + if (req == "") { + break; //no moar headers + } + int headerDiv = req.indexOf(':'); + if (headerDiv == -1) { + break; + } + headerName = req.substring(0, headerDiv); + headerValue = req.substring(headerDiv + 1); + headerValue.trim(); + _collectHeader(headerName.c_str(), headerValue.c_str()); + + log_v("headerName: %s", headerName.c_str()); + log_v("headerValue: %s", headerValue.c_str()); + + if (headerName.equalsIgnoreCase(FPSTR(Content_Type))) { + using namespace mime; + if (headerValue.startsWith(FPSTR(mimeTable[txt].mimeType))) { + isForm = false; + } else if (headerValue.startsWith(F("application/x-www-form-urlencoded"))) { + isForm = false; + isEncoded = true; + } else if (headerValue.startsWith(F("multipart/"))) { + boundaryStr = headerValue.substring(headerValue.indexOf('=') + 1); + boundaryStr.replace("\"", ""); + isForm = true; + } + } else if (headerName.equalsIgnoreCase(F("Content-Length"))) { + _clientContentLength = headerValue.toInt(); + } else if (headerName.equalsIgnoreCase(F("Host"))) { + _hostHeader = headerValue; + } + } + + if (!isForm && _currentHandler && _currentHandler->canRaw(*this, _currentUri)) { + log_v("Parse raw"); + _currentRaw.reset(new HTTPRaw()); + _currentRaw->status = RAW_START; + _currentRaw->totalSize = 0; + _currentRaw->currentSize = 0; + log_v("Start Raw"); + _currentHandler->raw(*this, _currentUri, *_currentRaw); + _currentRaw->status = RAW_WRITE; + + while (_currentRaw->totalSize < _clientContentLength) { + _currentRaw->currentSize = client.readBytes(_currentRaw->buf, HTTP_RAW_BUFLEN); + _currentRaw->totalSize += _currentRaw->currentSize; + if (_currentRaw->currentSize == 0) { + _currentRaw->status = RAW_ABORTED; + _currentHandler->raw(*this, _currentUri, *_currentRaw); + return false; + } + _currentHandler->raw(*this, _currentUri, *_currentRaw); + } + _currentRaw->status = RAW_END; + _currentHandler->raw(*this, _currentUri, *_currentRaw); + log_v("Finish Raw"); + } else if (!isForm) { + size_t plainLength; + char *plainBuf = readBytesWithTimeout(client, _clientContentLength, plainLength, HTTP_MAX_POST_WAIT); + if (plainLength < _clientContentLength) { + free(plainBuf); + return false; + } + if (_clientContentLength > 0) { + if (isEncoded) { + //url encoded form + if (searchStr != "") { + searchStr += '&'; + } + searchStr += plainBuf; + } + _parseArguments(searchStr); + if (!isEncoded) { + //plain post json or other data + RequestArgument &arg = _currentArgs[_currentArgCount++]; + arg.key = F("plain"); + arg.value = String(plainBuf); + } + + log_v("Plain: %s", plainBuf); + free(plainBuf); + } else { + // No content - but we can still have arguments in the URL. + _parseArguments(searchStr); + } + } else { + // it IS a form + _parseArguments(searchStr); + if (!_parseForm(client, boundaryStr, _clientContentLength)) { + return false; + } + } + } else { + String headerName; + String headerValue; + //parse headers + while (1) { + req = client.readStringUntil('\r'); + client.readStringUntil('\n'); + if (req == "") { + break; //no moar headers + } + int headerDiv = req.indexOf(':'); + if (headerDiv == -1) { + break; + } + headerName = req.substring(0, headerDiv); + headerValue = req.substring(headerDiv + 2); + _collectHeader(headerName.c_str(), headerValue.c_str()); + + log_v("headerName: %s", headerName.c_str()); + log_v("headerValue: %s", headerValue.c_str()); + + if (headerName.equalsIgnoreCase("Host")) { + _hostHeader = headerValue; + } + } + _parseArguments(searchStr); + } + client.flush(); + + log_v("Request: %s", url.c_str()); + log_v(" Arguments: %s", searchStr.c_str()); + + return true; +} + +bool WebServer::_collectHeader(const char *headerName, const char *headerValue) { + for (int i = 0; i < _headerKeysCount; i++) { + if (_currentHeaders[i].key.equalsIgnoreCase(headerName)) { + _currentHeaders[i].value = headerValue; + return true; + } + } + return false; +} + +void WebServer::_parseArguments(String data) { + log_v("args: %s", data.c_str()); + if (_currentArgs) { + delete[] _currentArgs; + } + _currentArgs = 0; + if (data.length() == 0) { + _currentArgCount = 0; + _currentArgs = new RequestArgument[1]; + return; + } + _currentArgCount = 1; + + for (int i = 0; i < (int)data.length();) { + i = data.indexOf('&', i); + if (i == -1) { + break; + } + ++i; + ++_currentArgCount; + } + log_v("args count: %d", _currentArgCount); + + _currentArgs = new RequestArgument[_currentArgCount + 1]; + int pos = 0; + int iarg; + for (iarg = 0; iarg < _currentArgCount;) { + int equal_sign_index = data.indexOf('=', pos); + int next_arg_index = data.indexOf('&', pos); + log_v("pos %d =@%d &@%d", pos, equal_sign_index, next_arg_index); + if ((equal_sign_index == -1) || ((equal_sign_index > next_arg_index) && (next_arg_index != -1))) { + log_e("arg missing value: %d", iarg); + if (next_arg_index == -1) { + break; + } + pos = next_arg_index + 1; + continue; + } + RequestArgument &arg = _currentArgs[iarg]; + arg.key = urlDecode(data.substring(pos, equal_sign_index)); + arg.value = urlDecode(data.substring(equal_sign_index + 1, next_arg_index)); + log_v("arg %d key: %s value: %s", iarg, arg.key.c_str(), arg.value.c_str()); + ++iarg; + if (next_arg_index == -1) { + break; + } + pos = next_arg_index + 1; + } + _currentArgCount = iarg; + log_v("args count: %d", _currentArgCount); +} + +void WebServer::_uploadWriteByte(uint8_t b) { + if (_currentUpload->currentSize == HTTP_UPLOAD_BUFLEN) { + if (_currentHandler && _currentHandler->canUpload(*this, _currentUri)) { + _currentHandler->upload(*this, _currentUri, *_currentUpload); + } + _currentUpload->totalSize += _currentUpload->currentSize; + _currentUpload->currentSize = 0; + } + _currentUpload->buf[_currentUpload->currentSize++] = b; +} + +int WebServer::_uploadReadByte(NetworkClient &client) { + int res = client.read(); + + if (res < 0) { + // keep trying until you either read a valid byte or timeout + const unsigned long startMillis = millis(); + const long timeoutIntervalMillis = client.getTimeout(); + bool timedOut = false; + for (;;) { + if (!client.connected()) { + return -1; + } + // loosely modeled after blinkWithoutDelay pattern + while (!timedOut && !client.available() && client.connected()) { + delay(2); + timedOut = (millis() - startMillis) >= timeoutIntervalMillis; + } + + res = client.read(); + if (res >= 0) { + return res; // exit on a valid read + } + // NOTE: it is possible to get here and have all of the following + // assertions hold true + // + // -- client.available() > 0 + // -- client.connected == true + // -- res == -1 + // + // a simple retry strategy overcomes this which is to say the + // assertion is not permanent, but the reason that this works + // is elusive, and possibly indicative of a more subtle underlying + // issue + + timedOut = (millis() - startMillis) >= timeoutIntervalMillis; + if (timedOut) { + return res; // exit on a timeout + } + } + } + + return res; +} + +bool WebServer::_parseForm(NetworkClient &client, String boundary, uint32_t len) { + (void)len; + log_v("Parse Form: Boundary: %s Length: %d", boundary.c_str(), len); + String line; + int retry = 0; + do { + line = client.readStringUntil('\r'); + ++retry; + } while (line.length() == 0 && retry < 3); + + client.readStringUntil('\n'); + //start reading the form + if (line == ("--" + boundary)) { + if (_postArgs) { + delete[] _postArgs; + } + _postArgs = new RequestArgument[WEBSERVER_MAX_POST_ARGS]; + _postArgsLen = 0; + while (1) { + String argName; + String argValue; + String argType; + String argFilename; + bool argIsFile = false; + + line = client.readStringUntil('\r'); + client.readStringUntil('\n'); + if (line.length() > 19 && line.substring(0, 19).equalsIgnoreCase(F("Content-Disposition"))) { + int nameStart = line.indexOf('='); + if (nameStart != -1) { + argName = line.substring(nameStart + 2); + nameStart = argName.indexOf('='); + if (nameStart == -1) { + argName = argName.substring(0, argName.length() - 1); + } else { + argFilename = argName.substring(nameStart + 2, argName.length() - 1); + argName = argName.substring(0, argName.indexOf('"')); + argIsFile = true; + log_v("PostArg FileName: %s", argFilename.c_str()); + //use GET to set the filename if uploading using blob + if (argFilename == F("blob") && hasArg(FPSTR(filename))) { + argFilename = arg(FPSTR(filename)); + } + } + log_v("PostArg Name: %s", argName.c_str()); + using namespace mime; + argType = FPSTR(mimeTable[txt].mimeType); + line = client.readStringUntil('\r'); + client.readStringUntil('\n'); + while (line.length() > 0) { + if (line.length() > 12 && line.substring(0, 12).equalsIgnoreCase(FPSTR(Content_Type))) { + argType = line.substring(line.indexOf(':') + 2); + } + //skip over any other headers + line = client.readStringUntil('\r'); + client.readStringUntil('\n'); + } + log_v("PostArg Type: %s", argType.c_str()); + if (!argIsFile) { + while (1) { + line = client.readStringUntil('\r'); + client.readStringUntil('\n'); + if (line.startsWith("--" + boundary)) { + break; + } + if (argValue.length() > 0) { + argValue += "\n"; + } + argValue += line; + } + log_v("PostArg Value: %s", argValue.c_str()); + + RequestArgument &arg = _postArgs[_postArgsLen++]; + arg.key = argName; + arg.value = argValue; + + if (line == ("--" + boundary + "--")) { + log_v("Done Parsing POST"); + break; + } else if (_postArgsLen >= WEBSERVER_MAX_POST_ARGS) { + log_e("Too many PostArgs (max: %d) in request.", WEBSERVER_MAX_POST_ARGS); + return false; + } + } else { + _currentUpload.reset(new HTTPUpload()); + _currentUpload->status = UPLOAD_FILE_START; + _currentUpload->name = argName; + _currentUpload->filename = argFilename; + _currentUpload->type = argType; + _currentUpload->totalSize = 0; + _currentUpload->len = len; + _currentUpload->currentSize = 0; + log_v("Start File: %s Type: %s", _currentUpload->filename.c_str(), _currentUpload->type.c_str()); + if (_currentHandler && _currentHandler->canUpload(*this, _currentUri)) { + _currentHandler->upload(*this, _currentUri, *_currentUpload); + } + _currentUpload->status = UPLOAD_FILE_WRITE; + + int fastBoundaryLen = 4 /* \r\n-- */ + boundary.length() + 1 /* \0 */; + char fastBoundary[fastBoundaryLen]; + snprintf(fastBoundary, fastBoundaryLen, "\r\n--%s", boundary.c_str()); + int boundaryPtr = 0; + while (true) { + int ret = _uploadReadByte(client); + if (ret < 0) { + // Unexpected, we should have had data available per above + return _parseFormUploadAborted(); + } + char in = (char)ret; + if (in == fastBoundary[boundaryPtr]) { + // The input matched the current expected character, advance and possibly exit this file + boundaryPtr++; + if (boundaryPtr == fastBoundaryLen - 1) { + // We read the whole boundary line, we're done here! + break; + } + } else { + // The char doesn't match what we want, so dump whatever matches we had, the read in char, and reset ptr to start + for (int i = 0; i < boundaryPtr; i++) { + _uploadWriteByte(fastBoundary[i]); + } + if (in == fastBoundary[0]) { + // This could be the start of the real end, mark it so and don't emit/skip it + boundaryPtr = 1; + } else { + // Not the 1st char of our pattern, so emit and ignore + _uploadWriteByte(in); + boundaryPtr = 0; + } + } + } + // Found the boundary string, finish processing this file upload + if (_currentHandler && _currentHandler->canUpload(*this, _currentUri)) { + _currentHandler->upload(*this, _currentUri, *_currentUpload); + } + _currentUpload->totalSize += _currentUpload->currentSize; + _currentUpload->status = UPLOAD_FILE_END; + if (_currentHandler && _currentHandler->canUpload(*this, _currentUri)) { + _currentHandler->upload(*this, _currentUri, *_currentUpload); + } + log_v("End File: %s Type: %s Size: %d", _currentUpload->filename.c_str(), _currentUpload->type.c_str(), (int)_currentUpload->totalSize); + if (!client.connected()) { + return _parseFormUploadAborted(); + } + line = client.readStringUntil('\r'); + client.readStringUntil('\n'); + if (line == "--") { // extra two dashes mean we reached the end of all form fields + log_v("Done Parsing POST"); + break; + } + continue; + } + } + } + } + + int iarg; + int totalArgs = ((WEBSERVER_MAX_POST_ARGS - _postArgsLen) < _currentArgCount) ? (WEBSERVER_MAX_POST_ARGS - _postArgsLen) : _currentArgCount; + for (iarg = 0; iarg < totalArgs; iarg++) { + RequestArgument &arg = _postArgs[_postArgsLen++]; + arg.key = _currentArgs[iarg].key; + arg.value = _currentArgs[iarg].value; + } + if (_currentArgs) { + delete[] _currentArgs; + } + _currentArgs = new RequestArgument[_postArgsLen]; + for (iarg = 0; iarg < _postArgsLen; iarg++) { + RequestArgument &arg = _currentArgs[iarg]; + arg.key = _postArgs[iarg].key; + arg.value = _postArgs[iarg].value; + } + _currentArgCount = iarg; + if (_postArgs) { + delete[] _postArgs; + _postArgs = nullptr; + _postArgsLen = 0; + } + return true; + } + log_e("Error: line: %s", line.c_str()); + return false; +} + +String WebServer::urlDecode(const String &text) { + String decoded = ""; + char temp[] = "0x00"; + unsigned int len = text.length(); + unsigned int i = 0; + while (i < len) { + char decodedChar; + char encodedChar = text.charAt(i++); + if ((encodedChar == '%') && (i + 1 < len)) { + temp[2] = text.charAt(i++); + temp[3] = text.charAt(i++); + + decodedChar = strtol(temp, NULL, 16); + } else { + if (encodedChar == '+') { + decodedChar = ' '; + } else { + decodedChar = encodedChar; // normal ascii char + } + } + decoded += decodedChar; + } + return decoded; +} + +bool WebServer::_parseFormUploadAborted() { + _currentUpload->status = UPLOAD_FILE_ABORTED; + if (_currentHandler && _currentHandler->canUpload(*this, _currentUri)) { + _currentHandler->upload(*this, _currentUri, *_currentUpload); + } + return false; +} diff --git a/lib/WebServer/src/Uri.h b/lib/WebServer/src/Uri.h new file mode 100644 index 0000000..34ec756 --- /dev/null +++ b/lib/WebServer/src/Uri.h @@ -0,0 +1,29 @@ +#ifndef URI_H +#define URI_H + +#include +#include + +class Uri { + +protected: + const String _uri; + +public: + Uri(const char *uri) : _uri(uri) {} + Uri(const String &uri) : _uri(uri) {} + Uri(const __FlashStringHelper *uri) : _uri((const char *)uri) {} + virtual ~Uri() {} + + virtual Uri *clone() const { + return new Uri(_uri); + }; + + virtual void initPathArgs(__attribute__((unused)) std::vector &pathArgs) {} + + virtual bool canHandle(const String &requestUri, __attribute__((unused)) std::vector &pathArgs) { + return _uri == requestUri; + } +}; + +#endif diff --git a/lib/WebServer/src/WebServer.cpp b/lib/WebServer/src/WebServer.cpp new file mode 100644 index 0000000..3996d3b --- /dev/null +++ b/lib/WebServer/src/WebServer.cpp @@ -0,0 +1,865 @@ +/* + WebServer.cpp - Dead simple web-server. + Supports only one simultaneous client, knows how to handle GET and POST. + + Copyright (c) 2014 Ivan Grokhotkov. 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 + Modified 8 May 2015 by Hristo Gochkov (proper post and file upload handling) +*/ + +#include +#include +#include +#include +#include "esp_random.h" +#include "NetworkServer.h" +#include "NetworkClient.h" +#include "WebServer.h" +#include "FS.h" +#include "detail/RequestHandlersImpl.h" +#include "MD5Builder.h" +#include "SHA1Builder.h" +#include "base64.h" + +static const char AUTHORIZATION_HEADER[] = "Authorization"; +static const char qop_auth[] PROGMEM = "qop=auth"; +static const char qop_auth_quoted[] PROGMEM = "qop=\"auth\""; +static const char WWW_Authenticate[] = "WWW-Authenticate"; +static const char Content_Length[] = "Content-Length"; +static const char ETAG_HEADER[] = "If-None-Match"; + +WebServer::WebServer(IPAddress addr, int port) + : _corsEnabled(false), _server(addr, port), _currentMethod(HTTP_ANY), _currentVersion(0), _currentStatus(HC_NONE), _statusChange(0), _nullDelay(true), + _currentHandler(nullptr), _firstHandler(nullptr), _lastHandler(nullptr), _currentArgCount(0), _currentArgs(nullptr), _postArgsLen(0), _postArgs(nullptr), + _headerKeysCount(0), _currentHeaders(nullptr), _contentLength(0), _clientContentLength(0), _chunked(false) { + log_v("WebServer::Webserver(addr=%s, port=%d)", addr.toString().c_str(), port); +} + +WebServer::WebServer(int port) + : _corsEnabled(false), _server(port), _currentMethod(HTTP_ANY), _currentVersion(0), _currentStatus(HC_NONE), _statusChange(0), _nullDelay(true), + _currentHandler(nullptr), _firstHandler(nullptr), _lastHandler(nullptr), _currentArgCount(0), _currentArgs(nullptr), _postArgsLen(0), _postArgs(nullptr), + _headerKeysCount(0), _currentHeaders(nullptr), _contentLength(0), _clientContentLength(0), _chunked(false) { + log_v("WebServer::Webserver(port=%d)", port); +} + +WebServer::~WebServer() { + _server.close(); + if (_currentHeaders) { + delete[] _currentHeaders; + } + RequestHandler *handler = _firstHandler; + while (handler) { + RequestHandler *next = handler->next(); + delete handler; + handler = next; + } +} + +void WebServer::begin() { + close(); + _server.begin(); + _server.setNoDelay(true); +} + +void WebServer::begin(uint16_t port) { + close(); + _server.begin(port); + _server.setNoDelay(true); +} + +String WebServer::_extractParam(String &authReq, const String ¶m, const char delimit) { + int _begin = authReq.indexOf(param); + if (_begin == -1) { + return ""; + } + return authReq.substring(_begin + param.length(), authReq.indexOf(delimit, _begin + param.length())); +} + +static String md5str(String &in) { + MD5Builder md5 = MD5Builder(); + md5.begin(); + md5.add(in); + md5.calculate(); + return md5.toString(); +} + +bool WebServer::authenticateBasicSHA1(const char *_username, const char *_sha1Base64orHex) { + return WebServer::authenticate([_username, _sha1Base64orHex](HTTPAuthMethod mode, String username, String params[]) -> String * { + // rather than work on a password to compare with; we take the sha1 of the + // password received over the wire and compare that to the base64 encoded + // sha1 passed as _sha1base64. That way there is no need to have a + // plaintext password in the code/binary (though note that SHA1 is well + // past its retirement age). When that matches - we `cheat' by returning + // the password we got in the first place; so the normal BasicAuth + // can be completed. Note that this cannot work for a digest auth - + // as there the password in the clear is part of the calculation. + + if (params == nullptr) { + log_e("Something went wrong. params is NULL"); + return NULL; + } + + uint8_t sha1[20]; + char sha1calc[48]; // large enough for base64 and Hex representation + String ret; + SHA1Builder sha_builder; + base64 b64; + + log_v("Trying to authenticate user %s using SHA1.", username.c_str()); + sha_builder.begin(); + sha_builder.add((uint8_t *)params[0].c_str(), params[0].length()); + sha_builder.calculate(); + sha_builder.getBytes(sha1); + + // we can either decode _sha1base64orHex and then compare the 20 bytes; + // or encode the sha we calculated. We pick the latter as encoding of a + // fixed array of 20 bytes is safer than operating on something external. + if (strlen(_sha1Base64orHex) == 20 * 2) { // 2 chars per byte + sha_builder.bytes2hex(sha1calc, sizeof(sha1calc), sha1, sizeof(sha1)); + log_v("Calculated SHA1 in hex: %s", sha1calc); + } else { + ret = b64.encode(sha1, sizeof(sha1)); + ret.toCharArray(sha1calc, sizeof(sha1calc)); + log_v("Calculated SHA1 in base64: %s", sha1calc); + } + + return ((username.equalsConstantTime(_username)) && (String((char *)sha1calc).equalsConstantTime(_sha1Base64orHex)) + && (mode == BASIC_AUTH) /* to keep things somewhat time constant. */ + ) + ? new String(params[0]) + : NULL; + }); +} + +bool WebServer::authenticate(const char *_username, const char *_password) { + return WebServer::authenticate([_username, _password](HTTPAuthMethod mode, String username, String params[]) -> String * { + return username.equalsConstantTime(_username) ? new String(_password) : NULL; + }); +} + +bool WebServer::authenticate(THandlerFunctionAuthCheck fn) { + if (!hasHeader(FPSTR(AUTHORIZATION_HEADER))) { + return false; + } + + String authReq = header(FPSTR(AUTHORIZATION_HEADER)); + if (authReq.startsWith(AuthTypeBasic)) { + log_v("Trying to authenticate using Basic Auth"); + bool ret = false; + + authReq = authReq.substring(6); // length of AuthTypeBasic including the space at the end. + authReq.trim(); + + /* base64 encoded string is always shorter (or equal) in length */ + char *decoded = (authReq.length() < HTTP_MAX_BASIC_AUTH_LEN) ? new char[authReq.length()] : NULL; + if (decoded) { + char *p; + if (base64_decode_chars(authReq.c_str(), authReq.length(), decoded) && (p = index(decoded, ':')) && p) { + authReq = ""; + /* Note: rfc7617 guarantees that there will not be an escaped colon in the username itself. + * Note: base64_decode_chars() guarantees a terminating \0 + */ + *p = '\0'; + char *_username = decoded, *_password = p + 1; + String params[] = {_password, _srealm}; + String *password = fn(BASIC_AUTH, _username, params); + + if (password) { + ret = password->equalsConstantTime(_password); + // we're more concerned about the password; as the attacker already + // knows the _pasword. Arduino's string handling is simple; it reallocs + // even when smaller; so a memset is enough (no capacity/size). + memset((void *)password->c_str(), 0, password->length()); + delete password; + } + } + delete[] decoded; + } + authReq = ""; + log_v("Authentication %s", ret ? "Success" : "Failed"); + return ret; + } else if (authReq.startsWith(AuthTypeDigest)) { + log_v("Trying to authenticate using Digest Auth"); + authReq = authReq.substring(7); + log_v("%s", authReq.c_str()); + + // extracting required parameters for RFC 2069 simpler Digest + String _username = _extractParam(authReq, F("username=\""), '\"'); + String _realm = _extractParam(authReq, F("realm=\""), '\"'); + String _uri = _extractParam(authReq, F("uri=\""), '\"'); + if (!_username.length()) { + goto exf; + } + + String params[] = {_realm, _uri}; + String *password = fn(DIGEST_AUTH, _username, params); + if (!password) { + goto exf; + } + + String _H1 = md5str(String(_username) + ':' + _realm + ':' + *password); + // we're extra concerned; as digest request us to know the password + // in the clear. + memset((void *)password->c_str(), 0, password->length()); + delete password; + _username = ""; + + String _nonce = _extractParam(authReq, F("nonce=\""), '\"'); + String _response = _extractParam(authReq, F("response=\""), '\"'); + String _opaque = _extractParam(authReq, F("opaque=\""), '\"'); + + if ((!_realm.length()) || (!_nonce.length()) || (!_uri.length()) || (!_response.length()) || (!_opaque.length())) { + goto exf; + } + + if ((_opaque != _sopaque) || (_nonce != _snonce) || (_realm != _srealm)) { + goto exf; + } + + // parameters for the RFC 2617 newer Digest + String _nc, _cnonce; + if (authReq.indexOf(FPSTR(qop_auth)) != -1 || authReq.indexOf(FPSTR(qop_auth_quoted)) != -1) { + _nc = _extractParam(authReq, F("nc="), ','); + _cnonce = _extractParam(authReq, F("cnonce=\""), '\"'); + } + + log_v("Hash of user:realm:pass=%s", _H1.c_str()); + String _H2 = ""; + if (_currentMethod == HTTP_GET) { + _H2 = md5str(String(F("GET:")) + _uri); + } else if (_currentMethod == HTTP_POST) { + _H2 = md5str(String(F("POST:")) + _uri); + } else if (_currentMethod == HTTP_PUT) { + _H2 = md5str(String(F("PUT:")) + _uri); + } else if (_currentMethod == HTTP_DELETE) { + _H2 = md5str(String(F("DELETE:")) + _uri); + } else { + _H2 = md5str(String(F("GET:")) + _uri); + } + log_v("Hash of GET:uri=%s", _H2.c_str()); + String _responsecheck = ""; + if (authReq.indexOf(FPSTR(qop_auth)) != -1 || authReq.indexOf(FPSTR(qop_auth_quoted)) != -1) { + _responsecheck = md5str(_H1 + ':' + _nonce + ':' + _nc + ':' + _cnonce + F(":auth:") + _H2); + } else { + _responsecheck = md5str(_H1 + ':' + _nonce + ':' + _H2); + } + authReq = ""; + + log_v("The Proper response=%s", _responsecheck.c_str()); + bool ret = _response == _responsecheck; + log_v("Authentication %s", ret ? "Success" : "Failed"); + return ret; + } else if (authReq.length()) { + // OTHER_AUTH + log_v("Trying to authenticate using Other Auth, authReq=%s", authReq.c_str()); + String *ret = fn(OTHER_AUTH, authReq, {}); + if (ret) { + log_v("Authentication Success"); + return true; + } + } +exf: + authReq = ""; + log_v("Authentication Failed"); + return false; +} + +String WebServer::_getRandomHexString() { + char buffer[33]; // buffer to hold 32 Hex Digit + /0 + int i; + for (i = 0; i < 4; i++) { + sprintf(buffer + (i * 8), "%08lx", esp_random()); + } + return String(buffer); +} + +void WebServer::requestAuthentication(HTTPAuthMethod mode, const char *realm, const String &authFailMsg) { + if (realm == NULL) { + _srealm = String(F("Login Required")); + } else { + _srealm = String(realm); + } + if (mode == BASIC_AUTH) { + sendHeader(String(FPSTR(WWW_Authenticate)), AuthTypeBasic + String(F(" realm=\"")) + _srealm + String(F("\""))); + } else { + _snonce = _getRandomHexString(); + _sopaque = _getRandomHexString(); + sendHeader( + String(FPSTR(WWW_Authenticate)), AuthTypeDigest + String(F(" realm=\"")) + _srealm + String(F("\", qop=\"auth\", nonce=\"")) + _snonce + + String(F("\", opaque=\"")) + _sopaque + String(F("\"")) + ); + } + using namespace mime; + send(401, String(FPSTR(mimeTable[html].mimeType)), authFailMsg); +} + +RequestHandler &WebServer::on(const Uri &uri, WebServer::THandlerFunction handler) { + return on(uri, HTTP_ANY, handler); +} + +RequestHandler &WebServer::on(const Uri &uri, HTTPMethod method, WebServer::THandlerFunction fn) { + return on(uri, method, fn, _fileUploadHandler); +} + +RequestHandler &WebServer::on(const Uri &uri, HTTPMethod method, WebServer::THandlerFunction fn, WebServer::THandlerFunction ufn) { + FunctionRequestHandler *handler = new FunctionRequestHandler(fn, ufn, uri, method); + _addRequestHandler(handler); + return *handler; +} + +bool WebServer::removeRoute(const char *uri) { + return removeRoute(String(uri), HTTP_ANY); +} + +bool WebServer::removeRoute(const char *uri, HTTPMethod method) { + return removeRoute(String(uri), method); +} + +bool WebServer::removeRoute(const String &uri) { + return removeRoute(uri, HTTP_ANY); +} + +bool WebServer::removeRoute(const String &uri, HTTPMethod method) { + bool anyHandlerRemoved = false; + RequestHandler *handler = _firstHandler; + RequestHandler *previousHandler = nullptr; + + while (handler) { + if (handler->canHandle(method, uri)) { + if (_removeRequestHandler(handler)) { + anyHandlerRemoved = true; + // Move to the next handler + if (previousHandler) { + handler = previousHandler->next(); + } else { + handler = _firstHandler; + } + continue; + } + } + previousHandler = handler; + handler = handler->next(); + } + + return anyHandlerRemoved; +} + +void WebServer::addHandler(RequestHandler *handler) { + _addRequestHandler(handler); +} + +bool WebServer::removeHandler(RequestHandler *handler) { + return _removeRequestHandler(handler); +} + +void WebServer::_addRequestHandler(RequestHandler *handler) { + if (!_lastHandler) { + _firstHandler = handler; + _lastHandler = handler; + } else { + _lastHandler->next(handler); + _lastHandler = handler; + } +} + +bool WebServer::_removeRequestHandler(RequestHandler *handler) { + RequestHandler *current = _firstHandler; + RequestHandler *previous = nullptr; + + while (current != nullptr) { + if (current == handler) { + if (previous == nullptr) { + _firstHandler = current->next(); + } else { + previous->next(current->next()); + } + + if (current == _lastHandler) { + _lastHandler = previous; + } + + // Delete 'matching' handler + delete current; + return true; + } + previous = current; + current = current->next(); + } + return false; +} + +void WebServer::serveStatic(const char *uri, FS &fs, const char *path, const char *cache_header) { + _addRequestHandler(new StaticRequestHandler(fs, path, uri, cache_header)); +} + +void WebServer::handleClient() { + if (_currentStatus == HC_NONE) { + _currentClient = _server.accept(); + if (!_currentClient) { + if (_nullDelay) { + delay(1); + } + return; + } + + log_v("New client: client.localIP()=%s", _currentClient.localIP().toString().c_str()); + + _currentStatus = HC_WAIT_READ; + _statusChange = millis(); + } + + bool keepCurrentClient = false; + bool callYield = false; + + if (_currentClient.connected()) { + switch (_currentStatus) { + case HC_NONE: + // No-op to avoid C++ compiler warning + break; + case HC_WAIT_READ: + // Wait for data from client to become available + if (_currentClient.available()) { + _currentClient.setTimeout(HTTP_MAX_SEND_WAIT); /* / 1000 removed, WifiClient setTimeout changed to ms */ + if (_parseRequest(_currentClient)) { + _contentLength = CONTENT_LENGTH_NOT_SET; + _handleRequest(); + + if (_currentClient.isSSE()) { + _currentStatus = HC_WAIT_CLOSE; + _statusChange = millis(); + keepCurrentClient = true; + } + // Fix for issue with Chrome based browsers: https://github.com/espressif/arduino-esp32/issues/3652 + // if (_currentClient.connected()) { + // _currentStatus = HC_WAIT_CLOSE; + // _statusChange = millis(); + // keepCurrentClient = true; + // } + } + } else { // !_currentClient.available() + if (millis() - _statusChange <= HTTP_MAX_DATA_WAIT) { + keepCurrentClient = true; + } + callYield = true; + } + break; + case HC_WAIT_CLOSE: + if (_currentClient.isSSE()) { + // Never close connection + _statusChange = millis(); + } + // Wait for client to close the connection + if (millis() - _statusChange <= HTTP_MAX_CLOSE_WAIT) { + keepCurrentClient = true; + callYield = true; + } + } + } + + if (!keepCurrentClient) { + _currentClient = NetworkClient(); + _currentStatus = HC_NONE; + _currentUpload.reset(); + _currentRaw.reset(); + } + + if (callYield) { + yield(); + } +} + +void WebServer::close() { + _server.close(); + _currentStatus = HC_NONE; + if (!_headerKeysCount) { + collectHeaders(0, 0); + } +} + +void WebServer::stop() { + close(); +} + +void WebServer::sendHeader(const String &name, const String &value, bool first) { + String headerLine = name; + headerLine += F(": "); + headerLine += value; + headerLine += "\r\n"; + + if (first) { + _responseHeaders = headerLine + _responseHeaders; + } else { + _responseHeaders += headerLine; + } +} + +void WebServer::setContentLength(const size_t contentLength) { + _contentLength = contentLength; +} + +void WebServer::enableDelay(boolean value) { + _nullDelay = value; +} + +void WebServer::enableCORS(boolean value) { + _corsEnabled = value; +} + +void WebServer::enableCrossOrigin(boolean value) { + enableCORS(value); +} + +void WebServer::enableETag(bool enable, ETagFunction fn) { + _eTagEnabled = enable; + _eTagFunction = fn; +} + +void WebServer::_prepareHeader(String &response, int code, const char *content_type, size_t contentLength) { + response = String(F("HTTP/1.")) + String(_currentVersion) + ' '; + response += String(code); + response += ' '; + response += _responseCodeToString(code); + response += "\r\n"; + + using namespace mime; + if (!content_type) { + content_type = mimeTable[html].mimeType; + } + + sendHeader(String(F("Content-Type")), String(FPSTR(content_type)), true); + if (_contentLength == CONTENT_LENGTH_NOT_SET) { + sendHeader(String(FPSTR(Content_Length)), String(contentLength)); + } else if (_contentLength != CONTENT_LENGTH_UNKNOWN) { + sendHeader(String(FPSTR(Content_Length)), String(_contentLength)); + } else if (_contentLength == CONTENT_LENGTH_UNKNOWN && _currentVersion) { //HTTP/1.1 or above client + //let's do chunked + _chunked = true; + sendHeader(String(F("Accept-Ranges")), String(F("none"))); + sendHeader(String(F("Transfer-Encoding")), String(F("chunked"))); + } + if (_corsEnabled) { + sendHeader(String(FPSTR("Access-Control-Allow-Origin")), String("*")); + sendHeader(String(FPSTR("Access-Control-Allow-Methods")), String("*")); + sendHeader(String(FPSTR("Access-Control-Allow-Headers")), String("*")); + } + sendHeader(String(F("Connection")), String(F("close"))); + + response += _responseHeaders; + response += "\r\n"; + _responseHeaders = ""; +} + +void WebServer::send(int code, const char *content_type, const String &content) { + String header; + // Can we assume the following? + //if(code == 200 && content.length() == 0 && _contentLength == CONTENT_LENGTH_NOT_SET) + // _contentLength = CONTENT_LENGTH_UNKNOWN; + if (content.length() == 0) { + log_w("content length is zero"); + } + _prepareHeader(header, code, content_type, content.length()); + _currentClientWrite(header.c_str(), header.length()); + if (content.length()) { + sendContent(content); + } +} + +void WebServer::send(int code, char *content_type, const String &content) { + send(code, (const char *)content_type, content); +} + +void WebServer::send(int code, const String &content_type, const String &content) { + send(code, (const char *)content_type.c_str(), content); +} + +void WebServer::send(int code, const char *content_type, const char *content) { + const String passStr = (String)content; + if (strlen(content) != passStr.length()) { + log_e("String cast failed. Use send_P for long arrays"); + } + send(code, content_type, passStr); +} + +void WebServer::send_P(int code, PGM_P content_type, PGM_P content) { + size_t contentLength = 0; + + if (content != NULL) { + contentLength = strlen_P(content); + } + + String header; + char type[64]; + memccpy_P((void *)type, (PGM_VOID_P)content_type, 0, sizeof(type)); + _prepareHeader(header, code, (const char *)type, contentLength); + _currentClientWrite(header.c_str(), header.length()); + sendContent_P(content); +} + +void WebServer::send_P(int code, PGM_P content_type, PGM_P content, size_t contentLength) { + String header; + char type[64]; + memccpy_P((void *)type, (PGM_VOID_P)content_type, 0, sizeof(type)); + _prepareHeader(header, code, (const char *)type, contentLength); + sendContent(header); + sendContent_P(content, contentLength); +} + +void WebServer::sendContent(const String &content) { + sendContent(content.c_str(), content.length()); +} + +void WebServer::sendContent(const char *content, size_t contentLength) { + const char *footer = "\r\n"; + if (_chunked) { + char *chunkSize = (char *)malloc(11); + if (chunkSize) { + sprintf(chunkSize, "%x%s", contentLength, footer); + _currentClientWrite(chunkSize, strlen(chunkSize)); + free(chunkSize); + } + } + _currentClientWrite(content, contentLength); + if (_chunked) { + _currentClient.write(footer, 2); + if (contentLength == 0) { + _chunked = false; + } + } +} + +void WebServer::sendContent_P(PGM_P content) { + sendContent_P(content, strlen_P(content)); +} + +void WebServer::sendContent_P(PGM_P content, size_t size) { + const char *footer = "\r\n"; + if (_chunked) { + char *chunkSize = (char *)malloc(11); + if (chunkSize) { + sprintf(chunkSize, "%x%s", size, footer); + _currentClientWrite(chunkSize, strlen(chunkSize)); + free(chunkSize); + } + } + _currentClientWrite_P(content, size); + if (_chunked) { + _currentClient.write(footer, 2); + if (size == 0) { + _chunked = false; + } + } +} + +void WebServer::_streamFileCore(const size_t fileSize, const String &fileName, const String &contentType, const int code) { + using namespace mime; + setContentLength(fileSize); + if (fileName.endsWith(String(FPSTR(mimeTable[gz].endsWith))) && contentType != String(FPSTR(mimeTable[gz].mimeType)) + && contentType != String(FPSTR(mimeTable[none].mimeType))) { + sendHeader(F("Content-Encoding"), F("gzip")); + } + send(code, contentType, ""); +} + +String WebServer::pathArg(unsigned int i) { + if (_currentHandler != nullptr) { + return _currentHandler->pathArg(i); + } + return ""; +} + +String WebServer::arg(String name) { + for (int j = 0; j < _postArgsLen; ++j) { + if (_postArgs[j].key == name) { + return _postArgs[j].value; + } + } + for (int i = 0; i < _currentArgCount; ++i) { + if (_currentArgs[i].key == name) { + return _currentArgs[i].value; + } + } + return ""; +} + +String WebServer::arg(int i) { + if (i < _currentArgCount) { + return _currentArgs[i].value; + } + return ""; +} + +String WebServer::argName(int i) { + if (i < _currentArgCount) { + return _currentArgs[i].key; + } + return ""; +} + +int WebServer::args() { + return _currentArgCount; +} + +bool WebServer::hasArg(String name) { + for (int j = 0; j < _postArgsLen; ++j) { + if (_postArgs[j].key == name) { + return true; + } + } + for (int i = 0; i < _currentArgCount; ++i) { + if (_currentArgs[i].key == name) { + return true; + } + } + return false; +} + +String WebServer::header(String name) { + for (int i = 0; i < _headerKeysCount; ++i) { + if (_currentHeaders[i].key.equalsIgnoreCase(name)) { + return _currentHeaders[i].value; + } + } + return ""; +} + +void WebServer::collectHeaders(const char *headerKeys[], const size_t headerKeysCount) { + _headerKeysCount = headerKeysCount + 2; + if (_currentHeaders) { + delete[] _currentHeaders; + } + _currentHeaders = new RequestArgument[_headerKeysCount]; + _currentHeaders[0].key = FPSTR(AUTHORIZATION_HEADER); + _currentHeaders[1].key = FPSTR(ETAG_HEADER); + for (int i = 2; i < _headerKeysCount; i++) { + _currentHeaders[i].key = headerKeys[i - 2]; + } +} + +String WebServer::header(int i) { + if (i < _headerKeysCount) { + return _currentHeaders[i].value; + } + return ""; +} + +String WebServer::headerName(int i) { + if (i < _headerKeysCount) { + return _currentHeaders[i].key; + } + return ""; +} + +int WebServer::headers() { + return _headerKeysCount; +} + +bool WebServer::hasHeader(String name) { + for (int i = 0; i < _headerKeysCount; ++i) { + if ((_currentHeaders[i].key.equalsIgnoreCase(name)) && (_currentHeaders[i].value.length() > 0)) { + return true; + } + } + return false; +} + +String WebServer::hostHeader() { + return _hostHeader; +} + +void WebServer::onFileUpload(THandlerFunction fn) { + _fileUploadHandler = fn; +} + +void WebServer::onNotFound(THandlerFunction fn) { + _notFoundHandler = fn; +} + +void WebServer::_handleRequest() { + bool handled = false; + if (!_currentHandler) { + log_e("request handler not found"); + } else { + handled = _currentHandler->handle(*this, _currentMethod, _currentUri); + if (!handled) { + log_e("request handler failed to handle request"); + } + } + if (!handled && _notFoundHandler) { + _notFoundHandler(); + handled = true; + } + if (!handled) { + using namespace mime; + send(404, String(FPSTR(mimeTable[html].mimeType)), String(F("Not found: ")) + _currentUri); + handled = true; + } + if (handled) { + _finalizeResponse(); + } + _currentUri = ""; +} + +void WebServer::_finalizeResponse() { + if (_chunked) { + sendContent(""); + } +} + +String WebServer::_responseCodeToString(int code) { + switch (code) { + case 100: return F("Continue"); + case 101: return F("Switching Protocols"); + case 200: return F("OK"); + case 201: return F("Created"); + case 202: return F("Accepted"); + case 203: return F("Non-Authoritative Information"); + case 204: return F("No Content"); + case 205: return F("Reset Content"); + case 206: return F("Partial Content"); + case 300: return F("Multiple Choices"); + case 301: return F("Moved Permanently"); + case 302: return F("Found"); + case 303: return F("See Other"); + case 304: return F("Not Modified"); + case 305: return F("Use Proxy"); + case 307: return F("Temporary Redirect"); + case 400: return F("Bad Request"); + case 401: return F("Unauthorized"); + case 402: return F("Payment Required"); + case 403: return F("Forbidden"); + case 404: return F("Not Found"); + case 405: return F("Method Not Allowed"); + case 406: return F("Not Acceptable"); + case 407: return F("Proxy Authentication Required"); + case 408: return F("Request Time-out"); + case 409: return F("Conflict"); + case 410: return F("Gone"); + case 411: return F("Length Required"); + case 412: return F("Precondition Failed"); + case 413: return F("Request Entity Too Large"); + case 414: return F("Request-URI Too Large"); + case 415: return F("Unsupported Media Type"); + case 416: return F("Requested range not satisfiable"); + case 417: return F("Expectation Failed"); + case 500: return F("Internal Server Error"); + case 501: return F("Not Implemented"); + case 502: return F("Bad Gateway"); + case 503: return F("Service Unavailable"); + case 504: return F("Gateway Time-out"); + case 505: return F("HTTP Version not supported"); + default: return F(""); + } +} diff --git a/lib/WebServer/src/WebServer.h b/lib/WebServer/src/WebServer.h new file mode 100644 index 0000000..9ebfedb --- /dev/null +++ b/lib/WebServer/src/WebServer.h @@ -0,0 +1,303 @@ +/* + WebServer.h - Dead simple web-server. + Supports only one simultaneous client, knows how to handle GET and POST. + + Copyright (c) 2014 Ivan Grokhotkov. 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 + Modified 8 May 2015 by Hristo Gochkov (proper post and file upload handling) +*/ + +#ifndef WEBSERVER_H +#define WEBSERVER_H + +#include +#include +#include "FS.h" +#include "Network.h" +#include "HTTP_Method.h" +#include "Uri.h" + +enum HTTPUploadStatus { + UPLOAD_FILE_START, + UPLOAD_FILE_WRITE, + UPLOAD_FILE_END, + UPLOAD_FILE_ABORTED +}; +enum HTTPRawStatus { + RAW_START, + RAW_WRITE, + RAW_END, + RAW_ABORTED +}; +enum HTTPClientStatus { + HC_NONE, + HC_WAIT_READ, + HC_WAIT_CLOSE +}; +enum HTTPAuthMethod { + BASIC_AUTH, + DIGEST_AUTH, + OTHER_AUTH +}; + +#define HTTP_DOWNLOAD_UNIT_SIZE 1436 + +#ifndef HTTP_UPLOAD_BUFLEN +#define HTTP_UPLOAD_BUFLEN 1436 +#endif + +#ifndef HTTP_RAW_BUFLEN +#define HTTP_RAW_BUFLEN 1436 +#endif + +#define HTTP_MAX_DATA_WAIT 5000 //ms to wait for the client to send the request +#define HTTP_MAX_POST_WAIT 5000 //ms to wait for POST data to arrive +#define HTTP_MAX_SEND_WAIT 5000 //ms to wait for data chunk to be ACKed +#define HTTP_MAX_CLOSE_WAIT 5000 //ms to wait for the client to close the connection +#define HTTP_MAX_BASIC_AUTH_LEN 256 // maximum length of a basic Auth base64 encoded username:password string + +#define CONTENT_LENGTH_UNKNOWN ((size_t) - 1) +#define CONTENT_LENGTH_NOT_SET ((size_t) - 2) + +class WebServer; + +typedef struct { + HTTPUploadStatus status; + String filename; + String name; + String type; + size_t totalSize; // file size + size_t len; + size_t currentSize; // size of data currently in buf + uint8_t buf[HTTP_UPLOAD_BUFLEN]; +} HTTPUpload; + +typedef struct { + HTTPRawStatus status; + size_t totalSize; // content size + size_t currentSize; // size of data currently in buf + uint8_t buf[HTTP_RAW_BUFLEN]; + void *data; // additional data +} HTTPRaw; + +#include "detail/RequestHandler.h" + +namespace fs { +class FS; +} + +class WebServer { +public: + WebServer(IPAddress addr, int port = 80); + WebServer(int port = 80); + virtual ~WebServer(); + + virtual void begin(); + virtual void begin(uint16_t port); + virtual void handleClient(); + + virtual void close(); + void stop(); + + const String AuthTypeDigest = F("Digest"); + const String AuthTypeBasic = F("Basic"); + + /* Callbackhandler for authentication. The extra parameters depend on the + * HTTPAuthMethod mode: + * + * BASIC_AUTH enteredUsernameOrReq contains the username entered by the user + * param[0] password entered (in the clear) + * param[1] authentication realm. + * + * To return - the password the user entered password is compared to. Or Null on fail. + * + * DIGEST_AUTH enteredUsernameOrReq contains the username entered by the user + * param[0] autenticaiton realm + * param[1] authentication URI + * + * To return - the password of which the digest will be based on for comparison. Or NULL + * to fail. + * + * OTHER_AUTH enteredUsernameOrReq rest of the auth line. + * params empty array + * + * To return - NULL to fail; or any string. + */ + typedef std::function THandlerFunctionAuthCheck; + + bool authenticate(THandlerFunctionAuthCheck fn); + bool authenticate(const char *username, const char *password); + bool authenticateBasicSHA1(const char *_username, const char *_sha1AsBase64orHex); + + void requestAuthentication(HTTPAuthMethod mode = BASIC_AUTH, const char *realm = NULL, const String &authFailMsg = String("")); + + typedef std::function THandlerFunction; + typedef std::function FilterFunction; + RequestHandler &on(const Uri &uri, THandlerFunction fn); + RequestHandler &on(const Uri &uri, HTTPMethod method, THandlerFunction fn); + RequestHandler &on(const Uri &uri, HTTPMethod method, THandlerFunction fn, THandlerFunction ufn); //ufn handles file uploads + bool removeRoute(const char *uri); + bool removeRoute(const char *uri, HTTPMethod method); + bool removeRoute(const String &uri); + bool removeRoute(const String &uri, HTTPMethod method); + void addHandler(RequestHandler *handler); + bool removeHandler(RequestHandler *handler); + void serveStatic(const char *uri, fs::FS &fs, const char *path, const char *cache_header = NULL); + void onNotFound(THandlerFunction fn); //called when handler is not assigned + void onFileUpload(THandlerFunction ufn); //handle file uploads + + String uri() { + return _currentUri; + } + HTTPMethod method() { + return _currentMethod; + } + virtual NetworkClient &client() { + return _currentClient; + } + HTTPUpload &upload() { + return *_currentUpload; + } + HTTPRaw &raw() { + return *_currentRaw; + } + + String pathArg(unsigned int i); // get request path argument by number + String arg(String name); // get request argument value by name + String arg(int i); // get request argument value by number + String argName(int i); // get request argument name by number + int args(); // get arguments count + bool hasArg(String name); // check if argument exists + void collectHeaders(const char *headerKeys[], const size_t headerKeysCount); // set the request headers to collect + String header(String name); // get request header value by name + String header(int i); // get request header value by number + String headerName(int i); // get request header name by number + int headers(); // get header count + bool hasHeader(String name); // check if header exists + + int clientContentLength() { + return _clientContentLength; + } // return "content-length" of incoming HTTP header from "_currentClient" + + String hostHeader(); // get request host header if available or empty String if not + + // send response to the client + // code - HTTP response code, can be 200 or 404 + // content_type - HTTP content type, like "text/plain" or "image/png" + // content - actual content body + void send(int code, const char *content_type = NULL, const String &content = String("")); + void send(int code, char *content_type, const String &content); + void send(int code, const String &content_type, const String &content); + void send(int code, const char *content_type, const char *content); + + void send_P(int code, PGM_P content_type, PGM_P content); + void send_P(int code, PGM_P content_type, PGM_P content, size_t contentLength); + + void enableDelay(boolean value); + void enableCORS(boolean value = true); + void enableCrossOrigin(boolean value = true); + typedef std::function ETagFunction; + void enableETag(bool enable, ETagFunction fn = nullptr); + + void setContentLength(const size_t contentLength); + void sendHeader(const String &name, const String &value, bool first = false); + void sendContent(const String &content); + void sendContent(const char *content, size_t contentLength); + void sendContent_P(PGM_P content); + void sendContent_P(PGM_P content, size_t size); + + static String urlDecode(const String &text); + + template size_t streamFile(T &file, const String &contentType, const int code = 200) { + _streamFileCore(file.size(), file.name(), contentType, code); + return _currentClient.write(file); + } + + bool _eTagEnabled = false; + ETagFunction _eTagFunction = nullptr; + +protected: + virtual size_t _currentClientWrite(const char *b, size_t l) { + return _currentClient.write(b, l); + } + virtual size_t _currentClientWrite_P(PGM_P b, size_t l) { + return _currentClient.write_P(b, l); + } + void _addRequestHandler(RequestHandler *handler); + bool _removeRequestHandler(RequestHandler *handler); + void _handleRequest(); + void _finalizeResponse(); + bool _parseRequest(NetworkClient &client); + void _parseArguments(String data); + static String _responseCodeToString(int code); + bool _parseForm(NetworkClient &client, String boundary, uint32_t len); + bool _parseFormUploadAborted(); + void _uploadWriteByte(uint8_t b); + int _uploadReadByte(NetworkClient &client); + void _prepareHeader(String &response, int code, const char *content_type, size_t contentLength); + bool _collectHeader(const char *headerName, const char *headerValue); + + void _streamFileCore(const size_t fileSize, const String &fileName, const String &contentType, const int code = 200); + + String _getRandomHexString(); + // for extracting Auth parameters + String _extractParam(String &authReq, const String ¶m, const char delimit = '"'); + + struct RequestArgument { + String key; + String value; + }; + + boolean _corsEnabled; + NetworkServer _server; + + NetworkClient _currentClient; + HTTPMethod _currentMethod; + String _currentUri; + uint8_t _currentVersion; + HTTPClientStatus _currentStatus; + unsigned long _statusChange; + boolean _nullDelay; + + RequestHandler *_currentHandler; + RequestHandler *_firstHandler; + RequestHandler *_lastHandler; + THandlerFunction _notFoundHandler; + THandlerFunction _fileUploadHandler; + + int _currentArgCount; + RequestArgument *_currentArgs; + int _postArgsLen; + RequestArgument *_postArgs; + + std::unique_ptr _currentUpload; + std::unique_ptr _currentRaw; + + int _headerKeysCount; + RequestArgument *_currentHeaders; + size_t _contentLength; + int _clientContentLength; // "Content-Length" from header of incoming POST or GET request + String _responseHeaders; + + String _hostHeader; + bool _chunked; + + String _snonce; // Store noance and opaque for future comparison + String _sopaque; + String _srealm; // Store the Auth realm between Calls +}; + +#endif //ESP8266WEBSERVER_H diff --git a/lib/WebServer/src/detail/RequestHandler.h b/lib/WebServer/src/detail/RequestHandler.h new file mode 100644 index 0000000..f19e7ab --- /dev/null +++ b/lib/WebServer/src/detail/RequestHandler.h @@ -0,0 +1,91 @@ +#ifndef REQUESTHANDLER_H +#define REQUESTHANDLER_H + +#include +#include + +class RequestHandler { +public: + virtual ~RequestHandler() {} + + /* + note: old handler API for backward compatibility + */ + + virtual bool canHandle(HTTPMethod method, String uri) { + (void)method; + (void)uri; + return false; + } + virtual bool canUpload(String uri) { + (void)uri; + return false; + } + virtual bool canRaw(String uri) { + (void)uri; + return false; + } + + /* + note: new handler API with support for filters etc. + */ + + virtual bool canHandle(WebServer &server, HTTPMethod method, String uri) { + (void)server; + (void)method; + (void)uri; + return false; + } + virtual bool canUpload(WebServer &server, String uri) { + (void)server; + (void)uri; + return false; + } + virtual bool canRaw(WebServer &server, String uri) { + (void)server; + (void)uri; + return false; + } + virtual bool handle(WebServer &server, HTTPMethod requestMethod, String requestUri) { + (void)server; + (void)requestMethod; + (void)requestUri; + return false; + } + virtual void upload(WebServer &server, String requestUri, HTTPUpload &upload) { + (void)server; + (void)requestUri; + (void)upload; + } + virtual void raw(WebServer &server, String requestUri, HTTPRaw &raw) { + (void)server; + (void)requestUri; + (void)raw; + } + + virtual RequestHandler &setFilter(std::function filter) { + (void)filter; + return *this; + } + + RequestHandler *next() { + return _next; + } + void next(RequestHandler *r) { + _next = r; + } + +private: + RequestHandler *_next = nullptr; + +protected: + std::vector pathArgs; + +public: + const String &pathArg(unsigned int i) { + assert(i < pathArgs.size()); + return pathArgs[i]; + } +}; + +#endif //REQUESTHANDLER_H diff --git a/lib/WebServer/src/detail/RequestHandlersImpl.h b/lib/WebServer/src/detail/RequestHandlersImpl.h new file mode 100644 index 0000000..b6eae6a --- /dev/null +++ b/lib/WebServer/src/detail/RequestHandlersImpl.h @@ -0,0 +1,263 @@ +#ifndef REQUESTHANDLERSIMPL_H +#define REQUESTHANDLERSIMPL_H + +#include "RequestHandler.h" +#include "mimetable.h" +#include "WString.h" +#include "Uri.h" +#include +#include + +using namespace mime; + +class FunctionRequestHandler : public RequestHandler { +public: + FunctionRequestHandler(WebServer::THandlerFunction fn, WebServer::THandlerFunction ufn, const Uri &uri, HTTPMethod method) + : _fn(fn), _ufn(ufn), _uri(uri.clone()), _method(method) { + _uri->initPathArgs(pathArgs); + } + + ~FunctionRequestHandler() { + delete _uri; + } + + bool canHandle(HTTPMethod requestMethod, String requestUri) override { + if (_method != HTTP_ANY && _method != requestMethod) { + return false; + } + + return _uri->canHandle(requestUri, pathArgs); + } + + bool canUpload(String requestUri) override { + if (!_ufn || !canHandle(HTTP_POST, requestUri)) { + return false; + } + + return true; + } + + bool canRaw(String requestUri) override { + if (!_ufn || _method == HTTP_GET) { + return false; + } + + return true; + } + + bool canHandle(WebServer &server, HTTPMethod requestMethod, String requestUri) override { + if (_method != HTTP_ANY && _method != requestMethod) { + return false; + } + + return _uri->canHandle(requestUri, pathArgs) && (_filter != NULL ? _filter(server) : true); + } + + bool canUpload(WebServer &server, String requestUri) override { + if (!_ufn || !canHandle(server, HTTP_POST, requestUri)) { + return false; + } + + return true; + } + + bool canRaw(WebServer &server, String requestUri) override { + if (!_ufn || _method == HTTP_GET || (_filter != NULL ? _filter(server) == false : false)) { + return false; + } + + return true; + } + + bool handle(WebServer &server, HTTPMethod requestMethod, String requestUri) override { + if (!canHandle(server, requestMethod, requestUri)) { + return false; + } + + _fn(); + return true; + } + + void upload(WebServer &server, String requestUri, HTTPUpload &upload) override { + (void)upload; + if (canUpload(server, requestUri)) { + _ufn(); + } + } + + void raw(WebServer &server, String requestUri, HTTPRaw &raw) override { + (void)raw; + if (canRaw(server, requestUri)) { + _ufn(); + } + } + + FunctionRequestHandler &setFilter(WebServer::FilterFunction filter) { + _filter = filter; + return *this; + } + +protected: + WebServer::THandlerFunction _fn; + WebServer::THandlerFunction _ufn; + // _filter should return 'true' when the request should be handled + // and 'false' when the request should be ignored + WebServer::FilterFunction _filter; + Uri *_uri; + HTTPMethod _method; +}; + +class StaticRequestHandler : public RequestHandler { +public: + StaticRequestHandler(FS &fs, const char *path, const char *uri, const char *cache_header) : _fs(fs), _uri(uri), _path(path), _cache_header(cache_header) { + File f = fs.open(path); + _isFile = (f && (!f.isDirectory())); + log_v( + "StaticRequestHandler: path=%s uri=%s isFile=%d, cache_header=%s\r\n", path, uri, _isFile, cache_header ? cache_header : "" + ); // issue 5506 - cache_header can be nullptr + _baseUriLength = _uri.length(); + } + + bool canHandle(HTTPMethod requestMethod, String requestUri) override { + if (requestMethod != HTTP_GET) { + return false; + } + + if ((_isFile && requestUri != _uri) || !requestUri.startsWith(_uri)) { + return false; + } + + return true; + } + + bool canHandle(WebServer &server, HTTPMethod requestMethod, String requestUri) override { + if (requestMethod != HTTP_GET) { + return false; + } + + if ((_isFile && requestUri != _uri) || !requestUri.startsWith(_uri)) { + return false; + } + + if (_filter != NULL ? _filter(server) == false : false) { + return false; + } + + return true; + } + + bool handle(WebServer &server, HTTPMethod requestMethod, String requestUri) override { + if (!canHandle(server, requestMethod, requestUri)) { + return false; + } + + log_v("StaticRequestHandler::handle: request=%s _uri=%s\r\n", requestUri.c_str(), _uri.c_str()); + + String path(_path); + String eTagCode; + + if (!_isFile) { + // Base URI doesn't point to a file. + // If a directory is requested, look for index file. + if (requestUri.endsWith("/")) { + requestUri += "index.htm"; + } + + // Append whatever follows this URI in request to get the file path. + path += requestUri.substring(_baseUriLength); + } + log_v("StaticRequestHandler::handle: path=%s, isFile=%d\r\n", path.c_str(), _isFile); + + String contentType = getContentType(path); + + // look for gz file, only if the original specified path is not a gz. So part only works to send gzip via content encoding when a non compressed is asked for + // if you point the the path to gzip you will serve the gzip as content type "application/x-gzip", not text or javascript etc... + if (!path.endsWith(FPSTR(mimeTable[gz].endsWith)) && !_fs.exists(path)) { + String pathWithGz = path + FPSTR(mimeTable[gz].endsWith); + if (_fs.exists(pathWithGz)) { + path += FPSTR(mimeTable[gz].endsWith); + } + } + + File f = _fs.open(path, "r"); + if (!f || !f.available()) { + return false; + } + + if (server._eTagEnabled) { + if (server._eTagFunction) { + eTagCode = (server._eTagFunction)(_fs, path); + } else { + eTagCode = calcETag(_fs, path); + } + + if (server.header("If-None-Match") == eTagCode) { + server.send(304); + return true; + } + } + + if (_cache_header.length() != 0) { + server.sendHeader("Cache-Control", _cache_header); + } + + if ((server._eTagEnabled) && (eTagCode.length() > 0)) { + server.sendHeader("ETag", eTagCode); + } + + server.streamFile(f, contentType); + return true; + } + + static String getContentType(const String &path) { + char buff[sizeof(mimeTable[0].mimeType)]; + // Check all entries but last one for match, return if found + for (size_t i = 0; i < sizeof(mimeTable) / sizeof(mimeTable[0]) - 1; i++) { + strcpy_P(buff, mimeTable[i].endsWith); + if (path.endsWith(buff)) { + strcpy_P(buff, mimeTable[i].mimeType); + return String(buff); + } + } + // Fall-through and just return default type + strcpy_P(buff, mimeTable[sizeof(mimeTable) / sizeof(mimeTable[0]) - 1].mimeType); + return String(buff); + } + + // calculate an ETag for a file in filesystem based on md5 checksum + // that can be used in the http headers - include quotes. + static String calcETag(FS &fs, const String &path) { + String result; + + // calculate eTag using md5 checksum + uint8_t md5_buf[16]; + File f = fs.open(path, "r"); + MD5Builder calcMD5; + calcMD5.begin(); + calcMD5.addStream(f, f.size()); + calcMD5.calculate(); + calcMD5.getBytes(md5_buf); + f.close(); + // create a minimal-length eTag using base64 byte[]->text encoding. + result = "\"" + base64::encode(md5_buf, 16) + "\""; + return (result); + } // calcETag + + StaticRequestHandler &setFilter(WebServer::FilterFunction filter) { + _filter = filter; + return *this; + } + +protected: + // _filter should return 'true' when the request should be handled + // and 'false' when the request should be ignored + WebServer::FilterFunction _filter; + FS _fs; + String _uri; + String _path; + String _cache_header; + bool _isFile; + size_t _baseUriLength; +}; + +#endif //REQUESTHANDLERSIMPL_H diff --git a/lib/WebServer/src/detail/mimetable.cpp b/lib/WebServer/src/detail/mimetable.cpp new file mode 100644 index 0000000..758f3ad --- /dev/null +++ b/lib/WebServer/src/detail/mimetable.cpp @@ -0,0 +1,33 @@ +#include "mimetable.h" +#include "pgmspace.h" + +namespace mime { + +// Table of extension->MIME strings stored in PROGMEM, needs to be global due to GCC section typing rules +const Entry mimeTable[maxType] = { + {".html", "text/html"}, + {".htm", "text/html"}, + {".css", "text/css"}, + {".txt", "text/plain"}, + {".js", "application/javascript"}, + {".json", "application/json"}, + {".png", "image/png"}, + {".gif", "image/gif"}, + {".jpg", "image/jpeg"}, + {".ico", "image/x-icon"}, + {".svg", "image/svg+xml"}, + {".ttf", "application/x-font-ttf"}, + {".otf", "application/x-font-opentype"}, + {".woff", "application/font-woff"}, + {".woff2", "application/font-woff2"}, + {".eot", "application/vnd.ms-fontobject"}, + {".sfnt", "application/font-sfnt"}, + {".xml", "text/xml"}, + {".pdf", "application/pdf"}, + {".zip", "application/zip"}, + {".gz", "application/x-gzip"}, + {".appcache", "text/cache-manifest"}, + {"", "application/octet-stream"} +}; + +} // namespace mime diff --git a/lib/WebServer/src/detail/mimetable.h b/lib/WebServer/src/detail/mimetable.h new file mode 100644 index 0000000..4732e59 --- /dev/null +++ b/lib/WebServer/src/detail/mimetable.h @@ -0,0 +1,41 @@ +#ifndef __MIMETABLE_H__ +#define __MIMETABLE_H__ + +namespace mime { + +enum type { + html, + htm, + css, + txt, + js, + json, + png, + gif, + jpg, + ico, + svg, + ttf, + otf, + woff, + woff2, + eot, + sfnt, + xml, + pdf, + zip, + gz, + appcache, + none, + maxType +}; + +struct Entry { + const char endsWith[16]; + const char mimeType[32]; +}; + +extern const Entry mimeTable[maxType]; +} // namespace mime + +#endif diff --git a/lib/WebServer/src/uri/UriBraces.h b/lib/WebServer/src/uri/UriBraces.h new file mode 100644 index 0000000..aa8fe60 --- /dev/null +++ b/lib/WebServer/src/uri/UriBraces.h @@ -0,0 +1,68 @@ +#ifndef URI_BRACES_H +#define URI_BRACES_H + +#include "Uri.h" + +class UriBraces : public Uri { + +public: + explicit UriBraces(const char *uri) : Uri(uri){}; + explicit UriBraces(const String &uri) : Uri(uri){}; + + Uri *clone() const override final { + return new UriBraces(_uri); + }; + + void initPathArgs(std::vector &pathArgs) override final { + int numParams = 0, start = 0; + do { + start = _uri.indexOf("{}", start); + if (start > 0) { + numParams++; + start += 2; + } + } while (start > 0); + pathArgs.resize(numParams); + } + + bool canHandle(const String &requestUri, std::vector &pathArgs) override final { + if (Uri::canHandle(requestUri, pathArgs)) { + return true; + } + + size_t uriLength = _uri.length(); + unsigned int pathArgIndex = 0; + unsigned int requestUriIndex = 0; + for (unsigned int i = 0; i < uriLength; i++, requestUriIndex++) { + char uriChar = _uri[i]; + char requestUriChar = requestUri[requestUriIndex]; + + if (uriChar == requestUriChar) { + continue; + } + if (uriChar != '{') { + return false; + } + + i += 2; // index of char after '}' + if (i >= uriLength) { + // there is no char after '}' + pathArgs[pathArgIndex] = requestUri.substring(requestUriIndex); + return pathArgs[pathArgIndex].indexOf("/") == -1; // path argument may not contain a '/' + } else { + char charEnd = _uri[i]; + int uriIndex = requestUri.indexOf(charEnd, requestUriIndex); + if (uriIndex < 0) { + return false; + } + pathArgs[pathArgIndex] = requestUri.substring(requestUriIndex, uriIndex); + requestUriIndex = (unsigned int)uriIndex; + } + pathArgIndex++; + } + + return requestUriIndex >= requestUri.length(); + } +}; + +#endif diff --git a/lib/WebServer/src/uri/UriGlob.h b/lib/WebServer/src/uri/UriGlob.h new file mode 100644 index 0000000..5e69954 --- /dev/null +++ b/lib/WebServer/src/uri/UriGlob.h @@ -0,0 +1,22 @@ +#ifndef URI_GLOB_H +#define URI_GLOB_H + +#include "Uri.h" +#include + +class UriGlob : public Uri { + +public: + explicit UriGlob(const char *uri) : Uri(uri){}; + explicit UriGlob(const String &uri) : Uri(uri){}; + + Uri *clone() const override final { + return new UriGlob(_uri); + }; + + bool canHandle(const String &requestUri, __attribute__((unused)) std::vector &pathArgs) override final { + return fnmatch(_uri.c_str(), requestUri.c_str(), 0) == 0; + } +}; + +#endif diff --git a/lib/WebServer/src/uri/UriRegex.h b/lib/WebServer/src/uri/UriRegex.h new file mode 100644 index 0000000..6412e7e --- /dev/null +++ b/lib/WebServer/src/uri/UriRegex.h @@ -0,0 +1,45 @@ +#ifndef URI_REGEX_H +#define URI_REGEX_H + +#include "Uri.h" +#include + +class UriRegex : public Uri { + +public: + explicit UriRegex(const char *uri) : Uri(uri){}; + explicit UriRegex(const String &uri) : Uri(uri){}; + + Uri *clone() const override final { + return new UriRegex(_uri); + }; + + void initPathArgs(std::vector &pathArgs) override final { + std::regex rgx((_uri + "|").c_str()); + std::smatch matches; + std::string s{""}; + std::regex_search(s, matches, rgx); + pathArgs.resize(matches.size() - 1); + } + + bool canHandle(const String &requestUri, std::vector &pathArgs) override final { + if (Uri::canHandle(requestUri, pathArgs)) { + return true; + } + + unsigned int pathArgIndex = 0; + std::regex rgx(_uri.c_str()); + std::smatch matches; + std::string s(requestUri.c_str()); + if (std::regex_search(s, matches, rgx)) { + for (size_t i = 1; i < matches.size(); ++i) { // skip first + pathArgs[pathArgIndex] = String(matches[i].str().c_str()); + pathArgIndex++; + } + return true; + } + return false; + } +}; + +#endif diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..ffc0ee4 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,19 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:esp32solo1] +platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.07.11/platform-espressif32.zip +framework = arduino +board = esp32-solo1 +build_flags = -DFRAMEWORK_ARDUINO_SOLO1 +lib_deps = + xoseperez/HLW8012@^1.1.2 + plerup/EspSoftwareSerial@^8.2.0 + knolleary/PubSubClient@^2.8 diff --git a/src/http_server.cpp b/src/http_server.cpp new file mode 100644 index 0000000..dfa7a67 --- /dev/null +++ b/src/http_server.cpp @@ -0,0 +1,765 @@ +#include "Http_Server.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "esp_wifi.h" + +#include "user.h" +#include "user_mqtt.h" + +//Http_Server httpserver; +DNSServer dnsServer; + +MQTT mqtt; + +WebServer httpserver_(80); + +bool linkwifi = false; +unsigned long WFilinkMillis; + +#define devdev "N2-IR" +String host; +String htmltou = ""; +String htmltou2 = ""; +String htmltou3 = ""; +String htmltou4 = "
"; +String htmltou5 = "
"; + +void h_index(){ + //httpserver_.sendHeader("Connection", "close"); + String html; + html = htmltou + devdev + htmltou2; + html += htmltou3 + htmltou4; + html += ""; + html += devdev; + html += ""; + html += "

"; + + html += "
"; + html += "
"; + html += htmltou5; + httpserver_.send(200, "text/html;charset=UTF-8", html); //返回保存成功页面 +} +void h_refresh(){ + //httpserver_.sendHeader("Connection", "close"); + //WiFi.setAutoReconnect(false); + httpserver_.send(200, "text/html", ""); +} +void h_config(){ + String message = "{"; + String mac = WiFi.macAddress(); + message += "\"mac\":\""; + message += mac; + message += "\",\"name\":\""; + message += host; + message += "\",\"aps\":[{}"; + int n = WiFi.scanNetworks(); + Serial.println("scan done"); + if (n == 0) { + Serial.println("no networks found"); + }else{ + Serial.print(n); + Serial.println("\tnetworks found"); + for (int i = 0; i < n; ++i) { + Serial.print(i + 1); + Serial.print(":\t"); + Serial.print(WiFi.SSID(i)); + Serial.print("\t("); + Serial.print(WiFi.RSSI(i)); + Serial.print(")\t"); + Serial.println((WiFi.encryptionType(i) == WIFI_AUTH_OPEN) ? " " : "*"); + if(WiFi.SSID(i) != ""){ + message += ", {\"ssid\":\""; + message += WiFi.SSID(i); + message += "\",\"rssi\":"; + message += WiFi.RSSI(i); + message += ",\"lock\":"; + if(WiFi.encryptionType(i) != WIFI_AUTH_OPEN) {message += "1";}else{message += "0";} + message += "}"; + } + } + } + message += "]}"; + httpserver_.send(200, "application/json", message); +} +void h_wifi__get(){ + String html; + //WiFi.setAutoReconnect(false); + //httpserver_.sendHeader("Connection", "close"); + // html = htmltou + "Set WiFi" + htmltou2; + // html += ""; + // html += htmltou3 + htmltou4; + // int n = WiFi.scanNetworks(); + // //WiFi.setAutoReconnect(true); + // Serial.println("scan done"); + // if (n == 0) { + // Serial.println("no networks found"); + // }else{ + // Serial.print(n); + // Serial.println("\tnetworks found"); + // for (int i = 0; i < n; ++i) { + // // Print SSID and RSSI for each network found + // Serial.print(i + 1); + // Serial.print(":\t"); + // Serial.print(WiFi.SSID(i)); + // Serial.print("\t("); + // Serial.print(WiFi.RSSI(i)); + // Serial.print(")\t"); + // Serial.println((WiFi.encryptionType(i) == WIFI_AUTH_OPEN) ? " " : "*"); + // if(WiFi.SSID(i) != ""){ + // html += "
"; + // html += WiFi.SSID(i); + // html += "
-60){ + // html += " q-4"; + // }else if(WiFi.RSSI(i) > -80){ + // html += " q-3"; + // }else if(WiFi.RSSI(i) > -85){ + // html += " q-2"; + // }else{ + // html += " q-1"; + // } + // html += " "; + // html += "\">
"; + // html += WiFi.RSSI(i); + // html += "%
"; + // html += ""; + // } + // } + // } + // WiFi.scanDelete(); + // html += "


"; + // //html += "
Save
"; + // html += "
"; + + // html += htmltou5; + html = "\ +\ + \ + \ + \ + \ + \ + \ + \ +
\ +

MAC Address

\ +

WiFi Networks

\ + \ + \ +

WiFi Settings

\ +
\ + \ +
\ + \ +
\ +
\ + \ +
\ +
\ +
\ + \ +\ +"; + httpserver_.send(200, "text/html;charset=UTF-8", html); + //WiFi.reconnect(); + return; +} +void h_wifi__post(){ + String wifi_ssid; + String wifi_pass; + + // WiFi.setAutoReconnect(false); + + if(httpserver_.hasArg("s")){ + Serial.print("got ssid:"); + wifi_ssid = httpserver_.arg("s"); + Serial.println(wifi_ssid); + }else{ + Serial.println("error, not found ssid"); + httpserver_.send(200, "text/html", "error, not found ssid");//返回错误页面 + return; + } + if (httpserver_.hasArg("p")) { + Serial.print("got password:"); + wifi_pass = httpserver_.arg("p"); //获取html表单输入框name名为"pwd"的内容 + //strcpy(sta_pass, httpserver_.arg("pass").c_str()); + Serial.println(wifi_pass); + } else { + Serial.println("error, not found password"); + httpserver_.send(200, "text/html", "error, not found password"); + return; + } + if(wifi_ssid != ""){ + //settingMode = 0; + //menuPage = 0; + //setMenuPage = 0; + Serial.println("WiFi Connect SSID:" + wifi_ssid + " PASS:" + wifi_pass); + //wifi_set_tmp = 252; + } + if(wifi_ssid != ""){ + String html; + html = htmltou + "Set WiFi" + htmltou2; + html += htmltou3 + htmltou4; + html += "SSID:" + wifi_ssid + "
password:" + wifi_pass + "
已取得WiFi信息,正在尝试连接,请手动关闭此页面。"; + html += "


"; + html += htmltou5; + httpserver_.send(200, "text/html;charset=UTF-8", html); //返回保存成功页面 + WiFi.begin(wifi_ssid,wifi_pass); + Serial2.write("\x4D\x00\x01\x03\x0D\x00\x00\x5E\x5A",9); + }else{ + httpserver_.send(200, "text/html", "啥也没有,你个吊毛干嘛呢"); //返回保存成功页面 + } + if(user_ > 199){ + WiFi.mode(WIFI_AP_STA); + } +} +void handleNotFound(){ + //digitalWrite(led, 1); + String message = "File Not Found\r\n\r\n"; + message += "URI: "; + message += httpserver_.uri(); + message += "\r\nMethod: "; + message += (httpserver_.method() == HTTP_GET) ? "GET" : "POST"; + message += "\r\nArguments: "; + message += httpserver_.args(); + message += "\r\n"; + + for (uint8_t i = 0; i < httpserver_.args(); i++) { + message += " " + httpserver_.argName(i) + ": " + httpserver_.arg(i) + "\r\n"; + } + + //Serial.print(message); + + httpserver_.send(200, "text/plain", message); + //digitalWrite(led, 0); +} +void h_update__get(){ + String html; + httpserver_.sendHeader("Connection", "close"); + html = htmltou + devdev + htmltou2; + html += htmltou3 + htmltou4; + html += "OTA"; + html += "
"; + html += htmltou5; + httpserver_.send(200, "text/html;charset=UTF-8", html); +} +void h_update__post(){ + String html; + // Serial.printf("hostHeader()"); + // Serial.printf(httpserver_.hasArg("curl").c_str()); + httpserver_.sendHeader("Connection", "close"); + if(httpserver_.hasArg("curl")){ + // Serial.printf("hostHeader()"); + if(Update.hasError()){ + html += "FAIL"; + }else{ + html += "OK"; + } + }else{ + html = htmltou + "OTA" + htmltou2; + html += htmltou3 + htmltou4; + if(Update.hasError()){ + html += "FAIL"; + }else{ + html += "OK"; + } + + html += "
"; + html += htmltou5; + } + httpserver_.send(200, "text/html;charset=UTF-8", html); +} +size_t upload_len = 0; +void uplood(HTTPUpload upload){ + upload_len += upload.currentSize; + Serial.printf("%u\r",(upload.totalSize / (upload.len / 100))); + // Serial.printf("%u\r\n",(upload.totalSize)); + // Serial.printf("%u\r\n",(upload_len )); +} +void upend(HTTPUpload upload){ + Serial.printf("Update Success: %u\r\n", upload.totalSize); + reboot_ = 1; +} +void upstart(HTTPUpload upload){ + upload_len = 0; + Serial.printf(" Update: %s\r\n", upload.filename.c_str()); +} +void h_update__post_(){ + HTTPUpload& upload = httpserver_.upload(); + if (upload.status == UPLOAD_FILE_START) { + upstart(upload); + if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { //start with max available size + Update.printError(Serial); + } + }else if (upload.status == UPLOAD_FILE_WRITE){ + if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { + Update.printError(Serial); + } + uplood(upload); + }else if (upload.status == UPLOAD_FILE_END){ + if (Update.end(true)) { //true to set the size to the current progress + upend(upload); + } else { + Update.printError(Serial); + } + } +} +void h_reboot(){ + +} +void h_reboot_set(){ + ESP.restart(); +} +void h_fsca(){ + String html; + html = htmltou + devdev + htmltou2; + html += htmltou3 + htmltou4; + html += "reinit"; + html += htmltou5; + httpserver_.send(200, "text/html;charset=UTF-8", html); //返回保存成功页面 + + LittleFS.end(); + LittleFS.format(); + esp_wifi_restore(); + reboot_ = 1; +} +void h_8012(){ + String html; + html = htmltou + devdev + htmltou2; + html += htmltou3 + htmltou4; + html += "HLW8012 \ +
正在加载
\ +
\ + \ + \ + \ +
\ +
\ + \ + \ + \ +
"; + html += htmltou5; + html += ""; + httpserver_.send(200, "text/html;charset=UTF-8", html); +} +void h_8012_get(){ + String message = "{"; + + message += "\"V\":\"" + String(hlw8012.getVoltage()) + "\","; + message += "\"C\":\"" + String(hlw8012.getCurrent()) + "\","; + message += "\"P\":\"" + String(hlw8012.getActivePower()) + "\","; + message += "\"VA\":\"" + String(hlw8012.getApparentPower()) + "\","; + message += "\"F\":\"" + String((int) (100 * hlw8012.getPowerFactor())) + "\""; + message += "}"; + httpserver_.send(200, "application/json", message); + Serial.print("[HLW] Active Power (W) : "); Serial.println(hlw8012.getActivePower()); + Serial.print("[HLW] Voltage (V) : "); Serial.println(hlw8012.getVoltage()); + Serial.print("[HLW] Current (A) : "); Serial.println(hlw8012.getCurrent()); + Serial.print("[HLW] Apparent Power (VA) : "); Serial.println(hlw8012.getApparentPower()); + Serial.print("[HLW] Power Factor (%) : "); Serial.println((int) (100 * hlw8012.getPowerFactor())); + +} +void h_8012_setv(){ + String v; + String html; + html = htmltou + "Setup" + htmltou2; + html += htmltou3 + htmltou4; + + + if(httpserver_.hasArg("V")){ + v = httpserver_.arg("V"); + unsigned int vv = v.toInt(); + double vvv = vv * 0.1; + Serial.print("[SET] Voltage (V) : "); Serial.println(vvv); + hlw8012.expectedVoltage(vvv); + + vvv = hlw8012.getVoltageMultiplier(); + if(LittleFS.exists("/8012v")){ + LittleFS.remove("/8012v"); + } + File file = LittleFS.open("/8012v",FILE_WRITE); + if(file){ + if(file.print(String(vvv).c_str())){ + html += "成功"; + }else{ + html += "失败,不知道为什么"; + } + }else{ + html += "失败,请重置设备"; + } + file.close(); + + + }else{ + html += "获取参数失败
请更换浏览器后重试"; + } + httpserver_.send(200, "text/html;charset=UTF-8", html); +} +void h_8012_setc(){ + String c; + String html; + html = htmltou + "Setup" + htmltou2; + html += htmltou3 + htmltou4; + + + if(httpserver_.hasArg("C")){ + c = httpserver_.arg("C"); + unsigned int cc = c.toInt(); + double ccc = cc * 0.1; + Serial.print("[SET] Current (A) : "); Serial.println(ccc); + hlw8012.expectedCurrent(ccc); + + ccc = hlw8012.getCurrentMultiplier(); + if(LittleFS.exists("/8012c")){ + LittleFS.remove("/8012c"); + } + File file = LittleFS.open("/8012c",FILE_WRITE); + if(file){ + if(file.print(String(ccc).c_str())){ + ccc = cc * 0.1; + unsigned int ppp = (unsigned int)ccc * hlw8012.getVoltage(); + hlw8012.expectedActivePower(ppp); + ppp = hlw8012.getPowerMultiplier(); + file.close(); + if(LittleFS.exists("/8012p")){ + LittleFS.remove("/8012p"); + } + file = LittleFS.open("/8012p",FILE_WRITE); + file.print(String(ppp).c_str()); + + html += "成功"; + }else{ + html += "失败,不知道为什么"; + } + }else{ + html += "失败,请重置设备"; + } + file.close(); + + + }else{ + html += "获取参数失败
请更换浏览器后重试"; + } + httpserver_.send(200, "text/html;charset=UTF-8", html); +} + +void Http_Server::setup(){ + Serial.println("Booting"); + WiFi.mode(WIFI_STA); + delay(100); + + String mac = WiFi.macAddress(); + host = (String)devdev + (String)"_" + mac.substring(9,11) + mac.substring(12,14) + mac.substring(15,17); + WiFi.setHostname(host.c_str()); + WiFi.softAP(host.c_str()); + Serial.print("> Host:\t"); + Serial.println(host); + Serial.print("> SETAP:\t"); + Serial.println(host.c_str()); + IPAddress apIP(192, 168, 4, 1); // The default android DNS + WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0)); //设置AP热点IP和子网掩码 + dnsServer.start(53, "*", apIP); + + Serial.print("> Set Web Server\r\n"); + httpserver_.onNotFound(handleNotFound); + httpserver_.on("/",h_index); + httpserver_.on("/generate_204",h_refresh); + httpserver_.on("/reboot",h_reboot); + httpserver_.on("/reboot/set",h_reboot); + httpserver_.on("/wifi",HTTP_GET,h_wifi__get); + httpserver_.on("/wifi",HTTP_POST,h_wifi__post); + httpserver_.on("/config.json",h_config); + httpserver_.on("/update", HTTP_GET,h_update__get); + httpserver_.on("/update", HTTP_POST,h_update__post,h_update__post_); + httpserver_.on("/fsca",h_fsca); + httpserver_.on("/8012",h_8012); + httpserver_.on("/8012/get",h_8012_get); + httpserver_.on("/8012/setv",h_8012_setv); + httpserver_.on("/8012/setc",h_8012_setc); + + httpserver_.begin(); + WFilinkMillis = millis(); + + mqtt.setup(); + + if(user_ < 200){ + WiFi.mode(WIFI_OFF); + WiFi.mode(WIFI_STA); + WiFi.begin(); + if (WiFi.waitForConnectResult() != WL_CONNECTED) { + // Serial.println("Connection Failed! Rebooting..."); + // delay(5000); + // ESP.restart(); + WiFi.mode(WIFI_AP); + Serial2.write("\x4D\x00\x01\x03\x0c\x00\x00\x5d\x5A",9); + delay(1800); + digitalWrite(25, HIGH); + delay(1000); + digitalWrite(25, LOW); + + }else{ + Serial.print("> LINK:\t"); + Serial.println(WiFi.SSID().c_str()); + Serial.print("> IP:\t"); + Serial.println(WiFi.localIP()); + linkwifi = true; + digitalWrite(19, LOW); + Serial2.write("\x4D\x00\x01\x03\x0E\x00\x00\x5F\x5A",9); + } + }else{ + Serial.println("> No Link Mode"); + + } +} + +void Http_Server::loop(){ + if(user_ < 200){ + if(millis() > WFilinkMillis){ + WFilinkMillis = millis() + 1000; + if (WiFi.waitForConnectResult() != WL_CONNECTED){ + digitalWrite(19, !digitalRead(19)); + if(linkwifi){ + linkwifi = false; + WiFi.mode(WIFI_AP_STA); + Serial.println("> AP: ON"); + // httpserver_.stop(); + // httpserver_.begin(); + + //WiFi.begin(); + + } + }else{ + if(!linkwifi){ + linkwifi = true; + WiFi.mode(WIFI_STA); + Serial.println("> AP: OFF"); + Serial.println("IP address: "); + Serial.println(WiFi.localIP()); + // httpserver_.stop(); + // httpserver_.begin(); + // WiFi.begin(); + digitalWrite(19, LOW); + Serial2.write("\x4D\x00\x01\x03\x0E\x00\x00\x5F\x5A",9); + } + } + } + mqtt.loop(); + }else{ + if(millis() > WFilinkMillis){ + WFilinkMillis = millis() + 1000; + if(user_ == 200){ + user_++; + digitalWrite(5, HIGH); + digitalWrite(18, LOW); + digitalWrite(19, LOW); + }else if(user_ == 201){ + user_++; + digitalWrite(5, LOW); + digitalWrite(18, HIGH); + digitalWrite(19, LOW); + }else if(user_ == 202){ + user_++; + digitalWrite(5, LOW); + digitalWrite(18, LOW); + digitalWrite(19, HIGH); + }else if(user_ == 203){ + user_++; + digitalWrite(5, LOW); + digitalWrite(18, HIGH); + digitalWrite(19, HIGH); + }else if(user_ == 204){ + user_++; + digitalWrite(5, HIGH); + digitalWrite(18, LOW); + digitalWrite(19, HIGH); + }else if(user_ == 205){ + user_++; + digitalWrite(5, HIGH); + digitalWrite(18, HIGH); + digitalWrite(19, LOW); + }else if(user_ == 206){ + user_ = 200; + digitalWrite(5, HIGH); + digitalWrite(18, HIGH); + digitalWrite(19, HIGH); + } + } + } + + httpserver_.handleClient(); + dnsServer.processNextRequest(); + +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..92fe86a --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,692 @@ +#include +#include +#include + +#include "SoftwareSerial.h" + +#include "Http_Server.h" +#include "user_mqtt.h" +#include "user.h" + +//MQTT mqtt; +Http_Server httpserver; +EspSoftwareSerial::UART mySerial; + +char reboot_ = 0; +/*重启标识符 +*/ +char user_ = 0; +/* +*/ + +uint8_t if_mode = 0x11; +/* +0x1X 关 +1 自动 +2 制冷 +3 除湿 +4 送风 +5 制热 +*/ +int fan_mode = 0; +/* +0 自 +1 低 +2 中 +3 高 +*/ +int swing_mode = 1; +/* +0 off +1 左右 +2 上下 +3 on +*/ +int temp_mode = 24 - 16; +/* ++16 +*/ + +// GPIOs +#define SEL_PIN 23 +#define CF1_PIN 13 +#define CF_PIN 15 +HLW8012 hlw8012; +// These are the nominal values for the resistors in the circuit +#define CURRENT_RESISTOR 0.001 +#define VOLTAGE_RESISTOR_UPSTREAM ( 5 * 470000 ) // Real: 2280k +#define VOLTAGE_RESISTOR_DOWNSTREAM ( 1000 ) // Real 1.009k + + +bool ifchar(unsigned char* ifdata,unsigned char* dataif,char iflen){ + // Serial.println("\n\rifchar"); + // Serial.println("len "); + // Serial.println("cat "); + // Serial.println("ifd "); + for(int i = 0;iflen > i;i++){ + // Serial.print("\x1B\x5B\x41"); + // Serial.print("\x1B\x5B\x41"); + // Serial.print(i,DEC); + // if(i > 9) {Serial.print(" ");}else{Serial.print(" ");} + // Serial.print("\x1B\x5B\x44");Serial.print("\x1B\x5B\x44");Serial.print("\x1B\x5B\x44"); + // Serial.print("\x1B\x5B\x42"); + // Serial.print(ifdata[i],HEX); + // if(ifdata[i] > 0x0F){Serial.print(" ");}else{Serial.print(" ");} + // Serial.print("\x1B\x5B\x44");Serial.print("\x1B\x5B\x44");Serial.print("\x1B\x5B\x44"); + // Serial.print("\x1B\x5B\x42"); + // Serial.print(dataif[i],HEX); + // if(dataif[i] > 0x0F){Serial.print(" ");}else{Serial.print(" ");} + if(ifdata[i] != dataif[i]){ + // Serial.println("false\r\n"); + return false; + } + } + // Serial.println("true\r\n"); + return true; +} + +void hlw8012_cf1_interrupt(){ + hlw8012.cf1_interrupt(); +} +void hlw8012_cf_interrupt(){ + hlw8012.cf_interrupt(); +} +void hlw8012_init(){ + // Initialize HLW8012 + // void begin(unsigned char cf_pin, unsigned char cf1_pin, unsigned char sel_pin, unsigned char currentWhen = HIGH, bool use_interrupts = false, unsigned long pulse_timeout = PULSE_TIMEOUT); + // * cf_pin, cf1_pin and sel_pin are GPIOs to the HLW8012 IC + // * currentWhen is the value in sel_pin to select current sampling + // * set use_interrupts to false, we will have to call handle() in the main loop to do the sampling + // * set pulse_timeout to 500ms for a fast response but losing precision (that's ~24W precision :( ) + hlw8012.begin(CF_PIN, CF1_PIN, SEL_PIN, HIGH, true); + // These values are used to calculate current, voltage and power factors as per datasheet formula + // These are the nominal values for the Sonoff POW resistors: + // * The CURRENT_RESISTOR is the 1milliOhm copper-manganese resistor in series with the main line + // * The VOLTAGE_RESISTOR_UPSTREAM are the 5 470kOhm resistors in the voltage divider that feeds the V2P pin in the HLW8012 + // * The VOLTAGE_RESISTOR_DOWNSTREAM is the 1kOhm resistor in the voltage divider that feeds the V2P pin in the HLW8012 + hlw8012.setResistors(CURRENT_RESISTOR, VOLTAGE_RESISTOR_UPSTREAM, VOLTAGE_RESISTOR_DOWNSTREAM); + + // Show default (as per datasheet) multipliers + Serial.print("[HLW] Default current multiplier : "); Serial.println(hlw8012.getCurrentMultiplier()); + Serial.print("[HLW] Default voltage multiplier : "); Serial.println(hlw8012.getVoltageMultiplier()); + Serial.print("[HLW] Default power multiplier : "); Serial.println(hlw8012.getPowerMultiplier()); + Serial.println(); + + attachInterrupt(CF1_PIN,hlw8012_cf1_interrupt,CHANGE); + attachInterrupt(CF_PIN ,hlw8012_cf_interrupt ,CHANGE); + + //打印当前功率 + //hlw8012.getActivePower(); + // Serial.print("[TEM] Active Power (W) : "); Serial.println(hlw8012.getActivePower()); + // hlw8012.setMode(MODE_CURRENT); + // delay(2000); + // //hlw8012.getCurrent(); + // Serial.print("[TEM] Current (A) : "); Serial.println(hlw8012.getCurrent()); + + // hlw8012.setMode(MODE_VOLTAGE); + // delay(2000); + // //hlw8012.getVoltage(); + // Serial.print("[TEM] Voltage (V) : "); Serial.println(hlw8012.getVoltage()); + delay(2000); + if(LittleFS.exists("/8012c")){ + File file = LittleFS.open("/8012c"); + if(!file || file.isDirectory()){ + Serial.println("ERROR NO 8012 C DATA"); + }else{ + String data; + data = ""; + while (file.available()) + { + data += file.readString(); + } + file.close(); + Serial.print("8012 C DATA "); + Serial.println(data); + hlw8012.setCurrentMultiplier(data.toDouble()); + } + } + if(LittleFS.exists("/8012v")){ + File file = LittleFS.open("/8012v"); + if(!file || file.isDirectory()){ + Serial.println("ERROR NO 8012 V DATA"); + }else{ + String data; + data = ""; + while (file.available()) + { + data += file.readString(); + } + file.close(); + Serial.print("8012 V DATA "); + Serial.println(data); + hlw8012.setVoltageMultiplier(data.toDouble()); + } + } + if(LittleFS.exists("/8012p")){ + File file = LittleFS.open("/8012p"); + if(!file || file.isDirectory()){ + Serial.println("ERROR NO 8012 P DATA"); + }else{ + String data; + data = ""; + while (file.available()) + { + data += file.readString(); + } + file.close(); + Serial.print("8012 P DATA "); + Serial.println(data); + hlw8012.setPowerMultiplier(data.toDouble()); + } + } + delay(2000); + //calibrate(); + /* + hlw8012.expectedActivePower(60.0); + hlw8012.expectedVoltage(218.0); + hlw8012.expectedCurrent(60.0 / 230.0); + + // Show corrected factors + Serial.print("[HLW] New current multiplier : "); Serial.println(hlw8012.getCurrentMultiplier()); + Serial.print("[HLW] New voltage multiplier : "); Serial.println(hlw8012.getVoltageMultiplier()); + Serial.print("[HLW] New power multiplier : "); Serial.println(hlw8012.getPowerMultiplier()); + Serial.println(); + + + */ + + Serial.print("[HLW] Active Power (W) : "); Serial.println(hlw8012.getActivePower()); + Serial.print("[HLW] Voltage (V) : "); Serial.println(hlw8012.getVoltage()); + Serial.print("[HLW] Current (A) : "); Serial.println(hlw8012.getCurrent()); + Serial.print("[HLW] Apparent Power (VA) : "); Serial.println(hlw8012.getApparentPower()); + Serial.print("[HLW] Power Factor (%) : "); Serial.println((int) (100 * hlw8012.getPowerFactor())); + +} + +void setup(){ + Serial.begin(115200); //设置串口波特率 + Serial.println(""); //启动消息 + + + + Serial2.begin(115200,SERIAL_8N1,16,17); + pinMode(26,INPUT); + pinMode(27,INPUT); + //digitalWrite(26, LOW); + //delay(10); + //digitalWrite(26, HIGH); + int i = 6000; + while (i>0) + { + delay(1); + i--; + if(!digitalRead(26)){ + Serial.println("> IR Init OK"); + break; + } + } + + Serial1.begin(19200); //(27,26) + Serial1.write("\x00",1); + delay(50); + Serial1.write("\x16\x08\x00\x1E\x08",5); + //mySerial.begin(19200,EspSoftwareSerial::SWSERIAL_8N1, 26, 27, false, 95, 11); + + pinMode(4,INPUT); + pinMode(5,OUTPUT); + pinMode(18,OUTPUT); + pinMode(19,OUTPUT); + + pinMode(23,OUTPUT); + pinMode(13,INPUT); + pinMode(15,INPUT); + + pinMode(25,OUTPUT); + //pinMode(14,OUTPUT); + if(!digitalRead(4)){ + user_ = 200; + } + + digitalWrite(5, LOW); + digitalWrite(18, LOW); + digitalWrite(19, HIGH); + + //digitalWrite(14, HIGH); + digitalWrite(25, HIGH); + delay(100); + digitalWrite(25, LOW); + delay(2000); + Serial2.write("\x4D\x00\x01\x03\x0D\x00\x00\x5E\x5A",9); + + // Serial2.write("\x4D\x00\x01\x03\x0E\x00\x00\x5F\x5A",9); + // Serial2.write((u8_t)0x4d); + // Serial2.write((u8_t)0x00); + // Serial2.write(0x01); + // Serial2.write(0x03); + // Serial2.write(0x0C); + // Serial2.write(0x00); + // Serial2.write(0x00); + // Serial2.write(0x5D); + // Serial2.write(0x5A); + // Serial.print(Serial2.readString()); + // delay(2000); + + hlw8012.toggleMode(); + delay(1000); + hlw8012.toggleMode(); + + + Serial.println("> FS Init..."); + if(!LittleFS.begin(false)){ + Serial.println("FS Mount Failed"); + delay(10000); + // blinker_key = ""; + }else{ + + } + + hlw8012_init(); + + httpserver.setup(); +} + +// unsigned char u0data[5]; +// char u0len = 0; +unsigned char u1data[220]; +char u1len = 0; +unsigned char u2data[20]; +char u2len = 0; +unsigned long U1dataMillis; + + +void loop(){ + httpserver.loop(); + if(Serial.available()){ + unsigned char get = Serial.read(); + Serial.print(get,HEX); + Serial.print(" "); + if(get == 0x30){ + Serial1.write("\x00",1); + delay(50); + //mySerial.write("\x16\x82\x00\x98\x08\x16\x8A\x0E\x01\x0B\x00\x00\x00\x00\x00\x00\x00\x00\x21\x00\x00\x00\xDB\x08",24); + Serial1.write("\x16\x02\x10\xFF\x00\x01\x0B\x00\x00\x00\x00\x00\x00\x00\x00\x21\x00\x00\x00\x54\x08",21); + }else if(get == 0x31){ + //xTaskCreate(TaskmySerial,"Task mySerial",2048,NULL,2,NULL); + }else if(get == 0x39){ + Serial1.write("\x00",1); + delay(50); + Serial1.write("\x16\x0C\x00\x22\x08",5); + Serial.println("U1 RELINK"); + }else{ + //mySerial.write("\x16\x88\x02\x55\x01\xF6\x08",7); + Serial1.write("\x16\x08\x00\x1E\x08",5); + //mySerial.available(); + } + } + if(Serial1.available()){ + unsigned char get = Serial1.read(); + u1data[u1len] = get; + u1len++; + if(u1len > 220){u1len = 0;Serial.println("> u1data len error");} + U1dataMillis = millis(); + Serial.println("U1 ++++++++++"); + // Serial.println("> u1data "); + // Serial.print(u2len,DEC); + // Serial.print(" "); + // Serial.print(get,HEX); + // Serial.print(" "); + } + if(u1len != 0){ + if(U1dataMillis + 100 < millis()){ + if(u1data[u1len -1] == 0x08){ + Serial.println("U1 U1U1U1U1U1U1"); + if(ifchar(u1data,(uint8_t*)"\x16\x82\x00\x98\x08\x16\x8A\x0E",8)){ + Serial.println("U1 OK DATA:"); + Serial.println("if_mode_ir"); + Serial.println(if_mode,HEX); + if(u1data[0x12] == 0x20){ + if_mode = 0x10 + u1data[0x08] + 1; + }else{ + if_mode = u1data[0x08] + 1; + } + Serial.println("if_mode_ir"); + Serial.println(if_mode,HEX); + fan_mode = u1data[10]; + if((u1data[12] == 0)&&(u1data[13] == 0)){ + swing_mode = 0; + }else if((u1data[12] == 1)&&(u1data[13] == 0)){ + swing_mode = 1; + }else if((u1data[12] == 0)&&(u1data[13] == 1)){ + swing_mode = 2; + }else if((u1data[12] == 1)&&(u1data[13] == 1)){ + swing_mode = 3; + } + temp_mode = u1data[9]; + + mqtt.pust(); + digitalWrite(5, LOW); + digitalWrite(19, LOW); + delay(500); + digitalWrite(18, LOW); + }else{ + if(ifchar(u1data,(uint8_t*)"\x16\x8C\xD0",3)){ + delay(1000); + Serial1.write("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",10); + } + digitalWrite(5, HIGH); + digitalWrite(18, HIGH); + digitalWrite(19, HIGH); + + digitalWrite(5, LOW); + delay(500); + digitalWrite(18, LOW); + digitalWrite(19, LOW); + Serial.println("U1 ERROR DATA:"); + // char i = 0; + // Serial.println("00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F "); + // for(;u1len > i;){ + // for(char x = 0;x < 17;x++){ + // //Serial.print(i,DEC); + // //Serial.print("\t"); + // //Serial.println(u2data[u2len],HEX); + // if(u1len > i){ + // if(u1data[i] < 0x10){ + // Serial.print("0"); + // } + // Serial.print(u1data[i],HEX); + // Serial.print(" "); + // i++; + // } + // } + // Serial.print("\r\n"); + // } + } + char i = 0; + Serial.println("00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F "); + for(;u1len > i;){ + for(char x = 0;x < 16;x++){ + //Serial.print(i,DEC); + //Serial.print("\t"); + //Serial.println(u2data[u2len],HEX); + if(u1len > i){ + if(u1data[i] < 0x10){ + Serial.print("0"); + } + Serial.print(u1data[i],HEX); + Serial.print(" "); + i++; + } + } + Serial.print("\r\n"); + } + u1len = 0; + } + } + } + if(Serial2.available()){ + unsigned char get = Serial2.read(); + // Serial.print(u2len,DEC); + // Serial.print(" "); + // Serial.print(get,HEX); + // Serial.print(" "); + u2data[u2len] = get; + u2len++; + if(u2len > 20){u2len = 0;Serial.println("> u2data len error");} + if(get == 0x5a){ + Serial.println("if_mode"); + Serial.println(if_mode,HEX); + if(ifchar(u2data,(uint8_t*)"\x4C\x00\x00\x00\x4C\x5A",6)){ + digitalWrite(5, HIGH); + digitalWrite(18, HIGH); + digitalWrite(19, HIGH); + }else if(ifchar(u2data,(uint8_t*)"\x4C\x00\x16\x02\x00\x00\x64\x5A",7)){ + digitalWrite(5, LOW); + digitalWrite(18, LOW); + delay(500); + digitalWrite(19, LOW); + }else if(ifchar(u2data,(uint8_t*)"\x4C\x00\x01\x00\x4D\x5A",6)){//打开空调 + digitalWrite(18, LOW); + digitalWrite(19, LOW); + delay(500); + digitalWrite(5, LOW); + //if_mode = 0x13; + if(if_mode >> 4 == 1){ + if_mode = if_mode - 0x10; + } + Serial.println("if_mode"); + Serial.println(if_mode,HEX); + mqtt.irpost_(); + Serial.println("if_mode"); + Serial.println(if_mode,HEX); + //mySerial.write("\x16\x82\x00\x98\x08\x16\x8A\x0E\x01\x0B\x00\x00\x00\x00\x00\x00\x00\x00\x21\x00\x00\x00\xDB\x08",24); + //mySerial.write("\x16\x02\x10\xFF\x00\x01\x0B\x00\x00\x00\x00\x00\x00\x00\x00\x21\x00\x00\x00\x54\x08",21); + }else if(ifchar(u2data,(uint8_t*)"\x4C\x00\x02\x00\x4E\x5A",6)){//关闭空调 + digitalWrite(18, LOW); + digitalWrite(19, LOW); + delay(500); + digitalWrite(5, LOW); + Serial.println("if_mode_sat"); + Serial.println(if_mode,HEX); + if(if_mode >> 4 != 1){ + if_mode = if_mode + 0x10; + } + Serial.println("if_mode_if"); + Serial.println(if_mode,HEX); + mqtt.irpost_(); + Serial.println("if_mode_ir"); + Serial.println(if_mode,HEX); + }else if(ifchar(u2data,(uint8_t*)"\x4C\x00\x03\x00\x4F\x5A",6)){//制热模式 + digitalWrite(18, LOW); + digitalWrite(19, LOW); + delay(500); + digitalWrite(5, LOW); + //if(if_mode >> 4 == 0){ + if_mode = 5; + //}else{ + // if_mode = 0x10 + 5; + //} + mqtt.irpost_(); + }else if(ifchar(u2data,(uint8_t*)"\x4C\x00\x04\x00\x50\x5A",6)){//制冷空调 + digitalWrite(18, LOW); + digitalWrite(19, LOW); + delay(500); + digitalWrite(5, LOW); + if(if_mode >> 4 == 0){ + if_mode = 0 + 2; + }else{ + if_mode = 0x10 + 2; + } + mqtt.irpost_(); + }else if(ifchar(u2data,(uint8_t*)"\x4C\x00\x05\x00\x51\x5A",6)){//风大 + digitalWrite(18, LOW); + digitalWrite(19, LOW); + delay(500); + digitalWrite(5, LOW); + if(fan_mode != 3){ + fan_mode++; + } + mqtt.irpost_(); + }else if(ifchar(u2data,(uint8_t*)"\x4C\x00\x06\x00\x52\x5A",6)){//风小 + digitalWrite(18, LOW); + digitalWrite(19, LOW); + delay(500); + digitalWrite(5, LOW); + if(fan_mode != 0){ + fan_mode--; + } + mqtt.irpost_(); + }else if(ifchar(u2data,(uint8_t*)"\x4C\x00\x08\x00\x54\x5A",6)){//开扫风 + digitalWrite(18, LOW); + digitalWrite(19, LOW); + delay(500); + digitalWrite(5, LOW); + swing_mode = 3; + mqtt.irpost_(); + }else if(ifchar(u2data,(uint8_t*)"\x4C\x00\x09\x00\x55\x5A",6)){//关扫风 + digitalWrite(18, LOW); + digitalWrite(19, LOW); + delay(500); + digitalWrite(5, LOW); + swing_mode = 0; + mqtt.irpost_(); + }else if(ifchar(u2data,(uint8_t*)"\x4C\x00\x12\x00\x5E\x5A",6)){//温度调高 + digitalWrite(18, LOW); + digitalWrite(19, LOW); + delay(500); + digitalWrite(5, LOW); + if(temp_mode != (32-16)){ + temp_mode++; + } + mqtt.irpost_(); + }else if(ifchar(u2data,(uint8_t*)"\x4C\x00\x13\x00\x5F\x5A",6)){//温度调低 + digitalWrite(18, LOW); + digitalWrite(19, LOW); + delay(500); + digitalWrite(5, LOW); + if(temp_mode != 0){ + temp_mode--; + } + mqtt.irpost_(); + }else if(ifchar(u2data,(uint8_t*)"\x4C\x00\x0B\x01\x1E\x76\x5A",6)){//30 + digitalWrite(18, LOW); + digitalWrite(19, LOW); + delay(500); + digitalWrite(5, LOW); + temp_mode = 30 - 16; + mqtt.irpost_(); + }else if(ifchar(u2data,(uint8_t*)"\x4C\x00\x0B\x01\x1D\x75\x5A",6)){//29 + digitalWrite(18, LOW); + digitalWrite(19, LOW); + delay(500); + digitalWrite(5, LOW); + temp_mode = 29 - 16; + mqtt.irpost_(); + }else if(ifchar(u2data,(uint8_t*)"\x4C\x00\x0B\x01\x1C\x74\x5A",6)){//28 + digitalWrite(18, LOW); + digitalWrite(19, LOW); + delay(500); + digitalWrite(5, LOW); + temp_mode = 28 - 16; + mqtt.irpost_(); + }else if(ifchar(u2data,(uint8_t*)"\x4C\x00\x0B\x01\x1B\x73\x5A",6)){//27 + digitalWrite(18, LOW); + digitalWrite(19, LOW); + delay(500); + digitalWrite(5, LOW); + temp_mode = 27 - 16; + mqtt.irpost_(); + }else if(ifchar(u2data,(uint8_t*)"\x4C\x00\x0B\x01\x1A\x72\x5A",6)){//26 + digitalWrite(18, LOW); + digitalWrite(19, LOW); + delay(500); + digitalWrite(5, LOW); + temp_mode = 26 - 16; + mqtt.irpost_(); + }else if(ifchar(u2data,(uint8_t*)"\x4C\x00\x0B\x01\x19\x71\x5A",6)){//25 + digitalWrite(18, LOW); + digitalWrite(19, LOW); + delay(500); + digitalWrite(5, LOW); + temp_mode = 25 - 16; + mqtt.irpost_(); + }else if(ifchar(u2data,(uint8_t*)"\x4C\x00\x0B\x01\x18\x70\x5A",6)){//24 + digitalWrite(18, LOW); + digitalWrite(19, LOW); + delay(500); + digitalWrite(5, LOW); + temp_mode = 24 - 16; + mqtt.irpost_(); + }else if(ifchar(u2data,(uint8_t*)"\x4C\x00\x0B\x01\x17\x6F\x5A",6)){//23 + digitalWrite(18, LOW); + digitalWrite(19, LOW); + delay(500); + digitalWrite(5, LOW); + temp_mode = 23 - 16; + mqtt.irpost_(); + }else if(ifchar(u2data,(uint8_t*)"\x4C\x00\x0B\x01\x16\x6E\x5A",6)){//22 + digitalWrite(18, LOW); + digitalWrite(19, LOW); + delay(500); + digitalWrite(5, LOW); + temp_mode = 28 - 16; + mqtt.irpost_(); + }else if(ifchar(u2data,(uint8_t*)"\x4C\x00\x0B\x01\x15\x6D\x5A",6)){//21 + digitalWrite(18, LOW); + digitalWrite(19, LOW); + delay(500); + digitalWrite(5, LOW); + temp_mode = 21 - 16; + mqtt.irpost_(); + }else if(ifchar(u2data,(uint8_t*)"\x4C\x00\x0B\x01\x14\x6C\x5A",6)){//20 + digitalWrite(18, LOW); + digitalWrite(19, LOW); + delay(500); + digitalWrite(5, LOW); + temp_mode = 20 - 16; + mqtt.irpost_(); + }else if(ifchar(u2data,(uint8_t*)"\x4C\x00\x0B\x01\x13\x6B\x5A",6)){//19 + digitalWrite(18, LOW); + digitalWrite(19, LOW); + delay(500); + digitalWrite(5, LOW); + temp_mode = 19 - 16; + mqtt.irpost_(); + }else if(ifchar(u2data,(uint8_t*)"\x4C\x00\x0B\x01\x12\x6A\x5A",6)){//18 + digitalWrite(18, LOW); + digitalWrite(19, LOW); + delay(500); + digitalWrite(5, LOW); + temp_mode = 18 - 16; + mqtt.irpost_(); + }else if(ifchar(u2data,(uint8_t*)"\x4C\x00\x0B\x01\x11\x69\x5A",6)){//17 + digitalWrite(18, LOW); + digitalWrite(19, LOW); + delay(500); + digitalWrite(5, LOW); + temp_mode = 17 - 16; + mqtt.irpost_(); + }else if(ifchar(u2data,(uint8_t*)"\x4C\x00\x0B\x01\x10\x68\x5A",6)){//16 + digitalWrite(18, LOW); + digitalWrite(19, LOW); + delay(500); + digitalWrite(5, LOW); + temp_mode = 16 - 16; + mqtt.irpost_(); + }else{ + Serial.println("U2 ERROR DATA:"); + char i = 0; + Serial.println("00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F "); + for(;u2len > i;){ + for(char x = 0;x < 17;x++){ + //Serial.print(i,DEC); + //Serial.print("\t"); + //Serial.println(u2data[u2len],HEX); + if(u2len > i){ + if(u2data[i] < 0x10){ + Serial.print("0"); + } + Serial.print(u2data[i],HEX); + Serial.print(" "); + i++; + } + } + Serial.print("\r\n"); + } + digitalWrite(5, LOW); + delay(500); + digitalWrite(18, LOW); + digitalWrite(19, LOW); + } + u2len = 0; + Serial.println("if_mode"); + Serial.println(if_mode,HEX); + } + } + + if(reboot_ > 0){ + if(reboot_ == 100){ + ESP.restart(); + } + reboot_++; + } +} \ No newline at end of file diff --git a/src/user_mqtt.cpp b/src/user_mqtt.cpp new file mode 100644 index 0000000..ab9ca7a --- /dev/null +++ b/src/user_mqtt.cpp @@ -0,0 +1,350 @@ +#include "user_mqtt.h" + +#include "user.h" + +#include +#include + +String mqtt_server; +String mqtt_user; +String mqtt_pass; +String mqtt_uid; + +WiFiClient esp_mqtt_Client; +PubSubClient mqtt_client(esp_mqtt_Client); + +unsigned long mqttrelinkMillis; + +uint8_t ir_dev[] = "\005\x0F\xFF\x02\x54\007"; +/* +0 mode +1 off +2 id1 +3 id2 +4 jy +5 fan +*/ + +void irpost(){ + digitalWrite(18, HIGH); + Serial1.write("\x00",1); + delay(49); + char data[] = "\x16\x02\x10\xFF\x00\x01\x0A\x00\x00\x00\x00\x00\x00\x00\x00\x20\x00\x00\x02\x54\x08"; + //char data[] = "\x16\x02\x10\x00\x00\x00\x08\x03\x00\x00\x00\x00\x00\x00\x00\x21\x00\x00\x00\x54\x08"; + data[3] = ir_dev[2]; + data[18] = ir_dev[3]; + data[19] = ir_dev[4]; + Serial.print("mode ");Serial.print(if_mode,HEX); + Serial.print(" fan ");Serial.print(fan_mode,HEX); + Serial.print(" swing ");Serial.print(swing_mode,HEX); + Serial.print(" temp ");Serial.println(temp_mode + 16,DEC); + if(if_mode < 0x10){ + //data[19]++; + data[ir_dev[0]] = if_mode - 1; + data[ir_dev[1]] = 0x21; + data[19] = data[19] + data[ir_dev[0]]; + data[ir_dev[5]] = fan_mode; + data[19] = data[19] + fan_mode; + if(swing_mode == 1){ + data[9] = 1; + data[19] = data[19] + 1; + }else if(swing_mode == 2){ + data[10] = 1; + data[19] = data[19] + 1; + }else if(swing_mode == 3){ + data[9] = 1; + data[10] = 1; + data[19] = data[19] + 2; + } + data[6] = data[6] - 0x0A + temp_mode; + data[19] = data[19] - 0x0A + temp_mode; + } + + Serial.println("ifok"); +char i = 0; +Serial.println("00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F "); +for(;21 > i;){ + for(char x = 0;x < 17;x++){ + //Serial.print(i,DEC); + //Serial.print("\t"); + //Serial.println(u2data[u2len],HEX); + if(21 > i){ + if(data[i] < 0x10){ + Serial.print("0"); + } + Serial.print(data[i],HEX); + Serial.print(" "); + i++; + } + } + Serial.print("\r\n"); +} + Serial1.write(data,21); +} + +void post(String id,String data){ + mqtt_client.beginPublish(id.c_str(), data.length(), false); + mqtt_client.print(data); + mqtt_client.endPublish(); +} + +void mqtt_pust(){ + String id; + String data; + data = ""; + + id = "jcmdev/" + mqtt_uid + "/rx/fan"; + switch (fan_mode) + { + case 0: + data = "auto"; + break; + case 1: + data = "low"; + break; + case 2: + data = "medium"; + break; + case 3: + data = "high"; + break; + + default: + data = String(fan_mode); + break; + } + post(id,data); + + id = "jcmdev/" + mqtt_uid + "/rx/mode"; + switch (if_mode) + { + case 0: + data = "off"; + break; + case 1: + data = "auto"; + break; + case 2: + data = "cool"; + break; + case 3: + data = "dry"; + break; + case 4: + data = "fan_only"; + break; + case 5: + data = "heat"; + break; + + default: + Serial.println(if_mode,HEX); + if(if_mode >> 4 == 1){ + data = "off"; + }else{ + data = String(if_mode); + } + break; + } + post(id,data); + + id = "jcmdev/" + mqtt_uid + "/rx/fan/sw"; + switch (swing_mode) + { + case 0: + data = "off"; + break; + case 1: + data = "左右"; + break; + case 2: + data = "上下"; + break; + case 3: + data = "on"; + break; + + default: + data = String(swing_mode); + break; + } + post(id,data); + + post("jcmdev/" + mqtt_uid + "/rx/t",String(temp_mode + 16)); +} + +void MQTT::pust(){ + mqtt_pust(); +} +void MQTT::irpost_(){ + //Serial.print("mode ");Serial.print(if_mode,HEX); + irpost(); +} + +void addstart(){ + String id; + String json; + id = "homeassistant/climate/" + mqtt_uid + "/IR/config"; + json = "{"; + json += "\"availability\":[{"; + json += "\"topic\":\"jcmdev/" + mqtt_uid + "/state\","; + json += "\"value_template\":\"{{ value_json.state }}\"}],"; + json += "\"device\":{"; + json += "\"identifiers\":[\"" + mqtt_uid + "\"],"; + json += "\"manufacturer\":\"ms__jiang\","; + json += "\"model\":\"" + mqtt_uid + "\","; + json += "\"name\":\"" + mqtt_uid + "\","; + json += "\"configuration_url\":\"http://" + WiFi.localIP().toString() + "\"},"; + json += "\"json_attributes_topic\":\"jcmdev/" + mqtt_uid + "\","; + json += "\"object_id\":\"" + mqtt_uid + "_" + "IR" + "\","; + json += "\"unique_id\":\"" + mqtt_uid + "_" + "IR" + "\","; + json += "\"max_temp\":\"32\","; + json += "\"min_temp\":\"16\","; + json += "\"mode_command_topic\":\"jcmdev/" + mqtt_uid + "/tx/mode\","; + json += "\"mode_state_topic\":\"jcmdev/" + mqtt_uid + "/rx/mode\","; + + json += "\"fan_mode_command_topic\":\"jcmdev/" + mqtt_uid + "/tx/fan\","; + json += "\"fan_mode_state_topic\":\"jcmdev/" + mqtt_uid + "/rx/fan\","; + + json += "\"swing_modes\":[\"on\",\"左右\",\"上下\",\"off\"],"; + json += "\"swing_mode_command_topic\":\"jcmdev/" + mqtt_uid + "/tx/fan/sw\","; + json += "\"swing_mode_state_topic\":\"jcmdev/" + mqtt_uid + "/rx/fan/sw\","; + + json += "\"temperature_command_topic\":\"jcmdev/" + mqtt_uid + "/tx/t\","; + json += "\"temperature_state_topic\":\"jcmdev/" + mqtt_uid + "/rx/t\","; + json += "\"temperature_unit\":\"C\","; + json += "\"current_temperature_topic\":\"jcmdev/" + mqtt_uid + "/rx/temp\""; + json += "}"; + post(id,json); + + id = "jcmdev/" + mqtt_uid + "/tx/mode"; + mqtt_client.subscribe(id.c_str()); + id = "jcmdev/" + mqtt_uid + "/tx/fan"; + mqtt_client.subscribe(id.c_str()); + id = "jcmdev/" + mqtt_uid + "/tx/fan/sw"; + mqtt_client.subscribe(id.c_str()); + id = "jcmdev/" + mqtt_uid + "/tx/t"; + mqtt_client.subscribe(id.c_str()); + + delay(500); + post("jcmdev/" + mqtt_uid + "/state","online"); +} + +void reconnect(){ + if(!mqtt_client.connected()){ + Serial.print("Attempting MQTT connection..."); + if (mqtt_client.connect(mqtt_uid.c_str(),mqtt_user.c_str(),mqtt_pass.c_str())) { + Serial.println("connected"); + addstart(); + delay(100); + // mqtt_client.subscribe("#"); + mqtt_client.subscribe("homeassistant/status"); + }else{ + Serial.print("failed, rc="); + Serial.print(mqtt_client.state()); + Serial.println(" try again in 10 seconds"); + } + } +} + +void callback(char* topic, byte* payload, unsigned int length) { + String data = ""; + String id = ""; + Serial.print("Message arrived ["); + Serial.print(topic); + Serial.print("] "); + for (int i = 0; i < length; i++) { + Serial.print((char)payload[i]); + data += (char)payload[i]; + } + id = topic; + Serial.println(); + //Serial.println(id); + + + if (id == "homeassistant/status"){ + if(data == "online"){ + addstart(); + } + }else if(id == "jcmdev/" + mqtt_uid + "/tx/mode"){ + if(data == "off"){ + if(if_mode < 0x10){if_mode += 0x10;} + irpost(); + }else if(data == "auto"){ + if_mode = 1; + irpost(); + }else if(data == "cool"){ + if_mode = 2; + irpost(); + }else if(data == "dry"){ + if_mode = 3; + irpost(); + }else if(data == "fan_only"){ + if_mode = 4; + irpost(); + }else if(data == "heat"){ + if_mode = 5; + irpost(); + } + }else if(id == "jcmdev/" + mqtt_uid + "/tx/fan"){ + if(data == "auto"){ + fan_mode = 0; + irpost(); + }else if(data == "low"){ + fan_mode = 1; + irpost(); + }else if(data == "medium"){ + fan_mode = 2; + irpost(); + }else if(data == "high"){ + fan_mode = 3; + irpost(); + } + }else if(id == "jcmdev/" + mqtt_uid + "/tx/fan/sw"){ + if(data == "off"){ + swing_mode = 0; + irpost(); + }else if(data == "左右"){ + swing_mode = 1; + irpost(); + }else if(data == "上下"){ + swing_mode = 2; + irpost(); + }else if(data == "on"){ + swing_mode = 3; + irpost(); + } + }else if(id == "jcmdev/" + mqtt_uid + "/tx/t"){ + temp_mode = data.toInt() - 16; + irpost(); + } + +} + +void MQTT::setup(){ + mqtt_server = "homeassistant.lan"; + mqtt_user = "user"; + mqtt_pass = "passwd"; + mqtt_uid = host; + mqtt_server = "192.168.2.168"; + mqtt_user = "mqtt"; + mqtt_pass = "mqtt"; + mqtt_uid = host; + + if(mqtt_server != ""){ + mqtt_client.setServer(mqtt_server.c_str(), 1883); + mqtt_client.setCallback(callback); + } +} + +void MQTT::loop(){ + mqtt_client.loop(); + if (WiFi.waitForConnectResult() == WL_CONNECTED){ + if(mqttrelinkMillis < millis()){ + mqttrelinkMillis = millis() + 10000; + if(!mqtt_client.connected()){ + reconnect(); + } + } + } +} \ No newline at end of file diff --git a/test/README b/test/README new file mode 100644 index 0000000..8bbc7e0 --- /dev/null +++ b/test/README @@ -0,0 +1,10 @@ +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html diff --git a/update.bat b/update.bat new file mode 100644 index 0000000..0f2170c --- /dev/null +++ b/update.bat @@ -0,0 +1 @@ +curl http://N2-IR_48B858.lan/update?curl= -F "file=@.pio\build\esp32solo1\firmware.bin" \ No newline at end of file diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..7b7a22c --- /dev/null +++ b/web/index.html @@ -0,0 +1,96 @@ + + + + + N2-IR + + + +
+ + N2-IR + +

+ +
+ + \ No newline at end of file diff --git a/web/wifi/index.html b/web/wifi/index.html new file mode 100644 index 0000000..bbd73c8 --- /dev/null +++ b/web/wifi/index.html @@ -0,0 +1,150 @@ + + + + + Set WiFi + + + + +
+ + 正在加载 +

+ +
+ + + \ No newline at end of file