From 3ac8d74799dae899deaa4463c161d8e87a34cd3b Mon Sep 17 00:00:00 2001 From: abacon118 Date: Mon, 29 May 2023 20:14:52 -0400 Subject: [PATCH 1/4] Add files via upload --- printermonitor/Settings.h | 38 +- printermonitor/printermonitor.ino | 2683 ++++++++++++++--------------- 2 files changed, 1340 insertions(+), 1381 deletions(-) diff --git a/printermonitor/Settings.h b/printermonitor/Settings.h index a149b82..a927cb3 100644 --- a/printermonitor/Settings.h +++ b/printermonitor/Settings.h @@ -45,7 +45,6 @@ SOFTWARE. #include #include #include "TimeClient.h" -#include "RepetierClient.h" #include "OctoPrintClient.h" #include "OpenWeatherMapClient.h" #include "WeatherStationFonts.h" @@ -58,20 +57,19 @@ SOFTWARE. // Start Settings //****************************** -// OctoPrint / Repetier Monitoring -- Monitor your 3D OctoPrint or Repetier Server -//#define USE_REPETIER_CLIENT // Uncomment this line to use the Repetier Printer Server -- OctoPrint is used by default and is most common -String PrinterApiKey = ""; // ApiKey from your User Account on OctoPrint / Repetier -String PrinterHostName = "octopi";// Default 'octopi' -- or hostname if different (optional if your IP changes) -String PrinterServer = ""; // IP or Address of your OctoPrint / Repetier Server (DO NOT include http://) -int PrinterPort = 80; // the port you are running your OctoPrint / Repetier server on (usually 80); -String PrinterAuthUser = ""; // only used if you have haproxy or basic athentintication turned on (not default) -String PrinterAuthPass = ""; // only used with haproxy or basic auth (only needed if you must authenticate) +// OctoPrint Monitoring -- Monitor your 3D printer OctoPrint Server +String OctoPrintApiKey = "ABCDEF1234"; // ApiKey from your User Account on OctoPrint +String OctoPrintHostName = "3D Printer";// Default 'octopi' -- or hostname if different (optional if your IP changes) +String OctoPrintServer = "192.168.0.10"; // IP or Address of your OctoPrint Server (DO NOT include http://) +int OctoPrintPort = 80; // the port you are running your OctoPrint server on (usually 80); +String OctoAuthUser = ""; // only used if you have haproxy or basic athentintication turned on (not default) +String OctoAuthPass = ""; // only used with haproxy or basic auth (only needed if you must authenticate) // Weather Configuration boolean DISPLAYWEATHER = true; // true = show weather when not printing / false = no weather -String WeatherApiKey = ""; // Your API Key from http://openweathermap.org/ +String WeatherApiKey = "ABCDEF1234"; // Your API Key from http://openweathermap.org/ // Default City Location (use http://openweathermap.org/find to find city ID) -int CityIDs[] = { 5304391 }; //Only USE ONE for weather marquee +int CityIDs[] = {4499612}; //Only USE ONE for weather marquee boolean IS_METRIC = false; // false = Imperial and true = Metric // Languages: ar, bg, ca, cz, de, el, en, fa, fi, fr, gl, hr, hu, it, ja, kr, la, lt, mk, nl, pl, pt, ro, ru, se, sk, sl, es, tr, ua, vi, zh_cn, zh_tw String WeatherLanguage = "en"; //Default (en) English @@ -80,25 +78,27 @@ String WeatherLanguage = "en"; //Default (en) English const int WEBSERVER_PORT = 80; // The port you can access this device on over HTTP const boolean WEBSERVER_ENABLED = true; // Device will provide a web interface via http://[ip]:[port]/ boolean IS_BASIC_AUTH = true; // true = require athentication to change configuration settings / false = no auth -char* www_username = "admin"; // User account for the Web Interface -char* www_password = "password"; // Password for the Web Interface +char* www_username = "user"; // User account for the Web Interface +char* www_password = "P@ssW0rd123"; // Password for the Web Interface // Date and Time -float UtcOffset = -7; // Hour offset from GMT for your timezone -boolean IS_24HOUR = false; // 23:00 millitary 24 hour clock +float UtcOffset = -4; // Hour offset from GMT for your timezone +boolean IS_24HOUR = true; // 23:00 millitary 24 hour clock int minutesBetweenDataRefresh = 15; boolean DISPLAYCLOCK = true; // true = Show Clock when not printing / false = turn off display when not printing // Display Settings const int I2C_DISPLAY_ADDRESS = 0x3c; // I2C Address of your Display (usually 0x3c or 0x3d) const int SDA_PIN = D2; -const int SCL_PIN = D5; // original code D5 -- Monitor Easy Board use D1 +const int SCL_PIN = D3; boolean INVERT_DISPLAY = false; // true = pins at top | false = pins at the bottom -//#define DISPLAY_SH1106 // Uncomment this line to use the SH1106 display -- SSD1306 is used by default and is most common +#define DISPLAY_SH1106 // Uncomment this line to use the SH1106 display -- SSD1306 is used by default and is most common // LED Settings -const int externalLight = LED_BUILTIN; // LED will always flash on bootup or Wifi Errors -boolean USE_FLASH = true; // true = System LED will Flash on Service Calls; false = disabled LED flashing +const int externalLight = D1; // Set to unused pin, like D1, to disable use of built-in LED (LED_BUILTIN) + +//Light Dependant Resistor (LDR) Port +const int ldrPin = A0; // PSU Control boolean HAS_PSU = false; // Set to true if https://github.com/kantlivelong/OctoPrint-PSUControl/ in use diff --git a/printermonitor/printermonitor.ino b/printermonitor/printermonitor.ino index 8849850..8344d43 100644 --- a/printermonitor/printermonitor.ino +++ b/printermonitor/printermonitor.ino @@ -1,1362 +1,1321 @@ -/** The MIT License (MIT) - -Copyright (c) 2018 David Payne - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -// Additional Contributions: -/* 15 Jan 2019 : Owen Carter : Add psucontrol option and processing */ - - /********************************************** - * Edit Settings.h for personalization - ***********************************************/ - -#include "Settings.h" - -#define VERSION "3.0" - -#define HOSTNAME "PrintMon-" -#define CONFIG "/conf.txt" - -/* Useful Constants */ -#define SECS_PER_MIN (60UL) -#define SECS_PER_HOUR (3600UL) - -/* Useful Macros for getting elapsed time */ -#define numberOfSeconds(_time_) (_time_ % SECS_PER_MIN) -#define numberOfMinutes(_time_) ((_time_ / SECS_PER_MIN) % SECS_PER_MIN) -#define numberOfHours(_time_) (_time_ / SECS_PER_HOUR) - -// Initialize the oled display for I2C_DISPLAY_ADDRESS -// SDA_PIN and SCL_PIN -#if defined(DISPLAY_SH1106) - SH1106Wire display(I2C_DISPLAY_ADDRESS, SDA_PIN, SCL_PIN); -#else - SSD1306Wire display(I2C_DISPLAY_ADDRESS, SDA_PIN, SCL_PIN); // this is the default -#endif - -OLEDDisplayUi ui( &display ); - -void drawProgress(OLEDDisplay *display, int percentage, String label); -void drawOtaProgress(unsigned int, unsigned int); -void drawScreen1(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y); -void drawScreen2(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y); -void drawScreen3(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y); -void drawHeaderOverlay(OLEDDisplay *display, OLEDDisplayUiState* state); -void drawClock(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y); -void drawWeather(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y); -void drawClockHeaderOverlay(OLEDDisplay *display, OLEDDisplayUiState* state); - -// Set the number of Frames supported -const int numberOfFrames = 3; -FrameCallback frames[numberOfFrames]; -FrameCallback clockFrame[2]; -boolean isClockOn = false; - -OverlayCallback overlays[] = { drawHeaderOverlay }; -OverlayCallback clockOverlay[] = { drawClockHeaderOverlay }; -int numberOfOverlays = 1; - -// Time -TimeClient timeClient(UtcOffset); -long lastEpoch = 0; -long firstEpoch = 0; -long displayOffEpoch = 0; -String lastMinute = "xx"; -String lastSecond = "xx"; -String lastReportStatus = ""; -boolean displayOn = true; - -// Printer Client -#if defined(USE_REPETIER_CLIENT) - RepetierClient printerClient(PrinterApiKey, PrinterServer, PrinterPort, PrinterAuthUser, PrinterAuthPass, HAS_PSU); -#else - OctoPrintClient printerClient(PrinterApiKey, PrinterServer, PrinterPort, PrinterAuthUser, PrinterAuthPass, HAS_PSU); -#endif -int printerCount = 0; - -// Weather Client -OpenWeatherMapClient weatherClient(WeatherApiKey, CityIDs, 1, IS_METRIC, WeatherLanguage); - -//declairing prototypes -void configModeCallback (WiFiManager *myWiFiManager); -int8_t getWifiQuality(); - -ESP8266WebServer server(WEBSERVER_PORT); -ESP8266HTTPUpdateServer serverUpdater; - -static const char WEB_ACTIONS[] PROGMEM = " Home" - " Configure" - " Weather" - " Reset Settings" - " Forget WiFi" - " Firmware Update" - " About"; - -String CHANGE_FORM = ""; // moved to config to make it dynamic - -static const char CLOCK_FORM[] PROGMEM = "

Display Clock when printer is off

" - "

Use 24 Hour Clock (military time)

" - "

Flip display orientation

" - "

Flash System LED on Service Calls

" - "

Use OctoPrint PSU control plugin for clock/blank

" - "

Clock Sync / Weather Refresh (minutes)

"; - -static const char THEME_FORM[] PROGMEM = "

Theme Color

" - "


" - "

Use Security Credentials for Configuration Changes

" - "

" - "

" - ""; - -static const char WEATHER_FORM[] PROGMEM = "

Weather Config:

" - "

Display Weather when printer is off

" - "" - "" - "

" - "

Use Metric (Celsius)

" - "

Weather Language

" - "
" - ""; - -static const char LANG_OPTIONS[] PROGMEM = "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - ""; - -static const char COLOR_THEMES[] PROGMEM = "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - ""; - - -void setup() { - Serial.begin(115200); - SPIFFS.begin(); - delay(10); - - //New Line to clear from start garbage - Serial.println(); - - // Initialize digital pin for LED (little blue light on the Wemos D1 Mini) - pinMode(externalLight, OUTPUT); - - //Some Defaults before loading from Config.txt - PrinterPort = printerClient.getPrinterPort(); - - readSettings(); - - // initialize display - display.init(); - if (INVERT_DISPLAY) { - display.flipScreenVertically(); // connections at top of OLED display - } - display.clear(); - display.display(); - - //display.flipScreenVertically(); - - display.setTextAlignment(TEXT_ALIGN_CENTER); - display.setContrast(255); // default is 255 - display.setFont(ArialMT_Plain_16); - display.drawString(64, 1, "Printer Monitor"); - display.setFont(ArialMT_Plain_10); - display.drawString(64, 18, "for " + printerClient.getPrinterType()); - display.setFont(ArialMT_Plain_16); - display.drawString(64, 30, "By Qrome"); - display.drawString(64, 46, "V" + String(VERSION)); - display.display(); - - //WiFiManager - //Local intialization. Once its business is done, there is no need to keep it around - WiFiManager wifiManager; - - // Uncomment for testing wifi manager - //wifiManager.resetSettings(); - wifiManager.setAPCallback(configModeCallback); - - String hostname(HOSTNAME); - hostname += String(ESP.getChipId(), HEX); - if (!wifiManager.autoConnect((const char *)hostname.c_str())) {// new addition - delay(3000); - WiFi.disconnect(true); - ESP.reset(); - delay(5000); - } - - // You can change the transition that is used - // SLIDE_LEFT, SLIDE_RIGHT, SLIDE_TOP, SLIDE_DOWN - ui.setFrameAnimation(SLIDE_LEFT); - ui.setTargetFPS(30); - ui.disableAllIndicators(); - ui.setFrames(frames, (numberOfFrames)); - frames[0] = drawScreen1; - frames[1] = drawScreen2; - frames[2] = drawScreen3; - clockFrame[0] = drawClock; - clockFrame[1] = drawWeather; - ui.setOverlays(overlays, numberOfOverlays); - - // Inital UI takes care of initalising the display too. - ui.init(); - if (INVERT_DISPLAY) { - display.flipScreenVertically(); //connections at top of OLED display - } - - // print the received signal strength: - Serial.print("Signal Strength (RSSI): "); - Serial.print(getWifiQuality()); - Serial.println("%"); - - if (ENABLE_OTA) { - ArduinoOTA.onStart([]() { - Serial.println("Start"); - }); - ArduinoOTA.onEnd([]() { - Serial.println("\nEnd"); - }); - ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { - Serial.printf("Progress: %u%%\r", (progress / (total / 100))); - }); - ArduinoOTA.onError([](ota_error_t error) { - Serial.printf("Error[%u]: ", error); - if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); - else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); - else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); - else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); - else if (error == OTA_END_ERROR) Serial.println("End Failed"); - }); - ArduinoOTA.setHostname((const char *)hostname.c_str()); - if (OTA_Password != "") { - ArduinoOTA.setPassword(((const char *)OTA_Password.c_str())); - } - ArduinoOTA.begin(); - } - - if (WEBSERVER_ENABLED) { - server.on("/", displayPrinterStatus); - server.on("/systemreset", handleSystemReset); - server.on("/forgetwifi", handleWifiReset); - server.on("/updateconfig", handleUpdateConfig); - server.on("/updateweatherconfig", handleUpdateWeather); - server.on("/configure", handleConfigure); - server.on("/configureweather", handleWeatherConfigure); - server.onNotFound(redirectHome); - serverUpdater.setup(&server, "/update", www_username, www_password); - // Start the server - server.begin(); - Serial.println("Server started"); - // Print the IP address - String webAddress = "http://" + WiFi.localIP().toString() + ":" + String(WEBSERVER_PORT) + "/"; - Serial.println("Use this URL : " + webAddress); - display.clear(); - display.setTextAlignment(TEXT_ALIGN_CENTER); - display.setFont(ArialMT_Plain_10); - display.drawString(64, 10, "Web Interface On"); - display.drawString(64, 20, "You May Connect to IP"); - display.setFont(ArialMT_Plain_16); - display.drawString(64, 30, WiFi.localIP().toString()); - display.drawString(64, 46, "Port: " + String(WEBSERVER_PORT)); - display.display(); - } else { - Serial.println("Web Interface is Disabled"); - display.clear(); - display.setTextAlignment(TEXT_ALIGN_CENTER); - display.setFont(ArialMT_Plain_10); - display.drawString(64, 10, "Web Interface is Off"); - display.drawString(64, 20, "Enable in Settings.h"); - display.display(); - } - flashLED(5, 100); - findMDNS(); //go find Printer Server by the hostname - Serial.println("*** Leaving setup()"); -} - -void findMDNS() { - if (PrinterHostName == "" || ENABLE_OTA == false) { - return; // nothing to do here - } - // We now query our network for 'web servers' service - // over tcp, and get the number of available devices - int n = MDNS.queryService("http", "tcp"); - if (n == 0) { - Serial.println("no services found - make sure Printer server is turned on"); - return; - } - Serial.println("*** Looking for " + PrinterHostName + " over mDNS"); - for (int i = 0; i < n; ++i) { - // Going through every available service, - // we're searching for the one whose hostname - // matches what we want, and then get its IP - Serial.println("Found: " + MDNS.hostname(i)); - if (MDNS.hostname(i) == PrinterHostName) { - IPAddress serverIp = MDNS.IP(i); - PrinterServer = serverIp.toString(); - PrinterPort = MDNS.port(i); // save the port - Serial.println("*** Found Printer Server " + PrinterHostName + " http://" + PrinterServer + ":" + PrinterPort); - writeSettings(); // update the settings - } - } -} - -//************************************************************ -// Main Loop -//************************************************************ -void loop() { - - //Get Time Update - if((getMinutesFromLastRefresh() >= minutesBetweenDataRefresh) || lastEpoch == 0) { - getUpdateTime(); - } - - if (lastMinute != timeClient.getMinutes() && !printerClient.isPrinting()) { - // Check status every 60 seconds - ledOnOff(true); - lastMinute = timeClient.getMinutes(); // reset the check value - printerClient.getPrinterJobResults(); - printerClient.getPrinterPsuState(); - ledOnOff(false); - } else if (printerClient.isPrinting()) { - if (lastSecond != timeClient.getSeconds() && timeClient.getSeconds().endsWith("0")) { - lastSecond = timeClient.getSeconds(); - // every 10 seconds while printing get an update - ledOnOff(true); - printerClient.getPrinterJobResults(); - printerClient.getPrinterPsuState(); - ledOnOff(false); - } - } - - checkDisplay(); // Check to see if the printer is on or offline and change display. - - ui.update(); - - if (WEBSERVER_ENABLED) { - server.handleClient(); - } - if (ENABLE_OTA) { - ArduinoOTA.handle(); - } -} - -void getUpdateTime() { - ledOnOff(true); // turn on the LED - Serial.println(); - - if (displayOn && DISPLAYWEATHER) { - Serial.println("Getting Weather Data..."); - weatherClient.updateWeather(); - } - - Serial.println("Updating Time..."); - //Update the Time - timeClient.updateTime(); - lastEpoch = timeClient.getCurrentEpoch(); - Serial.println("Local time: " + timeClient.getAmPmFormattedTime()); - - ledOnOff(false); // turn off the LED -} - -boolean authentication() { - if (IS_BASIC_AUTH && (strlen(www_username) >= 1 && strlen(www_password) >= 1)) { - return server.authenticate(www_username, www_password); - } - return true; // Authentication not required -} - -void handleSystemReset() { - if (!authentication()) { - return server.requestAuthentication(); - } - Serial.println("Reset System Configuration"); - if (SPIFFS.remove(CONFIG)) { - redirectHome(); - ESP.restart(); - } -} - -void handleUpdateWeather() { - if (!authentication()) { - return 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"); - writeSettings(); - isClockOn = false; // this will force a check for the display - checkDisplay(); - lastEpoch = 0; - redirectHome(); -} - -void handleUpdateConfig() { - boolean flipOld = INVERT_DISPLAY; - if (!authentication()) { - return server.requestAuthentication(); - } - if (server.hasArg("printer")) { - printerClient.setPrinterName(server.arg("printer")); - } - PrinterApiKey = server.arg("PrinterApiKey"); - PrinterHostName = server.arg("PrinterHostName"); - PrinterServer = server.arg("PrinterAddress"); - PrinterPort = server.arg("PrinterPort").toInt(); - PrinterAuthUser = server.arg("octoUser"); - PrinterAuthPass = server.arg("octoPass"); - DISPLAYCLOCK = server.hasArg("isClockEnabled"); - IS_24HOUR = server.hasArg("is24hour"); - INVERT_DISPLAY = server.hasArg("invDisp"); - USE_FLASH = server.hasArg("useFlash"); - HAS_PSU = server.hasArg("hasPSU"); - minutesBetweenDataRefresh = server.arg("refresh").toInt(); - themeColor = server.arg("theme"); - UtcOffset = server.arg("utcoffset").toFloat(); - String temp = server.arg("userid"); - temp.toCharArray(www_username, sizeof(temp)); - temp = server.arg("stationpassword"); - temp.toCharArray(www_password, sizeof(temp)); - writeSettings(); - findMDNS(); - printerClient.getPrinterJobResults(); - printerClient.getPrinterPsuState(); - if (INVERT_DISPLAY != flipOld) { - ui.init(); - if(INVERT_DISPLAY) - display.flipScreenVertically(); - ui.update(); - } - checkDisplay(); - lastEpoch = 0; - redirectHome(); -} - -void handleWifiReset() { - if (!authentication()) { - return 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 handleWeatherConfigure() { - if (!authentication()) { - return server.requestAuthentication(); - } - ledOnOff(true); - String html = ""; - - 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", ""); - - html = getHeader(); - server.sendContent(html); - - String form = FPSTR(WEATHER_FORM); - String isWeatherChecked = ""; - if (DISPLAYWEATHER) { - isWeatherChecked = "checked='checked'"; - } - form.replace("%IS_WEATHER_CHECKED%", isWeatherChecked); - form.replace("%WEATHERKEY%", WeatherApiKey); - 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); - server.sendContent(form); - - html = getFooter(); - server.sendContent(html); - server.sendContent(""); - server.client().stop(); - ledOnOff(false); -} - -void handleConfigure() { - if (!authentication()) { - return server.requestAuthentication(); - } - ledOnOff(true); - String html = ""; - - 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", ""); - - html = getHeader(); - 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 = ""; - - server.sendContent(html); - } else { - html = ""; - server.sendContent(html); - } - - String form = CHANGE_FORM; - - form.replace("%OCTOKEY%", PrinterApiKey); - form.replace("%OCTOHOST%", PrinterHostName); - form.replace("%OCTOADDRESS%", PrinterServer); - form.replace("%OCTOPORT%", String(PrinterPort)); - form.replace("%OCTOUSER%", PrinterAuthUser); - form.replace("%OCTOPASS%", PrinterAuthPass); - - server.sendContent(form); - - form = FPSTR(CLOCK_FORM); - - String isClockChecked = ""; - if (DISPLAYCLOCK) { - isClockChecked = "checked='checked'"; - } - form.replace("%IS_CLOCK_CHECKED%", isClockChecked); - String is24hourChecked = ""; - if (IS_24HOUR) { - is24hourChecked = "checked='checked'"; - } - form.replace("%IS_24HOUR_CHECKED%", is24hourChecked); - String isInvDisp = ""; - if (INVERT_DISPLAY) { - isInvDisp = "checked='checked'"; - } - form.replace("%IS_INVDISP_CHECKED%", isInvDisp); - String isFlashLED = ""; - if (USE_FLASH) { - isFlashLED = "checked='checked'"; - } - form.replace("%USEFLASH%", isFlashLED); - String hasPSUchecked = ""; - if (HAS_PSU) { - hasPSUchecked = "checked='checked'"; - } - form.replace("%HAS_PSU_CHECKED%", hasPSUchecked); - - String options = ""; - options.replace(">"+String(minutesBetweenDataRefresh)+"<", " selected>"+String(minutesBetweenDataRefresh)+"<"); - form.replace("%OPTIONS%", options); - - server.sendContent(form); - - form = FPSTR(THEME_FORM); - - String themeOptions = FPSTR(COLOR_THEMES); - themeOptions.replace(">"+String(themeColor)+"<", " selected>"+String(themeColor)+"<"); - form.replace("%THEME_OPTIONS%", themeOptions); - form.replace("%UTCOFFSET%", String(UtcOffset)); - String isUseSecurityChecked = ""; - if (IS_BASIC_AUTH) { - isUseSecurityChecked = "checked='checked'"; - } - form.replace("%IS_BASICAUTH_CHECKED%", isUseSecurityChecked); - form.replace("%USERID%", String(www_username)); - form.replace("%STATIONPASSWORD%", String(www_password)); - - server.sendContent(form); - - html = getFooter(); - server.sendContent(html); - server.sendContent(""); - server.client().stop(); - ledOnOff(false); -} - -void displayMessage(String message) { - ledOnOff(true); - - 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", ""); - String html = getHeader(); - server.sendContent(String(html)); - server.sendContent(String(message)); - html = getFooter(); - server.sendContent(String(html)); - server.sendContent(""); - server.client().stop(); - - ledOnOff(false); -} - -void redirectHome() { - // Send them back to the Root Directory - server.sendHeader("Location", String("/"), true); - server.sendHeader("Cache-Control", "no-cache, no-store"); - server.sendHeader("Pragma", "no-cache"); - server.sendHeader("Expires", "-1"); - server.send(302, "text/plain", ""); - server.client().stop(); -} - -String getHeader() { - return getHeader(false); -} - -String 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 getFooter() { - int8_t rssi = getWifiQuality(); - Serial.print("Signal Strength (RSSI): "); - Serial.print(rssi); - Serial.println("%"); - String html = "


