diff --git a/src/Clients/BasePrinterClient.h b/src/Clients/BasePrinterClient.h index 64f20d1..178c756 100644 --- a/src/Clients/BasePrinterClient.h +++ b/src/Clients/BasePrinterClient.h @@ -11,10 +11,13 @@ #pragma once #include #include -#include "../Global/GlobalDataController.h" class BasePrinterClient { public: + virtual void getPrinterJobResults(); + virtual void getPrinterPsuState(); + virtual void updatePrintClient(); + virtual String getAveragePrintTime() = 0; virtual String getEstimatedPrintTime() = 0; virtual String getFileName() = 0; diff --git a/src/Clients/KlipperClient.cpp b/src/Clients/KlipperClient.cpp index a6ae3bd..510d6a6 100644 --- a/src/Clients/KlipperClient.cpp +++ b/src/Clients/KlipperClient.cpp @@ -1,8 +1,8 @@ #include "KlipperClient.h" -KlipperClient::KlipperClient(GlobalDataController *globalDataController) { +KlipperClient::KlipperClient(GlobalDataController *globalDataController, DebugController *debugController) { this->globalDataController = globalDataController; - this->globalDataController->setPrinterClientType(this->getPrinterType()); + this->debugController = debugController; this->updatePrintClient(); } @@ -38,8 +38,8 @@ WiFiClient KlipperClient::getSubmitRequest(String apiGetData) { WiFiClient printClient; printClient.setTimeout(5000); - this->globalDataController->debugPrintLn("Getting Klipper Data via GET"); - this->globalDataController->debugPrintLn(apiGetData); + this->debugController->printLn("Getting Klipper Data via GET"); + this->debugController->printLn(apiGetData); result = ""; if (printClient.connect(myServer, myPort)) { //starts client connection, checks for connection printClient.println(apiGetData); @@ -52,16 +52,16 @@ WiFiClient KlipperClient::getSubmitRequest(String apiGetData) { printClient.println("User-Agent: ArduinoWiFi/1.1"); printClient.println("Connection: close"); if (printClient.println() == 0) { - this->globalDataController->debugPrintLn("Connection to " + String(myServer) + ":" + String(myPort) + " failed."); - this->globalDataController->debugPrintLn(""); + this->debugController->printLn("Connection to " + String(myServer) + ":" + String(myPort) + " failed."); + this->debugController->printLn(""); resetPrintData(); printerData.error = "Connection to " + String(myServer) + ":" + String(myPort) + " failed."; return printClient; } } else { - this->globalDataController->debugPrintLn("Connection to Klipper failed: " + String(myServer) + ":" + String(myPort)); //error message if no client connect - this->globalDataController->debugPrintLn(""); + this->debugController->printLn("Connection to Klipper failed: " + String(myServer) + ":" + String(myPort)); //error message if no client connect + this->debugController->printLn(""); resetPrintData(); printerData.error = "Connection to Klipper failed: " + String(myServer) + ":" + String(myPort); return printClient; @@ -71,8 +71,8 @@ WiFiClient KlipperClient::getSubmitRequest(String apiGetData) { char status[32] = {0}; printClient.readBytesUntil('\r', status, sizeof(status)); if (strcmp(status, "HTTP/1.1 200 OK") != 0 && strcmp(status, "HTTP/1.1 409 CONFLICT") != 0) { - this->globalDataController->debugPrintLn("Unexpected response: "); - this->globalDataController->debugPrintLn(status); + this->debugController->printLn("Unexpected response: "); + this->debugController->printLn(status); printerData.state = ""; printerData.error = "Response: " + String(status); return printClient; @@ -81,7 +81,7 @@ WiFiClient KlipperClient::getSubmitRequest(String apiGetData) { // Skip HTTP headers char endOfHeaders[] = "\r\n\r\n"; if (!printClient.find(endOfHeaders)) { - this->globalDataController->debugPrintLn("Invalid response"); + this->debugController->printLn("Invalid response"); printerData.error = "Invalid response from " + String(myServer) + ":" + String(myPort); printerData.state = ""; } @@ -93,8 +93,8 @@ WiFiClient KlipperClient::getPostRequest(String apiPostData, String apiPostBody) WiFiClient printClient; printClient.setTimeout(5000); - this->globalDataController->debugPrintLn("Getting Klipper Data via POST"); - this->globalDataController->debugPrintLn(apiPostData + " | " + apiPostBody); + this->debugController->printLn("Getting Klipper Data via POST"); + this->debugController->printLn(apiPostData + " | " + apiPostBody); result = ""; if (printClient.connect(myServer, myPort)) { //starts client connection, checks for connection printClient.println(apiPostData); @@ -112,16 +112,16 @@ WiFiClient KlipperClient::getPostRequest(String apiPostData, String apiPostBody) printClient.println(); printClient.println(apiPostBody); if (printClient.println() == 0) { - this->globalDataController->debugPrintLn("Connection to " + String(myServer) + ":" + String(myPort) + " failed."); - this->globalDataController->debugPrintLn(""); + this->debugController->printLn("Connection to " + String(myServer) + ":" + String(myPort) + " failed."); + this->debugController->printLn(""); resetPrintData(); printerData.error = "Connection to " + String(myServer) + ":" + String(myPort) + " failed."; return printClient; } } else { - this->globalDataController->debugPrintLn("Connection to Klipper failed: " + String(myServer) + ":" + String(myPort)); //error message if no client connect - this->globalDataController->debugPrintLn(""); + this->debugController->printLn("Connection to Klipper failed: " + String(myServer) + ":" + String(myPort)); //error message if no client connect + this->debugController->printLn(""); resetPrintData(); printerData.error = "Connection to Klipper failed: " + String(myServer) + ":" + String(myPort); return printClient; @@ -131,8 +131,8 @@ WiFiClient KlipperClient::getPostRequest(String apiPostData, String apiPostBody) char status[32] = {0}; printClient.readBytesUntil('\r', status, sizeof(status)); if (strcmp(status, "HTTP/1.1 200 OK") != 0 && strcmp(status, "HTTP/1.1 409 CONFLICT") != 0) { - this->globalDataController->debugPrint("Unexpected response: "); - this->globalDataController->debugPrintLn(status); + this->debugController->print("Unexpected response: "); + this->debugController->printLn(status); printerData.state = ""; printerData.error = "Response: " + String(status); return printClient; @@ -141,7 +141,7 @@ WiFiClient KlipperClient::getPostRequest(String apiPostData, String apiPostBody) // Skip HTTP headers char endOfHeaders[] = "\r\n\r\n"; if (!printClient.find(endOfHeaders)) { - this->globalDataController->debugPrintLn("Invalid response"); + this->debugController->printLn("Invalid response"); printerData.error = "Invalid response from " + String(myServer) + ":" + String(myPort); printerData.state = ""; } @@ -165,7 +165,7 @@ void KlipperClient::getPrinterJobResults() { // Parse JSON object DeserializationError error = deserializeJson(jsonBuffer, printClient); if (error) { - this->globalDataController->debugPrintLn("Klipper Data Parsing failed: " + String(myServer) + ":" + String(myPort)); + this->debugController->printLn("Klipper Data Parsing failed: " + String(myServer) + ":" + String(myPort)); printerData.error = "Klipper Data Parsing failed: " + String(myServer) + ":" + String(myPort); printerData.state = ""; return; @@ -184,9 +184,9 @@ void KlipperClient::getPrinterJobResults() { printerData.state = (const char*)jsonBuffer["result"]["status"]["print_stats"]["state"]; if (isOperational()) { - this->globalDataController->debugPrintLn("Status: " + printerData.state); + this->debugController->printLn("Status: " + printerData.state); } else { - this->globalDataController->debugPrintLn("Printer Not Operational"); + this->debugController->printLn("Printer Not Operational"); } //**** get the Printer Temps and Stat @@ -221,7 +221,7 @@ void KlipperClient::getPrinterJobResults() { printerData.bedTargetTemp = (const char*)jsonBuffer2["result"]["status"]["heater_bed"]["target"]; if (isPrinting()) { - this->globalDataController->debugPrintLn("Status: " + printerData.state + " " + printerData.fileName + "(" + printerData.progressCompletion + "%)"); + this->debugController->printLn("Status: " + printerData.state + " " + printerData.fileName + "(" + printerData.progressCompletion + "%)"); } } diff --git a/src/Clients/KlipperClient.h b/src/Clients/KlipperClient.h index 3d6f743..fd077a1 100644 --- a/src/Clients/KlipperClient.h +++ b/src/Clients/KlipperClient.h @@ -4,6 +4,7 @@ #include #include "Debug.h" #include "BasePrinterClient.h" +#include "../Global/GlobalDataController.h" class KlipperClient : public BasePrinterClient { private: @@ -45,9 +46,10 @@ private: PrinterStruct printerData; GlobalDataController *globalDataController; + DebugController *debugController; public: - KlipperClient(GlobalDataController *globalDataController); + KlipperClient(GlobalDataController *globalDataController, DebugController *debugController); void getPrinterJobResults(); void getPrinterPsuState(); void updatePrintClient(); diff --git a/src/Display/OledDisplay.cpp b/src/Display/OledDisplay.cpp index ab34126..d677d58 100644 --- a/src/Display/OledDisplay.cpp +++ b/src/Display/OledDisplay.cpp @@ -1,7 +1,8 @@ #include "OledDisplay.h" -OledDisplay::OledDisplay(OLEDDisplay *oledDisplay, GlobalDataController *globalDataController) { +OledDisplay::OledDisplay(OLEDDisplay *oledDisplay, GlobalDataController *globalDataController, DebugController *debugController) { this->globalDataController = globalDataController; + this->debugController = debugController; this->oledDisplay = oledDisplay; this->ui = new OLEDDisplayUi(oledDisplay); } @@ -47,7 +48,7 @@ void OledDisplay::showBootScreen() { this->oledDisplay->setFont(ArialMT_Plain_16); this->oledDisplay->drawString(64, 1, "Printer Monitor"); this->oledDisplay->setFont(ArialMT_Plain_10); - this->oledDisplay->drawString(64, 18, "for " + this->globalDataController->getPrinterClientType()); + 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, 46, "V" + this->globalDataController->getVersion()); @@ -70,7 +71,7 @@ void OledDisplay::showApAccessScreen(String apSsid, String apIp) { void OledDisplay::showWebserverSplashScreen(bool isEnabled) { if (isEnabled) { String webAddress = "http://" + WiFi.localIP().toString() + ":" + String(this->globalDataController->getWebserverPort()) + "/"; - this->globalDataController->debugPrintLn("Use this URL : " + webAddress); + this->debugController->printLn("Use this URL : " + webAddress); this->oledDisplay->clear(); this->oledDisplay->setTextAlignment(TEXT_ALIGN_CENTER); this->oledDisplay->setFont(ArialMT_Plain_10); @@ -81,7 +82,7 @@ void OledDisplay::showWebserverSplashScreen(bool isEnabled) { this->oledDisplay->drawString(64, 46, "Port: " + String(this->globalDataController->getWebserverPort())); this->oledDisplay->display(); } else { - this->globalDataController->debugPrintLn("Web Interface is Disabled"); + this->debugController->printLn("Web Interface is Disabled"); this->oledDisplay->clear(); this->oledDisplay->setTextAlignment(TEXT_ALIGN_CENTER); this->oledDisplay->setFont(ArialMT_Plain_10); diff --git a/src/Display/OledDisplay.h b/src/Display/OledDisplay.h index e837f34..e7c0315 100644 --- a/src/Display/OledDisplay.h +++ b/src/Display/OledDisplay.h @@ -8,6 +8,7 @@ class OledDisplay { private: GlobalDataController *globalDataController; + DebugController *debugController; OLEDDisplay *oledDisplay; OLEDDisplayUi *ui; @@ -18,7 +19,7 @@ private: OverlayCallback clockOverlay[1]; public: - OledDisplay(OLEDDisplay *oledDisplay, GlobalDataController *globalDataController); + OledDisplay(OLEDDisplay *oledDisplay, GlobalDataController *globalDataController, DebugController *debugController); void preSetup(); void postSetup(); void showBootScreen(); diff --git a/src/Global/DebugController.cpp b/src/Global/DebugController.cpp new file mode 100644 index 0000000..c84a23c --- /dev/null +++ b/src/Global/DebugController.cpp @@ -0,0 +1,40 @@ +#include "DebugController.h" + + +void DebugController::setup() { + Serial.begin(115200); + delay(10); + Serial.println("Debugger started"); +} + +void DebugController::print(const char *data) { + Serial.print(data); +} + +void DebugController::print(String data) { + Serial.print(data); +} + +void DebugController::print(int8_t data) { + Serial.print(data); +} + +void DebugController::printF(const char *data, unsigned int uInt) { + Serial.printf(data, uInt); +} + +void DebugController::printLn(const char *data) { + Serial.println(data); +} + +void DebugController::printLn(String data) { + Serial.println(data); +} + +void DebugController::printLn(long int data) { + Serial.println(data); +} + +void DebugController::printLn(int8_t data) { + Serial.println(data); +} \ No newline at end of file diff --git a/src/Global/DebugController.h b/src/Global/DebugController.h new file mode 100644 index 0000000..4000161 --- /dev/null +++ b/src/Global/DebugController.h @@ -0,0 +1,18 @@ +#pragma once +#include +#include +#include "Configuration.h" + +class DebugController { +public: + void setup(); + + void print(const char *data); + void print(String data); + void print(int8_t data); + void printF(const char *data, unsigned int uInt); + void printLn(const char *data); + void printLn(String data); + void printLn(long int data); + void printLn(int8_t data); +}; diff --git a/src/Global/GlobalDataController.cpp b/src/Global/GlobalDataController.cpp index 31244f8..c314042 100644 --- a/src/Global/GlobalDataController.cpp +++ b/src/Global/GlobalDataController.cpp @@ -1,31 +1,29 @@ #include "GlobalDataController.h" -GlobalDataController::GlobalDataController() { - +GlobalDataController::GlobalDataController(TimeClient *timeClient, OpenWeatherMapClient *weatherClient, DebugController *debugController) { + this->timeClient = timeClient; + this->weatherClient = weatherClient; + this->debugController = debugController; } - - void GlobalDataController::setup() { - Serial.begin(115200); - delay(10); - Serial.println("Debugger started"); + this->listSettingFiles(); this->readSettings(); } void GlobalDataController::listSettingFiles() { - this->debugPrintLn("========= FileSystem Files ================="); + this->debugController->printLn("========= FileSystem Files ================="); Dir dir = LittleFS.openDir("/"); while (dir.next()) { - this->debugPrintLn(dir.fileName()); + this->debugController->printLn(dir.fileName()); } } void GlobalDataController::readSettings() { if (LittleFS.exists(CONFIG) == false) { - this->debugPrintLn("Settings File does not yet exists."); + this->debugController->printLn("Settings File does not yet exists."); writeSettings(); return; } @@ -37,145 +35,134 @@ void GlobalDataController::readSettings() { if (line.indexOf("printerApiKey=") >= 0) { this->PrinterApiKey = line.substring(line.lastIndexOf("printerApiKey=") + 14); this->PrinterApiKey.trim(); - this->debugPrintLn("PrinterApiKey=" + this->PrinterApiKey); + this->debugController->printLn("PrinterApiKey=" + this->PrinterApiKey); } if (line.indexOf("printerHostName=") >= 0) { this->PrinterHostName = line.substring(line.lastIndexOf("printerHostName=") + 16); this->PrinterHostName.trim(); - this->debugPrintLn("PrinterHostName=" + this->PrinterHostName); + this->debugController->printLn("PrinterHostName=" + this->PrinterHostName); } if (line.indexOf("printerServer=") >= 0) { this->PrinterServer = line.substring(line.lastIndexOf("printerServer=") + 14); this->PrinterServer.trim(); - this->debugPrintLn("PrinterServer=" + this->PrinterServer); + this->debugController->printLn("PrinterServer=" + this->PrinterServer); } if (line.indexOf("printerPort=") >= 0) { this->PrinterPort = line.substring(line.lastIndexOf("printerPort=") + 12).toInt(); - this->debugPrintLn("PrinterPort=" + String(this->PrinterPort)); + this->debugController->printLn("PrinterPort=" + String(this->PrinterPort)); } - /*if (line.indexOf("printerName=") >= 0) { + if (line.indexOf("printerName=") >= 0) { String printer = line.substring(line.lastIndexOf("printerName=") + 12); printer.trim(); - printerClient.setPrinterName(printer); - this->debugPrintLn("PrinterName=" + printerClient.getPrinterName()); - }*/ + this->getPrinterClient()->setPrinterName(printer); + this->debugController->printLn("PrinterName=" + this->getPrinterClient()->getPrinterName()); + } if (line.indexOf("printerAuthUser=") >= 0) { this->PrinterAuthUser = line.substring(line.lastIndexOf("printerAuthUser=") + 16); this->PrinterAuthUser.trim(); - this->debugPrintLn("PrinterAuthUser=" + this->PrinterAuthUser); + this->debugController->printLn("PrinterAuthUser=" + this->PrinterAuthUser); } if (line.indexOf("printerAuthPass=") >= 0) { this->PrinterAuthPass = line.substring(line.lastIndexOf("printerAuthPass=") + 16); this->PrinterAuthPass.trim(); - this->debugPrintLn("PrinterAuthPass=" + this->PrinterAuthPass); + this->debugController->printLn("PrinterAuthPass=" + this->PrinterAuthPass); } if (line.indexOf("printerHasPsu=") >= 0) { this->PrinterHasPsu = line.substring(line.lastIndexOf("printerHasPsu=") + 14).toInt(); - this->debugPrintLn("PrinterHasPsu=" + String(this->PrinterHasPsu)); + this->debugController->printLn("PrinterHasPsu=" + String(this->PrinterHasPsu)); } #ifndef USE_NEXTION_DISPLAY if(line.indexOf("displayInvertDisplay=") >= 0) { this->DisplayInvertDisplay = line.substring(line.lastIndexOf("displayInvertDisplay=") + 21).toInt(); - this->debugPrintLn("DisplayInvertDisplay=" + String(this->DisplayInvertDisplay)); + this->debugController->printLn("DisplayInvertDisplay=" + String(this->DisplayInvertDisplay)); } #endif if (line.indexOf("webserverTheme=") >= 0) { this->WebserverTheme = line.substring(line.lastIndexOf("webserverTheme=") + 15); this->WebserverTheme.trim(); - this->debugPrintLn("webserverTheme=" + this->WebserverTheme); + this->debugController->printLn("webserverTheme=" + this->WebserverTheme); } if (line.indexOf("webserverIsBasicAuth=") >= 0) { this->WebserverIsBasicAuth = line.substring(line.lastIndexOf("webserverIsBasicAuth=") + 21).toInt(); - this->debugPrintLn("webserverIsBasicAuth=" + String(this->WebserverIsBasicAuth)); + this->debugController->printLn("webserverIsBasicAuth=" + String(this->WebserverIsBasicAuth)); } if (line.indexOf("webserverUsername=") >= 0) { this->WebserverUsername = line.substring(line.lastIndexOf("webserverUsername=") + 18); this->WebserverUsername.trim(); - this->debugPrintLn("webserverUsername=" + this->WebserverUsername); + this->debugController->printLn("webserverUsername=" + this->WebserverUsername); } if (line.indexOf("webserverPassword=") >= 0) { this->WebserverPassword = line.substring(line.lastIndexOf("webserverPassword=") + 18); this->WebserverPassword.trim(); - this->debugPrintLn("webserverPassword=" + this->WebserverPassword); + this->debugController->printLn("webserverPassword=" + this->WebserverPassword); } - - - - - /* - if (line.indexOf("UtcOffset=") >= 0) { - UtcOffset = line.substring(line.lastIndexOf("UtcOffset=") + 10).toFloat(); - debugHandle->printLn("UtcOffset=" + String(UtcOffset)); + if (line.indexOf("clockUtcOffset=") >= 0) { + this->ClockUtcOffset = line.substring(line.lastIndexOf("clockUtcOffset=") + 15).toFloat(); + this->debugController->printLn("clockUtcOffset=" + String(this->ClockUtcOffset)); } - - - - if (line.indexOf("refreshRate=") >= 0) { - minutesBetweenDataRefresh = line.substring(line.lastIndexOf("refreshRate=") + 12).toInt(); - debugHandle->printLn("minutesBetweenDataRefresh=" + String(minutesBetweenDataRefresh)); + if (line.indexOf("clockResyncMinutes=") >= 0) { + this->ClockResyncMinutes = line.substring(line.lastIndexOf("clockResyncMinutes=") + 19).toInt(); + this->debugController->printLn("clockResyncMinutes=" + String(this->ClockResyncMinutes)); } - - if (line.indexOf("DISPLAYCLOCK=") >= 0) { - DISPLAYCLOCK = line.substring(line.lastIndexOf("DISPLAYCLOCK=") + 13).toInt(); - debugHandle->printLn("DISPLAYCLOCK=" + String(DISPLAYCLOCK)); + if (line.indexOf("displayClock=") >= 0) { + this->DisplayClock = line.substring(line.lastIndexOf("displayClock=") + 13).toInt(); + this->debugController->printLn("displayClock=" + String(this->DisplayClock)); } - if (line.indexOf("is24hour=") >= 0) { - IS_24HOUR = line.substring(line.lastIndexOf("is24hour=") + 9).toInt(); - debugHandle->printLn("IS_24HOUR=" + String(IS_24HOUR)); + if (line.indexOf("clockIs24h=") >= 0) { + this->ClockIs24h = line.substring(line.lastIndexOf("clockIs24h=") + 11).toInt(); + this->debugController->printLn("clockIs24h=" + String(this->ClockIs24h)); } - - if(line.indexOf("USE_FLASH=") >= 0) { - USE_FLASH = line.substring(line.lastIndexOf("USE_FLASH=") + 10).toInt(); - debugHandle->printLn("USE_FLASH=" + String(USE_FLASH)); + if (line.indexOf("weatherShow=") >= 0) { + this->WeatherShow = line.substring(line.lastIndexOf("weatherShow=") + 12).toInt(); + this->debugController->printLn("weatherShow=" + String(this->WeatherShow)); } - - if (line.indexOf("isWeather=") >= 0) { - DISPLAYWEATHER = line.substring(line.lastIndexOf("isWeather=") + 10).toInt(); - debugHandle->printLn("DISPLAYWEATHER=" + String(DISPLAYWEATHER)); + if (line.indexOf("weatherApiKey=") >= 0) { + this->WeatherApiKey = line.substring(line.lastIndexOf("weatherApiKey=") + 14); + this->WeatherApiKey.trim(); + this->debugController->printLn("weatherApiKey=" + this->WeatherApiKey); } - if (line.indexOf("weatherKey=") >= 0) { - WeatherApiKey = line.substring(line.lastIndexOf("weatherKey=") + 11); - WeatherApiKey.trim(); - debugHandle->printLn("WeatherApiKey=" + WeatherApiKey); + if (line.indexOf("weatherCityId=") >= 0) { + this->WeatherCityId = line.substring(line.lastIndexOf("weatherCityId=") + 14).toInt(); + this->debugController->printLn("weatherCityId=" + String(this->WeatherCityId)); } - if (line.indexOf("CityID=") >= 0) { - CityIDs[0] = line.substring(line.lastIndexOf("CityID=") + 7).toInt(); - debugHandle->printLn("CityID: " + String(CityIDs[0])); + if (line.indexOf("weatherIsMetric=") >= 0) { + this->WeatherIsMetric = line.substring(line.lastIndexOf("weatherIsMetric=") + 16).toInt(); + this->debugController->printLn("weatherIsMetric=" + String(this->WeatherIsMetric)); } - if (line.indexOf("isMetric=") >= 0) { - IS_METRIC = line.substring(line.lastIndexOf("isMetric=") + 9).toInt(); - debugHandle->printLn("IS_METRIC=" + String(IS_METRIC)); + if (line.indexOf("weatherLang=") >= 0) { + this->WeatherLang = line.substring(line.lastIndexOf("weatherLang=") + 12); + this->WeatherLang.trim(); + this->debugController->printLn("weatherLang=" + this->WeatherLang); + } + if(line.indexOf("useLedFlash=") >= 0) { + this->useLedFlash = line.substring(line.lastIndexOf("useLedFlash=") + 12).toInt(); + this->debugController->printLn("useLedFlash=" + String(this->useLedFlash)); } - if (line.indexOf("language=") >= 0) { - WeatherLanguage = line.substring(line.lastIndexOf("language=") + 9); - WeatherLanguage.trim(); - debugHandle->printLn("WeatherLanguage=" + WeatherLanguage); - }*/ } fr.close(); - //printerClient.updatePrintClient(PrinterApiKey, PrinterServer, PrinterPort, PrinterAuthUser, PrinterAuthPass, HAS_PSU); + this->getPrinterClient()->updatePrintClient(); //weatherClient.updateWeatherApiKey(WeatherApiKey); //weatherClient.updateLanguage(WeatherLanguage); //weatherClient.setMetric(IS_METRIC); //weatherClient.updateCityIdList(CityIDs, 1); - //timeClient.setUtcOffset(UtcOffset); + this->timeClient->setUtcOffset(this->getClockUtcOffset()); } void GlobalDataController::writeSettings() { // Save decoded message to SPIFFS file for playback on power up. File f = LittleFS.open(CONFIG, "w"); if (!f) { - this->debugPrintLn("File open failed!"); + this->debugController->printLn("File open failed!"); } else { - this->debugPrintLn("Saving settings now..."); + this->debugController->printLn("Saving settings now..."); f.println("printerApiKey=" + this->PrinterApiKey); f.println("printerHostName=" + this->PrinterHostName); f.println("printerServer=" + this->PrinterServer); f.println("printerPort=" + String(this->PrinterPort)); - //f.println("printerName=" + printerClient.getPrinterName()); + f.println("printerName=" + this->getPrinterClient()->getPrinterName()); f.println("printerAuthUser=" + this->PrinterAuthUser); f.println("printerAuthPass=" + this->PrinterAuthPass); f.println("printerHasPsu=" + String(this->PrinterHasPsu)); @@ -186,41 +173,41 @@ void GlobalDataController::writeSettings() { f.println("webserverIsBasicAuth=" + String(this->WebserverIsBasicAuth)); f.println("webserverUsername=" + String(this->WebserverUsername)); f.println("webserverPassword=" + String(this->WebserverPassword)); - - - /*f.println("UtcOffset=" + String(UtcOffset)); - f.println("refreshRate=" + String(minutesBetweenDataRefresh)); - f.println("DISPLAYCLOCK=" + String(DISPLAYCLOCK)); - f.println("is24hour=" + String(IS_24HOUR)); - f.println("USE_FLASH=" + String(USE_FLASH)); - f.println("isWeather=" + String(DISPLAYWEATHER)); - f.println("weatherKey=" + WeatherApiKey); - f.println("CityID=" + String(CityIDs[0])); - f.println("isMetric=" + String(IS_METRIC)); - f.println("language=" + String(WeatherLanguage)); - */ + f.println("clockUtcOffset=" + String(this->ClockUtcOffset)); + f.println("clockResyncMinutes=" + String(this->ClockResyncMinutes)); + f.println("displayClock=" + String(this->DisplayClock)); + f.println("clockIs24h=" + String(this->ClockIs24h)); + f.println("weatherShow=" + String(this->WeatherShow)); + f.println("weatherApiKey=" + this->WeatherApiKey); + f.println("weatherCityId=" + String(this->WeatherCityId)); + f.println("weatherIsMetric=" + String(this->WeatherIsMetric)); + f.println("weatherLang=" + this->WeatherLang); + f.println("useLedFlash=" + String(this->useLedFlash)); } f.close(); readSettings(); - //timeClient.setUtcOffset(UtcOffset); + this->getTimeClient()->setUtcOffset(this->ClockUtcOffset); } String GlobalDataController::getVersion() { return VERSION; } -String GlobalDataController::getPrinterClientType() { - return this->printerClientTypeName; +String GlobalDataController::getLastReportStatus() { + return this->lastReportStatus; } -void GlobalDataController::setPrinterClientType(String clientTypeName) { - this->printerClientTypeName = clientTypeName; +TimeClient *GlobalDataController::getTimeClient() { + return this->timeClient; } - - - - +void GlobalDataController::setPrinterClient(BasePrinterClient *basePrinterClient) { + this->basePrinterClient = basePrinterClient; +} + +BasePrinterClient *GlobalDataController::getPrinterClient() { + return this->basePrinterClient; +} @@ -232,30 +219,58 @@ String GlobalDataController::getPrinterApiKey() { return this->PrinterApiKey; } +void GlobalDataController::setPrinterApiKey(String printerApiKey) { + this->PrinterApiKey = printerApiKey; +} + String GlobalDataController::getPrinterHostName() { return this->PrinterHostName; } +void GlobalDataController::setPrinterHostName(String printerHostName) { + this->PrinterHostName = printerHostName; +} + String GlobalDataController::getPrinterServer() { return this->PrinterServer; } +void GlobalDataController::setPrinterServer(String printerServer) { + this->PrinterServer = printerServer; +} + int GlobalDataController::getPrinterPort() { return this->PrinterPort; } +void GlobalDataController::setPrinterPort(int printerPort) { + this->PrinterPort = printerPort; +} + String GlobalDataController::getPrinterAuthUser() { return this->PrinterAuthUser; } +void GlobalDataController::setPrinterAuthUser(String printerAuthUser) { + this->PrinterAuthUser = printerAuthUser; +} + String GlobalDataController::getPrinterAuthPass() { return this->PrinterAuthPass; } +void GlobalDataController::setPrinterAuthPass(String printerAuthPass) { + this->PrinterAuthPass = printerAuthPass; +} + bool GlobalDataController::hasPrinterPsu() { return this->PrinterHasPsu; } +void GlobalDataController::setHasPrinterPsu(bool hasPsu) { + this->PrinterHasPsu = hasPsu; +} + int GlobalDataController::getWebserverPort() { return this->WebserverPort; } @@ -281,41 +296,22 @@ bool GlobalDataController::isDisplayInverted() { return this->DisplayInvertDisplay; } -/** - * Debug outputs - */ - -void GlobalDataController::debugPrint(const char *data) { - Serial.print(data); +int GlobalDataController::getClockUtcOffset() { + return this->ClockUtcOffset; } -void GlobalDataController::debugPrint(String data) { - Serial.print(data); +bool GlobalDataController::getDisplayClock() { + return this->DisplayClock; } -void GlobalDataController::debugPrint(int8_t data) { - Serial.print(data); +bool GlobalDataController::getClockIs24h() { + return this->ClockIs24h; } -void GlobalDataController::debugPrintF(const char *data, unsigned int uInt) { - Serial.printf(data, uInt); +int GlobalDataController::getClockResyncMinutes() { + return this->ClockResyncMinutes; } -void GlobalDataController::debugPrintLn(const char *data) { - Serial.println(data); -} - -void GlobalDataController::debugPrintLn(String data) { - Serial.println(data); -} - -void GlobalDataController::debugPrintLn(long int data) { - Serial.println(data); -} - -void GlobalDataController::debugPrintLn(int8_t data) { - Serial.println(data); -} /** * Notify LED @@ -341,6 +337,10 @@ void GlobalDataController::flashLED(int number, int delayTime) { } } +bool GlobalDataController::resetConfig() { + return LittleFS.remove(CONFIG); +} + /** * Global used functions */ diff --git a/src/Global/GlobalDataController.h b/src/Global/GlobalDataController.h index 3253367..0700636 100644 --- a/src/Global/GlobalDataController.h +++ b/src/Global/GlobalDataController.h @@ -3,13 +3,21 @@ #include #include #include "Configuration.h" +#include "../Network/TimeClient.h" +#include "../Network/OpenWeatherMapClient.h" +#include "../Clients/BasePrinterClient.h" +#include "DebugController.h" class GlobalDataController { private: /** * Internal */ - String printerClientTypeName = ""; + String lastReportStatus = ""; + TimeClient *timeClient; + BasePrinterClient *basePrinterClient; + OpenWeatherMapClient *weatherClient; + DebugController *debugController; /** * Configuration variables @@ -28,30 +36,51 @@ private: String WebserverPassword = WEBSERVER_PASSWORD; String WebserverTheme = WEBSERVER_THEMECOLOR; + int ClockUtcOffset = TIME_UTCOFFSET; + bool DisplayClock = DISPLAYCLOCK; + bool ClockIs24h = TIME_IS_24HOUR; + int ClockResyncMinutes = TIME_RESYNC_MINUTES_DELAY; + + bool useLedFlash = USE_FLASH; + + bool WeatherShow = DISPLAYWEATHER; + String WeatherApiKey = WEATHER_APIKEY; + int WeatherCityId = WEATHER_CITYID; + bool WeatherIsMetric = WEATHER_METRIC; + String WeatherLang = WEATHER_LANGUAGE; + #ifndef USE_NEXTION_DISPLAY bool DisplayInvertDisplay = DISPLAY_INVERT_DISPLAY; #endif public: - GlobalDataController(); + GlobalDataController(TimeClient *timeClient, OpenWeatherMapClient *weatherClient, DebugController *debugController); void setup(); void listSettingFiles(); void readSettings(); void writeSettings(); - + void setPrinterClient(BasePrinterClient *basePrinterClient); + TimeClient *getTimeClient(); + BasePrinterClient *getPrinterClient(); + String getLastReportStatus(); String getVersion(); - String getPrinterClientType(); - void setPrinterClientType(String clientTypeName); - + String getPrinterApiKey(); + void setPrinterApiKey(String printerApiKey); String getPrinterHostName(); + void setPrinterHostName(String printerHostName); String getPrinterServer(); + void setPrinterServer(String printerServer); int getPrinterPort(); + void setPrinterPort(int printerPort); String getPrinterAuthUser(); + void setPrinterAuthUser(String printerAuthUser); String getPrinterAuthPass(); + void setPrinterAuthPass(String printerAuthPass); bool hasPrinterPsu(); - + void setHasPrinterPsu(bool hasPsu); + int getWebserverPort(); bool getWebserverIsBasicAuth(); String getWebserverUsername(); @@ -60,17 +89,18 @@ public: bool isDisplayInverted(); - void debugPrint(const char *data); - void debugPrint(String data); - void debugPrint(int8_t data); - void debugPrintF(const char *data, unsigned int uInt); - void debugPrintLn(const char *data); - void debugPrintLn(String data); - void debugPrintLn(long int data); - void debugPrintLn(int8_t data); + int getClockUtcOffset(); + bool getDisplayClock(); + bool getClockIs24h(); + int getClockResyncMinutes(); void ledOnOff(boolean value); void flashLED(int number, int delayTime); + bool resetConfig(); int8_t getWifiQuality(); + int numberOfSeconds(int time) { return time % 60UL; } + int numberOfMinutes(int time) { return (time / 60UL) % 60UL; } + int numberOfHours(int time) { return time / 3600UL; } + String zeroPad(int value) { String rtnValue = String(value); if (value < 10) { rtnValue = "0" + rtnValue; } return rtnValue; } }; diff --git a/src/Includes.h b/src/Includes.h index 138aa53..e29b764 100644 --- a/src/Includes.h +++ b/src/Includes.h @@ -2,10 +2,13 @@ #include #include #include "Global/GlobalDataController.h" +#include "Global/DebugController.h" #include "Configuration.h" #if WEBSERVER_ENABLED #include "Network/WebServer.h" #endif +#include "Network/TimeClient.h" +#include "Network/OpenWeatherMapClient.h" #if (PRINTERCLIENT == REPETIER_CLIENT) #include "Clients/RepetierClient.h" #elif (PRINTERCLIENT == KLIPPER_CLIENT) @@ -22,18 +25,20 @@ #include "Display/OledDisplay.h" #endif - // Initilize all needed data -GlobalDataController globalDataController; +DebugController debugController; +TimeClient timeClient(TIME_UTCOFFSET, &debugController); +OpenWeatherMapClient weatherClient(WEATHER_APIKEY, WEATHER_CITYID, 1, WEATHER_METRIC, WEATHER_LANGUAGE, &debugController); +GlobalDataController globalDataController(&timeClient, &weatherClient, &debugController); #if WEBSERVER_ENABLED - WebServer webServer(&globalDataController); + WebServer webServer(&globalDataController, &debugController); #endif // Construct the correct printer client #if (PRINTERCLIENT == REPETIER_CLIENT) //RepetierClient printerClient(&globalDataController); #elif (PRINTERCLIENT == KLIPPER_CLIENT) - KlipperClient printerClient(&globalDataController); + KlipperClient printerClient(&globalDataController, &debugController); #elif (PRINTERCLIENT == DUET_CLIENT) //DuetClient printerClient(PrinterApiKey, PrinterServer, PrinterPort, PrinterAuthUser, PrinterAuthPass, HAS_PSU, debugHandle); #else @@ -48,5 +53,5 @@ GlobalDataController globalDataController; #else SSD1306Wire display(DISPLAY_I2C_DISPLAY_ADDRESS, DISPLAY_SDA_PIN, DISPLAY_SCL_PIN); #endif - OledDisplay displayClient(&display, &globalDataController); + OledDisplay displayClient(&display, &globalDataController, &debugController); #endif \ No newline at end of file diff --git a/src/Network/OpenWeatherMapClient.cpp b/src/Network/OpenWeatherMapClient.cpp index e69de29..36c4fc8 100644 --- a/src/Network/OpenWeatherMapClient.cpp +++ b/src/Network/OpenWeatherMapClient.cpp @@ -0,0 +1,284 @@ +#include "OpenWeatherMapClient.h" + +OpenWeatherMapClient::OpenWeatherMapClient(String ApiKey, int CityID, int cityCount, boolean isMetric, String language, DebugController *debugController) { + this->debugController = debugController; + int CityIDs[1]; + CityIDs[0] = CityID; + updateCityIdList(CityIDs, cityCount); + updateLanguage(language); + myApiKey = ApiKey; + setMetric(isMetric); +} + +void OpenWeatherMapClient::updateWeatherApiKey(String ApiKey) { + myApiKey = ApiKey; +} + +void OpenWeatherMapClient::updateLanguage(String language) { + lang = language; + if (lang == "") { + lang = "en"; + } +} + +void OpenWeatherMapClient::updateWeather() { + WiFiClient weatherClient; + String apiGetData = "GET /data/2.5/group?id=" + myCityIDs + "&units=" + units + "&cnt=1&APPID=" + myApiKey + "&lang=" + lang + " HTTP/1.1"; + + this->debugController->printLn("Getting Weather Data"); + this->debugController->printLn(apiGetData); + result = ""; + if (weatherClient.connect(servername, 80)) { //starts client connection, checks for connection + weatherClient.println(apiGetData); + weatherClient.println("Host: " + String(servername)); + weatherClient.println("User-Agent: ArduinoWiFi/1.1"); + weatherClient.println("Connection: close"); + weatherClient.println(); + } + else { + this->debugController->printLn("connection for weather data failed"); //error message if no client connect + this->debugController->printLn(""); + return; + } + + while(weatherClient.connected() && !weatherClient.available()) delay(1); //waits for data + + this->debugController->printLn("Waiting for data"); + + // Check HTTP status + char status[32] = {0}; + weatherClient.readBytesUntil('\r', status, sizeof(status)); + this->debugController->printLn("Response Header: " + String(status)); + if (strcmp(status, "HTTP/1.1 200 OK") != 0) { + this->debugController->print("Unexpected response: "); + this->debugController->printLn(status); + return; + } + + // Skip HTTP headers + char endOfHeaders[] = "\r\n\r\n"; + if (!weatherClient.find(endOfHeaders)) { + this->debugController->printLn(F("Invalid response")); + return; + } + + weathers[0].cached = false; + weathers[0].error = ""; + + const size_t bufferSize = 710; + DynamicJsonDocument jsonBuffer(bufferSize); + + // Parse JSON object + DeserializationError error = deserializeJson(jsonBuffer, weatherClient); + if (error) { + this->debugController->printLn("Weather Data Parsing failed!"); + weathers[0].error = "Weather Data Parsing failed!"; + return; + } + + weatherClient.stop(); //stop client + int count = jsonBuffer["cnt"]; + + for (int inx = 0; inx < count; inx++) { + weathers[inx].lat = (const char*)jsonBuffer["list"][inx]["coord"]["lat"]; + weathers[inx].lon = (const char*)jsonBuffer["list"][inx]["coord"]["lon"]; + weathers[inx].dt = (const char*)jsonBuffer["list"][inx]["dt"]; + weathers[inx].city = (const char*)jsonBuffer["list"][inx]["name"]; + weathers[inx].country = (const char*)jsonBuffer["list"][inx]["sys"]["country"]; + weathers[inx].temp = (const char*)jsonBuffer["list"][inx]["main"]["temp"]; + weathers[inx].humidity = (const char*)jsonBuffer["list"][inx]["main"]["humidity"]; + weathers[inx].condition = (const char*)jsonBuffer["list"][inx]["weather"][0]["main"]; + weathers[inx].wind = (const char*)jsonBuffer["list"][inx]["wind"]["speed"]; + weathers[inx].weatherId = (const char*)jsonBuffer["list"][inx]["weather"][0]["id"]; + weathers[inx].description = (const char*)jsonBuffer["list"][inx]["weather"][0]["description"]; + weathers[inx].icon = (const char*)jsonBuffer["list"][inx]["weather"][0]["icon"]; + + this->debugController->printLn("lat: " + weathers[inx].lat); + this->debugController->printLn("lon: " + weathers[inx].lon); + this->debugController->printLn("dt: " + weathers[inx].dt); + this->debugController->printLn("city: " + weathers[inx].city); + this->debugController->printLn("country: " + weathers[inx].country); + this->debugController->printLn("temp: " + weathers[inx].temp); + this->debugController->printLn("humidity: " + weathers[inx].humidity); + this->debugController->printLn("condition: " + weathers[inx].condition); + this->debugController->printLn("wind: " + weathers[inx].wind); + this->debugController->printLn("weatherId: " + weathers[inx].weatherId); + this->debugController->printLn("description: " + weathers[inx].description); + this->debugController->printLn("icon: " + weathers[inx].icon); + this->debugController->printLn(""); + + } +} + +String OpenWeatherMapClient::roundValue(String value) { + float f = value.toFloat(); + int rounded = (int)(f+0.5f); + return String(rounded); +} + +void OpenWeatherMapClient::updateCityIdList(int CityIDs[], int cityCount) { + myCityIDs = ""; + for (int inx = 0; inx < cityCount; inx++) { + if (CityIDs[inx] > 0) { + if (myCityIDs != "") { + myCityIDs = myCityIDs + ","; + } + myCityIDs = myCityIDs + String(CityIDs[inx]); + } + } +} + +void OpenWeatherMapClient::setMetric(boolean isMetric) { + if (isMetric) { + units = "metric"; + } else { + units = "imperial"; + } +} + +String OpenWeatherMapClient::getWeatherResults() { + return result; +} + +String OpenWeatherMapClient::getLat(int index) { + return weathers[index].lat; +} + +String OpenWeatherMapClient::getLon(int index) { + return weathers[index].lon; +} + +String OpenWeatherMapClient::getDt(int index) { + return weathers[index].dt; +} + +String OpenWeatherMapClient::getCity(int index) { + return weathers[index].city; +} + +String OpenWeatherMapClient::getCountry(int index) { + return weathers[index].country; +} + +String OpenWeatherMapClient::getTemp(int index) { + return weathers[index].temp; +} + +String OpenWeatherMapClient::getTempRounded(int index) { + return roundValue(getTemp(index)); +} + +String OpenWeatherMapClient::getHumidity(int index) { + return weathers[index].humidity; +} + +String OpenWeatherMapClient::getHumidityRounded(int index) { + return roundValue(getHumidity(index)); +} + +String OpenWeatherMapClient::getCondition(int index) { + return weathers[index].condition; +} + +String OpenWeatherMapClient::getWind(int index) { + return weathers[index].wind; +} + +String OpenWeatherMapClient::getWindRounded(int index) { + return roundValue(getWind(index)); +} + +String OpenWeatherMapClient::getWeatherId(int index) { + return weathers[index].weatherId; +} + +String OpenWeatherMapClient::getDescription(int index) { + return weathers[index].description; +} + +String OpenWeatherMapClient::getIcon(int index) { + return weathers[index].icon; +} + +boolean OpenWeatherMapClient::getCached() { + return weathers[0].cached; +} + +String OpenWeatherMapClient::getMyCityIDs() { + return myCityIDs; +} + +String OpenWeatherMapClient::getError() { + return weathers[0].error; +} + +String OpenWeatherMapClient::getWeatherIcon(int index) +{ + int id = getWeatherId(index).toInt(); + String W = ")"; + switch(id) + { + case 800: W = "B"; break; + case 801: W = "Y"; break; + case 802: W = "H"; break; + case 803: W = "H"; break; + case 804: W = "Y"; break; + + case 200: W = "0"; break; + case 201: W = "0"; break; + case 202: W = "0"; break; + case 210: W = "0"; break; + case 211: W = "0"; break; + case 212: W = "0"; break; + case 221: W = "0"; break; + case 230: W = "0"; break; + case 231: W = "0"; break; + case 232: W = "0"; break; + + case 300: W = "R"; break; + case 301: W = "R"; break; + case 302: W = "R"; break; + case 310: W = "R"; break; + case 311: W = "R"; break; + case 312: W = "R"; break; + case 313: W = "R"; break; + case 314: W = "R"; break; + case 321: W = "R"; break; + + case 500: W = "R"; break; + case 501: W = "R"; break; + case 502: W = "R"; break; + case 503: W = "R"; break; + case 504: W = "R"; break; + case 511: W = "R"; break; + case 520: W = "R"; break; + case 521: W = "R"; break; + case 522: W = "R"; break; + case 531: W = "R"; break; + + case 600: W = "W"; break; + case 601: W = "W"; break; + case 602: W = "W"; break; + case 611: W = "W"; break; + case 612: W = "W"; break; + case 615: W = "W"; break; + case 616: W = "W"; break; + case 620: W = "W"; break; + case 621: W = "W"; break; + case 622: W = "W"; break; + + case 701: W = "M"; break; + case 711: W = "M"; break; + case 721: W = "M"; break; + case 731: W = "M"; break; + case 741: W = "M"; break; + case 751: W = "M"; break; + case 761: W = "M"; break; + case 762: W = "M"; break; + case 771: W = "M"; break; + case 781: W = "M"; break; + + default:break; + } + return W; +} diff --git a/src/Network/OpenWeatherMapClient.h b/src/Network/OpenWeatherMapClient.h index e69de29..aef1f96 100644 --- a/src/Network/OpenWeatherMapClient.h +++ b/src/Network/OpenWeatherMapClient.h @@ -0,0 +1,68 @@ +#pragma once +#include +#include +#include +#include "../Global/DebugController.h" + +class OpenWeatherMapClient { +private: + String myCityIDs = ""; + String myApiKey = ""; + String units = ""; + String lang = ""; + + const char* servername = "api.openweathermap.org"; // remote server we will connect to + String result; + + typedef struct { + String lat; + String lon; + String dt; + String city; + String country; + String temp; + String humidity; + String condition; + String wind; + String weatherId; + String description; + String icon; + boolean cached; + String error; + } weather; + + weather weathers[5]; + + String roundValue(String value); + DebugController *debugController; + +public: + OpenWeatherMapClient(String ApiKey, int CityID, int cityCount, boolean isMetric, String language, DebugController *debugController); + void updateWeather(); + void updateWeatherApiKey(String ApiKey); + void updateCityIdList(int CityIDs[], int cityCount); + void updateLanguage(String language); + void setMetric(boolean isMetric); + + String getWeatherResults(); + + String getLat(int index); + String getLon(int index); + String getDt(int index); + String getCity(int index); + String getCountry(int index); + String getTemp(int index); + String getTempRounded(int index); + String getHumidity(int index); + String getHumidityRounded(int index); + String getCondition(int index); + String getWind(int index); + String getWindRounded(int index); + String getWeatherId(int index); + String getDescription(int index); + String getIcon(int index); + boolean getCached(); + String getMyCityIDs(); + String getWeatherIcon(int index); + String getError(); +}; \ No newline at end of file diff --git a/src/Network/TimeClient.cpp b/src/Network/TimeClient.cpp index e69de29..76cc67d 100644 --- a/src/Network/TimeClient.cpp +++ b/src/Network/TimeClient.cpp @@ -0,0 +1,125 @@ +#include "TimeClient.h" + +TimeClient::TimeClient(float utcOffset, DebugController * debugController) { + this->myUtcOffset = utcOffset; + this->debugController = debugController; +} + +void TimeClient::updateTime() { + WiFiClient client; + + if (!client.connect(ntpServerName, httpPort)) { + this->debugController->printLn("connection failed"); + return; + } + + // This will send the request to the server + client.print(String("GET / HTTP/1.1\r\n") + + String("Host: www.google.com\r\n") + + String("Connection: close\r\n\r\n")); + int repeatCounter = 0; + while(!client.available() && repeatCounter < 10) { + delay(1000); + this->debugController->printLn("."); + repeatCounter++; + } + + String line; + + int size = 0; + client.setNoDelay(false); + while(client.connected()) { + while((size = client.available()) > 0) { + line = client.readStringUntil('\n'); + line.toUpperCase(); + // example: + // date: Thu, 19 Nov 2015 20:25:40 GMT + if (line.startsWith("DATE: ")) { + this->debugController->printLn(line.substring(23, 25) + ":" + line.substring(26, 28) + ":" +line.substring(29, 31)); + int parsedHours = line.substring(23, 25).toInt(); + int parsedMinutes = line.substring(26, 28).toInt(); + int parsedSeconds = line.substring(29, 31).toInt(); + this->debugController->printLn(String(parsedHours) + ":" + String(parsedMinutes) + ":" + String(parsedSeconds)); + + localEpoc = (parsedHours * 60 * 60 + parsedMinutes * 60 + parsedSeconds); + this->debugController->printLn(localEpoc); + localMillisAtUpdate = millis(); + client.stop(); + } + } + } +} + +void TimeClient::setUtcOffset(float utcOffset) { + myUtcOffset = utcOffset; +} + +String TimeClient::getHours() { + if (localEpoc == 0) { + return "--"; + } + int hours = ((getCurrentEpochWithUtcOffset() % 86400L) / 3600) % 24; + if (hours < 10) { + return "0" + String(hours); + } + return String(hours); // print the hour (86400 equals secs per day) + +} +String TimeClient::getMinutes() { + if (localEpoc == 0) { + return "--"; + } + int minutes = ((getCurrentEpochWithUtcOffset() % 3600) / 60); + if (minutes < 10 ) { + // In the first 10 minutes of each hour, we'll want a leading '0' + return "0" + String(minutes); + } + return String(minutes); +} +String TimeClient::getSeconds() { + if (localEpoc == 0) { + return "--"; + } + int seconds = getCurrentEpochWithUtcOffset() % 60; + if ( seconds < 10 ) { + // In the first 10 seconds of each minute, we'll want a leading '0' + return "0" + String(seconds); + } + return String(seconds); +} + +String TimeClient::getAmPmHours() { + int hours = getHours().toInt(); + if (hours >= 13) { + hours = hours - 12; + } + if (hours == 0) { + hours = 12; + } + return String(hours); +} + +String TimeClient::getAmPm() { + int hours = getHours().toInt(); + String ampmValue = "AM"; + if (hours >= 12) { + ampmValue = "PM"; + } + return ampmValue; +} + +String TimeClient::getFormattedTime() { + return getHours() + ":" + getMinutes() + ":" + getSeconds(); +} + +String TimeClient::getAmPmFormattedTime() { + return getAmPmHours() + ":" + getMinutes() + " " + getAmPm(); +} + +long TimeClient::getCurrentEpoch() { + return localEpoc + ((millis() - localMillisAtUpdate) / 1000); +} + +long TimeClient::getCurrentEpochWithUtcOffset() { + return (long)round(getCurrentEpoch() + 3600 * myUtcOffset + 86400L) % 86400L; +} \ No newline at end of file diff --git a/src/Network/TimeClient.h b/src/Network/TimeClient.h index e69de29..62f3f08 100644 --- a/src/Network/TimeClient.h +++ b/src/Network/TimeClient.h @@ -0,0 +1,31 @@ +#pragma once +#include +#include "../Global/DebugController.h" + +#define NTP_PACKET_SIZE 48 + +class TimeClient { +private: + float myUtcOffset = 0; + long localEpoc = 0; + long localMillisAtUpdate; + const char* ntpServerName = "www.google.com"; + const int httpPort = 80; + byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets + DebugController * debugController; + +public: + TimeClient(float utcOffset, DebugController * debugController); + void updateTime(); + + void setUtcOffset(float utcOffset); + String getHours(); + String getAmPmHours(); + String getAmPm(); + String getMinutes(); + String getSeconds(); + String getFormattedTime(); + String getAmPmFormattedTime(); + long getCurrentEpoch(); + long getCurrentEpochWithUtcOffset(); +}; \ No newline at end of file diff --git a/src/Network/WebServer.cpp b/src/Network/WebServer.cpp index 7db5f83..e98ff16 100644 --- a/src/Network/WebServer.cpp +++ b/src/Network/WebServer.cpp @@ -93,11 +93,11 @@ static const char COLOR_THEMES[] PROGMEM = "" "" ""; -WebServer::WebServer(GlobalDataController *globalDataController) { +WebServer::WebServer(GlobalDataController *globalDataController, DebugController *debugController) { this->globalDataController = globalDataController; + this->debugController = debugController; } - void WebServer::setup() { static WebServer* obj = this; @@ -116,37 +116,456 @@ void WebServer::setup() { // Start the server this->server->begin(); - Serial.println("Server started"); + this->debugController->printLn("Server started"); } - - - - +void WebServer::handleClient() { + this->server->handleClient(); +} boolean WebServer::authentication() { + if (this->globalDataController->getWebserverIsBasicAuth() && + (this->globalDataController->getWebserverUsername().length() >= 1 && this->globalDataController->getWebserverPassword().length() >= 1) + ) { + return this->server->authenticate( + this->globalDataController->getWebserverUsername().c_str(), + this->globalDataController->getWebserverPassword().c_str() + ); + } + return true; // Authentication not required } void WebServer::redirectHome() { + // Send them back to the Root Directory + this->server->sendHeader("Location", String("/"), true); + this->server->sendHeader("Cache-Control", "no-cache, no-store"); + this->server->sendHeader("Pragma", "no-cache"); + this->server->sendHeader("Expires", "-1"); + this->server->send(302, "text/plain", ""); + this->server->client().stop(); } 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))); + + String displayTime = + this->globalDataController->getTimeClient()->getAmPmHours() + ":" + + this->globalDataController->getTimeClient()->getMinutes() + ":" + + this->globalDataController->getTimeClient()->getSeconds() + " " + + this->globalDataController->getTimeClient()->getAmPm(); + if (this->globalDataController->getClockIs24h()) { + displayTime = + this->globalDataController->getTimeClient()->getHours() + ":" + + this->globalDataController->getTimeClient()->getMinutes() + ":" + + this->globalDataController->getTimeClient()->getSeconds(); + } + + html += "

