ESP32_ChinaDieselHeater_Con.../Arduino/BTCDieselHeater/src/WiFi/BTCWebServer.cpp
Ray Jones 924a079fb2 Using new fantastic RMT based library for DS18B20, banished issues with dodgy readings from sensor.
Persistent variables now used for temperature, pump and cyclic mode enabled settings (not NV)
NV save is now staged, so it can be performed at an appropriate time, after reading DS18B20!
JSONpack, instead of lame JSONloose to allow single line JSON output
2019-06-29 18:08:37 +10:00

491 lines
15 KiB
C++

/*
* This file is part of the "bluetoothheater" distribution
* (https://gitlab.com/mrjones.id.au/bluetoothheater)
*
* Copyright (C) 2018 Ray Jones <ray@mrjones.id.au>
* Copyright (C) 2018 James Clark
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
#define USE_EMBEDDED_WEBUPDATECODE
#include "BTCWebServer.h"
#include "../Utility/DebugPort.h"
#include "../Protocol/TxManage.h"
#include "../Utility/helpers.h"
#include "../cfg/pins.h"
#include "../cfg/BTCConfig.h"
#include "Index.h"
#include "../Utility/BTC_JSON.h"
#include "../Utility/Moderator.h"
#include <WiFiManager.h>
#if USE_SPIFFS == 1
#include <FS.H>
#include <SPIFFS.h>
#endif
#include "../Utility/NVStorage.h"
extern WiFiManager wm;
File fsUploadFile; // a File object to temporarily store the received file
int SPIFFSupload = 0;
WebServer server(80);
WebSocketsServer webSocket = WebSocketsServer(81);
bool bRxWebData = false; // flags for OLED animation
bool bTxWebData = false;
bool bUpdateAccessed = false; // flag used to ensure web update always starts via /update. direct accesses to /updatenow will FAIL
long _SuppliedFileSize = 0;
const int led = 13;
#if USE_SPIFFS == 1
String getContentType(String filename) { // convert the file extension to the MIME type
if (filename.endsWith(".html")) return "text/html";
else if (filename.endsWith(".css")) return "text/css";
else if (filename.endsWith(".js")) return "application/javascript";
else if (filename.endsWith(".ico")) return "image/x-icon";
else if (filename.endsWith(".bin")) return "application/octet-stream";
return "text/plain";
}
bool handleFileRead(String path) { // send the right file to the client (if it exists)
DebugPort.println("handleFileRead: " + path);
if (path.endsWith("/")) path += "index.html"; // If a folder is requested, send the index file
String contentType = getContentType(path); // Get the MIME type
if (SPIFFS.exists(path)) { // If the file exists
File file = SPIFFS.open(path, "r"); // Open it
server.streamFile(file, contentType); // And send it to the client
file.close(); // Then close the file again
return true;
}
DebugPort.println("\tFile Not Found");
return false; // If the file doesn't exist, return false
}
void handleBTCRoot() {
handleFileRead("/index.html");
}
#else
void handleBTCRoot() {
String s = MAIN_PAGE; //Read HTML contents
server.send(200, "text/html", s); //Send web page
}
#endif
void handleWMConfig() {
DebugPort.println("WEB: GET /wmconfig");
server.send(200, "text/plain", "Start Config Portal - Retaining credential");
DebugPort.println("Starting web portal for wifi config");
delay(500);
wifiEnterConfigPortal(true, false, 3000);
}
void handleReset() {
DebugPort.println("WEB: GET /resetwifi");
server.send(200, "text/plain", "Start Config Portal - Resetting Wifi credentials!");
DebugPort.println("diconnecting client and wifi, then rebooting");
delay(500);
wifiEnterConfigPortal(true, true, 3000);
}
void handleFormat() {
DebugPort.println("WEB: GET /formatspiffs");
server.send(200, "text/plain", "Formatting SPIFFS partition!");
DebugPort.println("Formatting SPIFFS partition");
delay(500);
SPIFFS.format();
}
void handleBTCNotFound() {
digitalWrite(led, 1);
String message = "File Not Found\n\n";
message += "URI: ";
message += server.uri();
message += "\nMethod: ";
message += (server.method() == HTTP_GET) ? "GET" : "POST";
message += "\nArguments: ";
message += server.args();
message += "\n";
for (uint8_t i = 0; i < server.args(); i++) {
message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
}
server.send(404, "text/plain", message);
digitalWrite(led, 0);
}
// embedded HTML & Javascript to perform browser based updates of firmware or SPIFFS
const char* updateIndex = R"=====(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="-1">
<meta http-equiv="CACHE-CONTROL" content="NO-CACHE">
<script>
// global variables
var sendSize;
var ws;
function _(el) {
return document.getElementById(el);
}
function init() {
ws = new WebSocket('ws://' + window.location.hostname + ':81/');
ws.onmessage = function(event){
var response = JSON.parse(event.data);
var key;
for(key in response) {
switch(key) {
case "progress":
// actual data bytes received as fed back via web socket
var bytes = response[key];
_("loaded_n_total").innerHTML = "Uploaded " + bytes + " bytes of " + sendSize;
var percent = Math.round( 100 * (bytes / sendSize));
_("progressBar").value = percent;
_("status").innerHTML = percent+"% uploaded.. please wait";
break;
}
}
}
}
function uploadFile() {
_("cancel").hidden = true;
var file = _("file1").files[0];
sendSize = file.size;
var JSONmsg = {};
JSONmsg['UploadSize'] = sendSize;
var str = JSON.stringify(JSONmsg);
console.log("JSON Tx:", str);
ws.send(str);
var formdata = new FormData();
formdata.append("update", file);
var ajax = new XMLHttpRequest();
// progress is handled via websocket JSON sent from controller
// using server side progress only shows the buffer filling, not actual delivery.
ajax.addEventListener("load", completeHandler, false);
ajax.addEventListener("error", errorHandler, false);
ajax.addEventListener("abort", abortHandler, false);
ajax.open("POST", "/updatenow");
ajax.send(formdata);
}
function completeHandler(event) {
_("status").innerHTML = event.target.responseText;
_("progressBar").value = 0;
_("loaded_n_total").innerHTML = "Uploaded " + sendSize + " bytes of " + sendSize;
var file = _("file1").files[0];
if(file.name.endsWith(".bin")) {
setTimeout(function () {
window.location.assign("/");
}, 5000);
}
else {
setTimeout(function () {
window.location.reload();
}, 1000);
}
}
function errorHandler(event) {
_("status").innerHTML = "Upload Failed";
}
function abortHandler(event) {
_("status").innerHTML = "Upload Aborted";
}
</script>
<style>
body {font-family: Arial, Helvetica, sans-serif;}
</style>
<title>Afterburner firmware update</title>
</head>
<body onload="javascript:init()">
<h1>Afterburner firmware update</h1>
<form id="upload_form" method="POST" enctype="multipart/form-data" autocomplete="off">
<input type="file" name="file1" id="file1"> <BR>
<input type="button" value="Update" onclick="uploadFile()">
<progress id="progressBar" value="0" max="100" style="width:300px;"></progress><BR>
<h3 id="status"></h3>
<p id="loaded_n_total"></p>
<BR>
<input type="button" onclick=window.location.assign("/") value="Cancel" id="cancel">
</form>
</body>
</html>
)=====";
void rootRedirect()
{
server.sendHeader("Location","/"); // reselect the update page
server.send(303);
}
void initWebServer(void) {
Update
.onProgress([](unsigned int progress, unsigned int total) {
int percent = (progress / (total / 100));
DebugPort.printf("Progress: %u%%\r", percent);
DebugPort.handle(); // keep telnet spy alive
ShowOTAScreen(percent, eOTAWWW); // WWW update in place
DebugPort.print("^");
});
if (MDNS.begin("Afterburner")) {
DebugPort.println("MDNS responder started");
}
// server.on("/", handleBTCRoot);
server.on("/wmconfig", handleWMConfig);
server.on("/resetwifi", handleReset);
server.on("/formatspiffs", handleFormat);
server.on("/tst", HTTP_GET, []() {
DebugPort.println("WEB: GET /tst");
server.sendHeader("Location","/"); // reselect the update page
server.send(303);
});
// Magical code originally shamelessly lifted from Arduino WebUpdate example, then modified
// This allows pushing new firmware to the ESP from a WEB BROWSER!
// Added authentication and a sequencing flag to ensure this is not bypassed
//
// Initial launch page
server.on("/update", HTTP_GET, []() {
DebugPort.println("WEB: GET /update");
sCredentials creds = NVstore.getCredentials();
if (!server.authenticate(creds.webUpdateUsername, creds.webUpdatePassword)) {
return server.requestAuthentication();
}
bUpdateAccessed = true;
#ifdef USE_EMBEDDED_WEBUPDATECODE
server.send(200, "text/html", updateIndex);
#else
handleFileRead("/uploadfirmware.html");
#endif
});
// handle attempts to just browse the /updatenow path - force redirect to root
server.on("/updatenow", HTTP_GET, []() {
DebugPort.println("WEB: GET /updatenow - ILLEGAL - root redirect");
rootRedirect();
});
// actual guts that manages the new firmware upload
server.on("/updatenow", HTTP_POST, []() {
DebugPort.println("WEB: POST /updatenow completion");
// completion functionality
if(SPIFFSupload) {
if(SPIFFSupload == 1) {
DebugPort.println("WEB: SPIFFS OK");
server.send(200, "text/plain", "OK - File uploaded to SPIFFS");
// javascript reselects the /update page!
}
else {
DebugPort.println("WEB: SPIFFS FAIL");
server.send(500, "text/plain", "500: couldn't create file");
}
SPIFFSupload = 0;
}
else {
if(Update.hasError()) {
DebugPort.println("WEB: UPDATE FAIL");
server.send(200, "text/plain", "FAIL - Afterburner will reboot shortly");
}
else {
DebugPort.println("WEB: UPDATE OK");
server.send(200, "text/plain", "OK - Afterburner will reboot shortly");
}
delay(1000);
// javascript redirects to root page so we go there after reboot!
forceBootInit();
ESP.restart(); // reboot
}
}, []() {
if(bUpdateAccessed) { // only allow progression via /update, attempts to directly access /updatenow will fail
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
String filename = upload.filename;
DebugPort.setDebugOutput(true);
if(filename.endsWith(".bin")) {
DebugPort.printf("Update: %s %d\r\n", filename.c_str(), upload.totalSize);
if (!Update.begin()) { //start with max available size
Update.printError(DebugPort);
}
}
else {
if(!filename.startsWith("/")) filename = "/"+filename;
DebugPort.printf("handleFileUpload Name: %s\r\n", filename.c_str());
fsUploadFile = SPIFFS.open(filename, "w"); // Open the file for writing in SPIFFS (create if it doesn't exist)
SPIFFSupload = fsUploadFile ? 1 : 2;
//filename = String();
}
}
// handle file segments
else if (upload.status == UPLOAD_FILE_WRITE) {
#if USE_SW_WATCHDOG == 1
feedWatchdog(); // we get stuck here for a while, don't let the watchdog bite!
#endif
if(upload.totalSize) {
char JSON[64];
sprintf(JSON, "{\"progress\":%d}", upload.totalSize);
sendWebServerString(JSON); // feedback proper byte count of update to browser via websocket
}
int percent = 0;
if(_SuppliedFileSize)
percent = 100 * upload.totalSize / _SuppliedFileSize;
ShowOTAScreen(percent, eOTAbrowser); // browser update
DebugPort.print(".");
if(fsUploadFile) {
fsUploadFile.write(upload.buf, upload.currentSize); // Write the received bytes to the file
}
else {
if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
Update.printError(DebugPort);
}
}
}
// handle end of upload
else if (upload.status == UPLOAD_FILE_END) {
if(SPIFFSupload) {
if(fsUploadFile) {
fsUploadFile.close(); // Close the file again
DebugPort.printf("handleFileUpload Size: %d\r\n", upload.totalSize);
}
}
else {
if (Update.end(true)) { //true to set the size to the current progress
DebugPort.printf("Update Success: %u\r\nRebooting...\r\n", upload.totalSize);
} else {
Update.printError(DebugPort);
}
}
DebugPort.setDebugOutput(false);
bUpdateAccessed = false;
} else {
DebugPort.printf("Update Failed Unexpectedly (likely broken connection): status=%d\r\n", upload.status);
bUpdateAccessed = false;
}
}
else {
// attempt to POST without using /update - forced redirect to root
DebugPort.println("WEB: POST /updatenow forbidden entry");
rootRedirect();
}
});
#if USE_SPIFFS == 1
// NOTE: this serves the default home page, and favicon.ico
server.onNotFound([]()
{ // If the client requests any URI
if (!handleFileRead(server.uri())) { // send it if it exists
DebugPort.printf("WEB: NOT FOUND : %s\r\n", server.uri().c_str());
server.send(404, "text/plain", "404: Not Found"); // otherwise, respond with a 404 (Not Found) error
}
});
#else
server.onNotFound(handleBTCNotFound);
#endif
server.begin();
webSocket.begin();
webSocket.onEvent(webSocketEvent);
DebugPort.println("HTTP server started");
}
// called by main sketch loop()
bool doWebServer(void)
{
webSocket.loop();
server.handleClient();
return true;
}
bool isWebServerClientChange()
{
static int prevNumClients = -1;
int numClients = webSocket.connectedClients();
if(numClients != prevNumClients) {
prevNumClients = numClients;
DebugPort.println("Changed number of web clients, should reset JSON moderator");
return true;
}
return false;
}
bool sendWebServerString(const char* Str)
{
CProfile profile;
if(webSocket.connectedClients()) {
unsigned long tCon = profile.elapsed(true);
bTxWebData = true; // OLED tx data animation flag
webSocket.broadcastTXT(Str);
unsigned long tWeb = profile.elapsed(true);
DebugPort.printf("Websend times : %ld,%ld\r\n", tCon, tWeb);
return true;
}
return false;
}
void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length)
{
if (type == WStype_TEXT) {
bRxWebData = true;
char cmd[256];
memset(cmd, 0, 256);
for (int i = 0; i < length && i < 256; i++) {
cmd[i] = payload[i];
}
interpretJsonCommand(cmd); // send to the main heater controller decode routine
}
}
bool hasWebClientSpoken(bool reset)
{
bool retval = bRxWebData;
if(reset)
bRxWebData = false;
return retval;
}
bool hasWebServerSpoken(bool reset)
{
bool retval = bTxWebData;
if(reset)
bTxWebData = false;
return retval;
}
void setUploadSize(long val)
{
_SuppliedFileSize = val;
};