"; - html += "
"; - html += "
"; - if (lastReportStatus != "") { - html += " Report Status: " + lastReportStatus + "
"; - } - html += " Version: " + String(VERSION) + "
"; - html += " Signal Strength: "; - html += String(rssi) + "%"; - html += "
"; - html += ""; - return html; -} - -void displayPrinterStatus() { - ledOnOff(true); - String html = ""; - - 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(String(getHeader(true))); - - String displayTime = timeClient.getAmPmHours() + ":" + timeClient.getMinutes() + ":" + timeClient.getSeconds() + " " + timeClient.getAmPm(); - if (IS_24HOUR) { - displayTime = timeClient.getHours() + ":" + timeClient.getMinutes() + ":" + timeClient.getSeconds(); - } - - html += "

" + printerClient.getPrinterType() + " Monitor

"; - html += "

"; - if (printerClient.getPrinterType() == "Repetier") { - html += "Printer Name: " + printerClient.getPrinterName() + "
"; - } else { - html += "Host Name: " + PrinterHostName + "
"; - } - - if (printerClient.getError() != "") { - html += "Status: Offline
"; - html += "Reason: " + printerClient.getError() + "
"; - } else { - html += "Status: " + printerClient.getState(); - if (printerClient.isPSUoff() && HAS_PSU) { - 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 = numberOfHours(val); - int minutes = numberOfMinutes(val); - int seconds = numberOfSeconds(val); - html += "Est. Print Time Left: " + zeroPad(hours) + ":" + zeroPad(minutes) + ":" + zeroPad(seconds) + "
"; - - val = printerClient.getProgressPrintTime().toInt(); - hours = numberOfHours(val); - minutes = numberOfMinutes(val); - seconds = numberOfSeconds(val); - html += "Printing Time: " + zeroPad(hours) + ":" + zeroPad(minutes) + ":" + zeroPad(seconds) + "
"; - html += ""; - html += "

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

"; - - html += "

Time: " + displayTime + "

"; - - 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 - } - - server.sendContent(String(getFooter())); - server.sendContent(""); - server.client().stop(); - ledOnOff(false); -} - -void configModeCallback (WiFiManager *myWiFiManager) { - Serial.println("Entered config mode"); - Serial.println(WiFi.softAPIP()); - - display.clear(); - display.setTextAlignment(TEXT_ALIGN_CENTER); - display.setFont(ArialMT_Plain_10); - display.drawString(64, 0, "Wifi Manager"); - display.drawString(64, 10, "Please connect to AP"); - display.setFont(ArialMT_Plain_16); - display.drawString(64, 26, myWiFiManager->getConfigPortalSSID()); - display.setFont(ArialMT_Plain_10); - display.drawString(64, 46, "To setup Wifi connection"); - display.display(); - - Serial.println("Wifi Manager"); - Serial.println("Please connect to AP"); - Serial.println(myWiFiManager->getConfigPortalSSID()); - Serial.println("To setup Wifi Configuration"); - flashLED(20, 50); -} - -void ledOnOff(boolean value) { - if (USE_FLASH) { - if (value) { - digitalWrite(externalLight, LOW); // LED ON - } else { - digitalWrite(externalLight, HIGH); // LED OFF - } - } -} - -void flashLED(int number, int delayTime) { - for (int inx = 0; inx <= number; inx++) { - delay(delayTime); - digitalWrite(externalLight, LOW); // ON - delay(delayTime); - digitalWrite(externalLight, HIGH); // OFF - delay(delayTime); - } -} - -void drawScreen1(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { - String bed = printerClient.getValueRounded(printerClient.getTempBedActual()); - String tool = printerClient.getValueRounded(printerClient.getTempToolActual()); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(ArialMT_Plain_16); - if (bed != "0") { - display->drawString(29 + x, 0 + y, "Tool"); - display->drawString(89 + x, 0 + y, "Bed"); - } else { - display->drawString(64 + x, 0 + y, "Tool Temp"); - } - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(ArialMT_Plain_24); - if (bed != "0") { - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->drawString(12 + x, 14 + y, tool + "°"); - display->drawString(74 + x, 14 + y, bed + "°"); - } else { - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(64 + x, 14 + y, tool + "°"); - } -} - -void drawScreen2(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(ArialMT_Plain_16); - - display->drawString(64 + x, 0 + y, "Time Remaining"); - //display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(ArialMT_Plain_24); - int val = printerClient.getProgressPrintTimeLeft().toInt(); - int hours = numberOfHours(val); - int minutes = numberOfMinutes(val); - int seconds = numberOfSeconds(val); - - String time = zeroPad(hours) + ":" + zeroPad(minutes) + ":" + zeroPad(seconds); - display->drawString(64 + x, 14 + y, time); -} - -void drawScreen3(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(ArialMT_Plain_16); - - display->drawString(64 + x, 0 + y, "Printing Time"); - //display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(ArialMT_Plain_24); - int val = printerClient.getProgressPrintTime().toInt(); - int hours = numberOfHours(val); - int minutes = numberOfMinutes(val); - int seconds = numberOfSeconds(val); - - String time = zeroPad(hours) + ":" + zeroPad(minutes) + ":" + zeroPad(seconds); - display->drawString(64 + x, 14 + y, time); -} - -void drawClock(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { - display->setTextAlignment(TEXT_ALIGN_CENTER); - - String displayTime = timeClient.getAmPmHours() + ":" + timeClient.getMinutes() + ":" + timeClient.getSeconds(); - if (IS_24HOUR) { - displayTime = timeClient.getHours() + ":" + timeClient.getMinutes() + ":" + timeClient.getSeconds(); - } - String displayName = PrinterHostName; - if (printerClient.getPrinterType() == "Repetier") { - displayName = printerClient.getPrinterName(); - } - display->setFont(ArialMT_Plain_16); - display->drawString(64 + x, 0 + y, displayName); - display->setFont(ArialMT_Plain_24); - display->drawString(64 + x, 17 + y, displayTime); -} - -void drawWeather(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(ArialMT_Plain_24); - display->drawString(0 + x, 0 + y, weatherClient.getTempRounded(0) + getTempSymbol()); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(ArialMT_Plain_24); - - display->setFont(ArialMT_Plain_16); - display->drawString(0 + x, 24 + y, weatherClient.getCondition(0)); - display->setFont((const uint8_t*)Meteocons_Plain_42); - display->drawString(86 + x, 0 + y, weatherClient.getWeatherIcon(0)); -} - -String getTempSymbol() { - return getTempSymbol(false); -} - -String getTempSymbol(boolean forHTML) { - String rtnValue = "F"; - if (IS_METRIC) { - rtnValue = "C"; - } - if (forHTML) { - rtnValue = "°" + rtnValue; - } else { - rtnValue = "°" + rtnValue; - } - return rtnValue; -} - -String getSpeedSymbol() { - String rtnValue = "mph"; - if (IS_METRIC) { - rtnValue = "kph"; - } - return rtnValue; -} - -String zeroPad(int value) { - String rtnValue = String(value); - if (value < 10) { - rtnValue = "0" + rtnValue; - } - return rtnValue; -} - -void drawHeaderOverlay(OLEDDisplay *display, OLEDDisplayUiState* state) { - display->setColor(WHITE); - display->setFont(ArialMT_Plain_16); - String displayTime = timeClient.getAmPmHours() + ":" + timeClient.getMinutes(); - if (IS_24HOUR) { - displayTime = timeClient.getHours() + ":" + timeClient.getMinutes(); - } - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->drawString(0, 48, displayTime); - - if (!IS_24HOUR) { - String ampm = timeClient.getAmPm(); - display->setFont(ArialMT_Plain_10); - display->drawString(39, 54, ampm); - } - - display->setFont(ArialMT_Plain_16); - display->setTextAlignment(TEXT_ALIGN_LEFT); - String percent = String(printerClient.getProgressCompletion()) + "%"; - display->drawString(64, 48, percent); - - // Draw indicator to show next update - int updatePos = (printerClient.getProgressCompletion().toFloat() / float(100)) * 128; - display->drawRect(0, 41, 128, 6); - display->drawHorizontalLine(0, 42, updatePos); - display->drawHorizontalLine(0, 43, updatePos); - display->drawHorizontalLine(0, 44, updatePos); - display->drawHorizontalLine(0, 45, updatePos); - - drawRssi(display); -} - -void drawClockHeaderOverlay(OLEDDisplay *display, OLEDDisplayUiState* state) { - display->setColor(WHITE); - display->setFont(ArialMT_Plain_16); - display->setTextAlignment(TEXT_ALIGN_LEFT); - if (!IS_24HOUR) { - display->drawString(0, 48, timeClient.getAmPm()); - display->setTextAlignment(TEXT_ALIGN_CENTER); - if (printerClient.isPSUoff()) { - display->drawString(64, 47, "psu off"); - } else if (printerClient.getState() == "Operational") { - display->drawString(64, 47, "online"); - } else { - display->drawString(64, 47, "offline"); - } - } else { - if (printerClient.isPSUoff()) { - display->drawString(0, 47, "psu off"); - } else if (printerClient.getState() == "Operational") { - display->drawString(0, 47, "online"); - } else { - display->drawString(0, 47, "offline"); - } - } - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->drawRect(0, 43, 128, 2); - - drawRssi(display); -} - -void drawRssi(OLEDDisplay *display) { - - - int8_t quality = getWifiQuality(); - for (int8_t i = 0; i < 4; i++) { - for (int8_t j = 0; j < 3 * (i + 2); j++) { - if (quality > i * 25 || j == 0) { - display->setPixel(114 + 4 * i, 63 - j); - } - } - } -} - -// converts the dBm to a range between 0 and 100% -int8_t getWifiQuality() { - int32_t dbm = WiFi.RSSI(); - if(dbm <= -100) { - return 0; - } else if(dbm >= -50) { - return 100; - } else { - return 2 * (dbm + 100); - } -} - - -void writeSettings() { - // Save decoded message to SPIFFS file for playback on power up. - File f = SPIFFS.open(CONFIG, "w"); - if (!f) { - Serial.println("File open failed!"); - } else { - Serial.println("Saving settings now..."); - f.println("UtcOffset=" + String(UtcOffset)); - f.println("printerApiKey=" + PrinterApiKey); - f.println("printerHostName=" + PrinterHostName); - f.println("printerServer=" + PrinterServer); - f.println("printerPort=" + String(PrinterPort)); - f.println("printerName=" + printerClient.getPrinterName()); - f.println("printerAuthUser=" + PrinterAuthUser); - f.println("printerAuthPass=" + PrinterAuthPass); - f.println("refreshRate=" + String(minutesBetweenDataRefresh)); - f.println("themeColor=" + themeColor); - f.println("IS_BASIC_AUTH=" + String(IS_BASIC_AUTH)); - f.println("www_username=" + String(www_username)); - f.println("www_password=" + String(www_password)); - f.println("DISPLAYCLOCK=" + String(DISPLAYCLOCK)); - f.println("is24hour=" + String(IS_24HOUR)); - f.println("invertDisp=" + String(INVERT_DISPLAY)); - 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("hasPSU=" + String(HAS_PSU)); - } - f.close(); - readSettings(); - timeClient.setUtcOffset(UtcOffset); -} - -void readSettings() { - if (SPIFFS.exists(CONFIG) == false) { - Serial.println("Settings File does not yet exists."); - writeSettings(); - return; - } - File fr = SPIFFS.open(CONFIG, "r"); - String line; - while(fr.available()) { - line = fr.readStringUntil('\n'); - - if (line.indexOf("UtcOffset=") >= 0) { - UtcOffset = line.substring(line.lastIndexOf("UtcOffset=") + 10).toFloat(); - Serial.println("UtcOffset=" + String(UtcOffset)); - } - if (line.indexOf("printerApiKey=") >= 0) { - PrinterApiKey = line.substring(line.lastIndexOf("printerApiKey=") + 14); - PrinterApiKey.trim(); - Serial.println("PrinterApiKey=" + PrinterApiKey); - } - if (line.indexOf("printerHostName=") >= 0) { - PrinterHostName = line.substring(line.lastIndexOf("printerHostName=") + 16); - PrinterHostName.trim(); - Serial.println("PrinterHostName=" + PrinterHostName); - } - if (line.indexOf("printerServer=") >= 0) { - PrinterServer = line.substring(line.lastIndexOf("printerServer=") + 14); - PrinterServer.trim(); - Serial.println("PrinterServer=" + PrinterServer); - } - if (line.indexOf("printerPort=") >= 0) { - PrinterPort = line.substring(line.lastIndexOf("printerPort=") + 12).toInt(); - Serial.println("PrinterPort=" + String(PrinterPort)); - } - if (line.indexOf("printerName=") >= 0) { - String printer = line.substring(line.lastIndexOf("printerName=") + 12); - printer.trim(); - printerClient.setPrinterName(printer); - Serial.println("PrinterName=" + printerClient.getPrinterName()); - } - if (line.indexOf("printerAuthUser=") >= 0) { - PrinterAuthUser = line.substring(line.lastIndexOf("printerAuthUser=") + 16); - PrinterAuthUser.trim(); - Serial.println("PrinterAuthUser=" + PrinterAuthUser); - } - if (line.indexOf("printerAuthPass=") >= 0) { - PrinterAuthPass = line.substring(line.lastIndexOf("printerAuthPass=") + 16); - PrinterAuthPass.trim(); - Serial.println("PrinterAuthPass=" + PrinterAuthPass); - } - if (line.indexOf("refreshRate=") >= 0) { - minutesBetweenDataRefresh = line.substring(line.lastIndexOf("refreshRate=") + 12).toInt(); - Serial.println("minutesBetweenDataRefresh=" + String(minutesBetweenDataRefresh)); - } - if (line.indexOf("themeColor=") >= 0) { - themeColor = line.substring(line.lastIndexOf("themeColor=") + 11); - themeColor.trim(); - Serial.println("themeColor=" + themeColor); - } - if (line.indexOf("IS_BASIC_AUTH=") >= 0) { - IS_BASIC_AUTH = line.substring(line.lastIndexOf("IS_BASIC_AUTH=") + 14).toInt(); - Serial.println("IS_BASIC_AUTH=" + String(IS_BASIC_AUTH)); - } - if (line.indexOf("www_username=") >= 0) { - String temp = line.substring(line.lastIndexOf("www_username=") + 13); - temp.trim(); - temp.toCharArray(www_username, sizeof(temp)); - Serial.println("www_username=" + String(www_username)); - } - if (line.indexOf("www_password=") >= 0) { - String temp = line.substring(line.lastIndexOf("www_password=") + 13); - temp.trim(); - temp.toCharArray(www_password, sizeof(temp)); - Serial.println("www_password=" + String(www_password)); - } - if (line.indexOf("DISPLAYCLOCK=") >= 0) { - DISPLAYCLOCK = line.substring(line.lastIndexOf("DISPLAYCLOCK=") + 13).toInt(); - Serial.println("DISPLAYCLOCK=" + String(DISPLAYCLOCK)); - } - if (line.indexOf("is24hour=") >= 0) { - IS_24HOUR = line.substring(line.lastIndexOf("is24hour=") + 9).toInt(); - Serial.println("IS_24HOUR=" + String(IS_24HOUR)); - } - if(line.indexOf("invertDisp=") >= 0) { - INVERT_DISPLAY = line.substring(line.lastIndexOf("invertDisp=") + 11).toInt(); - Serial.println("INVERT_DISPLAY=" + String(INVERT_DISPLAY)); - } - if(line.indexOf("USE_FLASH=") >= 0) { - USE_FLASH = line.substring(line.lastIndexOf("USE_FLASH=") + 10).toInt(); - Serial.println("USE_FLASH=" + String(USE_FLASH)); - } - if (line.indexOf("hasPSU=") >= 0) { - HAS_PSU = line.substring(line.lastIndexOf("hasPSU=") + 7).toInt(); - Serial.println("HAS_PSU=" + String(HAS_PSU)); - } - if (line.indexOf("isWeather=") >= 0) { - DISPLAYWEATHER = line.substring(line.lastIndexOf("isWeather=") + 10).toInt(); - Serial.println("DISPLAYWEATHER=" + String(DISPLAYWEATHER)); - } - if (line.indexOf("weatherKey=") >= 0) { - WeatherApiKey = line.substring(line.lastIndexOf("weatherKey=") + 11); - WeatherApiKey.trim(); - Serial.println("WeatherApiKey=" + WeatherApiKey); - } - if (line.indexOf("CityID=") >= 0) { - CityIDs[0] = line.substring(line.lastIndexOf("CityID=") + 7).toInt(); - Serial.println("CityID: " + String(CityIDs[0])); - } - if (line.indexOf("isMetric=") >= 0) { - IS_METRIC = line.substring(line.lastIndexOf("isMetric=") + 9).toInt(); - Serial.println("IS_METRIC=" + String(IS_METRIC)); - } - if (line.indexOf("language=") >= 0) { - WeatherLanguage = line.substring(line.lastIndexOf("language=") + 9); - WeatherLanguage.trim(); - Serial.println("WeatherLanguage=" + WeatherLanguage); - } - } - fr.close(); - printerClient.updatePrintClient(PrinterApiKey, PrinterServer, PrinterPort, PrinterAuthUser, PrinterAuthPass, HAS_PSU); - weatherClient.updateWeatherApiKey(WeatherApiKey); - weatherClient.updateLanguage(WeatherLanguage); - weatherClient.setMetric(IS_METRIC); - weatherClient.updateCityIdList(CityIDs, 1); - timeClient.setUtcOffset(UtcOffset); -} - -int getMinutesFromLastRefresh() { - int minutes = (timeClient.getCurrentEpoch() - lastEpoch) / 60; - return minutes; -} - -int getMinutesFromLastDisplay() { - int minutes = (timeClient.getCurrentEpoch() - displayOffEpoch) / 60; - return minutes; -} - -// Toggle on and off the display if user defined times -void checkDisplay() { - if (!displayOn && DISPLAYCLOCK) { - enableDisplay(true); - } - if (displayOn && !printerClient.isPrinting() && !DISPLAYCLOCK) { - // Put Display to sleep - display.clear(); - display.display(); - display.setFont(ArialMT_Plain_16); - display.setTextAlignment(TEXT_ALIGN_CENTER); - display.setContrast(255); // default is 255 - display.drawString(64, 5, "Printer Offline\nSleep Mode..."); - display.display(); - delay(5000); - enableDisplay(false); - Serial.println("Printer is offline going down to sleep..."); - return; - } else if (!displayOn && !DISPLAYCLOCK) { - if (printerClient.isOperational()) { - // Wake the Screen up - enableDisplay(true); - display.clear(); - display.display(); - display.setFont(ArialMT_Plain_16); - display.setTextAlignment(TEXT_ALIGN_CENTER); - display.setContrast(255); // default is 255 - display.drawString(64, 5, "Printer Online\nWake up..."); - display.display(); - Serial.println("Printer is online waking up..."); - delay(5000); - return; - } - } else if (DISPLAYCLOCK) { - if ((!printerClient.isPrinting() || printerClient.isPSUoff()) && !isClockOn) { - Serial.println("Clock Mode is turned on."); - if (!DISPLAYWEATHER) { - ui.disableAutoTransition(); - ui.setFrames(clockFrame, 1); - clockFrame[0] = drawClock; - } else { - ui.enableAutoTransition(); - ui.setFrames(clockFrame, 2); - clockFrame[0] = drawClock; - clockFrame[1] = drawWeather; - } - ui.setOverlays(clockOverlay, numberOfOverlays); - isClockOn = true; - } else if (printerClient.isPrinting() && !printerClient.isPSUoff() && isClockOn) { - Serial.println("Printer Monitor is active."); - ui.setFrames(frames, numberOfFrames); - ui.setOverlays(overlays, numberOfOverlays); - ui.enableAutoTransition(); - isClockOn = false; - } - } -} - -void enableDisplay(boolean enable) { - displayOn = enable; - if (enable) { - if (getMinutesFromLastDisplay() >= minutesBetweenDataRefresh) { - // The display has been off longer than the minutes between refresh -- need to get fresh data - lastEpoch = 0; // this should force a data pull - displayOffEpoch = 0; // reset - } - display.displayOn(); - Serial.println("Display was turned ON: " + timeClient.getFormattedTime()); - } else { - display.displayOff(); - Serial.println("Display was turned OFF: " + timeClient.getFormattedTime()); - displayOffEpoch = lastEpoch; - } -} +#include + +/** The MIT License (MIT) + + Copyright (c) 2018 David Payne + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +// Additional Contributions: +/* 15 Jan 2019 : Owen Carter : Add psucontrol option and processing */ +/********************************************** + Edit Settings.h for personalization +***********************************************/ + +#include "Settings.h" + +#define VERSION "2.5" + +#define HOSTNAME "OctMon-" +#define CONFIG "/conf.txt" + +/* Useful Constants */ +#define SECS_PER_MIN (60UL) +#define SECS_PER_HOUR (3600UL) + +/* Useful Macros for getting elapsed time */ +#define numberOfSeconds(_time_) (_time_ % SECS_PER_MIN) +#define numberOfMinutes(_time_) ((_time_ / SECS_PER_MIN) % SECS_PER_MIN) +#define numberOfHours(_time_) (_time_ / SECS_PER_HOUR) + +// Initialize the oled display for I2C_DISPLAY_ADDRESS +// SDA_PIN and SCL_PIN +#if defined(DISPLAY_SH1106) +SH1106Wire display(I2C_DISPLAY_ADDRESS, SDA_PIN, SCL_PIN); +#else +SSD1306Wire display(I2C_DISPLAY_ADDRESS, SDA_PIN, SCL_PIN); // this is the default +#endif + +#include //for UDP +int ledstat; +unsigned int localPort = 2000; // local port to listen for UDP packets +IPAddress SendIP(192, 168, 0, 255); //UDP Broadcast IP data sent to all devicess on same network +WiFiUDP udp; +char packetBuffer[9]; //Where we get the UDP data +OLEDDisplayUi ui( &display ); + +void drawProgress(OLEDDisplay *display, int percentage, String label); +void drawOtaProgress(unsigned int, unsigned int); +void drawScreen1(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y); +void drawScreen2(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y); +void drawScreen3(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y); +void drawHeaderOverlay(OLEDDisplay *display, OLEDDisplayUiState* state); +void drawClock(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y); +void drawWeather(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y); +void drawClockHeaderOverlay(OLEDDisplay *display, OLEDDisplayUiState* state); + +// Set the number of Frames supported +const int numberOfFrames = 3; +FrameCallback frames[numberOfFrames]; +FrameCallback clockFrame[2]; +boolean isClockOn = false; + +OverlayCallback overlays[] = { drawHeaderOverlay }; +OverlayCallback clockOverlay[] = { drawClockHeaderOverlay }; +int numberOfOverlays = 1; + +// Time +TimeClient timeClient(UtcOffset); +long lastEpoch = 0; +long firstEpoch = 0; +long displayOffEpoch = 0; +String lastMinute = "xx"; +String lastSecond = "xx"; +String lastReportStatus = ""; +boolean displayOn = true; + +//LDR +int ldrStatus =0; + +// OctoPrint Client +OctoPrintClient printerClient(OctoPrintApiKey, OctoPrintServer, OctoPrintPort, OctoAuthUser, OctoAuthPass, HAS_PSU); +int printerCount = 0; + +// Weather Client +OpenWeatherMapClient weatherClient(WeatherApiKey, CityIDs, 1, IS_METRIC, WeatherLanguage); + +//declairing prototypes +void configModeCallback (WiFiManager *myWiFiManager); +int8_t getWifiQuality(); + +ESP8266WebServer server(WEBSERVER_PORT); +ESP8266HTTPUpdateServer serverUpdater; + +String WEB_ACTIONS = " Home" + " Configure" + " Weather" + " Reset Settings" + " Forget WiFi" + " Firmware Update" + " About"; + +String CHANGE_FORM = "

