咕咕嘎嘎
All checks were successful
Build / Build (push) Successful in 6m42s

This commit is contained in:
2026-06-10 23:04:48 +08:00
commit 4f833f7555
32 changed files with 4733 additions and 0 deletions

View File

@@ -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

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch

10
.vscode/extensions.json vendored Normal file
View File

@@ -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"
]
}

21
.vscode/settings.json vendored Normal file
View File

@@ -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"
}
}

17
Makefile Normal file
View File

@@ -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

14
include/Http_Server.h Normal file
View File

@@ -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

38
include/README Normal file
View File

@@ -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

23
include/user.h Normal file
View File

@@ -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

16
include/user_mqtt.h Normal file
View File

@@ -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

45
lib/README Normal file
View File

@@ -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 <Foo.h>
#include <Bar.h>
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

View File

@@ -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

View File

@@ -0,0 +1,9 @@
name=WebServer
version=2.0.0
author=Ivan Grokhotkov
maintainer=Ivan Grokhtkov <ivan@esp8266.com>
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

View File

@@ -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_ */

View File

@@ -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 <Arduino.h>
#include <esp32-hal-log.h>
#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;
}

29
lib/WebServer/src/Uri.h Normal file
View File

@@ -0,0 +1,29 @@
#ifndef URI_H
#define URI_H
#include <Arduino.h>
#include <vector>
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<String> &pathArgs) {}
virtual bool canHandle(const String &requestUri, __attribute__((unused)) std::vector<String> &pathArgs) {
return _uri == requestUri;
}
};
#endif

View File

@@ -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 <Arduino.h>
#include <esp32-hal-log.h>
#include <libb64/cdecode.h>
#include <libb64/cencode.h>
#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 &param, 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("");
}
}

View File

@@ -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 <functional>
#include <memory>
#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<String *(HTTPAuthMethod mode, String enteredUsernameOrReq, String extraParams[])> 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<void(void)> THandlerFunction;
typedef std::function<bool(WebServer &server)> 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<String(FS &fs, const String &fName)> 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<typename T> 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 &param, 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<HTTPUpload> _currentUpload;
std::unique_ptr<HTTPRaw> _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

View File

@@ -0,0 +1,91 @@
#ifndef REQUESTHANDLER_H
#define REQUESTHANDLER_H
#include <vector>
#include <assert.h>
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<bool(WebServer &)> filter) {
(void)filter;
return *this;
}
RequestHandler *next() {
return _next;
}
void next(RequestHandler *r) {
_next = r;
}
private:
RequestHandler *_next = nullptr;
protected:
std::vector<String> pathArgs;
public:
const String &pathArg(unsigned int i) {
assert(i < pathArgs.size());
return pathArgs[i];
}
};
#endif //REQUESTHANDLER_H

View File

@@ -0,0 +1,263 @@
#ifndef REQUESTHANDLERSIMPL_H
#define REQUESTHANDLERSIMPL_H
#include "RequestHandler.h"
#include "mimetable.h"
#include "WString.h"
#include "Uri.h"
#include <MD5Builder.h>
#include <base64.h>
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

View File

@@ -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

View File

@@ -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

View File

@@ -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<String> &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<String> &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

View File

@@ -0,0 +1,22 @@
#ifndef URI_GLOB_H
#define URI_GLOB_H
#include "Uri.h"
#include <fnmatch.h>
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<String> &pathArgs) override final {
return fnmatch(_uri.c_str(), requestUri.c_str(), 0) == 0;
}
};
#endif

View File

@@ -0,0 +1,45 @@
#ifndef URI_REGEX_H
#define URI_REGEX_H
#include "Uri.h"
#include <regex>
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<String> &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<String> &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

19
platformio.ini Normal file
View File

@@ -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

765
src/http_server.cpp Normal file
View File

