PrintBuddy is born

pull/125/head
Robert Stein 2020-12-15 14:50:38 +01:00
parent 912ef576c6
commit 21c08432cd
6 changed files with 284 additions and 129 deletions

View File

@ -10,8 +10,8 @@
/**
* Basic software settings
*/
#define VERSION "4.0"
#define HOSTNAME "PrintMon-"
#define VERSION "1.0"
#define HOSTNAME "PrintBuddy-"
#define CONFIG "/conf.txt"
// true = Enables debug message on terminal | false = disable all debug messages
#define DEBUG_MODE_ENABLE true

View File

@ -58,11 +58,11 @@ void OledDisplay::showBootScreen() {
this->oledDisplay->setTextAlignment(TEXT_ALIGN_CENTER);
this->oledDisplay->setContrast(255); // default is 255
this->oledDisplay->setFont(ArialMT_Plain_16);
this->oledDisplay->drawString(64, 1, "Printer Monitor");
this->oledDisplay->drawString(64, 1, "PrintBuddy");
this->oledDisplay->setFont(ArialMT_Plain_10);
this->oledDisplay->drawString(64, 18, "for " + this->globalDataController->getPrinterClient()->getPrinterType());
this->oledDisplay->setFont(ArialMT_Plain_16);
this->oledDisplay->drawString(64, 30, "By Qrome");
this->oledDisplay->drawString(64, 30, "By XXXXXX");
this->oledDisplay->drawString(64, 46, "V" + this->globalDataController->getVersion());
this->oledDisplay->display();
}
@ -158,7 +158,7 @@ void OledDisplay::checkDisplay() {
this->ui->setOverlays(clockOverlay, 1);
this->isClockOn = true;
} else if (printerClient->isPrinting() && !printerClient->isPSUoff() && this->isClockOn) {
this->debugController->printLn("Printer Monitor is active.");
this->debugController->printLn("PrintBuddy is active.");
this->ui->setFrames(frames, 3);
this->ui->setOverlays(overlays, 1);
this->ui->enableAutoTransition();

View File

@ -1,12 +1,6 @@
#include "WebServer.h"
static const char WEB_ACTIONS[] PROGMEM = "<a class='w3-bar-item w3-button' href='/'><i class='fa fa-home'></i> Home</a>"
"<a class='w3-bar-item w3-button' href='/configure'><i class='fa fa-cog'></i> Configure</a>"
"<a class='w3-bar-item w3-button' href='/configureweather'><i class='fa fa-cloud'></i> Weather</a>"
"<a class='w3-bar-item w3-button' href='/systemreset' onclick='return confirm(\"Do you want to reset to default settings?\")'><i class='fa fa-undo'></i> Reset Settings</a>"
"<a class='w3-bar-item w3-button' href='/forgetwifi' onclick='return confirm(\"Do you want to forget to WiFi connection?\")'><i class='fa fa-wifi'></i> Forget WiFi</a>"
"<a class='w3-bar-item w3-button' href='/update'><i class='fa fa-wrench'></i> Firmware Update</a>"
"<a class='w3-bar-item w3-button' href='https://github.com/Qrome' target='_blank'><i class='fa fa-question-circle'></i> About</a>";
String CHANGE_FORM = ""; // moved to config to make it dynamic
@ -111,9 +105,11 @@ void WebServer::setup() {
this->server->on("/updateweatherconfig", []() { obj->handleUpdateWeather(); });
this->server->on("/configure", []() { obj->handleConfigure(); });
this->server->on("/configureweather", []() { obj->handleWeatherConfigure(); });
this->server->on("/update", HTTP_GET, []() { obj->handleUpdatePage(); });
this->server->onNotFound([]() { obj->redirectHome(); });
this->serverUpdater->setup(this->server, "/update", this->globalDataController->getWebserverUsername(), this->globalDataController->getWebserverPassword());
// Start the server
this->server->begin();
this->debugController->printLn("Server started");
@ -146,16 +142,10 @@ void WebServer::redirectHome() {
}
void WebServer::displayPrinterStatus() {
this->globalDataController->ledOnOff(true);
BasePrinterClient *printerClient = this->globalDataController->getPrinterClient();
String html = "";
this->server->sendHeader("Cache-Control", "no-cache, no-store");
this->server->sendHeader("Pragma", "no-cache");
this->server->sendHeader("Expires", "-1");
this->server->setContentLength(CONTENT_LENGTH_UNKNOWN);
this->server->send(200, "text/html", "");
this->server->sendContent(String(getHeader(true)));
WebserverMemoryVariables::sendHeader(this->server, this->globalDataController, "Status", "Monitor", true);
String displayTime =
this->globalDataController->getTimeClient()->getAmPmHours() + ":" +
@ -259,10 +249,7 @@ void WebServer::displayPrinterStatus() {
html = ""; // fresh start
}
this->server->sendContent(String(getFooter()));
this->server->sendContent("");
this->server->client().stop();
this->globalDataController->ledOnOff(false);
WebserverMemoryVariables::sendFooter(this->server, this->globalDataController);
}
void WebServer::handleSystemReset() {
@ -383,21 +370,13 @@ void WebServer::handleConfigure() {
if (!this->authentication()) {
return this->server->requestAuthentication();
}
this->globalDataController->ledOnOff(true);
BasePrinterClient *printerClient = this->globalDataController->getPrinterClient();
String html = "";
this->server->sendHeader("Cache-Control", "no-cache, no-store");
this->server->sendHeader("Pragma", "no-cache");
this->server->sendHeader("Expires", "-1");
this->server->setContentLength(CONTENT_LENGTH_UNKNOWN);
this->server->send(200, "text/html", "");
WebserverMemoryVariables::sendHeader(this->server, this->globalDataController, "Configure", "Station");
html = this->getHeader();
this->server->sendContent(html);
// send javascript functions
if (printerClient->getPrinterType() == "Repetier") {
// send javascript functions for repetier server test
html = "<script>function testRepetier(){var e=document.getElementById(\"RepetierTest\"),r=document.getElementById(\"PrinterAddress\").value,"
"t=document.getElementById(\"PrinterPort\").value;if(\"\"==r||\"\"==t)return e.innerHTML=\"* Address and Port are required\","
"void(e.style.background=\"\");var n=\"http://\"+r+\":\"+t;n+=\"/printer/api/?a=listPrinter&apikey=\"+document.getElementById(\"PrinterApiKey\").value,"
@ -408,26 +387,59 @@ void WebServer::handleConfigure() {
"e.innerHTML=t,e.style.background=\"lime\"}else e.innerHTML=\"Error invalid API Key: \"+r.error,"
"e.style.background=\"red\"}else e.innerHTML=\"Error: \"+o.statusText,e.style.background=\"red\"},"
"o.onerror=function(){e.innerHTML=\"Error connecting to server -- check IP and Port\",e.style.background=\"red\"},o.send(null)}</script>";
this->server->sendContent(html);
}
else if (printerClient->getPrinterType() == "Klipper") {
// send javascript functions for klipper test
html = "<script>function testKlipper(){var e=document.getElementById(\"KlipperTest\"),t=document.getElementById(\"PrinterAddress\").value,"
"n=document.getElementById(\"PrinterPort\").value;if(e.innerHTML=\"\",\"\"==t||\"\"==n)return e.innerHTML=\"* Address and Port are required\","
"void(e.style.background=\"\");var r=\"http://\"+t+\":\"+n;r+=\"/printer/info\",window.open(r,\"_blank\").focus()}</script>";
this->server->sendContent(html);
}
else {
// send javascript functions for octoprint test
html = "<script>function testOctoPrint(){var e=document.getElementById(\"OctoPrintTest\"),t=document.getElementById(\"PrinterAddress\").value,"
"n=document.getElementById(\"PrinterPort\").value;if(e.innerHTML=\"\",\"\"==t||\"\"==n)return e.innerHTML=\"* Address and Port are required\","
"void(e.style.background=\"\");var r=\"http://\"+t+\":\"+n;r+=\"/api/job?apikey=\"+document.getElementById(\"PrinterApiKey\").value,window.open(r,\"_blank\").focus()}</script>";
this->server->sendContent(html);
// Let us create a form for all printers
html = "<form class='w3-container' action='/updateconfig' method='get'><h2>Station Config:</h2>";
html += "<table id='printerData' class='table table-striped table-condensed'></table><script>var target ='#printerData'; var customFields = { data: {";
this->server->sendContent(html);
for (int i=0; i<10; i++) {
html = "";
if (i > 0) {
html = ",";
}
html += String(i + 1) + ": { enabled:true, fieldName:'COMPANYCODE',prettyName:'Company Name', fieldType:'OEM', fieldValue:'' }";
this->server->sendContent(html);
}
html = "},";
html += "xref: { // Key ['Label','column width',0|1] 1=admin only";
html += " custom: ['#','10%',0],";
html += " actions: ['Actions','5%',1],";
html += " enabled: ['Enabled','5%',0],";
html += " fieldName: ['Field','20%',0],";
html += " prettyName: ['Pretty Name','20%',0],";
html += " fieldType: ['Type','30%',0],";
html += " fieldValue: ['Value','10%',0]";
html += "},";
html += "fieldTypes: [";
html += " [' ',' '],";
html += " ['ENV','Environment (ENV)'],";
html += " ['REG','Registry (REG)'],";
html += " ['WMI','Windows Management Inst. (WMI)'],";
html += " ['OEM','Original Equipment Manuf.(OEM)']";
html += "],";
html += "admin:true,";
html += "multiedit:false";
html += "}";
html += "</script>";
this->server->sendContent(html);
String form = "<form class='w3-container' action='/updateconfig' method='get'><h2>Station Config:</h2>";
String form = "";
if (printerClient->getPrinterType() != "Klipper") {
form += "<p><label>" + printerClient->getPrinterType() + " API Key (get from your server)</label>"
"<input class='w3-input w3-border w3-margin-bottom' type='text' name='PrinterApiKey' id='PrinterApiKey' value='%PRINTERAPIKEY%' maxlength='60'></p>";
@ -508,28 +520,36 @@ void WebServer::handleConfigure() {
this->server->sendContent(form);
html = this->getFooter();
this->server->sendContent(html);
this->server->sendContent("");
this->server->client().stop();
this->globalDataController->ledOnOff(false);
WebserverMemoryVariables::sendFooter(this->server, this->globalDataController);
}
/**
* @brief Send weather configuration page to client
*/
void WebServer::handleWeatherConfigure() {
if (!this->authentication()) {
return this->server->requestAuthentication();
}
this->globalDataController->ledOnOff(true);
WebserverMemoryVariables::sendHeader(this->server, this->globalDataController, "Configure", "Weather");
String html = "";
this->server->sendHeader("Cache-Control", "no-cache, no-store");
this->server->sendHeader("Pragma", "no-cache");
this->server->sendHeader("Expires", "-1");
this->server->setContentLength(CONTENT_LENGTH_UNKNOWN);
this->server->send(200, "text/html", "");
html = getHeader();
this->server->sendContent(html);
String form = FPSTR(WEATHER_FORM);
String isWeatherChecked = "";
@ -550,61 +570,18 @@ void WebServer::handleWeatherConfigure() {
form.replace("%LANGUAGEOPTIONS%", options);
this->server->sendContent(form);
html = this->getFooter();
this->server->sendContent(html);
this->server->sendContent("");
this->server->client().stop();
this->globalDataController->ledOnOff(false);
WebserverMemoryVariables::sendFooter(this->server, this->globalDataController);
}
String WebServer::getHeader() {
return this->getHeader(false);
/**
* @brief Send firmware/filesystem update page to client
*/
void WebServer::handleUpdatePage() {
if (!this->authentication()) {
return this->server->requestAuthentication();
}
String WebServer::getHeader(boolean refresh) {
String menu = FPSTR(WEB_ACTIONS);
String html = "<!DOCTYPE HTML>";
html += "<html><head><title>Printer Monitor</title><link rel='icon' href='data:;base64,='>";
html += "<meta charset='UTF-8'>";
html += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
if (refresh) {
html += "<meta http-equiv=\"refresh\" content=\"30\">";
}
html += "<link rel='stylesheet' href='https://www.w3schools.com/w3css/4/w3.css'>";
html += "<link rel='stylesheet' href='https://www.w3schools.com/lib/w3-theme-" + this->globalDataController->getWebserverTheme() + ".css'>";
html += "<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css'>";
html += "</head><body>";
html += "<nav class='w3-sidebar w3-bar-block w3-card' style='margin-top:88px' id='mySidebar'>";
html += "<div class='w3-container w3-theme-d2'>";
html += "<span onclick='closeSidebar()' class='w3-button w3-display-topright w3-large'><i class='fa fa-times'></i></span>";
html += "<div class='w3-cell w3-left w3-xxxlarge' style='width:60px'><i class='fa fa-cube'></i></div>";
html += "<div class='w3-padding'>Menu</div></div>";
html += menu;
html += "</nav>";
html += "<header class='w3-top w3-bar w3-theme'><button class='w3-bar-item w3-button w3-xxxlarge w3-hover-theme' onclick='openSidebar()'><i class='fa fa-bars'></i></button><h2 class='w3-bar-item'>Printer Monitor</h2></header>";
html += "<script>";
html += "function openSidebar(){document.getElementById('mySidebar').style.display='block'}function closeSidebar(){document.getElementById('mySidebar').style.display='none'}closeSidebar();";
html += "</script>";
html += "<br><div class='w3-container w3-large' style='margin-top:88px'>";
return html;
}
String WebServer::getFooter() {
int8_t rssi = this->globalDataController->getWifiQuality();
Serial.print("Signal Strength (RSSI): ");
Serial.print(rssi);
this->debugController->printLn("%");
String html = "<br><br><br>";
html += "</div>";
html += "<footer class='w3-container w3-bottom w3-theme w3-margin-top'>";
if (this->globalDataController->getLastReportStatus() != "") {
html += "<i class='fa fa-external-link'></i> Report Status: " + this->globalDataController->getLastReportStatus() + "<br>";
}
html += "<i class='fa fa-paper-plane-o'></i> Version: " + String(VERSION) + "<br>";
html += "<i class='fa fa-rss'></i> Signal Strength: ";
html += String(rssi) + "%";
html += "</footer>";
html += "</body></html>";
return html;
WebserverMemoryVariables::sendHeader(this->server, this->globalDataController, "Firmware", "Update");
WebserverMemoryVariables::sendUpdateForm(this->server, this->globalDataController);
WebserverMemoryVariables::sendFooter(this->server, this->globalDataController);
}

View File

@ -1,11 +1,11 @@
#pragma once
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266HTTPUpdateServer.h>
#include <WiFiManager.h>
#include <ESP8266mDNS.h>
#include "../Global/GlobalDataController.h"
#include "WebserverMemoryVariables.h"
class WebServer {
private:
@ -29,7 +29,5 @@ public:
void handleUpdateWeather();
void handleConfigure();
void handleWeatherConfigure();
String getHeader();
String getHeader(boolean refresh);
String getFooter();
void handleUpdatePage();
};

View File

@ -0,0 +1,111 @@
#include "WebserverMemoryVariables.h"
/**
* @brief Send out header for webpage
* @param server Send out instancce
* @param globalDataController Access to global data
*/
void WebserverMemoryVariables::sendHeader(ESP8266WebServer *server, GlobalDataController *globalDataController, String pageLabel, String pageTitle) {
WebserverMemoryVariables::sendHeader(server, globalDataController, pageLabel, pageTitle, false);
}
/**
* @brief Send out header for webpage
* @param server Send out instancce
* @param globalDataController Access to global data
* @param pageLabel Title label
* @param pageTitle Title
* @param refresh if true, auto refresh in header is set
*/
void WebserverMemoryVariables::sendHeader(
ESP8266WebServer *server,
GlobalDataController *globalDataController,
String pageLabel,
String pageTitle,
boolean refresh
) {
globalDataController->ledOnOff(true);
int8_t rssi = globalDataController->getWifiQuality();
server->sendHeader("Cache-Control", "no-cache, no-store");
server->sendHeader("Pragma", "no-cache");
server->sendHeader("Expires", "-1");
server->setContentLength(CONTENT_LENGTH_UNKNOWN);
server->send(200, "text/html", "");
server->sendContent("<!DOCTYPE HTML>"
"<html><head><title>PrintBuddy</title><link rel='icon' href='data:;base64,='>"
"<meta charset='UTF-8'>"
"<meta name='viewport' content='width=device-width, initial-scale=1'>"
);
if (refresh) {
server->sendContent("<meta http-equiv=\"refresh\" content=\"30\">");
}
server->sendContent("<link rel='stylesheet' href='https://www.w3schools.com/w3css/4/w3.css'>"
"<link rel='stylesheet' href='https://unpkg.com/carbon-components/css/carbon-components.min.css'></style>"
"<link rel='stylesheet' href='https://use.fontawesome.com/releases/v5.15.1/css/all.css'>"
"<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js'></script>"
"<style>.hidden{display:none}</style>"
"</head><body>"
"<header class='cv-header bx--header'>"
"<a href='/' class='cv-header-name bx--header__name'>"
);
server->sendContent("<span class='bx--header__name--prefix'>PrintBuddy&nbsp;</span>V" + String(VERSION));
server->sendContent("</a>"
"<nav class='cv-header-nav bx--header__nav'></nav>"
"<div class='bx--header__global'>"
"<button type='button' class='cv-header-global-action bx--header__action' onclick='openWifiInfo()'>"
"<i class='fas fa-wifi' style='color: white; font-size: 20px;'></i>"
"</button>"
"<button type='button' class='cv-header-global-action bx--header__action' onclick='openSidebar()'>"
"<svg focusable='false' preserveAspectRatio='xMidYMid meet' xmlns='http://www.w3.org/2000/svg' fill='currentColor' width='20' height='20' viewBox='0 0 32 32' aria-hidden='true'>"
"<path d='M14 4H18V8H14zM4 4H8V8H4zM24 4H28V8H24zM14 14H18V18H14zM4 14H8V18H4zM24 14H28V18H24zM14 24H18V28H14zM4 24H8V28H4zM24 24H28V28H24z'></path>"
"</svg>"
"</button>"
"</div>"
"<div id='sidebar' class='cv-header-panel bx--header-panel'>"
"<ul class='cv-switcher bx--switcher__item'>"
);
server->sendContent(String(FPSTR(MENUE_ITEMS)));
server->sendContent("</ul>"
"</div>"
"<div class='bx--toast-notification bx--toast-notification--info hidden' style='position: absolute; right: -16px; top: 40px;' id='wifiinfo'>"
"<div class='bx--toast-notification__details'>"
"<h3 class='bx--toast-notification__title'>WiFi Signal Strength</h3>"
"<div class='bx--toast-notification__subtitle'><span>"
);
server->sendContent(String(rssi) + "%");
server->sendContent("</span></div>"
"</div>"
"</div>"
"</header>"
"<script>function openSidebar(){document.getElementById('sidebar').classList.toggle('bx--header-panel--expanded');document.getElementById('wifiinfo').classList.add('hidden');};function openWifiInfo(){document.getElementById('sidebar').classList.remove('bx--header-panel--expanded');document.getElementById('wifiinfo').classList.toggle('hidden');}</script>"
"<br><div class='bx--grid bx--grid--full-width' style='margin-top:60px'>"
"<div class='page-header' style='margin-bottom:20px'><h4 class='page-header__label'>");
server->sendContent(pageLabel);
server->sendContent("</h4><h1 id='page-title' class='page-header__title'>");
server->sendContent(pageTitle);
server->sendContent("</h1></div><div class='bx--row'>");
}
/**
* @brief send out footer content for webpage
* @param server Send out instancce
* @param globalDataController Access to global data
*/
void WebserverMemoryVariables::sendFooter(ESP8266WebServer *server, GlobalDataController *globalDataController) {
server->sendContent("<br><br><br></div></div>");
server->sendContent("<script src='https://unpkg.com/carbon-components/scripts/carbon-components.min.js'></script></body></html>");
server->sendContent("");
server->client().stop();
globalDataController->ledOnOff(false);
}
/**
* @brief Send out upload form for updates
* @param server Send out instancce
* @param globalDataController Access to global data
*/
void WebserverMemoryVariables::sendUpdateForm(ESP8266WebServer *server, GlobalDataController *globalDataController) {
server->sendContent(FPSTR(UPDATE_FORM));
}

View File

@ -0,0 +1,69 @@
#pragma once
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include "../Global/GlobalDataController.h"
/**
* Webpage side menu right for main items
*/
static const char MENUE_ITEMS[] PROGMEM =
"<li class='cv-switcher-item bx--switcher__item'><a class='cv-switcher-item-link bx--switcher__item-link' href='/'><i class='fa fa-home'></i> Home</a></li>"
"<li class='cv-switcher-item bx--switcher__item'><a class='cv-switcher-item-link bx--switcher__item-link' href='/configure'><i class='fa fa-cog'></i> Configure</a></li>"
"<li class='cv-switcher-item bx--switcher__item'><a class='cv-switcher-item-link bx--switcher__item-link' href='/configureweather'><i class='fa fa-cloud'></i> Weather</a></li>"
"<li class='cv-switcher-item bx--switcher__item'><a class='cv-switcher-item-link bx--switcher__item-link' href='/systemreset' onclick='return confirm(\"Do you want to reset to default settings?\")'><i class='fa fa-undo'></i> Reset Settings</a></li>"
"<li class='cv-switcher-item bx--switcher__item'><a class='cv-switcher-item-link bx--switcher__item-link' href='/forgetwifi' onclick='return confirm(\"Do you want to forget to WiFi connection?\")'><i class='fa fa-wifi'></i> Forget WiFi</a></li>"
"<li class='cv-switcher-item bx--switcher__item'><a class='cv-switcher-item-link bx--switcher__item-link' href='/update'><i class='fa fa-wrench'></i> Firmware Update</a></li>"
"<li class='cv-switcher-item bx--switcher__item'><a class='cv-switcher-item-link bx--switcher__item-link' href='https://github.com/Qrome' target='_blank'><i class='fa fa-question-circle'></i> About</a></li>";
/**
* Controls for update firmware/filesystem
*/
static const char UPDATE_FORM[] PROGMEM = "<div class='bx--col-md-4'>"
"<form method='POST' action='' enctype='multipart/form-data'>"
"<div class='cv-file-uploader cv-form-item bx--form-item'>"
"<strong class='bx--file--label'>Update Firmware</strong>"
"<p class='bx--label-description'>Select the firmware you want to upload</p>"
"<div data-file='' class='bx--file'>"
"<label for='firmware' role='button' tabindex='0' class='bx--file-browse-btn'>"
"<div data-file-drop-container='' class='bx--file__drop-container'>"
"Drag and drop file here or upload"
"<input type='file' id='firmware' accept='.bin,.bin.gz' class='bx--file-input' name='firmware' onchange='document.getElementById(\"ffile\").innerHTML = \"\"'>"
"</div>"
"</label>"
"<div data-file-container='' class='bx--file-container' id='ffile'></div>"
"</div>"
"</div>"
"<input type='submit' value='Update Firmware' class='bx--btn bx--btn--danger'>"
"</form>"
"</div>"
"<div class='bx--col-md-4'>"
"<form method='POST' action='' enctype='multipart/form-data'>"
"<div class='cv-file-uploader cv-form-item bx--form-item'>"
"<strong class='bx--file--label'>Update FileSystem</strong>"
"<p class='bx--label-description'>Select the filesystem you want to upload</p>"
"<div data-file='' class='bx--file'>"
"<label for='filesystem' role='button' tabindex='0' class='bx--file-browse-btn'>"
"<div data-file-drop-container='' class='bx--file__drop-container'>"
"Drag and drop file here or upload"
"<input type='file' id='filesystem' accept='.bin,.bin.gz' class='bx--file-input' name='filesystem' onchange='document.getElementById(\"fsys\").innerHTML = \"\"'>"
"</div>"
"</label>"
"<div data-file-container='' class='bx--file-container' id='fsys'></div>"
"</div>"
"</div>"
"<input type='submit' value='Update FileSystem' class='bx--btn bx--btn--danger'>"
"</form>"
"</div>";
/**
* @brief Class to generate HTML content from Memory
*/
class WebserverMemoryVariables {
public:
static void sendHeader(ESP8266WebServer *server, GlobalDataController *globalDataController, String pageLabel, String pageTitle);
static void sendHeader(ESP8266WebServer *server, GlobalDataController *globalDataController, String pageLabel, String pageTitle, boolean refresh);
static void sendFooter(ESP8266WebServer *server, GlobalDataController *globalDataController);
static void sendUpdateForm(ESP8266WebServer *server, GlobalDataController *globalDataController);
};