Station Config:

" + "

" + "

" + "

" + "

" + "

" + "


" + "

Display Clock when printer is off

" + "

Use 24 Hour Clock (military time)

" + "

Flip display orientation

" + "

Use OctoPrint PSU control plugin for clock/blank

" + "

Clock Sync / Weather Refresh (minutes)

"; + +String THEME_FORM = "

Theme Color

" + "


" + "

Use Security Credentials for Configuration Changes

" + "

" + "

" + ""; + +String WEATHER_FORM = "

Weather Config:

" + "

Display Weather when printer is off

" + "" + "" + "

" + "

" + "

Use Metric (Celsius)

" + "

Weather Language

" + "
" + ""; + +String LANG_OPTIONS = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; + +String COLOR_THEMES = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; + + +void setup() { + Serial.begin(115200); + SPIFFS.begin(); + delay(10); + + //New Line to clear from start garbage + Serial.println(); + + // Initialize digital pin for LED (little blue light on the Wemos D1 Mini) + pinMode(externalLight, OUTPUT); + + readSettings(); + + // initialize display + display.init(); + if (INVERT_DISPLAY) { + display.flipScreenVertically(); // connections at top of OLED display + } + display.clear(); + display.display(); + + //display.flipScreenVertically(); + display.setFont(ArialMT_Plain_16); + display.setTextAlignment(TEXT_ALIGN_CENTER); + display.setContrast(255); // default is 255 + display.drawString(64, 5, "Printer Monitor\nBy Qrome\nV" + String(VERSION)); + display.display(); + + //WiFiManager + //Local intialization. Once its business is done, there is no need to keep it around + WiFiManager wifiManager; + + // Uncomment for testing wifi manager + //wifiManager.resetSettings(); + wifiManager.setAPCallback(configModeCallback); + + String hostname(HOSTNAME); + hostname += String(ESP.getChipId(), HEX); + if (!wifiManager.autoConnect((const char *)hostname.c_str())) {// new addition + delay(3000); + WiFi.disconnect(true); + ESP.reset(); + delay(5000); + } + + // You can change the transition that is used + // SLIDE_LEFT, SLIDE_RIGHT, SLIDE_TOP, SLIDE_DOWN + ui.setFrameAnimation(SLIDE_LEFT); + ui.setTargetFPS(30); + ui.disableAllIndicators(); + ui.setFrames(frames, (numberOfFrames)); + frames[0] = drawScreen1; + frames[1] = drawScreen2; + frames[2] = drawScreen3; + clockFrame[0] = drawClock; + clockFrame[1] = drawWeather; + ui.setOverlays(overlays, numberOfOverlays); + + // Inital UI takes care of initalising the display too. + ui.init(); + if (INVERT_DISPLAY) { + display.flipScreenVertically(); //connections at top of OLED display + } + + // print the received signal strength: + Serial.print("Signal Strength (RSSI): "); + Serial.print(getWifiQuality()); + Serial.println("%"); + + if (ENABLE_OTA) { + ArduinoOTA.onStart([]() { + Serial.println("Start"); + }); + ArduinoOTA.onEnd([]() { + Serial.println("\nEnd"); + }); + ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { + Serial.printf("Progress: %u%%\r", (progress / (total / 100))); + }); + ArduinoOTA.onError([](ota_error_t error) { + Serial.printf("Error[%u]: ", error); + if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); + else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); + else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); + else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); + else if (error == OTA_END_ERROR) Serial.println("End Failed"); + }); + ArduinoOTA.setHostname((const char *)hostname.c_str()); + if (OTA_Password != "") { + ArduinoOTA.setPassword(((const char *)OTA_Password.c_str())); + } + ArduinoOTA.begin(); + } + + if (WEBSERVER_ENABLED) { + server.on("/", displayPrinterStatus); + server.on("/systemreset", handleSystemReset); + server.on("/forgetwifi", handleWifiReset); + server.on("/updateconfig", handleUpdateConfig); + server.on("/updateweatherconfig", handleUpdateWeather); + server.on("/configure", handleConfigure); + server.on("/configureweather", handleWeatherConfigure); + server.onNotFound(redirectHome); + serverUpdater.setup(&server, "/update", www_username, www_password); + // Start the server + server.begin(); + Serial.println("Server started"); + // Print the IP address + String webAddress = "http://" + WiFi.localIP().toString() + ":" + String(WEBSERVER_PORT) + "/"; + Serial.println("Use this URL : " + webAddress); + display.clear(); + display.setTextAlignment(TEXT_ALIGN_CENTER); + display.setFont(ArialMT_Plain_10); + display.drawString(64, 10, "Web Interface On"); + display.drawString(64, 20, "You May Connect to IP"); + display.setFont(ArialMT_Plain_16); + display.drawString(64, 30, WiFi.localIP().toString()); + display.drawString(64, 46, "Port: " + String(WEBSERVER_PORT)); + display.display(); + } else { + Serial.println("Web Interface is Disabled"); + display.clear(); + display.setTextAlignment(TEXT_ALIGN_CENTER); + display.setFont(ArialMT_Plain_10); + display.drawString(64, 10, "Web Interface is Off"); + display.drawString(64, 20, "Enable in Settings.h"); + display.display(); + } + flashLED(5, 500); + findMDNS(); //go find Octoprint Server by the hostname + Serial.println("*** Leaving setup()"); + { + + + pinMode(D8, OUTPUT); + digitalWrite(D8, LOW); + Serial.println(""); + Serial.print("Connected to "); + Serial.print("IP address: "); + Serial.println(WiFi.localIP()); + Serial.println("Starting UDP"); + udp.begin(localPort); + Serial.print("Local port: "); + Serial.println(udp.localPort()); + } +} + +void findMDNS() { + if (OctoPrintHostName == "" || ENABLE_OTA == false) { + return; // nothing to do here + } + // We now query our network for 'web servers' service + // over tcp, and get the number of available devices + int n = MDNS.queryService("http", "tcp"); + if (n == 0) { + Serial.println("no services found - make sure OctoPrint server is turned on"); + return; + } + Serial.println("*** Looking for " + OctoPrintHostName + " over mDNS"); + for (int i = 0; i < n; ++i) { + // Going through every available service, + // we're searching for the one whose hostname + // matches what we want, and then get its IP + Serial.println("Found: " + MDNS.hostname(i)); + if (MDNS.hostname(i) == OctoPrintHostName) { + IPAddress serverIp = MDNS.IP(i); + OctoPrintServer = serverIp.toString(); + OctoPrintPort = MDNS.port(i); // save the port + Serial.println("*** Found OctoPrint Server " + OctoPrintHostName + " http://" + OctoPrintServer + ":" + OctoPrintPort); + writeSettings(); // update the settings + } + } +} + +//************************************************************ +// Main Looop +//************************************************************ +void loop() { + + //Get Time Update + if ((getMinutesFromLastRefresh() >= minutesBetweenDataRefresh) || lastEpoch == 0) { + getUpdateTime(); + } + + if (lastMinute != timeClient.getMinutes() && !printerClient.isPrinting()) { + // Check status every 60 seconds + digitalWrite(externalLight, LOW); + lastMinute = timeClient.getMinutes(); // reset the check value + printerClient.getPrinterJobResults(); + printerClient.getPrinterPsuState(); + digitalWrite(externalLight, HIGH); + } else if (printerClient.isPrinting()) { + if (lastSecond != timeClient.getSeconds() && timeClient.getSeconds().endsWith("0")) { + lastSecond = timeClient.getSeconds(); + // every 10 seconds while printing get an update + digitalWrite(externalLight, LOW); + printerClient.getPrinterJobResults(); + printerClient.getPrinterPsuState(); + digitalWrite(externalLight, HIGH); + } + } + + checkDisplay(); // Check to see if the printer is on or offline and change display. + lightOn(); + ui.update(); + + if (WEBSERVER_ENABLED) { + server.handleClient(); + } + if (ENABLE_OTA) { + ArduinoOTA.handle(); + } + { //Loop for motion LED + int cb = udp.parsePacket();//start of UDP + if (!cb) + { + //If serial data is recived send it to UDP + if (Serial.available() > 0) + { + udp.beginPacket(SendIP, 2000); //Send Data to Master unit + //Send UDP requests are to port + char a[1]; + a[0] = char(Serial.read()); //Serial Byte Read + udp.write(a, 1); //Send one byte to ESP8266 + udp.endPacket(); + } + } + else { + + digitalWrite(D8, HIGH); + delay(5000); + digitalWrite(D8, LOW); + delay(20); + } + }//end of UDP +} + +void lightOn() { + ldrStatus = analogRead(ldrPin); + if(ldrStatus>=250){ + Serial.print("LDR true"); + DISPLAYCLOCK = true; + //enableDisplay(true); + } + else{ + Serial.print("LDR false"); + DISPLAYCLOCK = false; + // enableDisplay(false); + } +} + +void getUpdateTime() { + digitalWrite(externalLight, LOW); // turn on the LED + Serial.println(); + + if (displayOn && DISPLAYWEATHER) { + Serial.println("Getting Weather Data..."); + weatherClient.updateWeather(); + } + + Serial.println("Updating Time..."); + //Update the Time + timeClient.updateTime(); + lastEpoch = timeClient.getCurrentEpoch(); + Serial.println("Local time: " + timeClient.getAmPmFormattedTime()); + + digitalWrite(externalLight, HIGH); // turn off the LED +} + +boolean authentication() { + if (IS_BASIC_AUTH && (strlen(www_username) >= 1 && strlen(www_password) >= 1)) { + return server.authenticate(www_username, www_password); + } + return true; // Authentication not required +} + +void handleSystemReset() { + if (!authentication()) { + return server.requestAuthentication(); + } + Serial.println("Reset System Configuration"); + if (SPIFFS.remove(CONFIG)) { + redirectHome(); + ESP.restart(); + } +} + +void handleUpdateWeather() { + if (!authentication()) { + return 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"); + writeSettings(); + isClockOn = false; // this will force a check for the display + checkDisplay(); + lastEpoch = 0; + redirectHome(); +} + +void handleUpdateConfig() { + boolean flipOld = INVERT_DISPLAY; + if (!authentication()) { + return server.requestAuthentication(); + } + OctoPrintApiKey = server.arg("octoPrintApiKey"); + OctoPrintHostName = server.arg("octoPrintHostName"); + OctoPrintServer = server.arg("octoPrintAddress"); + OctoPrintPort = server.arg("octoPrintPort").toInt(); + OctoAuthUser = server.arg("octoUser"); + OctoAuthPass = server.arg("octoPass"); + DISPLAYCLOCK = server.hasArg("isClockEnabled"); + IS_24HOUR = server.hasArg("is24hour"); + INVERT_DISPLAY = server.hasArg("invDisp"); + HAS_PSU = server.hasArg("hasPSU"); + minutesBetweenDataRefresh = server.arg("refresh").toInt(); + themeColor = server.arg("theme"); + UtcOffset = server.arg("utcoffset").toFloat(); + String temp = server.arg("userid"); + temp.toCharArray(www_username, sizeof(temp)); + temp = server.arg("stationpassword"); + temp.toCharArray(www_password, sizeof(temp)); + writeSettings(); + findMDNS(); + printerClient.getPrinterJobResults(); + printerClient.getPrinterPsuState(); + if (INVERT_DISPLAY != flipOld) { + ui.init(); + if (INVERT_DISPLAY) + display.flipScreenVertically(); + ui.update(); + } + checkDisplay(); + lastEpoch = 0; + redirectHome(); +} + +void handleWifiReset() { + if (!authentication()) { + return 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 handleWeatherConfigure() { + if (!authentication()) { + return server.requestAuthentication(); + } + digitalWrite(externalLight, LOW); + String html = ""; + + 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", ""); + + html = getHeader(); + server.sendContent(html); + + String form = WEATHER_FORM; + String isWeatherChecked = ""; + if (DISPLAYWEATHER) { + isWeatherChecked = "checked='checked'"; + } + form.replace("%IS_WEATHER_CHECKED%", isWeatherChecked); + form.replace("%WEATHERKEY%", WeatherApiKey); + 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 = LANG_OPTIONS; + options.replace(">" + String(WeatherLanguage) + "<", " selected>" + String(WeatherLanguage) + "<"); + form.replace("%LANGUAGEOPTIONS%", options); + server.sendContent(form); + + html = getFooter(); + server.sendContent(html); + server.sendContent(""); + server.client().stop(); + digitalWrite(externalLight, HIGH); +} + +void handleConfigure() { + if (!authentication()) { + return server.requestAuthentication(); + } + digitalWrite(externalLight, LOW); + String html = ""; + + 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", ""); + + html = getHeader(); + server.sendContent(html); + + String form = CHANGE_FORM; + + form.replace("%OCTOKEY%", OctoPrintApiKey); + form.replace("%OCTOHOST%", OctoPrintHostName); + form.replace("%OCTOADDRESS%", OctoPrintServer); + form.replace("%OCTOPORT%", String(OctoPrintPort)); + form.replace("%OCTOUSER%", OctoAuthUser); + form.replace("%OCTOPASS%", OctoAuthPass); + String isClockChecked = ""; + if (DISPLAYCLOCK) { + isClockChecked = "checked='checked'"; + } + form.replace("%IS_CLOCK_CHECKED%", isClockChecked); + String is24hourChecked = ""; + if (IS_24HOUR) { + is24hourChecked = "checked='checked'"; + } + form.replace("%IS_24HOUR_CHECKED%", is24hourChecked); + String isInvDisp = ""; + if (INVERT_DISPLAY) { + isInvDisp = "checked='checked'"; + } + form.replace("%IS_INVDISP_CHECKED%", isInvDisp); + String hasPSUchecked = ""; + if (HAS_PSU) { + hasPSUchecked = "checked='checked'"; + } + form.replace("%HAS_PSU_CHECKED%", hasPSUchecked); + + String options = ""; + options.replace(">" + String(minutesBetweenDataRefresh) + "<", " selected>" + String(minutesBetweenDataRefresh) + "<"); + form.replace("%OPTIONS%", options); + + server.sendContent(form); + + form = THEME_FORM; + + String themeOptions = COLOR_THEMES; + themeOptions.replace(">" + String(themeColor) + "<", " selected>" + String(themeColor) + "<"); + form.replace("%THEME_OPTIONS%", themeOptions); + form.replace("%UTCOFFSET%", String(UtcOffset)); + String isUseSecurityChecked = ""; + if (IS_BASIC_AUTH) { + isUseSecurityChecked = "checked='checked'"; + } + form.replace("%IS_BASICAUTH_CHECKED%", isUseSecurityChecked); + form.replace("%USERID%", String(www_username)); + form.replace("%STATIONPASSWORD%", String(www_password)); + + server.sendContent(form); + + html = getFooter(); + server.sendContent(html); + server.sendContent(""); + server.client().stop(); + digitalWrite(externalLight, HIGH); +} + +void displayMessage(String message) { + digitalWrite(externalLight, LOW); + + 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", ""); + String html = getHeader(); + server.sendContent(String(html)); + server.sendContent(String(message)); + html = getFooter(); + server.sendContent(String(html)); + server.sendContent(""); + server.client().stop(); + + digitalWrite(externalLight, HIGH); +} + +void redirectHome() { + // Send them back to the Root Directory + server.sendHeader("Location", String("/"), true); + server.sendHeader("Cache-Control", "no-cache, no-store"); + server.sendHeader("Pragma", "no-cache"); + server.sendHeader("Expires", "-1"); + server.send(302, "text/plain", ""); + server.client().stop(); +} + +String getHeader() { + return getHeader(false); +} + +String getHeader(boolean refresh) { + String menu = 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 getFooter() { + int8_t rssi = getWifiQuality(); + Serial.print("Signal Strength (RSSI): "); + Serial.print(rssi); + Serial.println("%"); + String html = "