@@ -0,0 +1,765 @@
#include "Http_Server.h"
#include <WiFi.h>
#include <WebServer.h>
#include <Update.h>
#include <Arduino.h>
#include <DNSServer.h>
#include <FS.h>
#include <LittleFS.h>
#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 = "<html lang=\"zh\"><head><meta name=\"format-detection\" content=\"telephone=no\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1,user-scalable=no\"><title>";
String htmltou2 = "</title>";
String htmltou3 = "<style>.c,body{text-align:center;font-family:verdana}div,input{padding:5px;font-size:1em;margin:5px 0;box-sizing:border-box;}input,button,.msg{border-radius:.3rem;width: 100%}button,input[type='button'],input[type='submit']{cursor:pointer;border:0;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%}input[type='file']{border:1px solid #1fa3ec}.wrap {text-align:left;display:inline-block;min-width:260px;max-width:500px}a{color:#000;font-weight:700;text-decoration:none}a:hover{color:#1fa3ec;text-decoration:underline}.q{height:16px;margin:0;padding:0 5px;text-align:right;min-width:38px;long:right}.q.q-0:after{background-position-x:0}.q.q-1:after{background-position-x:-16px}.q.q-2:after{background-position-x:-32px}.q.q-3:after{background-position-x:-48px}.q.q-4:after{background-position-x:-64px}.q.l:before{background-position-x:-80px;padding-right:5px}.ql .q{long:left}.q:after,.q:before{content:'';width:16px;height:16px;display:inline-block;background-repeat:no-repeat;background-position: 16px 0;background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAAAQCAMAAADeZIrLAAAAJFBMVEX///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADHJj5lAAAAC3RSTlMAIjN3iJmqu8zd7vF8pzcAAABsSURBVHja7Y1BCsAwCASNSVo3/v+/BUEiXnIoXkoX5jAQMxTHzK9cVSnvDxwD8bFx8PhZ9q8FmghXBhqA1faxk92PsxvRc2CCCFdhQCbRkLoAQ3q/wWUBqG35ZxtVzW4Ed6LngPyBU2CobdIDQ5oPWI5nCUwAAAAASUVORK5CYII=');}@media (-webkit-min-device-pixel-ratio: 2),(min-resolution: 192dpi){.q:before,.q:after {background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALwAAAAgCAMAAACfM+KhAAAALVBMVEX///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAOrOgAAAADnRSTlMAESIzRGZ3iJmqu8zd7gKjCLQAAACmSURBVHgB7dDBCoMwEEXRmKlVY3L//3NLhyzqIqSUggy8uxnhCR5Mo8xLt+14aZ7wwgsvvPA/ofv9+44334UXXngvb6XsFhO/VoC2RsSv9J7x8BnYLW+AjT56ud/uePMdb7IP8Bsc/e7h8Cfk912ghsNXWPpDC4hvN+D1560A1QPORyh84VKLjjdvfPFm++i9EWq0348XXnjhhT+4dIbCW+WjZim9AKk4UZMnnCEuAAAAAElFTkSuQmCC');background-size: 95px 16px;}}.msg{padding:20px;margin:20px 0;border:1px solid #eee;border-left-width:5px;border-left-color:#777}.msg h4{margin-top:0;margin-bottom:5px}.msg.P{border-left-color:#1fa3ec}.msg.P h4{color:#1fa3ec}.msg.D{border-left-color:#dc3630}.msg.D h4{color:#dc3630}.msg.S{border-left-color: #5cb85c}.msg.S h4{color: #5cb85c}dt{font-weight:bold}dd{margin:0;padding:0 0 0.5em 0;min-height:12px}td{vertical-align: top;}.h{display:none}button.D{background-color:#dc3630}button.F{background-color:#777}body.invert,body.invert a,body.invert h1 {background-color:#060606;color:#fff;}body.invert .msg{color:#fff;background-color:#282828;border-top:1px solid #555;border-right:1px solid #555;border-bottom:1px solid #555;}body.invert .q[role=img]{-webkit-filter:invert(1);filter:invert(1);}input:disabled {opacity: 0.5;}</style>";
String htmltou4 = "</head><body class=\"invert\"><div class=\"wrap\">";
String htmltou5 = "</div></body></html>";
void h_index(){
//httpserver_.sendHeader("Connection", "close");
String html;
html = htmltou + devdev + htmltou2;
html += htmltou3 + htmltou4;
html += "<meta charset='UTF-8'>";
html += devdev;
html += "</meta>";
html += "<button onclick='location.href=(\"wifi#p\")' type=\"button\">WiFi设置</button><br><br>";
html += "<form method=\"\" action=\"update\"><button type=\"submit\">OTA更新</button></form>";
html += "<form method=\"\" action=\"fsca\"><button class=\"D\" type=\"submit\">恢复出厂设置</button></form>";
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", "<meta http-equiv=\"refresh\" content=\"0; URL=http://192.168.4.1/\" />");
}
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 += "<script>function c(l){document.getElementById('s').value=l.innerText||l.textContent;p = l.nextElementSibling.classList.contains('l');document.getElementById('p').disabled = !p;if(p)document.getElementById('p').focus();}</script>";
// 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 += "<div><a href=\"wifi#p\" onclick=\"c(this)\">";
// html += WiFi.SSID(i);
// html += "</a><div role=\"img\" aria-label=\"";
// html += WiFi.RSSI(i);
// html += "%\" title=\"";
// html += "\" class=\"q ";
// if(WiFi.encryptionType(i) != WIFI_AUTH_OPEN) {html += "l";}
// html += " ";
// if(WiFi.RSSI(i) > -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 += "\"></div><div class=\"q h\">";
// html += WiFi.RSSI(i);
// html += "%</div></div>";
// html += "";
// }
// }
// }
// WiFi.scanDelete();
// html += "<form method=\"POST\" action=\"\"><label for=\"s\">SSID</label><input id=\"s\" name=\"s\" maxlength=\"32\" autocorrect=\"off\" autocapitalize=\"none\" placeholder=\"\"><br><label for=\"p\">Password</label><input id=\"p\" name=\"p\" maxlength=\"64\" type=\"password\" placeholder=\"********\"><hr><p></p>";
// //html += "<label for=\"CityCode\">blinker KEY</label><br><input id=\"key\" name=\"key\" maxlength=\"12\" value=\"";
// //html += "";
// html += "<br><br><button type=\"submit\">Save</button></form>";
// html += "<form method=\"\" action=\"/\"><button type=\"submit\">返回</button></form>";
// html += htmltou5;
html = "<!doctype html>\
<html lang=en>\
<head>\
<meta charset=UTF-8/><link rel=icon href=data:image/>\
<meta name=viewport content=\"width=device-width,initial-scale=1,user-scalable=no\"/>\
<script type=module>\
//assets/index.633c2a7f.js \r\n\
document.location.search === \"?save\" && (document.getElementsByTagName(\"aside\")[0].style.display = \"block\");\
function s(e) {\
let t = Math.max(Math.min(2 * (e + 100), 100), 0) / 100;\
return a(`<path d=\"m12.008 19.25-11.3-15c7-5 14-5 22.5 0z\" fill=\"#000\" stroke=\"currentColor\"/>\
<path d=\"m12.008 19.25-11.3-15c7-5 14-5 22.5 0z\" fill=\"#FFF\" transform=\"scale(${t} ${t})\" transform-origin=\"12 18\"/>`)\
}\
function a(e) {\
return o([`<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\">${e}</svg>`])\
}\
function i(e) {\
return e ? a(`<path d='M12 17a2 2 0 0 0 2-2 2 2 0 0 0-2-2 2 2 0 0 0-2 2 2 2 0 0 0 2 \
2m6-9a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V10a2\
2 0 0 1 2-2h1V6a5 5 0 0 1 5-5 5 5 0 0 1 5 \
5v2h1m-6-5a3 3 0 0 0-3 3v2h6V6a3 3 0 0 0-3-3z'/>`) : \"\"\
}\
function o(e) {\
return e.join(\"\")\
}\
fetch(\"/config.json\").then(function(e) {\
e.json().then(function(t) {\
document.title = t.name,\
document.body.getElementsByTagName(\"h1\")[0].innerText = \"MAC Address: \" + t.mac,\
document.body.getElementsByTagName(\"h1\")[1].innerText = \"WiFi Networks: \" + t.name;\
let r = t.aps.slice(1).map(function(n) {\
return `<div class=\"network\" \
onclick=\"document.getElementById('ssid').value = this.innerText;document.getElementById('psk').focus()\">\
<a href=\"#\" class=\"network-left\">\
${s(n.rssi)}\
<span class=\"network-ssid\">${n.ssid}</span>\
</a>\
${i(n.lock)}\
</div>`\
});\
document.querySelector(\"#net\").innerHTML = o(r),\
document.querySelector(\"link[rel~='icon']\").href = `data:image/svg+xml,${s(-65)}`\
})\
});\
</script>\
<style>\
* {\
box-sizing: inherit\
}\
\
div,input {\
padding: 5px;\
font-size: 1em\
}\
\
input {\
width: 95%\
}\
\
body {\
text-align: center;\
font-family: sans-serif\
}\
\
button {\
border: 0;\
border-radius: .3rem;\
background-color: #1fa3ec;\
color: #fff;\
line-height: 2.4rem;\
font-size: 1.2rem;\
width: 100%;\
padding: 0;\
cursor: pointer\
}\
\
main {\
text-align: left;\
display: inline-block;\
min-width: 260px\
}\
\
.network {\
display: flex;\
justify-content: space-between;\
align-items: center\
}\
\
.network-left {\
display: flex;\
align-items: center\
}\
\
.network-ssid {\
margin-bottom: -7px;\
margin-left: 10px\
}\
\
aside {\
border: 1px solid;\
margin: 10px 0;\
padding: 15px 10px;\
color: #4f8a10;\
background-color: #dff2bf;\
display: none\
}\
\
i {\
width: 24px;\
height: 24px\
}\
\
i.lock {\
background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 17a2 2 0 0 0 2-2 2 2 0 0 0-2-2 2 2 0 0 0-2 2 2 2 0 0 0 2 2m6-9a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2h1V6a5 5 0 0 1 5-5 5 5 0 0 1 5 5v2h1m-6-5a3 3 0 0 0-3 3v2h6V6a3 3 0 0 0-3-3z'/%3E%3C/svg%3E\")\
}\
\
i.sig1 {\
background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' %3E%3Cpath d='M12 3A18.9 18.9 0 0 0 .38 7C4.41 12.06 7.89 16.37 12 21.5L23.65 7C20.32 4.41 16.22 3 12 3m0 2c3.07 0 6.09.86 8.71 2.45l-5.1 6.36a8.43 8.43 0 0 0-7.22-.01L3.27 7.44A16.94 16.94 0 0 1 12 5z'/%3E%3C/svg%3E\")\
}\
\
i.sig2 {\
background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' %3E%3Cpath d='M12 3A18.9 18.9 0 0 0 .38 7C4.41 12.06 7.89 16.37 12 21.5L23.65 7C20.32 4.41 16.22 3 12 3m0 2c3.07 0 6.09.86 8.71 2.45l-3.21 3.98a11.32 11.32 0 0 0-11 0L3.27 7.44A16.94 16.94 0 0 1 12 5z'/%3E%3C/svg%3E\")\
}\
\
i.sig3 {\
background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' %3E%3Cpath d='M12 3A18.9 18.9 0 0 0 .38 7C4.41 12.06 7.89 16.37 12 21.5L23.65 7C20.32 4.41 16.22 3 12 3m0 2c3.07 0 6.09.86 8.71 2.45l-1.94 2.43A13.6 13.6 0 0 0 12 8C9 8 6.68 9 5.21 9.84l-1.94-2.4A16.94 16.94 0 0 1 12 5z'/%3E%3C/svg%3E\")\
}\
\
i.sig4 {\
background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' %3E%3Cpath d='M12 3A18.9 18.9 0 0 0 .38 7C4.41 12.06 7.89 16.37 12 21.5L23.65 7C20.32 4.41 16.22 3 12 3z'/%3E%3C/svg%3E\")\
}\
\
i.sig {\
background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' %3E%3Cpath d='M12 3A18.9 18.9 0 0 0 .38 7C4.41 12.06 7.89 16.37 12 21.5L23.65 7C20.32 4.41 16.22 3 12 3m0 2c3.07 0 6.09.86 8.71 2.45l-5.1 6.36a8.43 8.43 0 0 0-7.22-.01L3.27 7.44A16.94 16.94 0 0 1 12 5z'/%3E%3C/svg%3E\")\
}\
\
body.invert {\
background-color: #060606;\
color: #fff;\
}\
\
a {color:#fff;}\
</style>\
</head>\
<body class=\"invert\">\
<main>\
<h1 id=mac>MAC Address</h1>\
<h1 id=h1>WiFi Networks</h1>\
<aside>\
The ESP will now try to connect to the network...<br>\
Please give it some time to connect.<br>\
</aside>\
<span id=net></span>\
<h3>WiFi Settings</h3>\
<form method=\"POST\" action=>\
<input id=ssid name=s length=32 placeholder=SSID/>\
<br>\
<input id=psk name=p length=64 type=password placeholder=Password/>\
<br>\
<br>\
<button type=submit>Save</button>\
</form>\
<br>\
</main>\
</body>\
</html>\
";
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", "<meta charset='UTF-8'>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", "<meta charset='UTF-8'>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 += "<meta charset='UTF-8'>SSID:" + wifi_ssid + "<br />password:" + wifi_pass + "<br />已取得WiFi信息,正在尝试连接,请手动关闭此页面。";
html += "<hr><form method=\"\" action=\"\\\"><br><br><button type=\"submit\">返回</button></form>";
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", "<meta charset='UTF-8'>啥也没有,你个吊毛干嘛呢"); //返回保存成功页面
}
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 += "<meta charset='UTF-8'>OTA</meta>";
html += "<form method='POST' action='/update' enctype='multipart/form-data'><input type='file' name='update'><input type='submit' value='Update'></form>";
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 += "<meta charset='UTF-8'>FAIL</meta>";
}else{
html += "<meta charset='UTF-8'>OK</meta>";
}
html += "<form method='' action='/reboot/set'><input type='submit' value='REBOOT'></form>";
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 \
<div id=\"info\">正在加载</div> \
<form method=\"\" action=\"8012/setv?\"> \
<label for=\"s\" >设置实际测量电压0.1V :如 230V 设置 2300</label> \
<input id=\"id\" name=\"V\" type=\"number\" value=\"2300\" maxlength=\"\" placeholder=\"\"> \
<button type=\"submit\">校准电压</button> \
</form> \
<form method=\"\" action=\"8012/setc?\"> \
<label for=\"s\" >设置实际测量电流0.1A :如 3A 设置 30</label> \
<input id=\"id\" name=\"C\" type=\"number\" value=\"30\" maxlength=\"\" placeholder=\"\"> \
<button type=\"submit\">校准电压</button> \
</form> ";
html += htmltou5;
html += "<script type=\"text/javascript\"> \
window.setInterval(infoupdata, 1000); \
function infoupdata(){ \
const cpuHttp = new XMLHttpRequest(); \
cpuHttp.open(\"GET\", '8012/get'); \
cpuHttp.send(); \
cpuHttp.onreadystatechange = function() { \
if(cpuHttp.readyState == 4 && cpuHttp.status == 200){ \
var data = JSON.parse(cpuHttp.responseText); \
var res; \
res = '电压:' + data.V + \"V\"; \
res += '<br>电流:' + data.C + \"A\"; \
res += '<br>功率:' + data.P + \"W\"; \
res += '<br>视在功率:' + data.VA + \"VA\"; \
res += '<br>功率因数:' + data.F + \"%\"; \
document.getElementById('info').innerHTML = res; \
} \
} \
} \
</script>";
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 += "<meta charset='UTF-8'>成功";
}else{
html += "<meta charset='UTF-8'>失败,不知道为什么";
}
}else{
html += "<meta charset='UTF-8'>失败,请重置设备";
}
file.close();
}else{
html += "<meta charset='UTF-8'>获取参数失败<br />请更换浏览器后重试";
}
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 += "<meta charset='UTF-8'>成功";
}else{
html += "<meta charset='UTF-8'>失败,不知道为什么";
}
}else{
html += "<meta charset='UTF-8'>失败,请重置设备";
}
file.close();
}else{
html += "<meta charset='UTF-8'>获取参数失败<br />请更换浏览器后重试";
}
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();
}