" + printerClient->getPrinterType() + " Monitor

"; + html += "

"; + if (printerClient->getPrinterType() == "Repetier") { + html += "Printer Name: " + printerClient->getPrinterName() + "
"; + } else { + html += "Host Name: " + this->globalDataController->getPrinterHostName() + "
"; + } + + if (printerClient->getError() != "") { + html += "Status: Offline
"; + html += "Reason: " + printerClient->getError() + "
"; + } else { + html += "Status: " + printerClient->getState(); + if (printerClient->isPSUoff() && this->globalDataController->hasPrinterPsu()) { + html += ", PSU off"; + } + html += "
"; + } + + if (printerClient->isPrinting()) { + html += "File: " + printerClient->getFileName() + "
"; + float fileSize = printerClient->getFileSize().toFloat(); + if (fileSize > 0) { + fileSize = fileSize / 1024; + html += "File Size: " + String(fileSize) + "KB
"; + } + int filamentLength = printerClient->getFilamentLength().toInt(); + if (filamentLength > 0) { + float fLength = float(filamentLength) / 1000; + html += "Filament: " + String(fLength) + "m
"; + } + + html += "Tool Temperature: " + printerClient->getTempToolActual() + "° C
"; + if (printerClient->getTempBedActual() != 0 ) { + html += "Bed Temperature: " + printerClient->getTempBedActual() + "° C
"; + } + + int val = printerClient->getProgressPrintTimeLeft().toInt(); + int hours = this->globalDataController->numberOfHours(val); + int minutes = this->globalDataController->numberOfMinutes(val); + int seconds = this->globalDataController->numberOfSeconds(val); + html += "Est. Print Time Left: " + + this->globalDataController->zeroPad(hours) + ":" + + this->globalDataController->zeroPad(minutes) + ":" + + this->globalDataController->zeroPad(seconds) + "
"; + + val = printerClient->getProgressPrintTime().toInt(); + hours = this->globalDataController->numberOfHours(val); + minutes = this->globalDataController->numberOfMinutes(val); + seconds = this->globalDataController->numberOfSeconds(val); + html += "Printing Time: " + this->globalDataController->zeroPad(hours) + ":" + this->globalDataController->zeroPad(minutes) + ":" + this->globalDataController->zeroPad(seconds) + "
"; + html += ""; + html += "