"; + html += "
"; + html += "
"; + if (lastReportStatus != "") { + html += " Report Status: " + lastReportStatus + "
"; + } + html += " Version: " + String(VERSION) + "
"; + html += " Signal Strength: "; + html += String(rssi) + "%"; + html += "
"; + html += ""; + return html; +} + +void displayPrinterStatus() { + digitalWrite(externalLight, LOW); + String html = ""; + + 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(String(getHeader(true))); + + String displayTime = timeClient.getAmPmHours() + ":" + timeClient.getMinutes() + ":" + timeClient.getSeconds() + " " + timeClient.getAmPm(); + if (IS_24HOUR) { + displayTime = timeClient.getHours() + ":" + timeClient.getMinutes() + ":" + timeClient.getSeconds(); + } + + html += "

Time: " + displayTime + "

"; + html += "

"; + html += "Host Name: " + OctoPrintHostName + "
"; + if (printerClient.getError() != "") { + html += "Status: Offline
"; + html += "Reason: " + printerClient.getError() + "
"; + } else { + html += "Status: " + printerClient.getState(); + if (printerClient.isPSUoff() && HAS_PSU) { + 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 = numberOfHours(val); + int minutes = numberOfMinutes(val); + int seconds = numberOfSeconds(val); + html += "Est. Print Time Left: " + zeroPad(hours) + ":" + zeroPad(minutes) + ":" + zeroPad(seconds) + "
"; + + val = printerClient.getProgressPrintTime().toInt(); + hours = numberOfHours(val); + minutes = numberOfMinutes(val); + seconds = numberOfSeconds(val); + html += "Printing Time: " + zeroPad(hours) + ":" + zeroPad(minutes) + ":" + zeroPad(seconds) + "
"; + html += ""; + html += "

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

"; + + 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 + } + + server.sendContent(String(getFooter())); + server.sendContent(""); + server.client().stop(); + digitalWrite(externalLight, HIGH); +} + +void configModeCallback (WiFiManager *myWiFiManager) { + Serial.println("Entered config mode"); + Serial.println(WiFi.softAPIP()); + + display.clear(); + display.setTextAlignment(TEXT_ALIGN_CENTER); + display.setFont(ArialMT_Plain_10); + display.drawString(64, 0, "Wifi Manager"); + display.drawString(64, 10, "Please connect to AP"); + display.setFont(ArialMT_Plain_16); + display.drawString(64, 23, myWiFiManager->getConfigPortalSSID()); + display.setFont(ArialMT_Plain_10); + display.drawString(64, 42, "To setup Wifi connection"); + display.display(); + + Serial.println("Wifi Manager"); + Serial.println("Please connect to AP"); + Serial.println(myWiFiManager->getConfigPortalSSID()); + Serial.println("To setup Wifi Configuration"); + flashLED(20, 50); +} + +void flashLED(int number, int delayTime) { + for (int inx = 0; inx < number; inx++) { + delay(delayTime); + digitalWrite(externalLight, LOW); + delay(delayTime); + digitalWrite(externalLight, HIGH); + delay(delayTime); + } +} + +void drawScreen1(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + String bed = printerClient.getValueRounded(printerClient.getTempBedActual()); + String tool = printerClient.getValueRounded(printerClient.getTempToolActual()); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(ArialMT_Plain_16); + if (bed != "0") { + display->drawString(64 + x, 0 + y, "Bed / Tool Temp"); + } else { + display->drawString(64 + x, 0 + y, "Tool Temp"); + } + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(ArialMT_Plain_24); + if (bed != "0") { + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->drawString(2 + x, 14 + y, bed + "°"); + display->drawString(64 + x, 14 + y, tool + "°"); + } else { + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(64 + x, 14 + y, tool + "°"); + } +} + +void drawScreen2(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(ArialMT_Plain_16); + + display->drawString(64 + x, 0 + y, "Time Remaining"); + //display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(ArialMT_Plain_24); + int val = printerClient.getProgressPrintTimeLeft().toInt(); + int hours = numberOfHours(val); + int minutes = numberOfMinutes(val); + int seconds = numberOfSeconds(val); + + String time = zeroPad(hours) + ":" + zeroPad(minutes) + ":" + zeroPad(seconds); + display->drawString(64 + x, 14 + y, time); +} + +void drawScreen3(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(ArialMT_Plain_16); + + display->drawString(64 + x, 0 + y, "Printing Time"); + //display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(ArialMT_Plain_24); + int val = printerClient.getProgressPrintTime().toInt(); + int hours = numberOfHours(val); + int minutes = numberOfMinutes(val); + int seconds = numberOfSeconds(val); + + String time = zeroPad(hours) + ":" + zeroPad(minutes) + ":" + zeroPad(seconds); + display->drawString(64 + x, 14 + y, time); +} + +void drawClock(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + display->setTextAlignment(TEXT_ALIGN_CENTER); + + String displayTime = timeClient.getAmPmHours() + ":" + timeClient.getMinutes() + ":" + timeClient.getSeconds(); + if (IS_24HOUR) { + displayTime = timeClient.getHours() + ":" + timeClient.getMinutes() + ":" + timeClient.getSeconds(); + } + display->setFont(ArialMT_Plain_16); + display->drawString(64 + x, 0 + y, OctoPrintHostName); + display->setFont(ArialMT_Plain_24); + display->drawString(64 + x, 17 + y, displayTime); +} + +void drawWeather(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(ArialMT_Plain_24); + display->drawString(0 + x, 0 + y, weatherClient.getTempRounded(0) + getTempSymbol()); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(ArialMT_Plain_24); + + display->setFont(ArialMT_Plain_16); + display->drawString(0 + x, 24 + y, weatherClient.getCondition(0)); + display->setFont((const uint8_t*)Meteocons_Plain_42); + display->drawString(86 + x, 0 + y, weatherClient.getWeatherIcon(0)); +} + +String getTempSymbol() { + return getTempSymbol(false); +} + +String getTempSymbol(boolean forHTML) { + String rtnValue = "F"; + if (IS_METRIC) { + rtnValue = "C"; + } + if (forHTML) { + rtnValue = "°" + rtnValue; + } else { + rtnValue = "°" + rtnValue; + } + return rtnValue; +} + +String getSpeedSymbol() { + String rtnValue = "mph"; + if (IS_METRIC) { + rtnValue = "kph"; + } + return rtnValue; +} + +String zeroPad(int value) { + String rtnValue = String(value); + if (value < 10) { + rtnValue = "0" + rtnValue; + } + return rtnValue; +} + +void drawHeaderOverlay(OLEDDisplay *display, OLEDDisplayUiState* state) { + display->setColor(WHITE); + display->setFont(ArialMT_Plain_16); + String displayTime = timeClient.getAmPmHours() + ":" + timeClient.getMinutes(); + if (IS_24HOUR) { + displayTime = timeClient.getHours() + ":" + timeClient.getMinutes(); + } + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->drawString(0, 48, displayTime); + + if (!IS_24HOUR) { + String ampm = timeClient.getAmPm(); + display->setFont(ArialMT_Plain_10); + display->drawString(39, 54, ampm); + } + + display->setFont(ArialMT_Plain_16); + display->setTextAlignment(TEXT_ALIGN_LEFT); + String percent = String(printerClient.getProgressCompletion()) + "%"; + display->drawString(64, 48, percent); + + // Draw indicator to show next update + int updatePos = (printerClient.getProgressCompletion().toFloat() / float(100)) * 128; + display->drawRect(0, 41, 128, 6); + display->drawHorizontalLine(0, 42, updatePos); + display->drawHorizontalLine(0, 43, updatePos); + display->drawHorizontalLine(0, 44, updatePos); + display->drawHorizontalLine(0, 45, updatePos); + + drawRssi(display); +} + +void drawClockHeaderOverlay(OLEDDisplay *display, OLEDDisplayUiState* state) { + display->setColor(WHITE); + display->setFont(ArialMT_Plain_16); + display->setTextAlignment(TEXT_ALIGN_LEFT); + if (!IS_24HOUR) { + display->drawString(0, 48, timeClient.getAmPm()); + display->setTextAlignment(TEXT_ALIGN_CENTER); + if (printerClient.isPSUoff()) { + display->drawString(64, 47, "psu off"); + } else { + display->drawString(64, 47, "offline"); + } + } else { + if (printerClient.isPSUoff()) { + display->drawString(0, 47, "psu off"); + } else { + display->drawString(0, 47, "offline"); + } + } + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->drawRect(0, 43, 128, 2); + + drawRssi(display); +} + +void drawRssi(OLEDDisplay *display) { + + + int8_t quality = getWifiQuality(); + for (int8_t i = 0; i < 4; i++) { + for (int8_t j = 0; j < 3 * (i + 2); j++) { + if (quality > i * 25 || j == 0) { + display->setPixel(114 + 4 * i, 63 - j); + } + } + } +} + +// converts the dBm to a range between 0 and 100% +int8_t getWifiQuality() { + int32_t dbm = WiFi.RSSI(); + if (dbm <= -100) { + return 0; + } else if (dbm >= -50) { + return 100; + } else { + return 2 * (dbm + 100); + } +} + + +void writeSettings() { + // Save decoded message to SPIFFS file for playback on power up. + File f = SPIFFS.open(CONFIG, "w"); + if (!f) { + Serial.println("File open failed!"); + } else { + Serial.println("Saving settings now..."); + f.println("UtcOffset=" + String(UtcOffset)); + f.println("octoKey=" + OctoPrintApiKey); + f.println("octoHost=" + OctoPrintHostName); + f.println("octoServer=" + OctoPrintServer); + f.println("octoPort=" + String(OctoPrintPort)); + f.println("octoUser=" + OctoAuthUser); + f.println("octoPass=" + OctoAuthPass); + f.println("refreshRate=" + String(minutesBetweenDataRefresh)); + f.println("themeColor=" + themeColor); + f.println("IS_BASIC_AUTH=" + String(IS_BASIC_AUTH)); + f.println("www_username=" + String(www_username)); + f.println("www_password=" + String(www_password)); + f.println("DISPLAYCLOCK=" + String(DISPLAYCLOCK)); + f.println("is24hour=" + String(IS_24HOUR)); + f.println("invertDisp=" + String(INVERT_DISPLAY)); + 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("hasPSU=" + String(HAS_PSU)); + } + f.close(); + //readSettings(); + timeClient.setUtcOffset(UtcOffset); +} + +void readSettings() { + if (SPIFFS.exists(CONFIG) == false) { + Serial.println("Settings File does not yet exists."); + writeSettings(); + return; + } + File fr = SPIFFS.open(CONFIG, "r"); + String line; + while (fr.available()) { + line = fr.readStringUntil('\n'); + + if (line.indexOf("UtcOffset=") >= 0) { + UtcOffset = line.substring(line.lastIndexOf("UtcOffset=") + 10).toFloat(); + Serial.println("UtcOffset=" + String(UtcOffset)); + } + if (line.indexOf("octoKey=") >= 0) { + OctoPrintApiKey = line.substring(line.lastIndexOf("octoKey=") + 8); + OctoPrintApiKey.trim(); + Serial.println("OctoPrintApiKey=" + OctoPrintApiKey); + } + if (line.indexOf("octoHost=") >= 0) { + OctoPrintHostName = line.substring(line.lastIndexOf("octoHost=") + 9); + OctoPrintHostName.trim(); + Serial.println("OctoPrintHostName=" + OctoPrintHostName); + } + if (line.indexOf("octoServer=") >= 0) { + OctoPrintServer = line.substring(line.lastIndexOf("octoServer=") + 11); + OctoPrintServer.trim(); + Serial.println("OctoPrintServer=" + OctoPrintServer); + } + if (line.indexOf("octoPort=") >= 0) { + OctoPrintPort = line.substring(line.lastIndexOf("octoPort=") + 9).toInt(); + Serial.println("OctoPrintPort=" + String(OctoPrintPort)); + } + if (line.indexOf("octoUser=") >= 0) { + OctoAuthUser = line.substring(line.lastIndexOf("octoUser=") + 9); + OctoAuthUser.trim(); + Serial.println("OctoAuthUser=" + OctoAuthUser); + } + if (line.indexOf("octoPass=") >= 0) { + OctoAuthPass = line.substring(line.lastIndexOf("octoPass=") + 9); + OctoAuthPass.trim(); + Serial.println("OctoAuthPass=" + OctoAuthPass); + } + if (line.indexOf("refreshRate=") >= 0) { + minutesBetweenDataRefresh = line.substring(line.lastIndexOf("refreshRate=") + 12).toInt(); + Serial.println("minutesBetweenDataRefresh=" + String(minutesBetweenDataRefresh)); + } + if (line.indexOf("themeColor=") >= 0) { + themeColor = line.substring(line.lastIndexOf("themeColor=") + 11); + themeColor.trim(); + Serial.println("themeColor=" + themeColor); + } + if (line.indexOf("IS_BASIC_AUTH=") >= 0) { + IS_BASIC_AUTH = line.substring(line.lastIndexOf("IS_BASIC_AUTH=") + 14).toInt(); + Serial.println("IS_BASIC_AUTH=" + String(IS_BASIC_AUTH)); + } + if (line.indexOf("www_username=") >= 0) { + String temp = line.substring(line.lastIndexOf("www_username=") + 13); + temp.trim(); + temp.toCharArray(www_username, sizeof(temp)); + Serial.println("www_username=" + String(www_username)); + } + if (line.indexOf("www_password=") >= 0) { + String temp = line.substring(line.lastIndexOf("www_password=") + 13); + temp.trim(); + temp.toCharArray(www_password, sizeof(temp)); + Serial.println("www_password=" + String(www_password)); + } + if (line.indexOf("DISPLAYCLOCK=") >= 0) { + DISPLAYCLOCK = line.substring(line.lastIndexOf("DISPLAYCLOCK=") + 13).toInt(); + Serial.println("DISPLAYCLOCK=" + String(DISPLAYCLOCK)); + } + if (line.indexOf("is24hour=") >= 0) { + IS_24HOUR = line.substring(line.lastIndexOf("is24hour=") + 9).toInt(); + Serial.println("IS_24HOUR=" + String(IS_24HOUR)); + } + if (line.indexOf("invertDisp=") >= 0) { + INVERT_DISPLAY = line.substring(line.lastIndexOf("invertDisp=") + 11).toInt(); + Serial.println("INVERT_DISPLAY=" + String(INVERT_DISPLAY)); + } + if (line.indexOf("hasPSU=") >= 0) { + HAS_PSU = line.substring(line.lastIndexOf("hasPSU=") + 7).toInt(); + Serial.println("HAS_PSU=" + String(HAS_PSU)); + } + if (line.indexOf("isWeather=") >= 0) { + DISPLAYWEATHER = line.substring(line.lastIndexOf("isWeather=") + 10).toInt(); + Serial.println("DISPLAYWEATHER=" + String(DISPLAYWEATHER)); + } + if (line.indexOf("weatherKey=") >= 0) { + WeatherApiKey = line.substring(line.lastIndexOf("weatherKey=") + 11); + WeatherApiKey.trim(); + Serial.println("WeatherApiKey=" + WeatherApiKey); + } + if (line.indexOf("CityID=") >= 0) { + CityIDs[0] = line.substring(line.lastIndexOf("CityID=") + 7).toInt(); + Serial.println("CityID: " + String(CityIDs[0])); + } + if (line.indexOf("isMetric=") >= 0) { + IS_METRIC = line.substring(line.lastIndexOf("isMetric=") + 9).toInt(); + Serial.println("IS_METRIC=" + String(IS_METRIC)); + } + if (line.indexOf("language=") >= 0) { + WeatherLanguage = line.substring(line.lastIndexOf("language=") + 9); + WeatherLanguage.trim(); + Serial.println("WeatherLanguage=" + WeatherLanguage); + } + } + fr.close(); + printerClient.updateOctoPrintClient(OctoPrintApiKey, OctoPrintServer, OctoPrintPort, OctoAuthUser, OctoAuthPass, HAS_PSU); + weatherClient.updateWeatherApiKey(WeatherApiKey); + weatherClient.updateLanguage(WeatherLanguage); + weatherClient.setMetric(IS_METRIC); + weatherClient.updateCityIdList(CityIDs, 1); + timeClient.setUtcOffset(UtcOffset); +} + +int getMinutesFromLastRefresh() { + int minutes = (timeClient.getCurrentEpoch() - lastEpoch) / 60; + return minutes; +} + +int getMinutesFromLastDisplay() { + int minutes = (timeClient.getCurrentEpoch() - displayOffEpoch) / 60; + return minutes; +} + +// Toggle on and off the display if user defined times +void checkDisplay() { + if (!displayOn && DISPLAYCLOCK) { + enableDisplay(true); + } + if (displayOn && !(printerClient.isOperational() || printerClient.isPrinting()) && !DISPLAYCLOCK) { + // Put Display to sleep + display.clear(); + display.display(); + display.setFont(ArialMT_Plain_16); + display.setTextAlignment(TEXT_ALIGN_CENTER); + display.setContrast(255); // default is 255 + //display.drawString(64, 5, "Printer Offline\nSleep Mode..."); + // display.display(); + //delay(5000); + enableDisplay(false); + Serial.println("Printer is offline going down to sleep..."); + return; + } else if (!displayOn && !DISPLAYCLOCK) { + if (printerClient.isOperational()) { + // Wake the Screen up + enableDisplay(true); + display.clear(); + display.display(); + display.setFont(ArialMT_Plain_16); + display.setTextAlignment(TEXT_ALIGN_CENTER); + display.setContrast(255); // default is 255 + display.drawString(64, 5, "Printer Online\nWake up..."); + display.display(); + Serial.println("Printer is online waking up..."); + delay(5000); + return; + } + } else if (DISPLAYCLOCK) { + if ((!printerClient.isOperational() || printerClient.isPSUoff()) && !isClockOn) { + Serial.println("Clock Mode is turned on."); + if (!DISPLAYWEATHER) { + ui.disableAutoTransition(); + ui.setFrames(clockFrame, 1); + clockFrame[0] = drawClock; + } else { + ui.enableAutoTransition(); + ui.setFrames(clockFrame, 2); + clockFrame[0] = drawClock; + clockFrame[1] = drawWeather; + } + ui.setOverlays(clockOverlay, numberOfOverlays); + isClockOn = true; + } else if (printerClient.isOperational() && !printerClient.isPSUoff() && isClockOn) { + Serial.println("Printer Monitor is active."); + ui.setFrames(frames, numberOfFrames); + ui.setOverlays(overlays, numberOfOverlays); + ui.enableAutoTransition(); + isClockOn = false; + } + } +} + +void enableDisplay(boolean enable) { + displayOn = enable; + if (enable) { + if (getMinutesFromLastDisplay() >= minutesBetweenDataRefresh) { + // The display has been off longer than the minutes between refresh -- need to get fresh data + lastEpoch = 0; // this should force a data pull + displayOffEpoch = 0; // reset + } + display.displayOn(); + Serial.println("Display was turned ON: " + timeClient.getFormattedTime()); + } else { + display.displayOff(); + Serial.println("Display was turned OFF: " + timeClient.getFormattedTime()); + displayOffEpoch = lastEpoch; + } +} From 43ed65865d724d5c733e521d0f0530ef1f197157 Mon Sep 17 00:00:00 2001 From: abacon118 Date: Mon, 29 May 2023 20:20:38 -0400 Subject: [PATCH 2/4] Update Settings.h Changed to original code and added LDR definition --- printermonitor/Settings.h | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/printermonitor/Settings.h b/printermonitor/Settings.h index a927cb3..1a0c6bb 100644 --- a/printermonitor/Settings.h +++ b/printermonitor/Settings.h @@ -45,6 +45,7 @@ SOFTWARE. #include #include #include "TimeClient.h" +#include "RepetierClient.h" #include "OctoPrintClient.h" #include "OpenWeatherMapClient.h" #include "WeatherStationFonts.h" @@ -57,19 +58,20 @@ SOFTWARE. // Start Settings //****************************** -// OctoPrint Monitoring -- Monitor your 3D printer OctoPrint Server -String OctoPrintApiKey = "ABCDEF1234"; // ApiKey from your User Account on OctoPrint -String OctoPrintHostName = "3D Printer";// Default 'octopi' -- or hostname if different (optional if your IP changes) -String OctoPrintServer = "192.168.0.10"; // IP or Address of your OctoPrint Server (DO NOT include http://) -int OctoPrintPort = 80; // the port you are running your OctoPrint server on (usually 80); -String OctoAuthUser = ""; // only used if you have haproxy or basic athentintication turned on (not default) -String OctoAuthPass = ""; // only used with haproxy or basic auth (only needed if you must authenticate) +// OctoPrint / Repetier Monitoring -- Monitor your 3D OctoPrint or Repetier Server +//#define USE_REPETIER_CLIENT // Uncomment this line to use the Repetier Printer Server -- OctoPrint is used by default and is most common +String PrinterApiKey = ""; // ApiKey from your User Account on OctoPrint / Repetier +String PrinterHostName = "octopi";// Default 'octopi' -- or hostname if different (optional if your IP changes) +String PrinterServer = ""; // IP or Address of your OctoPrint / Repetier Server (DO NOT include http://) +int PrinterPort = 80; // the port you are running your OctoPrint / Repetier server on (usually 80); +String PrinterAuthUser = ""; // only used if you have haproxy or basic athentintication turned on (not default) +String PrinterAuthPass = ""; // only used with haproxy or basic auth (only needed if you must authenticate) // Weather Configuration boolean DISPLAYWEATHER = true; // true = show weather when not printing / false = no weather -String WeatherApiKey = "ABCDEF1234"; // Your API Key from http://openweathermap.org/ +String WeatherApiKey = ""; // Your API Key from http://openweathermap.org/ // Default City Location (use http://openweathermap.org/find to find city ID) -int CityIDs[] = {4499612}; //Only USE ONE for weather marquee +int CityIDs[] = { 5304391 }; //Only USE ONE for weather marquee boolean IS_METRIC = false; // false = Imperial and true = Metric // Languages: ar, bg, ca, cz, de, el, en, fa, fi, fr, gl, hr, hu, it, ja, kr, la, lt, mk, nl, pl, pt, ro, ru, se, sk, sl, es, tr, ua, vi, zh_cn, zh_tw String WeatherLanguage = "en"; //Default (en) English @@ -78,24 +80,25 @@ String WeatherLanguage = "en"; //Default (en) English const int WEBSERVER_PORT = 80; // The port you can access this device on over HTTP const boolean WEBSERVER_ENABLED = true; // Device will provide a web interface via http://[ip]:[port]/ boolean IS_BASIC_AUTH = true; // true = require athentication to change configuration settings / false = no auth -char* www_username = "user"; // User account for the Web Interface -char* www_password = "P@ssW0rd123"; // Password for the Web Interface +char* www_username = "admin"; // User account for the Web Interface +char* www_password = "password"; // Password for the Web Interface // Date and Time -float UtcOffset = -4; // Hour offset from GMT for your timezone -boolean IS_24HOUR = true; // 23:00 millitary 24 hour clock +float UtcOffset = -7; // Hour offset from GMT for your timezone +boolean IS_24HOUR = false; // 23:00 millitary 24 hour clock int minutesBetweenDataRefresh = 15; boolean DISPLAYCLOCK = true; // true = Show Clock when not printing / false = turn off display when not printing // Display Settings const int I2C_DISPLAY_ADDRESS = 0x3c; // I2C Address of your Display (usually 0x3c or 0x3d) const int SDA_PIN = D2; -const int SCL_PIN = D3; +const int SCL_PIN = D5; // original code D5 -- Monitor Easy Board use D1 boolean INVERT_DISPLAY = false; // true = pins at top | false = pins at the bottom -#define DISPLAY_SH1106 // Uncomment this line to use the SH1106 display -- SSD1306 is used by default and is most common +//#define DISPLAY_SH1106 // Uncomment this line to use the SH1106 display -- SSD1306 is used by default and is most common // LED Settings -const int externalLight = D1; // Set to unused pin, like D1, to disable use of built-in LED (LED_BUILTIN) +const int externalLight = LED_BUILTIN; // LED will always flash on bootup or Wifi Errors +boolean USE_FLASH = true; // true = System LED will Flash on Service Calls; false = disabled LED flashing //Light Dependant Resistor (LDR) Port const int ldrPin = A0; From 235cbd814ac3c9e22fdf799130cf484309688a88 Mon Sep 17 00:00:00 2001 From: abacon118 Date: Mon, 29 May 2023 20:36:48 -0400 Subject: [PATCH 3/4] Add files via upload Updated Printermonitor.ino and settings.h with correct version from master. --- printermonitor/Settings.h | 2 +- printermonitor/printermonitor.ino | 2700 +++++++++++++++-------------- 2 files changed, 1380 insertions(+), 1322 deletions(-) diff --git a/printermonitor/Settings.h b/printermonitor/Settings.h index 1a0c6bb..994b877 100644 --- a/printermonitor/Settings.h +++ b/printermonitor/Settings.h @@ -92,7 +92,7 @@ boolean DISPLAYCLOCK = true; // true = Show Clock when not printing / false = // Display Settings const int I2C_DISPLAY_ADDRESS = 0x3c; // I2C Address of your Display (usually 0x3c or 0x3d) const int SDA_PIN = D2; -const int SCL_PIN = D5; // original code D5 -- Monitor Easy Board use D1 +const int SCL_PIN = D3; // original code D5 -- Monitor Easy Board use D1 boolean INVERT_DISPLAY = false; // true = pins at top | false = pins at the bottom //#define DISPLAY_SH1106 // Uncomment this line to use the SH1106 display -- SSD1306 is used by default and is most common diff --git a/printermonitor/printermonitor.ino b/printermonitor/printermonitor.ino index 8344d43..bdd5763 100644 --- a/printermonitor/printermonitor.ino +++ b/printermonitor/printermonitor.ino @@ -1,1321 +1,1379 @@ -#include - -/** The MIT License (MIT) - - Copyright (c) 2018 David Payne - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. -*/ - -// Additional Contributions: -/* 15 Jan 2019 : Owen Carter : Add psucontrol option and processing */ -/********************************************** - Edit Settings.h for personalization -***********************************************/ - -#include "Settings.h" - -#define VERSION "2.5" - -#define HOSTNAME "OctMon-" -#define CONFIG "/conf.txt" - -/* Useful Constants */ -#define SECS_PER_MIN (60UL) -#define SECS_PER_HOUR (3600UL) - -/* Useful Macros for getting elapsed time */ -#define numberOfSeconds(_time_) (_time_ % SECS_PER_MIN) -#define numberOfMinutes(_time_) ((_time_ / SECS_PER_MIN) % SECS_PER_MIN) -#define numberOfHours(_time_) (_time_ / SECS_PER_HOUR) - -// Initialize the oled display for I2C_DISPLAY_ADDRESS -// SDA_PIN and SCL_PIN -#if defined(DISPLAY_SH1106) -SH1106Wire display(I2C_DISPLAY_ADDRESS, SDA_PIN, SCL_PIN); -#else -SSD1306Wire display(I2C_DISPLAY_ADDRESS, SDA_PIN, SCL_PIN); // this is the default -#endif - -#include //for UDP -int ledstat; -unsigned int localPort = 2000; // local port to listen for UDP packets -IPAddress SendIP(192, 168, 0, 255); //UDP Broadcast IP data sent to all devicess on same network -WiFiUDP udp; -char packetBuffer[9]; //Where we get the UDP data -OLEDDisplayUi ui( &display ); - -void drawProgress(OLEDDisplay *display, int percentage, String label); -void drawOtaProgress(unsigned int, unsigned int); -void drawScreen1(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y); -void drawScreen2(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y); -void drawScreen3(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y); -void drawHeaderOverlay(OLEDDisplay *display, OLEDDisplayUiState* state); -void drawClock(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y); -void drawWeather(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y); -void drawClockHeaderOverlay(OLEDDisplay *display, OLEDDisplayUiState* state); - -// Set the number of Frames supported -const int numberOfFrames = 3; -FrameCallback frames[numberOfFrames]; -FrameCallback clockFrame[2]; -boolean isClockOn = false; - -OverlayCallback overlays[] = { drawHeaderOverlay }; -OverlayCallback clockOverlay[] = { drawClockHeaderOverlay }; -int numberOfOverlays = 1; - -// Time -TimeClient timeClient(UtcOffset); -long lastEpoch = 0; -long firstEpoch = 0; -long displayOffEpoch = 0; -String lastMinute = "xx"; -String lastSecond = "xx"; -String lastReportStatus = ""; -boolean displayOn = true; - -//LDR -int ldrStatus =0; - -// OctoPrint Client -OctoPrintClient printerClient(OctoPrintApiKey, OctoPrintServer, OctoPrintPort, OctoAuthUser, OctoAuthPass, HAS_PSU); -int printerCount = 0; - -// Weather Client -OpenWeatherMapClient weatherClient(WeatherApiKey, CityIDs, 1, IS_METRIC, WeatherLanguage); - -//declairing prototypes -void configModeCallback (WiFiManager *myWiFiManager); -int8_t getWifiQuality(); - -ESP8266WebServer server(WEBSERVER_PORT); -ESP8266HTTPUpdateServer serverUpdater; - -String WEB_ACTIONS = " Home" - " Configure" - " Weather" - " Reset Settings" - " Forget WiFi" - " Firmware Update" - " About"; - -String CHANGE_FORM = "

