From b28f49a19503f2bffe617f75842e5de27a7ae757 Mon Sep 17 00:00:00 2001
From: Owen
Date: Tue, 15 Jan 2019 21:14:09 +0100
Subject: [PATCH 1/5] Add a function + option to request PSU status and enter
clock/blank mode depending on the PSU status
---
README.md | 1 +
printermonitor/OctoPrintClient.cpp | 120 ++++++++++++++++++++++++++---
printermonitor/OctoPrintClient.h | 15 +++-
printermonitor/Settings.h | 10 +++
printermonitor/printermonitor.ino | 54 ++++++++++---
5 files changed, 175 insertions(+), 25 deletions(-)
diff --git a/README.md b/README.md
index 3e7f920..d41dc61 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,7 @@ SOFTWARE.
* Supports OTA (loading firmware over WiFi connection on same LAN)
* Basic Authentication to protect your settings
* Version 2.2 added the ability to update firmware through web interface from a compiled binary
+* Can query the Octoprint [https://plugins.octoprint.org/plugins/psucontrol/](PSU Control plugin) to enter clock or blank mode when PSU is off
* Video: https://youtu.be/niRv9SCgAPk
* Detailed build video by Chris Riley: https://youtu.be/Rm-l1FSuJpI
diff --git a/printermonitor/OctoPrintClient.cpp b/printermonitor/OctoPrintClient.cpp
index b2879ca..4f669ad 100644
--- a/printermonitor/OctoPrintClient.cpp
+++ b/printermonitor/OctoPrintClient.cpp
@@ -21,13 +21,15 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
+/* 15 Jan 2019 : Owen Carter : Add psucontrol query via POST api call */
+
#include "OctoPrintClient.h"
-OctoPrintClient::OctoPrintClient(String ApiKey, String server, int port, String user, String pass) {
- updateOctoPrintClient(ApiKey, server, port, user, pass);
+OctoPrintClient::OctoPrintClient(String ApiKey, String server, int port, String user, String pass, boolean psu) {
+ updateOctoPrintClient(ApiKey, server, port, user, pass, psu);
}
-void OctoPrintClient::updateOctoPrintClient(String ApiKey, String server, int port, String user, String pass) {
+void OctoPrintClient::updateOctoPrintClient(String ApiKey, String server, int port, String user, String pass, boolean psu) {
server.toCharArray(myServer, 100);
myApiKey = ApiKey;
myPort = port;
@@ -37,6 +39,7 @@ void OctoPrintClient::updateOctoPrintClient(String ApiKey, String server, int po
base64 b64;
encodedAuth = b64.encode(userpass, true);
}
+ pollPsu = psu;
}
boolean OctoPrintClient::validate() {
@@ -58,7 +61,7 @@ WiFiClient OctoPrintClient::getSubmitRequest(String apiGetData) {
WiFiClient printClient;
printClient.setTimeout(5000);
- Serial.println("Getting Octoprint Data");
+ Serial.println("Getting Octoprint Data via GET");
Serial.println(apiGetData);
result = "";
if (printClient.connect(myServer, myPort)) { //starts client connection, checks for connection
@@ -109,18 +112,76 @@ WiFiClient OctoPrintClient::getSubmitRequest(String apiGetData) {
return printClient;
}
+WiFiClient OctoPrintClient::getPostRequest(String apiPostData, String apiPostBody) {
+ WiFiClient printClient;
+ printClient.setTimeout(5000);
+
+ Serial.println("Getting Octoprint Data via POST");
+ Serial.println(apiPostData + " | " + apiPostBody);
+ result = "";
+ if (printClient.connect(myServer, myPort)) { //starts client connection, checks for connection
+ printClient.println(apiPostData);
+ printClient.println("Host: " + String(myServer) + ":" + String(myPort));
+ printClient.println("Connection: close");
+ printClient.println("X-Api-Key: " + myApiKey);
+ if (encodedAuth != "") {
+ printClient.print("Authorization: ");
+ printClient.println("Basic " + encodedAuth);
+ }
+ printClient.println("User-Agent: ArduinoWiFi/1.1");
+ printClient.println("Content-Type: application/json");
+ printClient.print("Content-Length: ");
+ printClient.println(apiPostBody.length());
+ printClient.println();
+ printClient.println(apiPostBody);
+ if (printClient.println() == 0) {
+ Serial.println("Connection to " + String(myServer) + ":" + String(myPort) + " failed.");
+ Serial.println();
+ resetPrintData();
+ printerData.error = "Connection to " + String(myServer) + ":" + String(myPort) + " failed.";
+ return printClient;
+ }
+ }
+ else {
+ Serial.println("Connection to OctoPrint failed: " + String(myServer) + ":" + String(myPort)); //error message if no client connect
+ Serial.println();
+ resetPrintData();
+ printerData.error = "Connection to OctoPrint failed: " + String(myServer) + ":" + String(myPort);
+ return printClient;
+ }
+
+ // Check HTTP status
+ char status[32] = {0};
+ printClient.readBytesUntil('\r', status, sizeof(status));
+ if (strcmp(status, "HTTP/1.1 200 OK") != 0 && strcmp(status, "HTTP/1.1 409 CONFLICT") != 0) {
+ Serial.print(F("Unexpected response: "));
+ Serial.println(status);
+ printerData.state = "";
+ printerData.error = "Response: " + String(status);
+ return printClient;
+ }
+
+ // Skip HTTP headers
+ char endOfHeaders[] = "\r\n\r\n";
+ if (!printClient.find(endOfHeaders)) {
+ Serial.println(F("Invalid response"));
+ printerData.error = "Invalid response from " + String(myServer) + ":" + String(myPort);
+ printerData.state = "";
+ }
+
+ return printClient;
+}
+
void OctoPrintClient::getPrinterJobResults() {
if (!validate()) {
return;
}
+ //**** get the Printer Job status
String apiGetData = "GET /api/job HTTP/1.1";
-
WiFiClient printClient = getSubmitRequest(apiGetData);
-
if (printerData.error != "") {
return;
}
-
const size_t bufferSize = JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(3) + 2*JSON_OBJECT_SIZE(5) + JSON_OBJECT_SIZE(6) + 710;
DynamicJsonBuffer jsonBuffer(bufferSize);
@@ -148,7 +209,7 @@ void OctoPrintClient::getPrinterJobResults() {
if (isOperational()) {
Serial.println("Status: " + printerData.state);
} else {
- Serial.println("Printer Not Opperational");
+ Serial.println("Printer Not Operational");
}
//**** get the Printer Temps and Stat
@@ -183,8 +244,42 @@ void OctoPrintClient::getPrinterJobResults() {
if (isPrinting()) {
Serial.println("Status: " + printerData.state + " " + printerData.fileName + "(" + printerData.progressCompletion + "%)");
}
+}
+
+void OctoPrintClient::getPrinterPsuState() {
+ //**** get the PSU state (if enabled and printer operational)
+ if (pollPsu && isOperational()) {
+ if (!validate()) {
+ printerData.isPSUoff = false; // we do not know PSU state, so assume on.
+ return;
+ }
+ String apiPostData = "POST /api/plugin/psucontrol HTTP/1.1";
+ String apiPostBody = "{\"command\":\"getPSUState\"}";
+ WiFiClient printClient = getPostRequest(apiPostData,apiPostBody);
+ if (printerData.error != "") {
+ printerData.isPSUoff = false; // we do not know PSU state, so assume on.
+ return;
+ }
+ const size_t bufferSize3 = JSON_OBJECT_SIZE(2) + 300;
+ DynamicJsonBuffer jsonBuffer3(bufferSize3);
- printClient.stop(); //stop client
+ // Parse JSON object
+ JsonObject& root3 = jsonBuffer3.parseObject(printClient);
+ if (!root3.success()) {
+ printerData.isPSUoff = false; // we do not know PSU state, so assume on
+ return;
+ }
+
+ String psu = (const char*)root3["isPSUOn"];
+ if (psu == "true") {
+ printerData.isPSUoff = false; // PSU checked and is on
+ } else {
+ printerData.isPSUoff = true; // PSU checked and is off, set flag
+ }
+ printClient.stop(); //stop client
+ } else {
+ printerData.isPSUoff = false; // we are not checking PSU state, so assume on
+ }
}
// Reset all PrinterData
@@ -205,6 +300,7 @@ void OctoPrintClient::resetPrintData() {
printerData.bedTemp = "";
printerData.bedTargetTemp = "";
printerData.isPrinting = false;
+ printerData.isPSUoff = false;
printerData.error = "";
}
@@ -256,6 +352,10 @@ boolean OctoPrintClient::isPrinting() {
return printerData.isPrinting;
}
+boolean OctoPrintClient::isPSUoff() {
+ return printerData.isPSUoff;
+}
+
boolean OctoPrintClient::isOperational() {
boolean operational = false;
if (printerData.state == "Operational" || isPrinting()) {
@@ -292,4 +392,4 @@ String OctoPrintClient::getValueRounded(String value) {
float f = value.toFloat();
int rounded = (int)(f+0.5f);
return String(rounded);
-}
+}
diff --git a/printermonitor/OctoPrintClient.h b/printermonitor/OctoPrintClient.h
index 1d02836..3335536 100644
--- a/printermonitor/OctoPrintClient.h
+++ b/printermonitor/OctoPrintClient.h
@@ -21,6 +21,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
+/* 15 Jan 2019 : Owen Carter : Add psucontrol query via POST api call */
+
#pragma once
#include
#include
@@ -33,11 +35,13 @@ class OctoPrintClient {
int myPort = 80;
String myApiKey = "";
String encodedAuth = "";
+ boolean pollPsu;
void resetPrintData();
boolean validate();
WiFiClient getSubmitRequest(String apiGetData);
-
+ WiFiClient getPostRequest(String apiPostData, String apiPostBody);
+
String result;
typedef struct {
@@ -57,6 +61,7 @@ class OctoPrintClient {
String bedTemp;
String bedTargetTemp;
boolean isPrinting;
+ boolean isPSUoff;
String error;
} PrinterStruct;
@@ -64,9 +69,10 @@ class OctoPrintClient {
public:
- OctoPrintClient(String ApiKey, String server, int port, String user, String pass);
+ OctoPrintClient(String ApiKey, String server, int port, String user, String pass, boolean psu);
void getPrinterJobResults();
- void updateOctoPrintClient(String ApiKey, String server, int port, String user, String pass);
+ void getPrinterPsuState();
+ void updateOctoPrintClient(String ApiKey, String server, int port, String user, String pass, boolean psu);
String getAveragePrintTime();
String getEstimatedPrintTime();
@@ -80,6 +86,7 @@ class OctoPrintClient {
String getState();
boolean isPrinting();
boolean isOperational();
+ boolean isPSUoff();
String getTempBedActual();
String getTempBedTarget();
String getTempToolActual();
@@ -88,4 +95,4 @@ class OctoPrintClient {
String getValueRounded(String value);
String getError();
};
-
+
diff --git a/printermonitor/Settings.h b/printermonitor/Settings.h
index c064400..27f1fc2 100644
--- a/printermonitor/Settings.h
+++ b/printermonitor/Settings.h
@@ -21,6 +21,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
+/* 15 Jan 2019 : Owen Carter : Add psucontrol setting */
+
/******************************************************************************
* Printer Monitor is designed for the Wemos D1 ESP8266
* Wemos D1 Mini: https://amzn.to/2qLyKJd
@@ -71,11 +73,14 @@ 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
+// Webserver
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
+
+// Date and Time
float UtcOffset = -7; // Hour offset from GMT for your timezone
boolean IS_24HOUR = false; // 23:00 millitary 24 hour clock
int minutesBetweenDataRefresh = 15;
@@ -91,8 +96,13 @@ boolean INVERT_DISPLAY = false; // true = pins at top | false = pins at the bott
// LED Settings
const int externalLight = LED_BUILTIN; // Set to unused pin, like D1, to disable use of built-in LED (LED_BUILTIN)
+// PSU Control
+boolean HAS_PSU = false; // Set to true if https://github.com/kantlivelong/OctoPrint-PSUControl/ in use
+
+// OTA Updates
boolean ENABLE_OTA = true; // this will allow you to load firmware to the device over WiFi (see OTA for ESP8266)
String OTA_Password = ""; // Set an OTA password here -- leave blank if you don't want to be prompted for password
+
//******************************
// End Settings
//******************************
diff --git a/printermonitor/printermonitor.ino b/printermonitor/printermonitor.ino
index 21e201b..b5af388 100644
--- a/printermonitor/printermonitor.ino
+++ b/printermonitor/printermonitor.ino
@@ -21,6 +21,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
+/* 15 Jan 2019 : Owen Carter : Add psucontrol option and processing */
+
/**********************************************
* Edit Settings.h for personalization
***********************************************/
@@ -82,7 +84,7 @@ String lastReportStatus = "";
boolean displayOn = true;
// OctoPrint Client
-OctoPrintClient printerClient(OctoPrintApiKey, OctoPrintServer, OctoPrintPort, OctoAuthUser, OctoAuthPass);
+OctoPrintClient printerClient(OctoPrintApiKey, OctoPrintServer, OctoPrintPort, OctoAuthUser, OctoAuthPass, HAS_PSU);
int printerCount = 0;
// Weather Client
@@ -113,6 +115,7 @@ String CHANGE_FORM = "
"
"";
html += "Host Name: " + OctoPrintHostName + "
";
if (printerClient.getError() != "") {
- html += "Error: " + printerClient.getError() + "
";
+ html += "Status: Offline
";
+ html += "Reason: " + printerClient.getError() + "
";
+ } else {
+ html += "Status: " + printerClient.getState();
+ if (printerClient.isPSUoff() && HAS_PSU) {
+ html += ", PSU off";
+ }
+ html += "
";
}
- html += "Status: " + printerClient.getState() + "
";
-
+
if (printerClient.isPrinting()) {
html += "File: " + printerClient.getFileName() + "
";
float fileSize = printerClient.getFileSize().toFloat();
@@ -941,9 +960,17 @@ void drawClockHeaderOverlay(OLEDDisplay *display, OLEDDisplayUiState* state) {
if (!IS_24HOUR) {
display->drawString(0, 48, timeClient.getAmPm());
display->setTextAlignment(TEXT_ALIGN_CENTER);
- display->drawString(64, 48, "offline");
+ if (printerClient.isPSUoff()) {
+ display->drawString(64, 47, "psu off");
+ } else {
+ display->drawString(64, 47, "offline");
+ }
} else {
- display->drawString(0,48, "offline");
+ 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);
@@ -1004,6 +1031,7 @@ void writeSettings() {
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();
@@ -1091,6 +1119,10 @@ void readSettings() {
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));
@@ -1115,7 +1147,7 @@ void readSettings() {
}
}
fr.close();
- printerClient.updateOctoPrintClient(OctoPrintApiKey, OctoPrintServer, OctoPrintPort, OctoAuthUser, OctoAuthPass);
+ printerClient.updateOctoPrintClient(OctoPrintApiKey, OctoPrintServer, OctoPrintPort, OctoAuthUser, OctoAuthPass, HAS_PSU);
weatherClient.updateWeatherApiKey(WeatherApiKey);
weatherClient.updateLanguage(WeatherLanguage);
weatherClient.setMetric(IS_METRIC);
@@ -1167,7 +1199,7 @@ void checkDisplay() {
return;
}
} else if (DISPLAYCLOCK) {
- if (!printerClient.isOperational() && !isClockOn) {
+ if ((!printerClient.isOperational() || printerClient.isPSUoff()) && !isClockOn) {
Serial.println("Clock Mode is turned on.");
if (!DISPLAYWEATHER) {
ui.disableAutoTransition();
@@ -1181,7 +1213,7 @@ void checkDisplay() {
}
ui.setOverlays(clockOverlay, numberOfOverlays);
isClockOn = true;
- } else if (printerClient.isOperational() && isClockOn) {
+ } else if (printerClient.isOperational() && !printerClient.isPSUoff() && isClockOn) {
Serial.println("Printer Monitor is active.");
ui.setFrames(frames, numberOfFrames);
ui.setOverlays(overlays, numberOfOverlays);
@@ -1206,4 +1238,4 @@ void enableDisplay(boolean enable) {
Serial.println("Display was turned OFF: " + timeClient.getFormattedTime());
displayOffEpoch = lastEpoch;
}
-}
+}
From 8283e0ca85cd8de239a6c880455925ae61fe4535 Mon Sep 17 00:00:00 2001
From: Owen
Date: Tue, 15 Jan 2019 22:03:40 +0100
Subject: [PATCH 2/5] Correct the readme link
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index d41dc61..d3e464d 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,7 @@ SOFTWARE.
* Supports OTA (loading firmware over WiFi connection on same LAN)
* Basic Authentication to protect your settings
* Version 2.2 added the ability to update firmware through web interface from a compiled binary
-* Can query the Octoprint [https://plugins.octoprint.org/plugins/psucontrol/](PSU Control plugin) to enter clock or blank mode when PSU is off
+* Can query the Octoprint [PSU Control plugin](https://plugins.octoprint.org/plugins/psucontrol/) to enter clock or blank mode when PSU is off
* Video: https://youtu.be/niRv9SCgAPk
* Detailed build video by Chris Riley: https://youtu.be/Rm-l1FSuJpI
From 8ee6d1da307411603e0037cd78cb9d56549bf93c Mon Sep 17 00:00:00 2001
From: Chrome Legion
Date: Wed, 16 Jan 2019 20:21:04 -0700
Subject: [PATCH 3/5] Qrome - updated to 2.4 and fixed the Configure Web Form
---
printermonitor/OctoPrintClient.cpp | 791 ++++-----
printermonitor/OctoPrintClient.h | 196 +--
printermonitor/Settings.h | 221 +--
printermonitor/printermonitor.ino | 2489 ++++++++++++++--------------
4 files changed, 1853 insertions(+), 1844 deletions(-)
diff --git a/printermonitor/OctoPrintClient.cpp b/printermonitor/OctoPrintClient.cpp
index 4f669ad..7d19d2c 100644
--- a/printermonitor/OctoPrintClient.cpp
+++ b/printermonitor/OctoPrintClient.cpp
@@ -1,395 +1,396 @@
-/** 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.
-*/
-
-/* 15 Jan 2019 : Owen Carter : Add psucontrol query via POST api call */
-
-#include "OctoPrintClient.h"
-
-OctoPrintClient::OctoPrintClient(String ApiKey, String server, int port, String user, String pass, boolean psu) {
- updateOctoPrintClient(ApiKey, server, port, user, pass, psu);
-}
-
-void OctoPrintClient::updateOctoPrintClient(String ApiKey, String server, int port, String user, String pass, boolean psu) {
- server.toCharArray(myServer, 100);
- myApiKey = ApiKey;
- myPort = port;
- encodedAuth = "";
- if (user != "") {
- String userpass = user + ":" + pass;
- base64 b64;
- encodedAuth = b64.encode(userpass, true);
- }
- pollPsu = psu;
-}
-
-boolean OctoPrintClient::validate() {
- boolean rtnValue = false;
- printerData.error = "";
- if (String(myServer) == "") {
- printerData.error += "Server address is required; ";
- }
- if (myApiKey == "") {
- printerData.error += "ApiKey is required; ";
- }
- if (printerData.error == "") {
- rtnValue = true;
- }
- return rtnValue;
-}
-
-WiFiClient OctoPrintClient::getSubmitRequest(String apiGetData) {
- WiFiClient printClient;
- printClient.setTimeout(5000);
-
- Serial.println("Getting Octoprint Data via GET");
- Serial.println(apiGetData);
- result = "";
- if (printClient.connect(myServer, myPort)) { //starts client connection, checks for connection
- printClient.println(apiGetData);
- printClient.println("Host: " + String(myServer) + ":" + String(myPort));
- printClient.println("X-Api-Key: " + myApiKey);
- if (encodedAuth != "") {
- printClient.print("Authorization: ");
- printClient.println("Basic " + encodedAuth);
- }
- printClient.println("User-Agent: ArduinoWiFi/1.1");
- printClient.println("Connection: close");
- if (printClient.println() == 0) {
- Serial.println("Connection to " + String(myServer) + ":" + String(myPort) + " failed.");
- Serial.println();
- resetPrintData();
- printerData.error = "Connection to " + String(myServer) + ":" + String(myPort) + " failed.";
- return printClient;
- }
- }
- else {
- Serial.println("Connection to OctoPrint failed: " + String(myServer) + ":" + String(myPort)); //error message if no client connect
- Serial.println();
- resetPrintData();
- printerData.error = "Connection to OctoPrint failed: " + String(myServer) + ":" + String(myPort);
- return printClient;
- }
-
- // Check HTTP status
- char status[32] = {0};
- printClient.readBytesUntil('\r', status, sizeof(status));
- if (strcmp(status, "HTTP/1.1 200 OK") != 0 && strcmp(status, "HTTP/1.1 409 CONFLICT") != 0) {
- Serial.print(F("Unexpected response: "));
- Serial.println(status);
- printerData.state = "";
- printerData.error = "Response: " + String(status);
- return printClient;
- }
-
- // Skip HTTP headers
- char endOfHeaders[] = "\r\n\r\n";
- if (!printClient.find(endOfHeaders)) {
- Serial.println(F("Invalid response"));
- printerData.error = "Invalid response from " + String(myServer) + ":" + String(myPort);
- printerData.state = "";
- }
-
- return printClient;
-}
-
-WiFiClient OctoPrintClient::getPostRequest(String apiPostData, String apiPostBody) {
- WiFiClient printClient;
- printClient.setTimeout(5000);
-
- Serial.println("Getting Octoprint Data via POST");
- Serial.println(apiPostData + " | " + apiPostBody);
- result = "";
- if (printClient.connect(myServer, myPort)) { //starts client connection, checks for connection
- printClient.println(apiPostData);
- printClient.println("Host: " + String(myServer) + ":" + String(myPort));
- printClient.println("Connection: close");
- printClient.println("X-Api-Key: " + myApiKey);
- if (encodedAuth != "") {
- printClient.print("Authorization: ");
- printClient.println("Basic " + encodedAuth);
- }
- printClient.println("User-Agent: ArduinoWiFi/1.1");
- printClient.println("Content-Type: application/json");
- printClient.print("Content-Length: ");
- printClient.println(apiPostBody.length());
- printClient.println();
- printClient.println(apiPostBody);
- if (printClient.println() == 0) {
- Serial.println("Connection to " + String(myServer) + ":" + String(myPort) + " failed.");
- Serial.println();
- resetPrintData();
- printerData.error = "Connection to " + String(myServer) + ":" + String(myPort) + " failed.";
- return printClient;
- }
- }
- else {
- Serial.println("Connection to OctoPrint failed: " + String(myServer) + ":" + String(myPort)); //error message if no client connect
- Serial.println();
- resetPrintData();
- printerData.error = "Connection to OctoPrint failed: " + String(myServer) + ":" + String(myPort);
- return printClient;
- }
-
- // Check HTTP status
- char status[32] = {0};
- printClient.readBytesUntil('\r', status, sizeof(status));
- if (strcmp(status, "HTTP/1.1 200 OK") != 0 && strcmp(status, "HTTP/1.1 409 CONFLICT") != 0) {
- Serial.print(F("Unexpected response: "));
- Serial.println(status);
- printerData.state = "";
- printerData.error = "Response: " + String(status);
- return printClient;
- }
-
- // Skip HTTP headers
- char endOfHeaders[] = "\r\n\r\n";
- if (!printClient.find(endOfHeaders)) {
- Serial.println(F("Invalid response"));
- printerData.error = "Invalid response from " + String(myServer) + ":" + String(myPort);
- printerData.state = "";
- }
-
- return printClient;
-}
-
-void OctoPrintClient::getPrinterJobResults() {
- if (!validate()) {
- return;
- }
- //**** get the Printer Job status
- String apiGetData = "GET /api/job HTTP/1.1";
- WiFiClient printClient = getSubmitRequest(apiGetData);
- if (printerData.error != "") {
- return;
- }
- const size_t bufferSize = JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(3) + 2*JSON_OBJECT_SIZE(5) + JSON_OBJECT_SIZE(6) + 710;
- DynamicJsonBuffer jsonBuffer(bufferSize);
-
- // Parse JSON object
- JsonObject& root = jsonBuffer.parseObject(printClient);
- if (!root.success()) {
- Serial.println("OctoPrint Data Parsing failed: " + String(myServer) + ":" + String(myPort));
- printerData.error = "OctoPrint Data Parsing failed: " + String(myServer) + ":" + String(myPort);
- printerData.state = "";
- return;
- }
-
- printerData.averagePrintTime = (const char*)root["job"]["averagePrintTime"];
- printerData.estimatedPrintTime = (const char*)root["job"]["estimatedPrintTime"];
- printerData.fileName = (const char*)root["job"]["file"]["name"];
- printerData.fileSize = (const char*)root["job"]["file"]["size"];
- printerData.lastPrintTime = (const char*)root["job"]["lastPrintTime"];
- printerData.progressCompletion = (const char*)root["progress"]["completion"];
- printerData.progressFilepos = (const char*)root["progress"]["filepos"];
- printerData.progressPrintTime = (const char*)root["progress"]["printTime"];
- printerData.progressPrintTimeLeft = (const char*)root["progress"]["printTimeLeft"];
- printerData.filamentLength = (const char*)root["job"]["filament"]["tool0"]["length"];
- printerData.state = (const char*)root["state"];
-
- if (isOperational()) {
- Serial.println("Status: " + printerData.state);
- } else {
- Serial.println("Printer Not Operational");
- }
-
- //**** get the Printer Temps and Stat
- apiGetData = "GET /api/printer?exclude=sd,history HTTP/1.1";
- printClient = getSubmitRequest(apiGetData);
- if (printerData.error != "") {
- return;
- }
- const size_t bufferSize2 = 3*JSON_OBJECT_SIZE(2) + 2*JSON_OBJECT_SIZE(3) + JSON_OBJECT_SIZE(9) + 300;
- DynamicJsonBuffer jsonBuffer2(bufferSize2);
-
- // Parse JSON object
- JsonObject& root2 = jsonBuffer2.parseObject(printClient);
- if (!root2.success()) {
- printerData.isPrinting = false;
- printerData.toolTemp = "";
- printerData.toolTargetTemp = "";
- printerData.bedTemp = "";
- printerData.bedTargetTemp = (const char*)root2["temperature"]["bed"]["target"];
- return;
- }
-
- String printing = (const char*)root2["state"]["flags"]["printing"];
- if (printing == "true") {
- printerData.isPrinting = true;
- }
- printerData.toolTemp = (const char*)root2["temperature"]["tool0"]["actual"];
- printerData.toolTargetTemp = (const char*)root2["temperature"]["tool0"]["target"];
- printerData.bedTemp = (const char*)root2["temperature"]["bed"]["actual"];
- printerData.bedTargetTemp = (const char*)root2["temperature"]["bed"]["target"];
-
- if (isPrinting()) {
- Serial.println("Status: " + printerData.state + " " + printerData.fileName + "(" + printerData.progressCompletion + "%)");
- }
-}
-
-void OctoPrintClient::getPrinterPsuState() {
- //**** get the PSU state (if enabled and printer operational)
- if (pollPsu && isOperational()) {
- if (!validate()) {
- printerData.isPSUoff = false; // we do not know PSU state, so assume on.
- return;
- }
- String apiPostData = "POST /api/plugin/psucontrol HTTP/1.1";
- String apiPostBody = "{\"command\":\"getPSUState\"}";
- WiFiClient printClient = getPostRequest(apiPostData,apiPostBody);
- if (printerData.error != "") {
- printerData.isPSUoff = false; // we do not know PSU state, so assume on.
- return;
- }
- const size_t bufferSize3 = JSON_OBJECT_SIZE(2) + 300;
- DynamicJsonBuffer jsonBuffer3(bufferSize3);
-
- // Parse JSON object
- JsonObject& root3 = jsonBuffer3.parseObject(printClient);
- if (!root3.success()) {
- printerData.isPSUoff = false; // we do not know PSU state, so assume on
- return;
- }
-
- String psu = (const char*)root3["isPSUOn"];
- if (psu == "true") {
- printerData.isPSUoff = false; // PSU checked and is on
- } else {
- printerData.isPSUoff = true; // PSU checked and is off, set flag
- }
- printClient.stop(); //stop client
- } else {
- printerData.isPSUoff = false; // we are not checking PSU state, so assume on
- }
-}
-
-// Reset all PrinterData
-void OctoPrintClient::resetPrintData() {
- printerData.averagePrintTime = "";
- printerData.estimatedPrintTime = "";
- printerData.fileName = "";
- printerData.fileSize = "";
- printerData.lastPrintTime = "";
- printerData.progressCompletion = "";
- printerData.progressFilepos = "";
- printerData.progressPrintTime = "";
- printerData.progressPrintTimeLeft = "";
- printerData.state = "";
- printerData.toolTemp = "";
- printerData.toolTargetTemp = "";
- printerData.filamentLength = "";
- printerData.bedTemp = "";
- printerData.bedTargetTemp = "";
- printerData.isPrinting = false;
- printerData.isPSUoff = false;
- printerData.error = "";
-}
-
-String OctoPrintClient::getAveragePrintTime(){
- return printerData.averagePrintTime;
-}
-
-String OctoPrintClient::getEstimatedPrintTime() {
- return printerData.estimatedPrintTime;
-}
-
-String OctoPrintClient::getFileName() {
- return printerData.fileName;
-}
-
-String OctoPrintClient::getFileSize() {
- return printerData.fileSize;
-}
-
-String OctoPrintClient::getLastPrintTime(){
- return printerData.lastPrintTime;
-}
-
-String OctoPrintClient::getProgressCompletion() {
- return String(printerData.progressCompletion.toInt());
-}
-
-String OctoPrintClient::getProgressFilepos() {
- return printerData.progressFilepos;
-}
-
-String OctoPrintClient::getProgressPrintTime() {
- return printerData.progressPrintTime;
-}
-
-String OctoPrintClient::getProgressPrintTimeLeft() {
- String rtnValue = printerData.progressPrintTimeLeft;
- if (getProgressCompletion() == "100") {
- rtnValue = "0"; // Print is done so this should be 0 this is a fix for OctoPrint
- }
- return rtnValue;
-}
-
-String OctoPrintClient::getState() {
- return printerData.state;
-}
-
-boolean OctoPrintClient::isPrinting() {
- return printerData.isPrinting;
-}
-
-boolean OctoPrintClient::isPSUoff() {
- return printerData.isPSUoff;
-}
-
-boolean OctoPrintClient::isOperational() {
- boolean operational = false;
- if (printerData.state == "Operational" || isPrinting()) {
- operational = true;
- }
- return operational;
-}
-
-String OctoPrintClient::getTempBedActual() {
- return printerData.bedTemp;
-}
-
-String OctoPrintClient::getTempBedTarget() {
- return printerData.bedTargetTemp;
-}
-
-String OctoPrintClient::getTempToolActual() {
- return printerData.toolTemp;
-}
-
-String OctoPrintClient::getTempToolTarget() {
- return printerData.toolTargetTemp;
-}
-
-String OctoPrintClient::getFilamentLength() {
- return printerData.filamentLength;
-}
-
-String OctoPrintClient::getError() {
- return printerData.error;
-}
-
-String OctoPrintClient::getValueRounded(String value) {
- float f = value.toFloat();
- int rounded = (int)(f+0.5f);
- return String(rounded);
-}
+/** 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 query via POST api call */
+
+#include "OctoPrintClient.h"
+
+OctoPrintClient::OctoPrintClient(String ApiKey, String server, int port, String user, String pass, boolean psu) {
+ updateOctoPrintClient(ApiKey, server, port, user, pass, psu);
+}
+
+void OctoPrintClient::updateOctoPrintClient(String ApiKey, String server, int port, String user, String pass, boolean psu) {
+ server.toCharArray(myServer, 100);
+ myApiKey = ApiKey;
+ myPort = port;
+ encodedAuth = "";
+ if (user != "") {
+ String userpass = user + ":" + pass;
+ base64 b64;
+ encodedAuth = b64.encode(userpass, true);
+ }
+ pollPsu = psu;
+}
+
+boolean OctoPrintClient::validate() {
+ boolean rtnValue = false;
+ printerData.error = "";
+ if (String(myServer) == "") {
+ printerData.error += "Server address is required; ";
+ }
+ if (myApiKey == "") {
+ printerData.error += "ApiKey is required; ";
+ }
+ if (printerData.error == "") {
+ rtnValue = true;
+ }
+ return rtnValue;
+}
+
+WiFiClient OctoPrintClient::getSubmitRequest(String apiGetData) {
+ WiFiClient printClient;
+ printClient.setTimeout(5000);
+
+ Serial.println("Getting Octoprint Data via GET");
+ Serial.println(apiGetData);
+ result = "";
+ if (printClient.connect(myServer, myPort)) { //starts client connection, checks for connection
+ printClient.println(apiGetData);
+ printClient.println("Host: " + String(myServer) + ":" + String(myPort));
+ printClient.println("X-Api-Key: " + myApiKey);
+ if (encodedAuth != "") {
+ printClient.print("Authorization: ");
+ printClient.println("Basic " + encodedAuth);
+ }
+ printClient.println("User-Agent: ArduinoWiFi/1.1");
+ printClient.println("Connection: close");
+ if (printClient.println() == 0) {
+ Serial.println("Connection to " + String(myServer) + ":" + String(myPort) + " failed.");
+ Serial.println();
+ resetPrintData();
+ printerData.error = "Connection to " + String(myServer) + ":" + String(myPort) + " failed.";
+ return printClient;
+ }
+ }
+ else {
+ Serial.println("Connection to OctoPrint failed: " + String(myServer) + ":" + String(myPort)); //error message if no client connect
+ Serial.println();
+ resetPrintData();
+ printerData.error = "Connection to OctoPrint failed: " + String(myServer) + ":" + String(myPort);
+ return printClient;
+ }
+
+ // Check HTTP status
+ char status[32] = {0};
+ printClient.readBytesUntil('\r', status, sizeof(status));
+ if (strcmp(status, "HTTP/1.1 200 OK") != 0 && strcmp(status, "HTTP/1.1 409 CONFLICT") != 0) {
+ Serial.print(F("Unexpected response: "));
+ Serial.println(status);
+ printerData.state = "";
+ printerData.error = "Response: " + String(status);
+ return printClient;
+ }
+
+ // Skip HTTP headers
+ char endOfHeaders[] = "\r\n\r\n";
+ if (!printClient.find(endOfHeaders)) {
+ Serial.println(F("Invalid response"));
+ printerData.error = "Invalid response from " + String(myServer) + ":" + String(myPort);
+ printerData.state = "";
+ }
+
+ return printClient;
+}
+
+WiFiClient OctoPrintClient::getPostRequest(String apiPostData, String apiPostBody) {
+ WiFiClient printClient;
+ printClient.setTimeout(5000);
+
+ Serial.println("Getting Octoprint Data via POST");
+ Serial.println(apiPostData + " | " + apiPostBody);
+ result = "";
+ if (printClient.connect(myServer, myPort)) { //starts client connection, checks for connection
+ printClient.println(apiPostData);
+ printClient.println("Host: " + String(myServer) + ":" + String(myPort));
+ printClient.println("Connection: close");
+ printClient.println("X-Api-Key: " + myApiKey);
+ if (encodedAuth != "") {
+ printClient.print("Authorization: ");
+ printClient.println("Basic " + encodedAuth);
+ }
+ printClient.println("User-Agent: ArduinoWiFi/1.1");
+ printClient.println("Content-Type: application/json");
+ printClient.print("Content-Length: ");
+ printClient.println(apiPostBody.length());
+ printClient.println();
+ printClient.println(apiPostBody);
+ if (printClient.println() == 0) {
+ Serial.println("Connection to " + String(myServer) + ":" + String(myPort) + " failed.");
+ Serial.println();
+ resetPrintData();
+ printerData.error = "Connection to " + String(myServer) + ":" + String(myPort) + " failed.";
+ return printClient;
+ }
+ }
+ else {
+ Serial.println("Connection to OctoPrint failed: " + String(myServer) + ":" + String(myPort)); //error message if no client connect
+ Serial.println();
+ resetPrintData();
+ printerData.error = "Connection to OctoPrint failed: " + String(myServer) + ":" + String(myPort);
+ return printClient;
+ }
+
+ // Check HTTP status
+ char status[32] = {0};
+ printClient.readBytesUntil('\r', status, sizeof(status));
+ if (strcmp(status, "HTTP/1.1 200 OK") != 0 && strcmp(status, "HTTP/1.1 409 CONFLICT") != 0) {
+ Serial.print(F("Unexpected response: "));
+ Serial.println(status);
+ printerData.state = "";
+ printerData.error = "Response: " + String(status);
+ return printClient;
+ }
+
+ // Skip HTTP headers
+ char endOfHeaders[] = "\r\n\r\n";
+ if (!printClient.find(endOfHeaders)) {
+ Serial.println(F("Invalid response"));
+ printerData.error = "Invalid response from " + String(myServer) + ":" + String(myPort);
+ printerData.state = "";
+ }
+
+ return printClient;
+}
+
+void OctoPrintClient::getPrinterJobResults() {
+ if (!validate()) {
+ return;
+ }
+ //**** get the Printer Job status
+ String apiGetData = "GET /api/job HTTP/1.1";
+ WiFiClient printClient = getSubmitRequest(apiGetData);
+ if (printerData.error != "") {
+ return;
+ }
+ const size_t bufferSize = JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(3) + 2*JSON_OBJECT_SIZE(5) + JSON_OBJECT_SIZE(6) + 710;
+ DynamicJsonBuffer jsonBuffer(bufferSize);
+
+ // Parse JSON object
+ JsonObject& root = jsonBuffer.parseObject(printClient);
+ if (!root.success()) {
+ Serial.println("OctoPrint Data Parsing failed: " + String(myServer) + ":" + String(myPort));
+ printerData.error = "OctoPrint Data Parsing failed: " + String(myServer) + ":" + String(myPort);
+ printerData.state = "";
+ return;
+ }
+
+ printerData.averagePrintTime = (const char*)root["job"]["averagePrintTime"];
+ printerData.estimatedPrintTime = (const char*)root["job"]["estimatedPrintTime"];
+ printerData.fileName = (const char*)root["job"]["file"]["name"];
+ printerData.fileSize = (const char*)root["job"]["file"]["size"];
+ printerData.lastPrintTime = (const char*)root["job"]["lastPrintTime"];
+ printerData.progressCompletion = (const char*)root["progress"]["completion"];
+ printerData.progressFilepos = (const char*)root["progress"]["filepos"];
+ printerData.progressPrintTime = (const char*)root["progress"]["printTime"];
+ printerData.progressPrintTimeLeft = (const char*)root["progress"]["printTimeLeft"];
+ printerData.filamentLength = (const char*)root["job"]["filament"]["tool0"]["length"];
+ printerData.state = (const char*)root["state"];
+
+ if (isOperational()) {
+ Serial.println("Status: " + printerData.state);
+ } else {
+ Serial.println("Printer Not Operational");
+ }
+
+ //**** get the Printer Temps and Stat
+ apiGetData = "GET /api/printer?exclude=sd,history HTTP/1.1";
+ printClient = getSubmitRequest(apiGetData);
+ if (printerData.error != "") {
+ return;
+ }
+ const size_t bufferSize2 = 3*JSON_OBJECT_SIZE(2) + 2*JSON_OBJECT_SIZE(3) + JSON_OBJECT_SIZE(9) + 300;
+ DynamicJsonBuffer jsonBuffer2(bufferSize2);
+
+ // Parse JSON object
+ JsonObject& root2 = jsonBuffer2.parseObject(printClient);
+ if (!root2.success()) {
+ printerData.isPrinting = false;
+ printerData.toolTemp = "";
+ printerData.toolTargetTemp = "";
+ printerData.bedTemp = "";
+ printerData.bedTargetTemp = (const char*)root2["temperature"]["bed"]["target"];
+ return;
+ }
+
+ String printing = (const char*)root2["state"]["flags"]["printing"];
+ if (printing == "true") {
+ printerData.isPrinting = true;
+ }
+ printerData.toolTemp = (const char*)root2["temperature"]["tool0"]["actual"];
+ printerData.toolTargetTemp = (const char*)root2["temperature"]["tool0"]["target"];
+ printerData.bedTemp = (const char*)root2["temperature"]["bed"]["actual"];
+ printerData.bedTargetTemp = (const char*)root2["temperature"]["bed"]["target"];
+
+ if (isPrinting()) {
+ Serial.println("Status: " + printerData.state + " " + printerData.fileName + "(" + printerData.progressCompletion + "%)");
+ }
+}
+
+void OctoPrintClient::getPrinterPsuState() {
+ //**** get the PSU state (if enabled and printer operational)
+ if (pollPsu && isOperational()) {
+ if (!validate()) {
+ printerData.isPSUoff = false; // we do not know PSU state, so assume on.
+ return;
+ }
+ String apiPostData = "POST /api/plugin/psucontrol HTTP/1.1";
+ String apiPostBody = "{\"command\":\"getPSUState\"}";
+ WiFiClient printClient = getPostRequest(apiPostData,apiPostBody);
+ if (printerData.error != "") {
+ printerData.isPSUoff = false; // we do not know PSU state, so assume on.
+ return;
+ }
+ const size_t bufferSize3 = JSON_OBJECT_SIZE(2) + 300;
+ DynamicJsonBuffer jsonBuffer3(bufferSize3);
+
+ // Parse JSON object
+ JsonObject& root3 = jsonBuffer3.parseObject(printClient);
+ if (!root3.success()) {
+ printerData.isPSUoff = false; // we do not know PSU state, so assume on
+ return;
+ }
+
+ String psu = (const char*)root3["isPSUOn"];
+ if (psu == "true") {
+ printerData.isPSUoff = false; // PSU checked and is on
+ } else {
+ printerData.isPSUoff = true; // PSU checked and is off, set flag
+ }
+ printClient.stop(); //stop client
+ } else {
+ printerData.isPSUoff = false; // we are not checking PSU state, so assume on
+ }
+}
+
+// Reset all PrinterData
+void OctoPrintClient::resetPrintData() {
+ printerData.averagePrintTime = "";
+ printerData.estimatedPrintTime = "";
+ printerData.fileName = "";
+ printerData.fileSize = "";
+ printerData.lastPrintTime = "";
+ printerData.progressCompletion = "";
+ printerData.progressFilepos = "";
+ printerData.progressPrintTime = "";
+ printerData.progressPrintTimeLeft = "";
+ printerData.state = "";
+ printerData.toolTemp = "";
+ printerData.toolTargetTemp = "";
+ printerData.filamentLength = "";
+ printerData.bedTemp = "";
+ printerData.bedTargetTemp = "";
+ printerData.isPrinting = false;
+ printerData.isPSUoff = false;
+ printerData.error = "";
+}
+
+String OctoPrintClient::getAveragePrintTime(){
+ return printerData.averagePrintTime;
+}
+
+String OctoPrintClient::getEstimatedPrintTime() {
+ return printerData.estimatedPrintTime;
+}
+
+String OctoPrintClient::getFileName() {
+ return printerData.fileName;
+}
+
+String OctoPrintClient::getFileSize() {
+ return printerData.fileSize;
+}
+
+String OctoPrintClient::getLastPrintTime(){
+ return printerData.lastPrintTime;
+}
+
+String OctoPrintClient::getProgressCompletion() {
+ return String(printerData.progressCompletion.toInt());
+}
+
+String OctoPrintClient::getProgressFilepos() {
+ return printerData.progressFilepos;
+}
+
+String OctoPrintClient::getProgressPrintTime() {
+ return printerData.progressPrintTime;
+}
+
+String OctoPrintClient::getProgressPrintTimeLeft() {
+ String rtnValue = printerData.progressPrintTimeLeft;
+ if (getProgressCompletion() == "100") {
+ rtnValue = "0"; // Print is done so this should be 0 this is a fix for OctoPrint
+ }
+ return rtnValue;
+}
+
+String OctoPrintClient::getState() {
+ return printerData.state;
+}
+
+boolean OctoPrintClient::isPrinting() {
+ return printerData.isPrinting;
+}
+
+boolean OctoPrintClient::isPSUoff() {
+ return printerData.isPSUoff;
+}
+
+boolean OctoPrintClient::isOperational() {
+ boolean operational = false;
+ if (printerData.state == "Operational" || isPrinting()) {
+ operational = true;
+ }
+ return operational;
+}
+
+String OctoPrintClient::getTempBedActual() {
+ return printerData.bedTemp;
+}
+
+String OctoPrintClient::getTempBedTarget() {
+ return printerData.bedTargetTemp;
+}
+
+String OctoPrintClient::getTempToolActual() {
+ return printerData.toolTemp;
+}
+
+String OctoPrintClient::getTempToolTarget() {
+ return printerData.toolTargetTemp;
+}
+
+String OctoPrintClient::getFilamentLength() {
+ return printerData.filamentLength;
+}
+
+String OctoPrintClient::getError() {
+ return printerData.error;
+}
+
+String OctoPrintClient::getValueRounded(String value) {
+ float f = value.toFloat();
+ int rounded = (int)(f+0.5f);
+ return String(rounded);
+}
diff --git a/printermonitor/OctoPrintClient.h b/printermonitor/OctoPrintClient.h
index 3335536..c32358a 100644
--- a/printermonitor/OctoPrintClient.h
+++ b/printermonitor/OctoPrintClient.h
@@ -1,98 +1,98 @@
-/** 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.
-*/
-
-/* 15 Jan 2019 : Owen Carter : Add psucontrol query via POST api call */
-
-#pragma once
-#include
-#include
-#include
-
-class OctoPrintClient {
-
-private:
- char myServer[100];
- int myPort = 80;
- String myApiKey = "";
- String encodedAuth = "";
- boolean pollPsu;
-
- void resetPrintData();
- boolean validate();
- WiFiClient getSubmitRequest(String apiGetData);
- WiFiClient getPostRequest(String apiPostData, String apiPostBody);
-
- String result;
-
- typedef struct {
- String averagePrintTime;
- String estimatedPrintTime;
- String fileName;
- String fileSize;
- String lastPrintTime;
- String progressCompletion;
- String progressFilepos;
- String progressPrintTime;
- String progressPrintTimeLeft;
- String state;
- String toolTemp;
- String toolTargetTemp;
- String filamentLength;
- String bedTemp;
- String bedTargetTemp;
- boolean isPrinting;
- boolean isPSUoff;
- String error;
- } PrinterStruct;
-
- PrinterStruct printerData;
-
-
-public:
- OctoPrintClient(String ApiKey, String server, int port, String user, String pass, boolean psu);
- void getPrinterJobResults();
- void getPrinterPsuState();
- void updateOctoPrintClient(String ApiKey, String server, int port, String user, String pass, boolean psu);
-
- String getAveragePrintTime();
- String getEstimatedPrintTime();
- String getFileName();
- String getFileSize();
- String getLastPrintTime();
- String getProgressCompletion();
- String getProgressFilepos();
- String getProgressPrintTime();
- String getProgressPrintTimeLeft();
- String getState();
- boolean isPrinting();
- boolean isOperational();
- boolean isPSUoff();
- String getTempBedActual();
- String getTempBedTarget();
- String getTempToolActual();
- String getTempToolTarget();
- String getFilamentLength();
- String getValueRounded(String value);
- String getError();
-};
-
+/** 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 query via POST api call */
+
+#pragma once
+#include
+#include
+#include
+
+class OctoPrintClient {
+
+private:
+ char myServer[100];
+ int myPort = 80;
+ String myApiKey = "";
+ String encodedAuth = "";
+ boolean pollPsu;
+
+ void resetPrintData();
+ boolean validate();
+ WiFiClient getSubmitRequest(String apiGetData);
+ WiFiClient getPostRequest(String apiPostData, String apiPostBody);
+
+ String result;
+
+ typedef struct {
+ String averagePrintTime;
+ String estimatedPrintTime;
+ String fileName;
+ String fileSize;
+ String lastPrintTime;
+ String progressCompletion;
+ String progressFilepos;
+ String progressPrintTime;
+ String progressPrintTimeLeft;
+ String state;
+ String toolTemp;
+ String toolTargetTemp;
+ String filamentLength;
+ String bedTemp;
+ String bedTargetTemp;
+ boolean isPrinting;
+ boolean isPSUoff;
+ String error;
+ } PrinterStruct;
+
+ PrinterStruct printerData;
+
+
+public:
+ OctoPrintClient(String ApiKey, String server, int port, String user, String pass, boolean psu);
+ void getPrinterJobResults();
+ void getPrinterPsuState();
+ void updateOctoPrintClient(String ApiKey, String server, int port, String user, String pass, boolean psu);
+
+ String getAveragePrintTime();
+ String getEstimatedPrintTime();
+ String getFileName();
+ String getFileSize();
+ String getLastPrintTime();
+ String getProgressCompletion();
+ String getProgressFilepos();
+ String getProgressPrintTime();
+ String getProgressPrintTimeLeft();
+ String getState();
+ boolean isPrinting();
+ boolean isOperational();
+ boolean isPSUoff();
+ String getTempBedActual();
+ String getTempBedTarget();
+ String getTempToolActual();
+ String getTempToolTarget();
+ String getFilamentLength();
+ String getValueRounded(String value);
+ String getError();
+};
diff --git a/printermonitor/Settings.h b/printermonitor/Settings.h
index 27f1fc2..d478fec 100644
--- a/printermonitor/Settings.h
+++ b/printermonitor/Settings.h
@@ -1,110 +1,111 @@
-/** 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.
-*/
-
-/* 15 Jan 2019 : Owen Carter : Add psucontrol setting */
-
-/******************************************************************************
- * Printer Monitor is designed for the Wemos D1 ESP8266
- * Wemos D1 Mini: https://amzn.to/2qLyKJd
- * 0.96" OLED I2C 128x64 Display (12864) SSD1306
- * OLED Display: https://amzn.to/2JDEAUF
- ******************************************************************************/
-/******************************************************************************
- * NOTE: The settings here are the default settings for the first loading.
- * After loading you will manage changes to the settings via the Web Interface.
- * If you want to change settings again in the settings.h, you will need to
- * erase the file system on the Wemos or use the “Reset Settings” option in
- * the Web Interface.
- ******************************************************************************/
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include "TimeClient.h"
-#include "OctoPrintClient.h"
-#include "OpenWeatherMapClient.h"
-#include "WeatherStationFonts.h"
-#include "FS.h"
-#include "SH1106Wire.h"
-#include "SSD1306Wire.h"
-#include "OLEDDisplayUi.h"
-
-//******************************
-// Start Settings
-//******************************
-
-// OctoPrint Monitoring -- Monitor your 3D printer OctoPrint Server
-String OctoPrintApiKey = ""; // ApiKey from your User Account on OctoPrint
-String OctoPrintHostName = "octopi";// Default 'octopi' -- or hostname if different (optional if your IP changes)
-String OctoPrintServer = ""; // 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/
-// Default City Location (use http://openweathermap.org/find to find city ID)
-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
-
-// Webserver
-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
-
-// Date and Time
-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 = D5;
-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
-
-// LED Settings
-const int externalLight = LED_BUILTIN; // Set to unused pin, like D1, to disable use of built-in LED (LED_BUILTIN)
-
-// PSU Control
-boolean HAS_PSU = false; // Set to true if https://github.com/kantlivelong/OctoPrint-PSUControl/ in use
-
-// OTA Updates
-boolean ENABLE_OTA = true; // this will allow you to load firmware to the device over WiFi (see OTA for ESP8266)
-String OTA_Password = ""; // Set an OTA password here -- leave blank if you don't want to be prompted for password
-
-//******************************
-// End Settings
-//******************************
-
-String themeColor = "light-green"; // this can be changed later in the web interface.
+/** 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 setting */
+
+/******************************************************************************
+ * Printer Monitor is designed for the Wemos D1 ESP8266
+ * Wemos D1 Mini: https://amzn.to/2qLyKJd
+ * 0.96" OLED I2C 128x64 Display (12864) SSD1306
+ * OLED Display: https://amzn.to/2JDEAUF
+ ******************************************************************************/
+/******************************************************************************
+ * NOTE: The settings here are the default settings for the first loading.
+ * After loading you will manage changes to the settings via the Web Interface.
+ * If you want to change settings again in the settings.h, you will need to
+ * erase the file system on the Wemos or use the “Reset Settings” option in
+ * the Web Interface.
+ ******************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include "TimeClient.h"
+#include "OctoPrintClient.h"
+#include "OpenWeatherMapClient.h"
+#include "WeatherStationFonts.h"
+#include "FS.h"
+#include "SH1106Wire.h"
+#include "SSD1306Wire.h"
+#include "OLEDDisplayUi.h"
+
+//******************************
+// Start Settings
+//******************************
+
+// OctoPrint Monitoring -- Monitor your 3D printer OctoPrint Server
+String OctoPrintApiKey = ""; // ApiKey from your User Account on OctoPrint
+String OctoPrintHostName = "octopi";// Default 'octopi' -- or hostname if different (optional if your IP changes)
+String OctoPrintServer = ""; // 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/
+// Default City Location (use http://openweathermap.org/find to find city ID)
+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
+
+// Webserver
+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
+
+// Date and Time
+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 = D5;
+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
+
+// LED Settings
+const int externalLight = LED_BUILTIN; // Set to unused pin, like D1, to disable use of built-in LED (LED_BUILTIN)
+
+// PSU Control
+boolean HAS_PSU = false; // Set to true if https://github.com/kantlivelong/OctoPrint-PSUControl/ in use
+
+// OTA Updates
+boolean ENABLE_OTA = true; // this will allow you to load firmware to the device over WiFi (see OTA for ESP8266)
+String OTA_Password = ""; // Set an OTA password here -- leave blank if you don't want to be prompted for password
+
+//******************************
+// End Settings
+//******************************
+
+String themeColor = "light-green"; // this can be changed later in the web interface.
diff --git a/printermonitor/printermonitor.ino b/printermonitor/printermonitor.ino
index b5af388..428f36a 100644
--- a/printermonitor/printermonitor.ino
+++ b/printermonitor/printermonitor.ino
@@ -1,1241 +1,1248 @@
-/** 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.
-*/
-
-/* 15 Jan 2019 : Owen Carter : Add psucontrol option and processing */
-
- /**********************************************
- * Edit Settings.h for personalization
- ***********************************************/
-
-#include "Settings.h"
-
-#define VERSION "2.3"
-
-#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
-
-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;
-
-// 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 = "";
-
-String WEATHER_FORM = ""
- "";
-
-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()");
-}
-
-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.
-
- ui.update();
-
- if (WEBSERVER_ENABLED) {
- server.handleClient();
- }
- if (ENABLE_OTA) {
- ArduinoOTA.handle();
- }
-}
-
-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);
- 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 += "";
- html += "";
- html += "
";
- return html;
-}
-
-String getFooter() {
- int8_t rssi = getWifiQuality();
- Serial.print("Signal Strength (RSSI): ");
- Serial.print(rssi);
- Serial.println("%");
- String html = "
";
- html += "
";
- 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
";
- 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 += "
";
- 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) {
- display->setTextAlignment(TEXT_ALIGN_CENTER);
- display->setFont(ArialMT_Plain_16);
- display->drawString(64 + x, 0 + y, "Bed / Tool Temp");
- display->setTextAlignment(TEXT_ALIGN_LEFT);
- display->setFont(ArialMT_Plain_24);
- String bed = printerClient.getValueRounded(printerClient.getTempBedActual());
- String tool = printerClient.getValueRounded(printerClient.getTempToolActual());
- display->drawString(2 + x, 14 + y, bed + "°");
- 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 "2.4"
+
+#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
+
+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;
+
+// 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 = "";
+
+String WEATHER_FORM = ""
+ "";
+
+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()");
+}
+
+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.
+
+ ui.update();
+
+ if (WEBSERVER_ENABLED) {
+ server.handleClient();
+ }
+ if (ENABLE_OTA) {
+ ArduinoOTA.handle();
+ }
+}
+
+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 += "";
+ html += "";
+ html += "
";
+ return html;
+}
+
+String getFooter() {
+ int8_t rssi = getWifiQuality();
+ Serial.print("Signal Strength (RSSI): ");
+ Serial.print(rssi);
+ Serial.println("%");
+ String html = "
";
+ html += "
";
+ 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
";
+ 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 += "
";
+ 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) {
+ display->setTextAlignment(TEXT_ALIGN_CENTER);
+ display->setFont(ArialMT_Plain_16);
+ display->drawString(64 + x, 0 + y, "Bed / Tool Temp");
+ display->setTextAlignment(TEXT_ALIGN_LEFT);
+ display->setFont(ArialMT_Plain_24);
+ String bed = printerClient.getValueRounded(printerClient.getTempBedActual());
+ String tool = printerClient.getValueRounded(printerClient.getTempToolActual());
+ display->drawString(2 + x, 14 + y, bed + "°");
+ 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 78e3a72610945dfc7f9e8314075631bd4580d4ec Mon Sep 17 00:00:00 2001
From: Chrome Legion
Date: Wed, 16 Jan 2019 20:34:32 -0700
Subject: [PATCH 4/5] Qrome - updated compiled binary files for 2.4
---
printermonitor.ino.d1_mini_SH1106-2.4.bin | Bin 0 -> 598784 bytes
printermonitor.ino.d1_mini_SH1106.bin | Bin 451760 -> 0 bytes
printermonitor.ino.d1_mini_SSD1306-2.4.bin | Bin 0 -> 598880 bytes
printermonitor.ino.d1_mini_SSD1306.bin | Bin 451856 -> 0 bytes
4 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 printermonitor.ino.d1_mini_SH1106-2.4.bin
delete mode 100644 printermonitor.ino.d1_mini_SH1106.bin
create mode 100644 printermonitor.ino.d1_mini_SSD1306-2.4.bin
delete mode 100644 printermonitor.ino.d1_mini_SSD1306.bin
diff --git a/printermonitor.ino.d1_mini_SH1106-2.4.bin b/printermonitor.ino.d1_mini_SH1106-2.4.bin
new file mode 100644
index 0000000000000000000000000000000000000000..a5b3a39bc1c5671a0412f3df0bbaf584280f8722
GIT binary patch
literal 598784
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 |