" + printerClient->getProgressCompletion() + "%
"; + } else { + html += "
"; + } + + html += "

"; + + html += "

Time: " + displayTime + "

"; + + this->server->sendContent(html); // spit out what we got + html = ""; + + /* + if (DISPLAYWEATHER) { + if (weatherClient.getCity(0) == "") { + html += "

Please Configure Weather API

"; + if (weatherClient.getError() != "") { + html += "

Weather Error: " + weatherClient.getError() + "

"; + } + } else { + html += "

" + weatherClient.getCity(0) + ", " + weatherClient.getCountry(0) + "

"; + html += "
"; + html += "" + weatherClient.getDescription(0) + "
"; + html += weatherClient.getHumidity(0) + "% Humidity
"; + html += weatherClient.getWind(0) + " " + getSpeedSymbol() + " Wind
"; + html += "
"; + html += "

"; + html += weatherClient.getCondition(0) + " (" + weatherClient.getDescription(0) + ")
"; + html += weatherClient.getTempRounded(0) + getTempSymbol(true) + "
"; + html += " Map It!
"; + html += "

"; + } + + server.sendContent(html); // spit out what we got + html = ""; // fresh start + }*/ + + this->server->sendContent(String(getFooter())); + this->server->sendContent(""); + this->server->client().stop(); + this->globalDataController->ledOnOff(false); } void WebServer::handleSystemReset() { + if (!this->authentication()) { + return this->server->requestAuthentication(); + } + this->debugController->printLn("Reset System Configuration"); + if (this->globalDataController->resetConfig()) { + redirectHome(); + ESP.restart(); + } } void WebServer::handleWifiReset() { + if (!this->authentication()) { + return this->server->requestAuthentication(); + } + //WiFiManager + //Local intialization. Once its business is done, there is no need to keep it around + redirectHome(); + WiFiManager wifiManager; + wifiManager.resetSettings(); + ESP.restart(); } void WebServer::handleUpdateConfig() { + boolean flipOld = this->globalDataController->isDisplayInverted(); + if (!this->authentication()) { + return this->server->requestAuthentication(); + } + if (this->server->hasArg("printer")) { + this->globalDataController->getPrinterClient()->setPrinterName( + this->server->arg("printer") + ); + } + + this->globalDataController->setPrinterApiKey(this->server->arg("PrinterApiKey")); + this->globalDataController->setPrinterHostName(this->server->arg("PrinterHostName")); + this->globalDataController->setPrinterServer(this->server->arg("PrinterAddress")); + this->globalDataController->setPrinterPort(this->server->arg("PrinterPort").toInt()); + this->globalDataController->setPrinterAuthUser(this->server->arg("octoUser")); + this->globalDataController->setPrinterAuthPass(this->server->arg("octoPass")); + this->globalDataController->setHasPrinterPsu(this->server->hasArg("hasPSU")); + + /*DISPLAYCLOCK = this->server->hasArg("isClockEnabled"); + IS_24HOUR = this->server->hasArg("is24hour"); + INVERT_DISPLAY = this->server->hasArg("invDisp"); + USE_FLASH = this->server->hasArg("useFlash"); + + minutesBetweenDataRefresh = this->server->arg("refresh").toInt(); + themeColor = this->server->arg("theme"); + UtcOffset = this->server->arg("utcoffset").toFloat(); + String temp = this->server->arg("userid"); + temp.toCharArray(www_username, sizeof(temp)); + temp = this->server->arg("stationpassword"); + temp.toCharArray(www_password, sizeof(temp));*/ + + this->globalDataController->writeSettings(); + //findMDNS(); + this->globalDataController->getPrinterClient()->getPrinterJobResults(); + this->globalDataController->getPrinterClient()->getPrinterPsuState(); + /*if (INVERT_DISPLAY != flipOld) { + ui.init(); + if(INVERT_DISPLAY) + display.flipScreenVertically(); + ui.update(); + } + checkDisplay(); + lastEpoch = 0; */ + this->redirectHome(); } void WebServer::handleUpdateWeather() { + if (!this->authentication()) { + return this->server->requestAuthentication(); + } + //DISPLAYWEATHER = server.hasArg("isWeatherEnabled"); + //WeatherApiKey = server.arg("openWeatherMapApiKey"); + //CityIDs[0] = server.arg("city1").toInt(); + //IS_METRIC = server.hasArg("metric"); + //WeatherLanguage = server.arg("language"); + + this->globalDataController->writeSettings(); + //isClockOn = false; // this will force a check for the display + //checkDisplay(); + //lastEpoch = 0; + this->redirectHome(); } 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", ""); + + html = this->getHeader(); + this->server->sendContent(html); + + CHANGE_FORM = "