Station Config:

" - "

" - "

" - "

" - "

" - "

" - "


" - "

Display Clock when printer is off

" - "

Use 24 Hour Clock (military time)

" - "

Flip display orientation

" - "

Use OctoPrint PSU control plugin for clock/blank

" - "

Clock Sync / Weather Refresh (minutes)

"; - -String THEME_FORM = "

Theme Color

" - "


" - "

Use Security Credentials for Configuration Changes

" - "

" - "

" - "
"; - -String WEATHER_FORM = "

Weather Config:

" - "

Display Weather when printer is off

" - "" - "" - "

" - "

" - "

Use Metric (Celsius)

" - "

Weather Language

" - "
" - ""; - -String LANG_OPTIONS = "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - ""; - -String COLOR_THEMES = "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - ""; - - -void setup() { - Serial.begin(115200); - SPIFFS.begin(); - delay(10); - - //New Line to clear from start garbage - Serial.println(); - - // Initialize digital pin for LED (little blue light on the Wemos D1 Mini) - pinMode(externalLight, OUTPUT); - - readSettings(); - - // initialize display - display.init(); - if (INVERT_DISPLAY) { - display.flipScreenVertically(); // connections at top of OLED display - } - display.clear(); - display.display(); - - //display.flipScreenVertically(); - display.setFont(ArialMT_Plain_16); - display.setTextAlignment(TEXT_ALIGN_CENTER); - display.setContrast(255); // default is 255 - display.drawString(64, 5, "Printer Monitor\nBy Qrome\nV" + String(VERSION)); - display.display(); - - //WiFiManager - //Local intialization. Once its business is done, there is no need to keep it around - WiFiManager wifiManager; - - // Uncomment for testing wifi manager - //wifiManager.resetSettings(); - wifiManager.setAPCallback(configModeCallback); - - String hostname(HOSTNAME); - hostname += String(ESP.getChipId(), HEX); - if (!wifiManager.autoConnect((const char *)hostname.c_str())) {// new addition - delay(3000); - WiFi.disconnect(true); - ESP.reset(); - delay(5000); - } - - // You can change the transition that is used - // SLIDE_LEFT, SLIDE_RIGHT, SLIDE_TOP, SLIDE_DOWN - ui.setFrameAnimation(SLIDE_LEFT); - ui.setTargetFPS(30); - ui.disableAllIndicators(); - ui.setFrames(frames, (numberOfFrames)); - frames[0] = drawScreen1; - frames[1] = drawScreen2; - frames[2] = drawScreen3; - clockFrame[0] = drawClock; - clockFrame[1] = drawWeather; - ui.setOverlays(overlays, numberOfOverlays); - - // Inital UI takes care of initalising the display too. - ui.init(); - if (INVERT_DISPLAY) { - display.flipScreenVertically(); //connections at top of OLED display - } - - // print the received signal strength: - Serial.print("Signal Strength (RSSI): "); - Serial.print(getWifiQuality()); - Serial.println("%"); - - if (ENABLE_OTA) { - ArduinoOTA.onStart([]() { - Serial.println("Start"); - }); - ArduinoOTA.onEnd([]() { - Serial.println("\nEnd"); - }); - ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { - Serial.printf("Progress: %u%%\r", (progress / (total / 100))); - }); - ArduinoOTA.onError([](ota_error_t error) { - Serial.printf("Error[%u]: ", error); - if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); - else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); - else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); - else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); - else if (error == OTA_END_ERROR) Serial.println("End Failed"); - }); - ArduinoOTA.setHostname((const char *)hostname.c_str()); - if (OTA_Password != "") { - ArduinoOTA.setPassword(((const char *)OTA_Password.c_str())); - } - ArduinoOTA.begin(); - } - - if (WEBSERVER_ENABLED) { - server.on("/", displayPrinterStatus); - server.on("/systemreset", handleSystemReset); - server.on("/forgetwifi", handleWifiReset); - server.on("/updateconfig", handleUpdateConfig); - server.on("/updateweatherconfig", handleUpdateWeather); - server.on("/configure", handleConfigure); - server.on("/configureweather", handleWeatherConfigure); - server.onNotFound(redirectHome); - serverUpdater.setup(&server, "/update", www_username, www_password); - // Start the server - server.begin(); - Serial.println("Server started"); - // Print the IP address - String webAddress = "http://" + WiFi.localIP().toString() + ":" + String(WEBSERVER_PORT) + "/"; - Serial.println("Use this URL : " + webAddress); - display.clear(); - display.setTextAlignment(TEXT_ALIGN_CENTER); - display.setFont(ArialMT_Plain_10); - display.drawString(64, 10, "Web Interface On"); - display.drawString(64, 20, "You May Connect to IP"); - display.setFont(ArialMT_Plain_16); - display.drawString(64, 30, WiFi.localIP().toString()); - display.drawString(64, 46, "Port: " + String(WEBSERVER_PORT)); - display.display(); - } else { - Serial.println("Web Interface is Disabled"); - display.clear(); - display.setTextAlignment(TEXT_ALIGN_CENTER); - display.setFont(ArialMT_Plain_10); - display.drawString(64, 10, "Web Interface is Off"); - display.drawString(64, 20, "Enable in Settings.h"); - display.display(); - } - flashLED(5, 500); - findMDNS(); //go find Octoprint Server by the hostname - Serial.println("*** Leaving setup()"); - { - - - pinMode(D8, OUTPUT); - digitalWrite(D8, LOW); - Serial.println(""); - Serial.print("Connected to "); - Serial.print("IP address: "); - Serial.println(WiFi.localIP()); - Serial.println("Starting UDP"); - udp.begin(localPort); - Serial.print("Local port: "); - Serial.println(udp.localPort()); - } -} - -void findMDNS() { - if (OctoPrintHostName == "" || ENABLE_OTA == false) { - return; // nothing to do here - } - // We now query our network for 'web servers' service - // over tcp, and get the number of available devices - int n = MDNS.queryService("http", "tcp"); - if (n == 0) { - Serial.println("no services found - make sure OctoPrint server is turned on"); - return; - } - Serial.println("*** Looking for " + OctoPrintHostName + " over mDNS"); - for (int i = 0; i < n; ++i) { - // Going through every available service, - // we're searching for the one whose hostname - // matches what we want, and then get its IP - Serial.println("Found: " + MDNS.hostname(i)); - if (MDNS.hostname(i) == OctoPrintHostName) { - IPAddress serverIp = MDNS.IP(i); - OctoPrintServer = serverIp.toString(); - OctoPrintPort = MDNS.port(i); // save the port - Serial.println("*** Found OctoPrint Server " + OctoPrintHostName + " http://" + OctoPrintServer + ":" + OctoPrintPort); - writeSettings(); // update the settings - } - } -} - -//************************************************************ -// Main Looop -//************************************************************ -void loop() { - - //Get Time Update - if ((getMinutesFromLastRefresh() >= minutesBetweenDataRefresh) || lastEpoch == 0) { - getUpdateTime(); - } - - if (lastMinute != timeClient.getMinutes() && !printerClient.isPrinting()) { - // Check status every 60 seconds - digitalWrite(externalLight, LOW); - lastMinute = timeClient.getMinutes(); // reset the check value - printerClient.getPrinterJobResults(); - printerClient.getPrinterPsuState(); - digitalWrite(externalLight, HIGH); - } else if (printerClient.isPrinting()) { - if (lastSecond != timeClient.getSeconds() && timeClient.getSeconds().endsWith("0")) { - lastSecond = timeClient.getSeconds(); - // every 10 seconds while printing get an update - digitalWrite(externalLight, LOW); - printerClient.getPrinterJobResults(); - printerClient.getPrinterPsuState(); - digitalWrite(externalLight, HIGH); - } - } - - checkDisplay(); // Check to see if the printer is on or offline and change display. - lightOn(); - ui.update(); - - if (WEBSERVER_ENABLED) { - server.handleClient(); - } - if (ENABLE_OTA) { - ArduinoOTA.handle(); - } - { //Loop for motion LED - int cb = udp.parsePacket();//start of UDP - if (!cb) - { - //If serial data is recived send it to UDP - if (Serial.available() > 0) - { - udp.beginPacket(SendIP, 2000); //Send Data to Master unit - //Send UDP requests are to port - char a[1]; - a[0] = char(Serial.read()); //Serial Byte Read - udp.write(a, 1); //Send one byte to ESP8266 - udp.endPacket(); - } - } - else { - - digitalWrite(D8, HIGH); - delay(5000); - digitalWrite(D8, LOW); - delay(20); - } - }//end of UDP -} - -void lightOn() { - ldrStatus = analogRead(ldrPin); - if(ldrStatus>=250){ - Serial.print("LDR true"); - DISPLAYCLOCK = true; - //enableDisplay(true); - } - else{ - Serial.print("LDR false"); - DISPLAYCLOCK = false; - // enableDisplay(false); - } -} - -void getUpdateTime() { - digitalWrite(externalLight, LOW); // turn on the LED - Serial.println(); - - if (displayOn && DISPLAYWEATHER) { - Serial.println("Getting Weather Data..."); - weatherClient.updateWeather(); - } - - Serial.println("Updating Time..."); - //Update the Time - timeClient.updateTime(); - lastEpoch = timeClient.getCurrentEpoch(); - Serial.println("Local time: " + timeClient.getAmPmFormattedTime()); - - digitalWrite(externalLight, HIGH); // turn off the LED -} - -boolean authentication() { - if (IS_BASIC_AUTH && (strlen(www_username) >= 1 && strlen(www_password) >= 1)) { - return server.authenticate(www_username, www_password); - } - return true; // Authentication not required -} - -void handleSystemReset() { - if (!authentication()) { - return server.requestAuthentication(); - } - Serial.println("Reset System Configuration"); - if (SPIFFS.remove(CONFIG)) { - redirectHome(); - ESP.restart(); - } -} - -void handleUpdateWeather() { - if (!authentication()) { - return 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"); - writeSettings(); - isClockOn = false; // this will force a check for the display - checkDisplay(); - lastEpoch = 0; - redirectHome(); -} - -void handleUpdateConfig() { - boolean flipOld = INVERT_DISPLAY; - if (!authentication()) { - return server.requestAuthentication(); - } - OctoPrintApiKey = server.arg("octoPrintApiKey"); - OctoPrintHostName = server.arg("octoPrintHostName"); - OctoPrintServer = server.arg("octoPrintAddress"); - OctoPrintPort = server.arg("octoPrintPort").toInt(); - OctoAuthUser = server.arg("octoUser"); - OctoAuthPass = server.arg("octoPass"); - DISPLAYCLOCK = server.hasArg("isClockEnabled"); - IS_24HOUR = server.hasArg("is24hour"); - INVERT_DISPLAY = server.hasArg("invDisp"); - HAS_PSU = server.hasArg("hasPSU"); - minutesBetweenDataRefresh = server.arg("refresh").toInt(); - themeColor = server.arg("theme"); - UtcOffset = server.arg("utcoffset").toFloat(); - String temp = server.arg("userid"); - temp.toCharArray(www_username, sizeof(temp)); - temp = server.arg("stationpassword"); - temp.toCharArray(www_password, sizeof(temp)); - writeSettings(); - findMDNS(); - printerClient.getPrinterJobResults(); - printerClient.getPrinterPsuState(); - if (INVERT_DISPLAY != flipOld) { - ui.init(); - if (INVERT_DISPLAY) - display.flipScreenVertically(); - ui.update(); - } - checkDisplay(); - lastEpoch = 0; - redirectHome(); -} - -void handleWifiReset() { - if (!authentication()) { - return 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 handleWeatherConfigure() { - if (!authentication()) { - return server.requestAuthentication(); - } - digitalWrite(externalLight, LOW); - String html = ""; - - 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", ""); - - html = getHeader(); - server.sendContent(html); - - String form = WEATHER_FORM; - String isWeatherChecked = ""; - if (DISPLAYWEATHER) { - isWeatherChecked = "checked='checked'"; - } - form.replace("%IS_WEATHER_CHECKED%", isWeatherChecked); - form.replace("%WEATHERKEY%", WeatherApiKey); - 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 = LANG_OPTIONS; - options.replace(">" + String(WeatherLanguage) + "<", " selected>" + String(WeatherLanguage) + "<"); - form.replace("%LANGUAGEOPTIONS%", options); - server.sendContent(form); - - html = getFooter(); - server.sendContent(html); - server.sendContent(""); - server.client().stop(); - digitalWrite(externalLight, HIGH); -} - -void handleConfigure() { - if (!authentication()) { - return server.requestAuthentication(); - } - digitalWrite(externalLight, LOW); - String html = ""; - - 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", ""); - - html = getHeader(); - server.sendContent(html); - - String form = CHANGE_FORM; - - form.replace("%OCTOKEY%", OctoPrintApiKey); - form.replace("%OCTOHOST%", OctoPrintHostName); - form.replace("%OCTOADDRESS%", OctoPrintServer); - form.replace("%OCTOPORT%", String(OctoPrintPort)); - form.replace("%OCTOUSER%", OctoAuthUser); - form.replace("%OCTOPASS%", OctoAuthPass); - String isClockChecked = ""; - if (DISPLAYCLOCK) { - isClockChecked = "checked='checked'"; - } - form.replace("%IS_CLOCK_CHECKED%", isClockChecked); - String is24hourChecked = ""; - if (IS_24HOUR) { - is24hourChecked = "checked='checked'"; - } - form.replace("%IS_24HOUR_CHECKED%", is24hourChecked); - String isInvDisp = ""; - if (INVERT_DISPLAY) { - isInvDisp = "checked='checked'"; - } - form.replace("%IS_INVDISP_CHECKED%", isInvDisp); - String hasPSUchecked = ""; - if (HAS_PSU) { - hasPSUchecked = "checked='checked'"; - } - form.replace("%HAS_PSU_CHECKED%", hasPSUchecked); - - String options = ""; - options.replace(">" + String(minutesBetweenDataRefresh) + "<", " selected>" + String(minutesBetweenDataRefresh) + "<"); - form.replace("%OPTIONS%", options); - - server.sendContent(form); - - form = THEME_FORM; - - String themeOptions = COLOR_THEMES; - themeOptions.replace(">" + String(themeColor) + "<", " selected>" + String(themeColor) + "<"); - form.replace("%THEME_OPTIONS%", themeOptions); - form.replace("%UTCOFFSET%", String(UtcOffset)); - String isUseSecurityChecked = ""; - if (IS_BASIC_AUTH) { - isUseSecurityChecked = "checked='checked'"; - } - form.replace("%IS_BASICAUTH_CHECKED%", isUseSecurityChecked); - form.replace("%USERID%", String(www_username)); - form.replace("%STATIONPASSWORD%", String(www_password)); - - server.sendContent(form); - - html = getFooter(); - server.sendContent(html); - server.sendContent(""); - server.client().stop(); - digitalWrite(externalLight, HIGH); -} - -void displayMessage(String message) { - digitalWrite(externalLight, LOW); - - 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", ""); - String html = getHeader(); - server.sendContent(String(html)); - server.sendContent(String(message)); - html = getFooter(); - server.sendContent(String(html)); - server.sendContent(""); - server.client().stop(); - - digitalWrite(externalLight, HIGH); -} - -void redirectHome() { - // Send them back to the Root Directory - server.sendHeader("Location", String("/"), true); - server.sendHeader("Cache-Control", "no-cache, no-store"); - server.sendHeader("Pragma", "no-cache"); - server.sendHeader("Expires", "-1"); - server.send(302, "text/plain", ""); - server.client().stop(); -} - -String getHeader() { - return getHeader(false); -} - -String getHeader(boolean refresh) { - String menu = 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 getFooter() { - int8_t rssi = getWifiQuality(); - Serial.print("Signal Strength (RSSI): "); - Serial.print(rssi); - Serial.println("%"); - String html = "