692
src/main.cpp Normal file
View File

@@ -0,0 +1,692 @@
#include <Arduino.h>
#include <FS.h>
#include <LittleFS.h>
#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_++;
}
}

350
src/user_mqtt.cpp Normal file
View File

@@ -0,0 +1,350 @@
#include "user_mqtt.h"
#include "user.h"
#include <WiFi.h>
#include <PubSubClient.h>
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();
}
}
}
}

10
test/README Normal file
View File

@@ -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

1
update.bat Normal file
View File

@@ -0,0 +1 @@
curl http://N2-IR_48B858.lan/update?curl= -F "file=@.pio\build\esp32solo1\firmware.bin"

96
web/index.html Normal file
View File

@@ -0,0 +1,96 @@
<html lang="zh">
<head>
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>N2-IR</title>
<style>
.c,body{text-align:center;font-family:verdana}
div,input{padding:5px;font-size:1em;margin:5px 0;box-sizing:border-box;}
input,button,.msg{border-radius:.3rem;width: 100%}
button,input[type='button'],input[type='submit']
{
cursor:pointer;
border:0;
background-color:#1fa3ec;
color:#fff;
line-height:2.4rem;
font-size:1.2rem;
width:100%
}
input[type='file']{border:1px solid #1fa3ec}
.wrap {text-align:left;display:inline-block;min-width:260px;max-width:500px}
a{color:#000;font-weight:700;text-decoration:none}
a:hover{color:#1fa3ec;text-decoration:underline}
.q
{
height:16px;
margin:0;
padding:0 5px;
text-align:right;
min-width:38px;
long:right
}
.q.q-0:after{background-position-x:0}
.q.q-1:after{background-position-x:-16px}
.q.q-2:after{background-position-x:-32px}
.q.q-3:after{background-position-x:-48px}
.q.q-4:after{background-position-x:-64px}
.q.l:before{background-position-x:-80px;padding-right:5px}
.ql .q{long:left}
.q:after,
.q:before{
content:'';
width:16px;
height:16px;
display:inline-block;
background-repeat:no-repeat;
background-position: 16px 0;
background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAAAQCAMAAADeZIrLAAAAJFBMVEX///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADHJj5lAAAAC3RSTlMAIjN3iJmqu8zd7vF8pzcAAABsSURBVHja7Y1BCsAwCASNSVo3/v+/BUEiXnIoXkoX5jAQMxTHzK9cVSnvDxwD8bFx8PhZ9q8FmghXBhqA1faxk92PsxvRc2CCCFdhQCbRkLoAQ3q/wWUBqG35ZxtVzW4Ed6LngPyBU2CobdIDQ5oPWI5nCUwAAAAASUVORK5CYII=');
}@media (-webkit-min-device-pixel-ratio: 2),(min-resolution: 192dpi){
.q:before,
.q:after {
background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALwAAAAgCAMAAACfM+KhAAAALVBMVEX///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAOrOgAAAADnRSTlMAESIzRGZ3iJmqu8zd7gKjCLQAAACmSURBVHgB7dDBCoMwEEXRmKlVY3L//3NLhyzqIqSUggy8uxnhCR5Mo8xLt+14aZ7wwgsvvPA/ofv9+44334UXXngvb6XsFhO/VoC2RsSv9J7x8BnYLW+AjT56ud/uePMdb7IP8Bsc/e7h8Cfk912ghsNXWPpDC4hvN+D1560A1QPORyh84VKLjjdvfPFm++i9EWq0348XXnjhhT+4dIbCW+WjZim9AKk4UZMnnCEuAAAAAElFTkSuQmCC');
background-size: 95px 16px;
}
}.msg{
padding:20px;
margin:20px 0;
border:1px solid #eee;
border-left-width:5px;
border-left-color:#777
}.msg h4{margin-top:0;margin-bottom:5px}
.msg.P{border-left-color:#1fa3ec}
.msg.P h4{color:#1fa3ec}
.msg.D{border-left-color:#dc3630}
.msg.D h4{color:#dc3630}
.msg.S{border-left-color: #5cb85c}
.msg.S h4{color: #5cb85c}
dt{font-weight:bold}
dd{margin:0;padding:0 0 0.5em 0;min-height:12px}
td{vertical-align: top;}
.h{display:none}
button.D{background-color:#dc3630}
button.F{background-color:#777}
body.invert,body.invert a,body.invert h1 {
background-color:#060606;color:#fff;
}body.invert
.msg{
color:#fff;
background-color:#282828;
border-top:1px solid #555;
border-right:1px solid #555;
border-bottom:1px solid #555;
}body.invert .q[role=img]{-webkit-filter:invert(1);filter:invert(1);}
input:disabled {opacity: 0.5;}
</style>
</head>
<body class="invert">
<div class="wrap">
<meta charset='UTF-8'>N2-IR</meta>
<button onclick='location.href=("wifi#p")' type="button">WiFi设置</button>
<br><br>
</div>
</body>
</html>

150
web/wifi/index.html Normal file
View File

@@ -0,0 +1,150 @@
<html lang="zh">
<head>
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>Set WiFi</title>
<style>
.c,body{text-align:center;font-family:verdana}
div,input{padding:5px;font-size:1em;margin:5px 0;box-sizing:border-box;}
input,button,.msg{border-radius:.3rem;width: 100%}
button,input[type='button'],input[type='submit']
{
cursor:pointer;
border:0;
background-color:#1fa3ec;
color:#fff;
line-height:2.4rem;
font-size:1.2rem;
width:100%
}
input[type='file']{border:1px solid #1fa3ec}
.wrap {text-align:left;display:inline-block;min-width:260px;max-width:500px}
a{color:#000;font-weight:700;text-decoration:none}
a:hover{color:#1fa3ec;text-decoration:underline}
.q
{
height:16px;
margin:0;
padding:0 5px;
text-align:right;
min-width:38px;
long:right
}
.q.q-0:after{background-position-x:0}
.q.q-1:after{background-position-x:-16px}
.q.q-2:after{background-position-x:-32px}
.q.q-3:after{background-position-x:-48px}
.q.q-4:after{background-position-x:-64px}
.q.l:before{background-position-x:-80px;padding-right:5px}
.ql .q{long:left}
.q:after,
.q:before{
content:'';
width:16px;
height:16px;
display:inline-block;
background-repeat:no-repeat;
background-position: 16px 0;
background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAAAQCAMAAADeZIrLAAAAJFBMVEX///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADHJj5lAAAAC3RSTlMAIjN3iJmqu8zd7vF8pzcAAABsSURBVHja7Y1BCsAwCASNSVo3/v+/BUEiXnIoXkoX5jAQMxTHzK9cVSnvDxwD8bFx8PhZ9q8FmghXBhqA1faxk92PsxvRc2CCCFdhQCbRkLoAQ3q/wWUBqG35ZxtVzW4Ed6LngPyBU2CobdIDQ5oPWI5nCUwAAAAASUVORK5CYII=');
}@media (-webkit-min-device-pixel-ratio: 2),(min-resolution: 192dpi){
.q:before,
.q:after {
background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALwAAAAgCAMAAACfM+KhAAAALVBMVEX///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAOrOgAAAADnRSTlMAESIzRGZ3iJmqu8zd7gKjCLQAAACmSURBVHgB7dDBCoMwEEXRmKlVY3L//3NLhyzqIqSUggy8uxnhCR5Mo8xLt+14aZ7wwgsvvPA/ofv9+44334UXXngvb6XsFhO/VoC2RsSv9J7x8BnYLW+AjT56ud/uePMdb7IP8Bsc/e7h8Cfk912ghsNXWPpDC4hvN+D1560A1QPORyh84VKLjjdvfPFm++i9EWq0348XXnjhhT+4dIbCW+WjZim9AKk4UZMnnCEuAAAAAElFTkSuQmCC');
background-size: 95px 16px;
}
}.msg{
padding:20px;
margin:20px 0;
border:1px solid #eee;
border-left-width:5px;
border-left-color:#777
}.msg h4{margin-top:0;margin-bottom:5px}
.msg.P{border-left-color:#1fa3ec}
.msg.P h4{color:#1fa3ec}
.msg.D{border-left-color:#dc3630}
.msg.D h4{color:#dc3630}
.msg.S{border-left-color: #5cb85c}
.msg.S h4{color: #5cb85c}
dt{font-weight:bold}
dd{margin:0;padding:0 0 0.5em 0;min-height:12px}
td{vertical-align: top;}
.h{display:none}
button.D{background-color:#dc3630}
button.F{background-color:#777}
body.invert,body.invert a,body.invert h1 {
background-color:#060606;color:#fff;
}body.invert
.msg{
color:#fff;
background-color:#282828;
border-top:1px solid #555;
border-right:1px solid #555;
border-bottom:1px solid #555;
}body.invert .q[role=img]{-webkit-filter:invert(1);filter:invert(1);}
input:disabled {opacity: 0.5;}
</style>
<script>
function c(l){
document.getElementById('s').value=l.innerText||l.textContent;
p = l.nextElementSibling.classList.contains('l');
document.getElementById('p').disabled = !p;
if(p)document.getElementById('p').focus();
}
</script>
</head>
<body class="invert">
<div class="wrap" id="main">
<meta charset='UTF-8'>正在加载</meta>
<br><br>
</div>
</body>
<script type="text/javascript">
window.onload = function (){
const Http = new XMLHttpRequest();
Http.open("GET",'wifi/scan');
Http.send();
Http.onreadystatechange = (e) => {
var readyState = Http.readyState;
var status = Http.status;
if(readyState == 4){
var html = '';
cathttp = Http.responseText;
var data = JSON.parse(cathttp);
for(i = 0;i < data.length;i++){
w = data[i];
if((!w[5])&&(w[0] != "")){
html += "<div><a href=\"wifi#p\" onclick=\"c(this)\">";
html += w[0];
html += "</a><div role=\"img\" aria-label=\"";
html += w[3];
html += "%\" title=\"";
html += "\" class=\"q ";
if(w[4] != 0) {html += "l";}
html += " ";
if(w[3] > -60){
html += " q-4";
}else if(w[3] > -80){
html += " q-3";
}else if(w[3] > -85){
html += " q-2";
}else{
html += " q-1";
}
html += " ";
html += "\"></div><div class=\"q h\">";
html += w[3];
html += "%</div></div>";
html += "";
}
}
html += "<form method=\"GET\" action=\"wifi/set\"><label for=\"s\">SSID</label><input id=\"s\" name=\"s\" maxlength=\"32\" autocorrect=\"off\" autocapitalize=\"none\" placeholder=\"\"><br><label for=\"p\">Password</label><input id=\"p\" name=\"p\" maxlength=\"64\" type=\"password\" placeholder=\"********\"><hr><p></p>";
html += "<br><br><button type=\"submit\">Save</button></form>";
html += "<form method=\"\" action=\"/\"><button type=\"submit\">返回</button></form>";
document.getElementById('main').innerHTML = html;
}
}
}
</script>
</html>