Station Config:

" + "

" + "

"; + if (printerClient->getPrinterType() == "OctoPrint") { + CHANGE_FORM += "

"; + } + CHANGE_FORM += "

" + "

" + "

" + "

"; + if (printerClient->getPrinterType() == "Repetier") { + CHANGE_FORM += "" + "

" + ""; + } else { + CHANGE_FORM += "

"; + } + CHANGE_FORM += "

" + "

"; + + if (printerClient->getPrinterType() == "Repetier") { + html = ""; + + this->server->sendContent(html); + } else { + html = ""; + this->server->sendContent(html); + } + + String form = CHANGE_FORM; + + form.replace("%OCTOKEY%", this->globalDataController->getPrinterApiKey()); + form.replace("%OCTOHOST%", this->globalDataController->getPrinterHostName()); + form.replace("%OCTOADDRESS%", this->globalDataController->getPrinterServer()); + form.replace("%OCTOPORT%", String(this->globalDataController->getPrinterPort())); + form.replace("%OCTOUSER%", this->globalDataController->getPrinterAuthUser()); + form.replace("%OCTOPASS%", this->globalDataController->getPrinterAuthPass()); + + this->server->sendContent(form); + + form = FPSTR(CLOCK_FORM); + + String isClockChecked = ""; + if (DISPLAYCLOCK) { + isClockChecked = "checked='checked'"; + } + form.replace("%IS_CLOCK_CHECKED%", isClockChecked); + String is24hourChecked = ""; + if (this->globalDataController->getClockIs24h()) { + is24hourChecked = "checked='checked'"; + } + form.replace("%IS_24HOUR_CHECKED%", is24hourChecked); + String isInvDisp = ""; + if (this->globalDataController->isDisplayInverted()) { + isInvDisp = "checked='checked'"; + } + form.replace("%IS_INVDISP_CHECKED%", isInvDisp); + String isFlashLED = ""; + if (USE_FLASH) { + isFlashLED = "checked='checked'"; + } + form.replace("%USEFLASH%", isFlashLED); + String hasPSUchecked = ""; + if (this->globalDataController->hasPrinterPsu()) { + hasPSUchecked = "checked='checked'"; + } + form.replace("%HAS_PSU_CHECKED%", hasPSUchecked); + + String options = ""; + options.replace(">"+String(this->globalDataController->getClockResyncMinutes())+"<", " selected>"+String(this->globalDataController->getClockResyncMinutes())+"<"); + form.replace("%OPTIONS%", options); + + this->server->sendContent(form); + + form = FPSTR(THEME_FORM); + + String themeOptions = FPSTR(COLOR_THEMES); + themeOptions.replace(">"+String(this->globalDataController->getWebserverTheme())+"<", " selected>"+String(this->globalDataController->getWebserverTheme())+"<"); + form.replace("%THEME_OPTIONS%", themeOptions); + form.replace("%UTCOFFSET%", String(this->globalDataController->getClockUtcOffset())); + String isUseSecurityChecked = ""; + if (this->globalDataController->getWebserverIsBasicAuth()) { + isUseSecurityChecked = "checked='checked'"; + } + form.replace("%IS_BASICAUTH_CHECKED%", isUseSecurityChecked); + form.replace("%USERID%", String(this->globalDataController->getWebserverUsername())); + form.replace("%STATIONPASSWORD%", String(this->globalDataController->getWebserverPassword())); + + this->server->sendContent(form); + + html = this->getFooter(); + this->server->sendContent(html); + this->server->sendContent(""); + this->server->client().stop(); + this->globalDataController->ledOnOff(false); } void WebServer::handleWeatherConfigure() { + if (!this->authentication()) { + return this->server->requestAuthentication(); + } + this->globalDataController->ledOnOff(true); + 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 = ""; + if (DISPLAYWEATHER) { + isWeatherChecked = "checked='checked'"; + } + /*form.replace("%IS_WEATHER_CHECKED%", isWeatherChecked); + form.replace("%WEATHERKEY%", this->globalDataController()); + form.replace("%CITYNAME1%", weatherClient.getCity(0)); + form.replace("%CITY1%", String(CityIDs[0])); + String checked = ""; + if (IS_METRIC) { + checked = "checked='checked'"; + } + form.replace("%METRIC%", checked); + String options = FPSTR(LANG_OPTIONS); + options.replace(">"+String(WeatherLanguage)+"<", " selected>"+String(WeatherLanguage)+"<"); + 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); +} + +String WebServer::getHeader() { + return this->getHeader(false); +} + +String WebServer::getHeader(boolean refresh) { + String menu = FPSTR(WEB_ACTIONS); + + String html = ""; + html += "Printer Monitor"; + html += ""; + html += ""; + if (refresh) { + html += ""; + } + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + html += "