"; - html += "
"; - html += "
"; - if (lastReportStatus != "") { - html += " Report Status: " + lastReportStatus + "
"; - } - html += " Version: " + String(VERSION) + "
"; - html += " Signal Strength: "; - html += String(rssi) + "%"; - html += "
"; - html += ""; - return html; -} - -void displayPrinterStatus() { - digitalWrite(externalLight, LOW); - String html = ""; - - 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(String(getHeader(true))); - - String displayTime = timeClient.getAmPmHours() + ":" + timeClient.getMinutes() + ":" + timeClient.getSeconds() + " " + timeClient.getAmPm(); - if (IS_24HOUR) { - displayTime = timeClient.getHours() + ":" + timeClient.getMinutes() + ":" + timeClient.getSeconds(); - } - - html += "

Time: " + displayTime + "

"; - html += "

"; - html += "Host Name: " + OctoPrintHostName + "
"; - if (printerClient.getError() != "") { - html += "Status: Offline
"; - html += "Reason: " + printerClient.getError() + "
"; - } else { - html += "Status: " + printerClient.getState(); - if (printerClient.isPSUoff() && HAS_PSU) { - 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 = numberOfHours(val); - int minutes = numberOfMinutes(val); - int seconds = numberOfSeconds(val); - html += "Est. Print Time Left: " + zeroPad(hours) + ":" + zeroPad(minutes) + ":" + zeroPad(seconds) + "
"; - - val = printerClient.getProgressPrintTime().toInt(); - hours = numberOfHours(val); - minutes = numberOfMinutes(val); - seconds = numberOfSeconds(val); - html += "Printing Time: " + zeroPad(hours) + ":" + zeroPad(minutes) + ":" + zeroPad(seconds) + "
"; - html += ""; - html += "

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

"; - - 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 - } - - server.sendContent(String(getFooter())); - server.sendContent(""); - server.client().stop(); - digitalWrite(externalLight, HIGH); -} - -void configModeCallback (WiFiManager *myWiFiManager) { - Serial.println("Entered config mode"); - Serial.println(WiFi.softAPIP()); - - display.clear(); - display.setTextAlignment(TEXT_ALIGN_CENTER); - display.setFont(ArialMT_Plain_10); - display.drawString(64, 0, "Wifi Manager"); - display.drawString(64, 10, "Please connect to AP"); - display.setFont(ArialMT_Plain_16); - display.drawString(64, 23, myWiFiManager->getConfigPortalSSID()); - display.setFont(ArialMT_Plain_10); - display.drawString(64, 42, "To setup Wifi connection"); - display.display(); - - Serial.println("Wifi Manager"); - Serial.println("Please connect to AP"); - Serial.println(myWiFiManager->getConfigPortalSSID()); - Serial.println("To setup Wifi Configuration"); - flashLED(20, 50); -} - -void flashLED(int number, int delayTime) { - for (int inx = 0; inx < number; inx++) { - delay(delayTime); - digitalWrite(externalLight, LOW); - delay(delayTime); - digitalWrite(externalLight, HIGH); - delay(delayTime); - } -} - -void drawScreen1(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { - String bed = printerClient.getValueRounded(printerClient.getTempBedActual()); - String tool = printerClient.getValueRounded(printerClient.getTempToolActual()); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(ArialMT_Plain_16); - if (bed != "0") { - display->drawString(64 + x, 0 + y, "Bed / Tool Temp"); - } else { - display->drawString(64 + x, 0 + y, "Tool Temp"); - } - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(ArialMT_Plain_24); - if (bed != "0") { - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->drawString(2 + x, 14 + y, bed + "°"); - display->drawString(64 + x, 14 + y, tool + "°"); - } else { - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(64 + x, 14 + y, tool + "°"); - } -} - -void drawScreen2(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(ArialMT_Plain_16); - - display->drawString(64 + x, 0 + y, "Time Remaining"); - //display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(ArialMT_Plain_24); - int val = printerClient.getProgressPrintTimeLeft().toInt(); - int hours = numberOfHours(val); - int minutes = numberOfMinutes(val); - int seconds = numberOfSeconds(val); - - String time = zeroPad(hours) + ":" + zeroPad(minutes) + ":" + zeroPad(seconds); - display->drawString(64 + x, 14 + y, time); -} - -void drawScreen3(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(ArialMT_Plain_16); - - display->drawString(64 + x, 0 + y, "Printing Time"); - //display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(ArialMT_Plain_24); - int val = printerClient.getProgressPrintTime().toInt(); - int hours = numberOfHours(val); - int minutes = numberOfMinutes(val); - int seconds = numberOfSeconds(val); - - String time = zeroPad(hours) + ":" + zeroPad(minutes) + ":" + zeroPad(seconds); - display->drawString(64 + x, 14 + y, time); -} - -void drawClock(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { - display->setTextAlignment(TEXT_ALIGN_CENTER); - - String displayTime = timeClient.getAmPmHours() + ":" + timeClient.getMinutes() + ":" + timeClient.getSeconds(); - if (IS_24HOUR) { - displayTime = timeClient.getHours() + ":" + timeClient.getMinutes() + ":" + timeClient.getSeconds(); - } - display->setFont(ArialMT_Plain_16); - display->drawString(64 + x, 0 + y, OctoPrintHostName); - display->setFont(ArialMT_Plain_24); - display->drawString(64 + x, 17 + y, displayTime); -} - -void drawWeather(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(ArialMT_Plain_24); - display->drawString(0 + x, 0 + y, weatherClient.getTempRounded(0) + getTempSymbol()); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(ArialMT_Plain_24); - - display->setFont(ArialMT_Plain_16); - display->drawString(0 + x, 24 + y, weatherClient.getCondition(0)); - display->setFont((const uint8_t*)Meteocons_Plain_42); - display->drawString(86 + x, 0 + y, weatherClient.getWeatherIcon(0)); -} - -String getTempSymbol() { - return getTempSymbol(false); -} - -String getTempSymbol(boolean forHTML) { - String rtnValue = "F"; - if (IS_METRIC) { - rtnValue = "C"; - } - if (forHTML) { - rtnValue = "°" + rtnValue; - } else { - rtnValue = "°" + rtnValue; - } - return rtnValue; -} - -String getSpeedSymbol() { - String rtnValue = "mph"; - if (IS_METRIC) { - rtnValue = "kph"; - } - return rtnValue; -} - -String zeroPad(int value) { - String rtnValue = String(value); - if (value < 10) { - rtnValue = "0" + rtnValue; - } - return rtnValue; -} - -void drawHeaderOverlay(OLEDDisplay *display, OLEDDisplayUiState* state) { - display->setColor(WHITE); - display->setFont(ArialMT_Plain_16); - String displayTime = timeClient.getAmPmHours() + ":" + timeClient.getMinutes(); - if (IS_24HOUR) { - displayTime = timeClient.getHours() + ":" + timeClient.getMinutes(); - } - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->drawString(0, 48, displayTime); - - if (!IS_24HOUR) { - String ampm = timeClient.getAmPm(); - display->setFont(ArialMT_Plain_10); - display->drawString(39, 54, ampm); - } - - display->setFont(ArialMT_Plain_16); - display->setTextAlignment(TEXT_ALIGN_LEFT); - String percent = String(printerClient.getProgressCompletion()) + "%"; - display->drawString(64, 48, percent); - - // Draw indicator to show next update - int updatePos = (printerClient.getProgressCompletion().toFloat() / float(100)) * 128; - display->drawRect(0, 41, 128, 6); - display->drawHorizontalLine(0, 42, updatePos); - display->drawHorizontalLine(0, 43, updatePos); - display->drawHorizontalLine(0, 44, updatePos); - display->drawHorizontalLine(0, 45, updatePos); - - drawRssi(display); -} - -void drawClockHeaderOverlay(OLEDDisplay *display, OLEDDisplayUiState* state) { - display->setColor(WHITE); - display->setFont(ArialMT_Plain_16); - display->setTextAlignment(TEXT_ALIGN_LEFT); - if (!IS_24HOUR) { - display->drawString(0, 48, timeClient.getAmPm()); - display->setTextAlignment(TEXT_ALIGN_CENTER); - if (printerClient.isPSUoff()) { - display->drawString(64, 47, "psu off"); - } else { - display->drawString(64, 47, "offline"); - } - } else { - if (printerClient.isPSUoff()) { - display->drawString(0, 47, "psu off"); - } else { - display->drawString(0, 47, "offline"); - } - } - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->drawRect(0, 43, 128, 2); - - drawRssi(display); -} - -void drawRssi(OLEDDisplay *display) { - - - int8_t quality = getWifiQuality(); - for (int8_t i = 0; i < 4; i++) { - for (int8_t j = 0; j < 3 * (i + 2); j++) { - if (quality > i * 25 || j == 0) { - display->setPixel(114 + 4 * i, 63 - j); - } - } - } -} - -// converts the dBm to a range between 0 and 100% -int8_t getWifiQuality() { - int32_t dbm = WiFi.RSSI(); - if (dbm <= -100) { - return 0; - } else if (dbm >= -50) { - return 100; - } else { - return 2 * (dbm + 100); - } -} - - -void writeSettings() { - // Save decoded message to SPIFFS file for playback on power up. - File f = SPIFFS.open(CONFIG, "w"); - if (!f) { - Serial.println("File open failed!"); - } else { - Serial.println("Saving settings now..."); - f.println("UtcOffset=" + String(UtcOffset)); - f.println("octoKey=" + OctoPrintApiKey); - f.println("octoHost=" + OctoPrintHostName); - f.println("octoServer=" + OctoPrintServer); - f.println("octoPort=" + String(OctoPrintPort)); - f.println("octoUser=" + OctoAuthUser); - f.println("octoPass=" + OctoAuthPass); - f.println("refreshRate=" + String(minutesBetweenDataRefresh)); - f.println("themeColor=" + themeColor); - f.println("IS_BASIC_AUTH=" + String(IS_BASIC_AUTH)); - f.println("www_username=" + String(www_username)); - f.println("www_password=" + String(www_password)); - f.println("DISPLAYCLOCK=" + String(DISPLAYCLOCK)); - f.println("is24hour=" + String(IS_24HOUR)); - f.println("invertDisp=" + String(INVERT_DISPLAY)); - 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("hasPSU=" + String(HAS_PSU)); - } - f.close(); - //readSettings(); - timeClient.setUtcOffset(UtcOffset); -} - -void readSettings() { - if (SPIFFS.exists(CONFIG) == false) { - Serial.println("Settings File does not yet exists."); - writeSettings(); - return; - } - File fr = SPIFFS.open(CONFIG, "r"); - String line; - while (fr.available()) { - line = fr.readStringUntil('\n'); - - if (line.indexOf("UtcOffset=") >= 0) { - UtcOffset = line.substring(line.lastIndexOf("UtcOffset=") + 10).toFloat(); - Serial.println("UtcOffset=" + String(UtcOffset)); - } - if (line.indexOf("octoKey=") >= 0) { - OctoPrintApiKey = line.substring(line.lastIndexOf("octoKey=") + 8); - OctoPrintApiKey.trim(); - Serial.println("OctoPrintApiKey=" + OctoPrintApiKey); - } - if (line.indexOf("octoHost=") >= 0) { - OctoPrintHostName = line.substring(line.lastIndexOf("octoHost=") + 9); - OctoPrintHostName.trim(); - Serial.println("OctoPrintHostName=" + OctoPrintHostName); - } - if (line.indexOf("octoServer=") >= 0) { - OctoPrintServer = line.substring(line.lastIndexOf("octoServer=") + 11); - OctoPrintServer.trim(); - Serial.println("OctoPrintServer=" + OctoPrintServer); - } - if (line.indexOf("octoPort=") >= 0) { - OctoPrintPort = line.substring(line.lastIndexOf("octoPort=") + 9).toInt(); - Serial.println("OctoPrintPort=" + String(OctoPrintPort)); - } - if (line.indexOf("octoUser=") >= 0) { - OctoAuthUser = line.substring(line.lastIndexOf("octoUser=") + 9); - OctoAuthUser.trim(); - Serial.println("OctoAuthUser=" + OctoAuthUser); - } - if (line.indexOf("octoPass=") >= 0) { - OctoAuthPass = line.substring(line.lastIndexOf("octoPass=") + 9); - OctoAuthPass.trim(); - Serial.println("OctoAuthPass=" + OctoAuthPass); - } - if (line.indexOf("refreshRate=") >= 0) { - minutesBetweenDataRefresh = line.substring(line.lastIndexOf("refreshRate=") + 12).toInt(); - Serial.println("minutesBetweenDataRefresh=" + String(minutesBetweenDataRefresh)); - } - if (line.indexOf("themeColor=") >= 0) { - themeColor = line.substring(line.lastIndexOf("themeColor=") + 11); - themeColor.trim(); - Serial.println("themeColor=" + themeColor); - } - if (line.indexOf("IS_BASIC_AUTH=") >= 0) { - IS_BASIC_AUTH = line.substring(line.lastIndexOf("IS_BASIC_AUTH=") + 14).toInt(); - Serial.println("IS_BASIC_AUTH=" + String(IS_BASIC_AUTH)); - } - if (line.indexOf("www_username=") >= 0) { - String temp = line.substring(line.lastIndexOf("www_username=") + 13); - temp.trim(); - temp.toCharArray(www_username, sizeof(temp)); - Serial.println("www_username=" + String(www_username)); - } - if (line.indexOf("www_password=") >= 0) { - String temp = line.substring(line.lastIndexOf("www_password=") + 13); - temp.trim(); - temp.toCharArray(www_password, sizeof(temp)); - Serial.println("www_password=" + String(www_password)); - } - if (line.indexOf("DISPLAYCLOCK=") >= 0) { - DISPLAYCLOCK = line.substring(line.lastIndexOf("DISPLAYCLOCK=") + 13).toInt(); - Serial.println("DISPLAYCLOCK=" + String(DISPLAYCLOCK)); - } - if (line.indexOf("is24hour=") >= 0) { - IS_24HOUR = line.substring(line.lastIndexOf("is24hour=") + 9).toInt(); - Serial.println("IS_24HOUR=" + String(IS_24HOUR)); - } - if (line.indexOf("invertDisp=") >= 0) { - INVERT_DISPLAY = line.substring(line.lastIndexOf("invertDisp=") + 11).toInt(); - Serial.println("INVERT_DISPLAY=" + String(INVERT_DISPLAY)); - } - if (line.indexOf("hasPSU=") >= 0) { - HAS_PSU = line.substring(line.lastIndexOf("hasPSU=") + 7).toInt(); - Serial.println("HAS_PSU=" + String(HAS_PSU)); - } - if (line.indexOf("isWeather=") >= 0) { - DISPLAYWEATHER = line.substring(line.lastIndexOf("isWeather=") + 10).toInt(); - Serial.println("DISPLAYWEATHER=" + String(DISPLAYWEATHER)); - } - if (line.indexOf("weatherKey=") >= 0) { - WeatherApiKey = line.substring(line.lastIndexOf("weatherKey=") + 11); - WeatherApiKey.trim(); - Serial.println("WeatherApiKey=" + WeatherApiKey); - } - if (line.indexOf("CityID=") >= 0) { - CityIDs[0] = line.substring(line.lastIndexOf("CityID=") + 7).toInt(); - Serial.println("CityID: " + String(CityIDs[0])); - } - if (line.indexOf("isMetric=") >= 0) { - IS_METRIC = line.substring(line.lastIndexOf("isMetric=") + 9).toInt(); - Serial.println("IS_METRIC=" + String(IS_METRIC)); - } - if (line.indexOf("language=") >= 0) { - WeatherLanguage = line.substring(line.lastIndexOf("language=") + 9); - WeatherLanguage.trim(); - Serial.println("WeatherLanguage=" + WeatherLanguage); - } - } - fr.close(); - printerClient.updateOctoPrintClient(OctoPrintApiKey, OctoPrintServer, OctoPrintPort, OctoAuthUser, OctoAuthPass, HAS_PSU); - weatherClient.updateWeatherApiKey(WeatherApiKey); - weatherClient.updateLanguage(WeatherLanguage); - weatherClient.setMetric(IS_METRIC); - weatherClient.updateCityIdList(CityIDs, 1); - timeClient.setUtcOffset(UtcOffset); -} - -int getMinutesFromLastRefresh() { - int minutes = (timeClient.getCurrentEpoch() - lastEpoch) / 60; - return minutes; -} - -int getMinutesFromLastDisplay() { - int minutes = (timeClient.getCurrentEpoch() - displayOffEpoch) / 60; - return minutes; -} - -// Toggle on and off the display if user defined times -void checkDisplay() { - if (!displayOn && DISPLAYCLOCK) { - enableDisplay(true); - } - if (displayOn && !(printerClient.isOperational() || printerClient.isPrinting()) && !DISPLAYCLOCK) { - // Put Display to sleep - display.clear(); - display.display(); - display.setFont(ArialMT_Plain_16); - display.setTextAlignment(TEXT_ALIGN_CENTER); - display.setContrast(255); // default is 255 - //display.drawString(64, 5, "Printer Offline\nSleep Mode..."); - // display.display(); - //delay(5000); - enableDisplay(false); - Serial.println("Printer is offline going down to sleep..."); - return; - } else if (!displayOn && !DISPLAYCLOCK) { - if (printerClient.isOperational()) { - // Wake the Screen up - enableDisplay(true); - display.clear(); - display.display(); - display.setFont(ArialMT_Plain_16); - display.setTextAlignment(TEXT_ALIGN_CENTER); - display.setContrast(255); // default is 255 - display.drawString(64, 5, "Printer Online\nWake up..."); - display.display(); - Serial.println("Printer is online waking up..."); - delay(5000); - return; - } - } else if (DISPLAYCLOCK) { - if ((!printerClient.isOperational() || printerClient.isPSUoff()) && !isClockOn) { - Serial.println("Clock Mode is turned on."); - if (!DISPLAYWEATHER) { - ui.disableAutoTransition(); - ui.setFrames(clockFrame, 1); - clockFrame[0] = drawClock; - } else { - ui.enableAutoTransition(); - ui.setFrames(clockFrame, 2); - clockFrame[0] = drawClock; - clockFrame[1] = drawWeather; - } - ui.setOverlays(clockOverlay, numberOfOverlays); - isClockOn = true; - } else if (printerClient.isOperational() && !printerClient.isPSUoff() && isClockOn) { - Serial.println("Printer Monitor is active."); - ui.setFrames(frames, numberOfFrames); - ui.setOverlays(overlays, numberOfOverlays); - ui.enableAutoTransition(); - isClockOn = false; - } - } -} - -void enableDisplay(boolean enable) { - displayOn = enable; - if (enable) { - if (getMinutesFromLastDisplay() >= minutesBetweenDataRefresh) { - // The display has been off longer than the minutes between refresh -- need to get fresh data - lastEpoch = 0; // this should force a data pull - displayOffEpoch = 0; // reset - } - display.displayOn(); - Serial.println("Display was turned ON: " + timeClient.getFormattedTime()); - } else { - display.displayOff(); - Serial.println("Display was turned OFF: " + timeClient.getFormattedTime()); - displayOffEpoch = lastEpoch; - } -} +/** The MIT License (MIT) + +Copyright (c) 2018 David Payne + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +// Additional Contributions: +/* 15 Jan 2019 : Owen Carter : Add psucontrol option and processing */ + + /********************************************** + * Edit Settings.h for personalization + ***********************************************/ + +#include "Settings.h" + +#define VERSION "3.0" + +#define HOSTNAME "PrintMon-" +#define CONFIG "/conf.txt" + +/* Useful Constants */ +#define SECS_PER_MIN (60UL) +#define SECS_PER_HOUR (3600UL) + +/* Useful Macros for getting elapsed time */ +#define numberOfSeconds(_time_) (_time_ % SECS_PER_MIN) +#define numberOfMinutes(_time_) ((_time_ / SECS_PER_MIN) % SECS_PER_MIN) +#define numberOfHours(_time_) (_time_ / SECS_PER_HOUR) + +// Initialize the oled display for I2C_DISPLAY_ADDRESS +// SDA_PIN and SCL_PIN +#if defined(DISPLAY_SH1106) + SH1106Wire display(I2C_DISPLAY_ADDRESS, SDA_PIN, SCL_PIN); +#else + SSD1306Wire display(I2C_DISPLAY_ADDRESS, SDA_PIN, SCL_PIN); // this is the default +#endif + +OLEDDisplayUi ui( &display ); + +void drawProgress(OLEDDisplay *display, int percentage, String label); +void drawOtaProgress(unsigned int, unsigned int); +void drawScreen1(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y); +void drawScreen2(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y); +void drawScreen3(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y); +void drawHeaderOverlay(OLEDDisplay *display, OLEDDisplayUiState* state); +void drawClock(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y); +void drawWeather(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y); +void drawClockHeaderOverlay(OLEDDisplay *display, OLEDDisplayUiState* state); + +// Set the number of Frames supported +const int numberOfFrames = 3; +FrameCallback frames[numberOfFrames]; +FrameCallback clockFrame[2]; +boolean isClockOn = false; + +OverlayCallback overlays[] = { drawHeaderOverlay }; +OverlayCallback clockOverlay[] = { drawClockHeaderOverlay }; +int numberOfOverlays = 1; + +// Time +TimeClient timeClient(UtcOffset); +long lastEpoch = 0; +long firstEpoch = 0; +long displayOffEpoch = 0; +String lastMinute = "xx"; +String lastSecond = "xx"; +String lastReportStatus = ""; +boolean displayOn = true; + +//LDR Status +int ldrStatus =0; + +// Printer Client +#if defined(USE_REPETIER_CLIENT) + RepetierClient printerClient(PrinterApiKey, PrinterServer, PrinterPort, PrinterAuthUser, PrinterAuthPass, HAS_PSU); +#else + OctoPrintClient printerClient(PrinterApiKey, PrinterServer, PrinterPort, PrinterAuthUser, PrinterAuthPass, HAS_PSU); +#endif +int printerCount = 0; + +// Weather Client +OpenWeatherMapClient weatherClient(WeatherApiKey, CityIDs, 1, IS_METRIC, WeatherLanguage); + +//declairing prototypes +void configModeCallback (WiFiManager *myWiFiManager); +int8_t getWifiQuality(); + +ESP8266WebServer server(WEBSERVER_PORT); +ESP8266HTTPUpdateServer serverUpdater; + +static const char WEB_ACTIONS[] PROGMEM = " Home" + " Configure" + " Weather" + " Reset Settings" + " Forget WiFi" + " Firmware Update" + " About"; + +String CHANGE_FORM = ""; // moved to config to make it dynamic + +static const char CLOCK_FORM[] PROGMEM = "