Printer Monitor

"; + html += ""; + html += "
"; + return html; +} + +String WebServer::getFooter() { + int8_t rssi = this->globalDataController->getWifiQuality(); + Serial.print("Signal Strength (RSSI): "); + Serial.print(rssi); + this->debugController->printLn("%"); + String html = "


"; + html += "
"; + html += "
"; + if (this->globalDataController->getLastReportStatus() != "") { + html += " Report Status: " + this->globalDataController->getLastReportStatus() + "
"; + } + html += " Version: " + String(VERSION) + "
"; + html += " Signal Strength: "; + html += String(rssi) + "%"; + html += "
"; + html += ""; + return html; } \ No newline at end of file diff --git a/src/Network/WebServer.h b/src/Network/WebServer.h index d357a77..98cddc3 100644 --- a/src/Network/WebServer.h +++ b/src/Network/WebServer.h @@ -3,6 +3,7 @@ #include #include #include +#include #include "../Global/GlobalDataController.h" class WebServer { @@ -10,12 +11,13 @@ private: GlobalDataController *globalDataController; ESP8266WebServer *server; ESP8266HTTPUpdateServer *serverUpdater; + DebugController *debugController; public: - WebServer(GlobalDataController *globalDataController); + WebServer(GlobalDataController *globalDataController, DebugController *debugController); void setup(); - + void handleClient(); boolean authentication(); void redirectHome(); void displayPrinterStatus(); @@ -25,4 +27,7 @@ public: void handleUpdateWeather(); void handleConfigure(); void handleWeatherConfigure(); + String getHeader(); + String getHeader(boolean refresh); + String getFooter(); }; diff --git a/src/main.cpp b/src/main.cpp index 076687b..48180a4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,6 +8,8 @@ void configModeCallback(WiFiManager *myWiFiManager); void setup() { LittleFS.begin(); + debugController.setup(); + globalDataController.setPrinterClient(&printerClient); globalDataController.setup(); displayClient.preSetup(); displayClient.showBootScreen(); @@ -29,27 +31,27 @@ void setup() { displayClient.postSetup(); // print the received signal strength: - globalDataController.debugPrint("Signal Strength (RSSI): "); - globalDataController.debugPrint(globalDataController.getWifiQuality()); - globalDataController.debugPrintLn("%"); + debugController.print("Signal Strength (RSSI): "); + debugController.print(globalDataController.getWifiQuality()); + debugController.printLn("%"); if (ENABLE_OTA) { ArduinoOTA.onStart([]() { - globalDataController.debugPrintLn("Start"); + debugController.printLn("Start"); }); ArduinoOTA.onEnd([]() { - globalDataController.debugPrintLn("\nEnd"); + debugController.printLn("\nEnd"); }); ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { - globalDataController.debugPrintF("Progress: %u%%\r", (progress / (total / 100))); + debugController.printF("Progress: %u%%\r", (progress / (total / 100))); }); ArduinoOTA.onError([](ota_error_t error) { - globalDataController.debugPrintF("Error[%u]: ", error); - if (error == OTA_AUTH_ERROR) globalDataController.debugPrintLn("Auth Failed"); - else if (error == OTA_BEGIN_ERROR) globalDataController.debugPrintLn("Begin Failed"); - else if (error == OTA_CONNECT_ERROR) globalDataController.debugPrintLn("Connect Failed"); - else if (error == OTA_RECEIVE_ERROR) globalDataController.debugPrintLn("Receive Failed"); - else if (error == OTA_END_ERROR) globalDataController.debugPrintLn("End Failed"); + debugController.printF("Error[%u]: ", error); + if (error == OTA_AUTH_ERROR) debugController.printLn("Auth Failed"); + else if (error == OTA_BEGIN_ERROR) debugController.printLn("Begin Failed"); + else if (error == OTA_CONNECT_ERROR) debugController.printLn("Connect Failed"); + else if (error == OTA_RECEIVE_ERROR) debugController.printLn("Receive Failed"); + else if (error == OTA_END_ERROR) debugController.printLn("End Failed"); }); ArduinoOTA.setHostname((const char *)hostname.c_str()); if (String(OTA_Password) != "") { @@ -58,8 +60,8 @@ void setup() { ArduinoOTA.begin(); } - globalDataController.debugPrintLn("local ip"); - globalDataController.debugPrintLn(WiFi.localIP().toString()); + debugController.printLn("local ip"); + debugController.printLn(WiFi.localIP().toString()); #if WEBSERVER_ENABLED webServer.setup(); @@ -69,11 +71,26 @@ void setup() { #endif globalDataController.flashLED(5, 100); - globalDataController.debugPrintLn("*** Leaving setup()"); + debugController.printLn("*** Leaving setup()"); } void loop() { // put your main code here, to run repeatedly: + + + + + + + + + + if (WEBSERVER_ENABLED) { + webServer.handleClient(); + } + if (ENABLE_OTA) { + ArduinoOTA.handle(); + } } @@ -91,12 +108,12 @@ void loop() { void configModeCallback(WiFiManager *myWiFiManager) { - globalDataController.debugPrintLn("Entered config mode"); - globalDataController.debugPrintLn(WiFi.softAPIP().toString()); + debugController.printLn("Entered config mode"); + debugController.printLn(WiFi.softAPIP().toString()); displayClient.showApAccessScreen(myWiFiManager->getConfigPortalSSID(), WiFi.softAPIP().toString()); - globalDataController.debugPrintLn("Wifi Manager"); - globalDataController.debugPrintLn("Please connect to AP"); - globalDataController.debugPrintLn(myWiFiManager->getConfigPortalSSID()); - globalDataController.debugPrintLn("To setup Wifi Configuration"); + debugController.printLn("Wifi Manager"); + debugController.printLn("Please connect to AP"); + debugController.printLn(myWiFiManager->getConfigPortalSSID()); + debugController.printLn("To setup Wifi Configuration"); globalDataController.flashLED(20, 50); } \ No newline at end of file