Display Clock when printer is off

" + "

Use 24 Hour Clock (military time)

" + "

Flip display orientation

" + "

Flash System LED on Service Calls

" + "

Use OctoPrint PSU control plugin for clock/blank

" + "

Clock Sync / Weather Refresh (minutes)

"; + +static const char THEME_FORM[] PROGMEM = "

Theme Color

" + "


" + "

Use Security Credentials for Configuration Changes

" + "

" + "

" + ""; + +static const char WEATHER_FORM[] PROGMEM = "

Weather Config:

" + "

Display Weather when printer is off

" + "" + "" + "

" + "

Use Metric (Celsius)

" + "

Weather Language

" + "
" + ""; + +static const char LANG_OPTIONS[] PROGMEM = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; + +static const char COLOR_THEMES[] PROGMEM = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; + + +void setup() { + Serial.begin(115200); + SPIFFS.begin(); + delay(10); + + //New Line to clear from start garbage + Serial.println(); + + // Initialize digital pin for LED (little blue light on the Wemos D1 Mini) + pinMode(externalLight, OUTPUT); + + //Some Defaults before loading from Config.txt + PrinterPort = printerClient.getPrinterPort(); + + readSettings(); + + // initialize display + display.init(); + if (INVERT_DISPLAY) { + display.flipScreenVertically(); // connections at top of OLED display + } + display.clear(); + display.display(); + + //display.flipScreenVertically(); + + display.setTextAlignment(TEXT_ALIGN_CENTER); + display.setContrast(255); // default is 255 + display.setFont(ArialMT_Plain_16); + display.drawString(64, 1, "Printer Monitor"); + display.setFont(ArialMT_Plain_10); + display.drawString(64, 18, "for " + printerClient.getPrinterType()); + display.setFont(ArialMT_Plain_16); + display.drawString(64, 30, "By Qrome"); + display.drawString(64, 46, "V" + String(VERSION)); + display.display(); + + //WiFiManager + //Local intialization. Once its business is done, there is no need to keep it around + WiFiManager wifiManager; + + // Uncomment for testing wifi manager + //wifiManager.resetSettings(); + wifiManager.setAPCallback(configModeCallback); + + String hostname(HOSTNAME); + hostname += String(ESP.getChipId(), HEX); + if (!wifiManager.autoConnect((const char *)hostname.c_str())) {// new addition + delay(3000); + WiFi.disconnect(true); + ESP.reset(); + delay(5000); + } + + // You can change the transition that is used + // SLIDE_LEFT, SLIDE_RIGHT, SLIDE_TOP, SLIDE_DOWN + ui.setFrameAnimation(SLIDE_LEFT); + ui.setTargetFPS(30); + ui.disableAllIndicators(); + ui.setFrames(frames, (numberOfFrames)); + frames[0] = drawScreen1; + frames[1] = drawScreen2; + frames[2] = drawScreen3; + clockFrame[0] = drawClock; + clockFrame[1] = drawWeather; + ui.setOverlays(overlays, numberOfOverlays); + + // Inital UI takes care of initalising the display too. + ui.init(); + if (INVERT_DISPLAY) { + display.flipScreenVertically(); //connections at top of OLED display + } + + // print the received signal strength: + Serial.print("Signal Strength (RSSI): "); + Serial.print(getWifiQuality()); + Serial.println("%"); + + if (ENABLE_OTA) { + ArduinoOTA.onStart([]() { + Serial.println("Start"); + }); + ArduinoOTA.onEnd([]() { + Serial.println("\nEnd"); + }); + ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { + Serial.printf("Progress: %u%%\r", (progress / (total / 100))); + }); + ArduinoOTA.onError([](ota_error_t error) { + Serial.printf("Error[%u]: ", error); + if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); + else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); + else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); + else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); + else if (error == OTA_END_ERROR) Serial.println("End Failed"); + }); + ArduinoOTA.setHostname((const char *)hostname.c_str()); + if (OTA_Password != "") { + ArduinoOTA.setPassword(((const char *)OTA_Password.c_str())); + } + ArduinoOTA.begin(); + } + + if (WEBSERVER_ENABLED) { + server.on("/", displayPrinterStatus); + server.on("/systemreset", handleSystemReset); + server.on("/forgetwifi", handleWifiReset); + server.on("/updateconfig", handleUpdateConfig); + server.on("/updateweatherconfig", handleUpdateWeather); + server.on("/configure", handleConfigure); + server.on("/configureweather", handleWeatherConfigure); + server.onNotFound(redirectHome); + serverUpdater.setup(&server, "/update", www_username, www_password); + // Start the server + server.begin(); + Serial.println("Server started"); + // Print the IP address + String webAddress = "http://" + WiFi.localIP().toString() + ":" + String(WEBSERVER_PORT) + "/"; + Serial.println("Use this URL : " + webAddress); + display.clear(); + display.setTextAlignment(TEXT_ALIGN_CENTER); + display.setFont(ArialMT_Plain_10); + display.drawString(64, 10, "Web Interface On"); + display.drawString(64, 20, "You May Connect to IP"); + display.setFont(ArialMT_Plain_16); + display.drawString(64, 30, WiFi.localIP().toString()); + display.drawString(64, 46, "Port: " + String(WEBSERVER_PORT)); + display.display(); + } else { + Serial.println("Web Interface is Disabled"); + display.clear(); + display.setTextAlignment(TEXT_ALIGN_CENTER); + display.setFont(ArialMT_Plain_10); + display.drawString(64, 10, "Web Interface is Off"); + display.drawString(64, 20, "Enable in Settings.h"); + display.display(); + } + flashLED(5, 100); + findMDNS(); //go find Printer Server by the hostname + Serial.println("*** Leaving setup()"); +} + +void findMDNS() { + if (PrinterHostName == "" || ENABLE_OTA == false) { + return; // nothing to do here + } + // We now query our network for 'web servers' service + // over tcp, and get the number of available devices + int n = MDNS.queryService("http", "tcp"); + if (n == 0) { + Serial.println("no services found - make sure Printer server is turned on"); + return; + } + Serial.println("*** Looking for " + PrinterHostName + " over mDNS"); + for (int i = 0; i < n; ++i) { + // Going through every available service, + // we're searching for the one whose hostname + // matches what we want, and then get its IP + Serial.println("Found: " + MDNS.hostname(i)); + if (MDNS.hostname(i) == PrinterHostName) { + IPAddress serverIp = MDNS.IP(i); + PrinterServer = serverIp.toString(); + PrinterPort = MDNS.port(i); // save the port + Serial.println("*** Found Printer Server " + PrinterHostName + " http://" + PrinterServer + ":" + PrinterPort); + writeSettings(); // update the settings + } + } +} + +//************************************************************ +// Main Loop +//************************************************************ +void loop() { + + //Get Time Update + if((getMinutesFromLastRefresh() >= minutesBetweenDataRefresh) || lastEpoch == 0) { + getUpdateTime(); + } + + if (lastMinute != timeClient.getMinutes() && !printerClient.isPrinting()) { + // Check status every 60 seconds + ledOnOff(true); + lastMinute = timeClient.getMinutes(); // reset the check value + printerClient.getPrinterJobResults(); + printerClient.getPrinterPsuState(); + ledOnOff(false); + } else if (printerClient.isPrinting()) { + if (lastSecond != timeClient.getSeconds() && timeClient.getSeconds().endsWith("0")) { + lastSecond = timeClient.getSeconds(); + // every 10 seconds while printing get an update + ledOnOff(true); + printerClient.getPrinterJobResults(); + printerClient.getPrinterPsuState(); + ledOnOff(false); + } + } + + checkDisplay(); // Check to see if the printer is on or offline and change display. + lightOn(); //Check if the lights are on + ui.update(); + + if (WEBSERVER_ENABLED) { + server.handleClient(); + } + if (ENABLE_OTA) { + ArduinoOTA.handle(); + } +} + +void lightOn() { //Check LDR status + ldrStatus = analogRead(ldrPin); + if(ldrStatus>=250){ //Change this value to change the sensitivity of the LDR + Serial.print("LDR value will turn on screen"); + DISPLAYCLOCK = true; + //enableDisplay(true); + } + else{ + Serial.print("LDR will NOT turn on screen"); + DISPLAYCLOCK = false; + // enableDisplay(false); + } +} + +void getUpdateTime() { + ledOnOff(true); // turn on the LED + Serial.println(); + + if (displayOn && DISPLAYWEATHER) { + Serial.println("Getting Weather Data..."); + weatherClient.updateWeather(); + } + + Serial.println("Updating Time..."); + //Update the Time + timeClient.updateTime(); + lastEpoch = timeClient.getCurrentEpoch(); + Serial.println("Local time: " + timeClient.getAmPmFormattedTime()); + + ledOnOff(false); // turn off the LED +} + +boolean authentication() { + if (IS_BASIC_AUTH && (strlen(www_username) >= 1 && strlen(www_password) >= 1)) { + return server.authenticate(www_username, www_password); + } + return true; // Authentication not required +} + +void handleSystemReset() { + if (!authentication()) { + return server.requestAuthentication(); + } + Serial.println("Reset System Configuration"); + if (SPIFFS.remove(CONFIG)) { + redirectHome(); + ESP.restart(); + } +} + +void handleUpdateWeather() { + if (!authentication()) { + return 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"); + writeSettings(); + isClockOn = false; // this will force a check for the display + checkDisplay(); + lastEpoch = 0; + redirectHome(); +} + +void handleUpdateConfig() { + boolean flipOld = INVERT_DISPLAY; + if (!authentication()) { + return server.requestAuthentication(); + } + if (server.hasArg("printer")) { + printerClient.setPrinterName(server.arg("printer")); + } + PrinterApiKey = server.arg("PrinterApiKey"); + PrinterHostName = server.arg("PrinterHostName"); + PrinterServer = server.arg("PrinterAddress"); + PrinterPort = server.arg("PrinterPort").toInt(); + PrinterAuthUser = server.arg("octoUser"); + PrinterAuthPass = server.arg("octoPass"); + DISPLAYCLOCK = server.hasArg("isClockEnabled"); + IS_24HOUR = server.hasArg("is24hour"); + INVERT_DISPLAY = server.hasArg("invDisp"); + USE_FLASH = server.hasArg("useFlash"); + HAS_PSU = server.hasArg("hasPSU"); + minutesBetweenDataRefresh = server.arg("refresh").toInt(); + themeColor = server.arg("theme"); + UtcOffset = server.arg("utcoffset").toFloat(); + String temp = server.arg("userid"); + temp.toCharArray(www_username, sizeof(temp)); + temp = server.arg("stationpassword"); + temp.toCharArray(www_password, sizeof(temp)); + writeSettings(); + findMDNS(); + printerClient.getPrinterJobResults(); + printerClient.getPrinterPsuState(); + if (INVERT_DISPLAY != flipOld) { + ui.init(); + if(INVERT_DISPLAY) + display.flipScreenVertically(); + ui.update(); + } + checkDisplay(); + lastEpoch = 0; + redirectHome(); +} + +void handleWifiReset() { + if (!authentication()) { + return 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 handleWeatherConfigure() { + if (!authentication()) { + return server.requestAuthentication(); + } + ledOnOff(true); + String html = ""; + + 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", ""); + + html = getHeader(); + server.sendContent(html); + + String form = FPSTR(WEATHER_FORM); + String isWeatherChecked = ""; + if (DISPLAYWEATHER) { + isWeatherChecked = "checked='checked'"; + } + form.replace("%IS_WEATHER_CHECKED%", isWeatherChecked); + form.replace("%WEATHERKEY%", WeatherApiKey); + 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); + server.sendContent(form); + + html = getFooter(); + server.sendContent(html); + server.sendContent(""); + server.client().stop(); + ledOnOff(false); +} + +void handleConfigure() { + if (!authentication()) { + return server.requestAuthentication(); + } + ledOnOff(true); + String html = ""; + + 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", ""); + + html = getHeader(); + 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 = ""; + + server.sendContent(html); + } else { + html = ""; + server.sendContent(html); + } + + String form = CHANGE_FORM; + + form.replace("%OCTOKEY%", PrinterApiKey); + form.replace("%OCTOHOST%", PrinterHostName); + form.replace("%OCTOADDRESS%", PrinterServer); + form.replace("%OCTOPORT%", String(PrinterPort)); + form.replace("%OCTOUSER%", PrinterAuthUser); + form.replace("%OCTOPASS%", PrinterAuthPass); + + server.sendContent(form); + + form = FPSTR(CLOCK_FORM); + + String isClockChecked = ""; + if (DISPLAYCLOCK) { + isClockChecked = "checked='checked'"; + } + form.replace("%IS_CLOCK_CHECKED%", isClockChecked); + String is24hourChecked = ""; + if (IS_24HOUR) { + is24hourChecked = "checked='checked'"; + } + form.replace("%IS_24HOUR_CHECKED%", is24hourChecked); + String isInvDisp = ""; + if (INVERT_DISPLAY) { + isInvDisp = "checked='checked'"; + } + form.replace("%IS_INVDISP_CHECKED%", isInvDisp); + String isFlashLED = ""; + if (USE_FLASH) { + isFlashLED = "checked='checked'"; + } + form.replace("%USEFLASH%", isFlashLED); + String hasPSUchecked = ""; + if (HAS_PSU) { + hasPSUchecked = "checked='checked'"; + } + form.replace("%HAS_PSU_CHECKED%", hasPSUchecked); + + String options = ""; + options.replace(">"+String(minutesBetweenDataRefresh)+"<", " selected>"+String(minutesBetweenDataRefresh)+"<"); + form.replace("%OPTIONS%", options); + + server.sendContent(form); + + form = FPSTR(THEME_FORM); + + String themeOptions = FPSTR(COLOR_THEMES); + themeOptions.replace(">"+String(themeColor)+"<", " selected>"+String(themeColor)+"<"); + form.replace("%THEME_OPTIONS%", themeOptions); + form.replace("%UTCOFFSET%", String(UtcOffset)); + String isUseSecurityChecked = ""; + if (IS_BASIC_AUTH) { + isUseSecurityChecked = "checked='checked'"; + } + form.replace("%IS_BASICAUTH_CHECKED%", isUseSecurityChecked); + form.replace("%USERID%", String(www_username)); + form.replace("%STATIONPASSWORD%", String(www_password)); + + server.sendContent(form); + + html = getFooter(); + server.sendContent(html); + server.sendContent(""); + server.client().stop(); + ledOnOff(false); +} + +void displayMessage(String message) { + ledOnOff(true); + + 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", ""); + String html = getHeader(); + server.sendContent(String(html)); + server.sendContent(String(message)); + html = getFooter(); + server.sendContent(String(html)); + server.sendContent(""); + server.client().stop(); + + ledOnOff(false); +} + +void redirectHome() { + // Send them back to the Root Directory + server.sendHeader("Location", String("/"), true); + server.sendHeader("Cache-Control", "no-cache, no-store"); + server.sendHeader("Pragma", "no-cache"); + server.sendHeader("Expires", "-1"); + server.send(302, "text/plain", ""); + server.client().stop(); +} + +String getHeader() { + return getHeader(false); +} + +String 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 getFooter() { + int8_t rssi = getWifiQuality(); + Serial.print("Signal Strength (RSSI): "); + Serial.print(rssi); + Serial.println("%"); + String html = "


"; + html += "
"; + html += "
"; + if (lastReportStatus != "") { + html += " Report Status: " + lastReportStatus + "
"; + } + html += " Version: " + String(VERSION) + "
"; + html += " Signal Strength: "; + html += String(rssi) + "%"; + html += "
"; + html += ""; + return html; +} + +void displayPrinterStatus() { + ledOnOff(true); + String html = ""; + + 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(String(getHeader(true))); + + String displayTime = timeClient.getAmPmHours() + ":" + timeClient.getMinutes() + ":" + timeClient.getSeconds() + " " + timeClient.getAmPm(); + if (IS_24HOUR) { + displayTime = timeClient.getHours() + ":" + timeClient.getMinutes() + ":" + timeClient.getSeconds(); + } + + html += "

" + printerClient.getPrinterType() + " Monitor

"; + html += "

"; + if (printerClient.getPrinterType() == "Repetier") { + html += "Printer Name: " + printerClient.getPrinterName() + "
"; + } else { + html += "Host Name: " + PrinterHostName + "
"; + } + + if (printerClient.getError() != "") { + html += "Status: Offline
"; + html += "Reason: " + printerClient.getError() + "
"; + } else { + html += "Status: " + printerClient.getState(); + if (printerClient.isPSUoff() && HAS_PSU) { + 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 = numberOfHours(val); + int minutes = numberOfMinutes(val); + int seconds = numberOfSeconds(val); + html += "Est. Print Time Left: " + zeroPad(hours) + ":" + zeroPad(minutes) + ":" + zeroPad(seconds) + "
"; + + val = printerClient.getProgressPrintTime().toInt(); + hours = numberOfHours(val); + minutes = numberOfMinutes(val); + seconds = numberOfSeconds(val); + html += "Printing Time: " + zeroPad(hours) + ":" + zeroPad(minutes) + ":" + zeroPad(seconds) + "
"; + html += ""; + html += "

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

"; + + html += "

Time: " + displayTime + "

"; + + 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 + } + + server.sendContent(String(getFooter())); + server.sendContent(""); + server.client().stop(); + ledOnOff(false); +} + +void configModeCallback (WiFiManager *myWiFiManager) { + Serial.println("Entered config mode"); + Serial.println(WiFi.softAPIP()); + + display.clear(); + display.setTextAlignment(TEXT_ALIGN_CENTER); + display.setFont(ArialMT_Plain_10); + display.drawString(64, 0, "Wifi Manager"); + display.drawString(64, 10, "Please connect to AP"); + display.setFont(ArialMT_Plain_16); + display.drawString(64, 26, myWiFiManager->getConfigPortalSSID()); + display.setFont(ArialMT_Plain_10); + display.drawString(64, 46, "To setup Wifi connection"); + display.display(); + + Serial.println("Wifi Manager"); + Serial.println("Please connect to AP"); + Serial.println(myWiFiManager->getConfigPortalSSID()); + Serial.println("To setup Wifi Configuration"); + flashLED(20, 50); +} + +void ledOnOff(boolean value) { + if (USE_FLASH) { + if (value) { + digitalWrite(externalLight, LOW); // LED ON + } else { + digitalWrite(externalLight, HIGH); // LED OFF + } + } +} + +void flashLED(int number, int delayTime) { + for (int inx = 0; inx <= number; inx++) { + delay(delayTime); + digitalWrite(externalLight, LOW); // ON + delay(delayTime); + digitalWrite(externalLight, HIGH); // OFF + delay(delayTime); + } +} + +void drawScreen1(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + String bed = printerClient.getValueRounded(printerClient.getTempBedActual()); + String tool = printerClient.getValueRounded(printerClient.getTempToolActual()); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(ArialMT_Plain_16); + if (bed != "0") { + display->drawString(29 + x, 0 + y, "Tool"); + display->drawString(89 + x, 0 + y, "Bed"); + } else { + display->drawString(64 + x, 0 + y, "Tool Temp"); + } + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(ArialMT_Plain_24); + if (bed != "0") { + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->drawString(12 + x, 14 + y, tool + "°"); + display->drawString(74 + x, 14 + y, bed + "°"); + } else { + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(64 + x, 14 + y, tool + "°"); + } +} + +void drawScreen2(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(ArialMT_Plain_16); + + display->drawString(64 + x, 0 + y, "Time Remaining"); + //display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(ArialMT_Plain_24); + int val = printerClient.getProgressPrintTimeLeft().toInt(); + int hours = numberOfHours(val); + int minutes = numberOfMinutes(val); + int seconds = numberOfSeconds(val); + + String time = zeroPad(hours) + ":" + zeroPad(minutes) + ":" + zeroPad(seconds); + display->drawString(64 + x, 14 + y, time); +} + +void drawScreen3(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(ArialMT_Plain_16); + + display->drawString(64 + x, 0 + y, "Printing Time"); + //display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(ArialMT_Plain_24); + int val = printerClient.getProgressPrintTime().toInt(); + int hours = numberOfHours(val); + int minutes = numberOfMinutes(val); + int seconds = numberOfSeconds(val); + + String time = zeroPad(hours) + ":" + zeroPad(minutes) + ":" + zeroPad(seconds); + display->drawString(64 + x, 14 + y, time); +} + +void drawClock(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + display->setTextAlignment(TEXT_ALIGN_CENTER); + + String displayTime = timeClient.getAmPmHours() + ":" + timeClient.getMinutes() + ":" + timeClient.getSeconds(); + if (IS_24HOUR) { + displayTime = timeClient.getHours() + ":" + timeClient.getMinutes() + ":" + timeClient.getSeconds(); + } + String displayName = PrinterHostName; + if (printerClient.getPrinterType() == "Repetier") { + displayName = printerClient.getPrinterName(); + } + display->setFont(ArialMT_Plain_16); + display->drawString(64 + x, 0 + y, displayName); + display->setFont(ArialMT_Plain_24); + display->drawString(64 + x, 17 + y, displayTime); +} + +void drawWeather(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(ArialMT_Plain_24); + display->drawString(0 + x, 0 + y, weatherClient.getTempRounded(0) + getTempSymbol()); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(ArialMT_Plain_24); + + display->setFont(ArialMT_Plain_16); + display->drawString(0 + x, 24 + y, weatherClient.getCondition(0)); + display->setFont((const uint8_t*)Meteocons_Plain_42); + display->drawString(86 + x, 0 + y, weatherClient.getWeatherIcon(0)); +} + +String getTempSymbol() { + return getTempSymbol(false); +} + +String getTempSymbol(boolean forHTML) { + String rtnValue = "F"; + if (IS_METRIC) { + rtnValue = "C"; + } + if (forHTML) { + rtnValue = "°" + rtnValue; + } else { + rtnValue = "°" + rtnValue; + } + return rtnValue; +} + +String getSpeedSymbol() { + String rtnValue = "mph"; + if (IS_METRIC) { + rtnValue = "kph"; + } + return rtnValue; +} + +String zeroPad(int value) { + String rtnValue = String(value); + if (value < 10) { + rtnValue = "0" + rtnValue; + } + return rtnValue; +} + +void drawHeaderOverlay(OLEDDisplay *display, OLEDDisplayUiState* state) { + display->setColor(WHITE); + display->setFont(ArialMT_Plain_16); + String displayTime = timeClient.getAmPmHours() + ":" + timeClient.getMinutes(); + if (IS_24HOUR) { + displayTime = timeClient.getHours() + ":" + timeClient.getMinutes(); + } + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->drawString(0, 48, displayTime); + + if (!IS_24HOUR) { + String ampm = timeClient.getAmPm(); + display->setFont(ArialMT_Plain_10); + display->drawString(39, 54, ampm); + } + + display->setFont(ArialMT_Plain_16); + display->setTextAlignment(TEXT_ALIGN_LEFT); + String percent = String(printerClient.getProgressCompletion()) + "%"; + display->drawString(64, 48, percent); + + // Draw indicator to show next update + int updatePos = (printerClient.getProgressCompletion().toFloat() / float(100)) * 128; + display->drawRect(0, 41, 128, 6); + display->drawHorizontalLine(0, 42, updatePos); + display->drawHorizontalLine(0, 43, updatePos); + display->drawHorizontalLine(0, 44, updatePos); + display->drawHorizontalLine(0, 45, updatePos); + + drawRssi(display); +} + +void drawClockHeaderOverlay(OLEDDisplay *display, OLEDDisplayUiState* state) { + display->setColor(WHITE); + display->setFont(ArialMT_Plain_16); + display->setTextAlignment(TEXT_ALIGN_LEFT); + if (!IS_24HOUR) { + display->drawString(0, 48, timeClient.getAmPm()); + display->setTextAlignment(TEXT_ALIGN_CENTER); + if (printerClient.isPSUoff()) { + display->drawString(64, 47, "psu off"); + } else if (printerClient.getState() == "Operational") { + display->drawString(64, 47, "online"); + } else { + display->drawString(64, 47, "offline"); + } + } else { + if (printerClient.isPSUoff()) { + display->drawString(0, 47, "psu off"); + } else if (printerClient.getState() == "Operational") { + display->drawString(0, 47, "online"); + } else { + display->drawString(0, 47, "offline"); + } + } + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->drawRect(0, 43, 128, 2); + + drawRssi(display); +} + +void drawRssi(OLEDDisplay *display) { + + + int8_t quality = getWifiQuality(); + for (int8_t i = 0; i < 4; i++) { + for (int8_t j = 0; j < 3 * (i + 2); j++) { + if (quality > i * 25 || j == 0) { + display->setPixel(114 + 4 * i, 63 - j); + } + } + } +} + +// converts the dBm to a range between 0 and 100% +int8_t getWifiQuality() { + int32_t dbm = WiFi.RSSI(); + if(dbm <= -100) { + return 0; + } else if(dbm >= -50) { + return 100; + } else { + return 2 * (dbm + 100); + } +} + + +void writeSettings() { + // Save decoded message to SPIFFS file for playback on power up. + File f = SPIFFS.open(CONFIG, "w"); + if (!f) { + Serial.println("File open failed!"); + } else { + Serial.println("Saving settings now..."); + f.println("UtcOffset=" + String(UtcOffset)); + f.println("printerApiKey=" + PrinterApiKey); + f.println("printerHostName=" + PrinterHostName); + f.println("printerServer=" + PrinterServer); + f.println("printerPort=" + String(PrinterPort)); + f.println("printerName=" + printerClient.getPrinterName()); + f.println("printerAuthUser=" + PrinterAuthUser); + f.println("printerAuthPass=" + PrinterAuthPass); + f.println("refreshRate=" + String(minutesBetweenDataRefresh)); + f.println("themeColor=" + themeColor); + f.println("IS_BASIC_AUTH=" + String(IS_BASIC_AUTH)); + f.println("www_username=" + String(www_username)); + f.println("www_password=" + String(www_password)); + f.println("DISPLAYCLOCK=" + String(DISPLAYCLOCK)); + f.println("is24hour=" + String(IS_24HOUR)); + f.println("invertDisp=" + String(INVERT_DISPLAY)); + 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("hasPSU=" + String(HAS_PSU)); + } + f.close(); + readSettings(); + timeClient.setUtcOffset(UtcOffset); +} + +void readSettings() { + if (SPIFFS.exists(CONFIG) == false) { + Serial.println("Settings File does not yet exists."); + writeSettings(); + return; + } + File fr = SPIFFS.open(CONFIG, "r"); + String line; + while(fr.available()) { + line = fr.readStringUntil('\n'); + + if (line.indexOf("UtcOffset=") >= 0) { + UtcOffset = line.substring(line.lastIndexOf("UtcOffset=") + 10).toFloat(); + Serial.println("UtcOffset=" + String(UtcOffset)); + } + if (line.indexOf("printerApiKey=") >= 0) { + PrinterApiKey = line.substring(line.lastIndexOf("printerApiKey=") + 14); + PrinterApiKey.trim(); + Serial.println("PrinterApiKey=" + PrinterApiKey); + } + if (line.indexOf("printerHostName=") >= 0) { + PrinterHostName = line.substring(line.lastIndexOf("printerHostName=") + 16); + PrinterHostName.trim(); + Serial.println("PrinterHostName=" + PrinterHostName); + } + if (line.indexOf("printerServer=") >= 0) { + PrinterServer = line.substring(line.lastIndexOf("printerServer=") + 14); + PrinterServer.trim(); + Serial.println("PrinterServer=" + PrinterServer); + } + if (line.indexOf("printerPort=") >= 0) { + PrinterPort = line.substring(line.lastIndexOf("printerPort=") + 12).toInt(); + Serial.println("PrinterPort=" + String(PrinterPort)); + } + if (line.indexOf("printerName=") >= 0) { + String printer = line.substring(line.lastIndexOf("printerName=") + 12); + printer.trim(); + printerClient.setPrinterName(printer); + Serial.println("PrinterName=" + printerClient.getPrinterName()); + } + if (line.indexOf("printerAuthUser=") >= 0) { + PrinterAuthUser = line.substring(line.lastIndexOf("printerAuthUser=") + 16); + PrinterAuthUser.trim(); + Serial.println("PrinterAuthUser=" + PrinterAuthUser); + } + if (line.indexOf("printerAuthPass=") >= 0) { + PrinterAuthPass = line.substring(line.lastIndexOf("printerAuthPass=") + 16); + PrinterAuthPass.trim(); + Serial.println("PrinterAuthPass=" + PrinterAuthPass); + } + if (line.indexOf("refreshRate=") >= 0) { + minutesBetweenDataRefresh = line.substring(line.lastIndexOf("refreshRate=") + 12).toInt(); + Serial.println("minutesBetweenDataRefresh=" + String(minutesBetweenDataRefresh)); + } + if (line.indexOf("themeColor=") >= 0) { + themeColor = line.substring(line.lastIndexOf("themeColor=") + 11); + themeColor.trim(); + Serial.println("themeColor=" + themeColor); + } + if (line.indexOf("IS_BASIC_AUTH=") >= 0) { + IS_BASIC_AUTH = line.substring(line.lastIndexOf("IS_BASIC_AUTH=") + 14).toInt(); + Serial.println("IS_BASIC_AUTH=" + String(IS_BASIC_AUTH)); + } + if (line.indexOf("www_username=") >= 0) { + String temp = line.substring(line.lastIndexOf("www_username=") + 13); + temp.trim(); + temp.toCharArray(www_username, sizeof(temp)); + Serial.println("www_username=" + String(www_username)); + } + if (line.indexOf("www_password=") >= 0) { + String temp = line.substring(line.lastIndexOf("www_password=") + 13); + temp.trim(); + temp.toCharArray(www_password, sizeof(temp)); + Serial.println("www_password=" + String(www_password)); + } + if (line.indexOf("DISPLAYCLOCK=") >= 0) { + DISPLAYCLOCK = line.substring(line.lastIndexOf("DISPLAYCLOCK=") + 13).toInt(); + Serial.println("DISPLAYCLOCK=" + String(DISPLAYCLOCK)); + } + if (line.indexOf("is24hour=") >= 0) { + IS_24HOUR = line.substring(line.lastIndexOf("is24hour=") + 9).toInt(); + Serial.println("IS_24HOUR=" + String(IS_24HOUR)); + } + if(line.indexOf("invertDisp=") >= 0) { + INVERT_DISPLAY = line.substring(line.lastIndexOf("invertDisp=") + 11).toInt(); + Serial.println("INVERT_DISPLAY=" + String(INVERT_DISPLAY)); + } + if(line.indexOf("USE_FLASH=") >= 0) { + USE_FLASH = line.substring(line.lastIndexOf("USE_FLASH=") + 10).toInt(); + Serial.println("USE_FLASH=" + String(USE_FLASH)); + } + if (line.indexOf("hasPSU=") >= 0) { + HAS_PSU = line.substring(line.lastIndexOf("hasPSU=") + 7).toInt(); + Serial.println("HAS_PSU=" + String(HAS_PSU)); + } + if (line.indexOf("isWeather=") >= 0) { + DISPLAYWEATHER = line.substring(line.lastIndexOf("isWeather=") + 10).toInt(); + Serial.println("DISPLAYWEATHER=" + String(DISPLAYWEATHER)); + } + if (line.indexOf("weatherKey=") >= 0) { + WeatherApiKey = line.substring(line.lastIndexOf("weatherKey=") + 11); + WeatherApiKey.trim(); + Serial.println("WeatherApiKey=" + WeatherApiKey); + } + if (line.indexOf("CityID=") >= 0) { + CityIDs[0] = line.substring(line.lastIndexOf("CityID=") + 7).toInt(); + Serial.println("CityID: " + String(CityIDs[0])); + } + if (line.indexOf("isMetric=") >= 0) { + IS_METRIC = line.substring(line.lastIndexOf("isMetric=") + 9).toInt(); + Serial.println("IS_METRIC=" + String(IS_METRIC)); + } + if (line.indexOf("language=") >= 0) { + WeatherLanguage = line.substring(line.lastIndexOf("language=") + 9); + WeatherLanguage.trim(); + Serial.println("WeatherLanguage=" + WeatherLanguage); + } + } + fr.close(); + printerClient.updatePrintClient(PrinterApiKey, PrinterServer, PrinterPort, PrinterAuthUser, PrinterAuthPass, HAS_PSU); + weatherClient.updateWeatherApiKey(WeatherApiKey); + weatherClient.updateLanguage(WeatherLanguage); + weatherClient.setMetric(IS_METRIC); + weatherClient.updateCityIdList(CityIDs, 1); + timeClient.setUtcOffset(UtcOffset); +} + +int getMinutesFromLastRefresh() { + int minutes = (timeClient.getCurrentEpoch() - lastEpoch) / 60; + return minutes; +} + +int getMinutesFromLastDisplay() { + int minutes = (timeClient.getCurrentEpoch() - displayOffEpoch) / 60; + return minutes; +} + +// Toggle on and off the display if user defined times +void checkDisplay() { + if (!displayOn && DISPLAYCLOCK) { + enableDisplay(true); + } + if (displayOn && !printerClient.isPrinting() && !DISPLAYCLOCK) { + // Put Display to sleep + display.clear(); + display.display(); + display.setFont(ArialMT_Plain_16); + display.setTextAlignment(TEXT_ALIGN_CENTER); + display.setContrast(255); // default is 255 + display.drawString(64, 5, "Printer Offline\nSleep Mode..."); + display.display(); + delay(5000); + enableDisplay(false); + Serial.println("Printer is offline going down to sleep..."); + return; + } else if (!displayOn && !DISPLAYCLOCK) { + if (printerClient.isOperational()) { + // Wake the Screen up + enableDisplay(true); + display.clear(); + display.display(); + display.setFont(ArialMT_Plain_16); + display.setTextAlignment(TEXT_ALIGN_CENTER); + display.setContrast(255); // default is 255 + display.drawString(64, 5, "Printer Online\nWake up..."); + display.display(); + Serial.println("Printer is online waking up..."); + delay(5000); + return; + } + } else if (DISPLAYCLOCK) { + if ((!printerClient.isPrinting() || printerClient.isPSUoff()) && !isClockOn) { + Serial.println("Clock Mode is turned on."); + if (!DISPLAYWEATHER) { + ui.disableAutoTransition(); + ui.setFrames(clockFrame, 1); + clockFrame[0] = drawClock; + } else { + ui.enableAutoTransition(); + ui.setFrames(clockFrame, 2); + clockFrame[0] = drawClock; + clockFrame[1] = drawWeather; + } + ui.setOverlays(clockOverlay, numberOfOverlays); + isClockOn = true; + } else if (printerClient.isPrinting() && !printerClient.isPSUoff() && isClockOn) { + Serial.println("Printer Monitor is active."); + ui.setFrames(frames, numberOfFrames); + ui.setOverlays(overlays, numberOfOverlays); + ui.enableAutoTransition(); + isClockOn = false; + } + } +} + +void enableDisplay(boolean enable) { + displayOn = enable; + if (enable) { + if (getMinutesFromLastDisplay() >= minutesBetweenDataRefresh) { + // The display has been off longer than the minutes between refresh -- need to get fresh data + lastEpoch = 0; // this should force a data pull + displayOffEpoch = 0; // reset + } + display.displayOn(); + Serial.println("Display was turned ON: " + timeClient.getFormattedTime()); + } else { + display.displayOff(); + Serial.println("Display was turned OFF: " + timeClient.getFormattedTime()); + displayOffEpoch = lastEpoch; + } +} From 719f144103c180fcc63e2dcd17b80238c5ec579f Mon Sep 17 00:00:00 2001 From: abacon118 Date: Mon, 29 May 2023 20:40:11 -0400 Subject: [PATCH 4/4] Add files via upload --- PrinterMonitor_LDR.png | Bin 0 -> 115222 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 PrinterMonitor_LDR.png diff --git a/PrinterMonitor_LDR.png b/PrinterMonitor_LDR.png new file mode 100644 index 0000000000000000000000000000000000000000..031fc33c8cf874ec0414b80a9d61982bae40f9e4 GIT binary patch literal 115222 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%0AQaxo9 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 yw-bp(7IVw!#9|I2K*f2dpwMq*CD%>> literal 0 HcmV?d00001