diff --git a/README.md b/README.md
index 206f8f7..8520a6b 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,12 @@
# therminator
-ESP8266 + MAX31855 thermocouple thermometer with web interface
\ No newline at end of file
+### a modular ESP8266/ESP32 + MAX31855 based logging thermometer
+
+therminator is a logging thermometer, based on the ESP8266/ESP32 microcontrollers and the MAX31855 thermocouple to digital converter.
+
+### features:
+
+- support for up to 6 (ESP8266) or 8 (ESP32) sensors
+- live temperature charts
+- data recording capabilities for writing logged data to CSV files
+- timed recording (recording stops after specified time passes)
\ No newline at end of file
diff --git a/data/favicon.ico b/data/favicon.ico
new file mode 100644
index 0000000..c2038d7
Binary files /dev/null and b/data/favicon.ico differ
diff --git a/data/index.html b/data/index.html
index 0bf41a4..c001dc1 100644
--- a/data/index.html
+++ b/data/index.html
@@ -4,10 +4,30 @@
therminator
+
- /* therminator */
+
+
+
[X]
+
+
/* therminator */
+
a modular ESP8266/ESP32+MAX31855 logging thermometer
+
+
+ licensed under the MIT license copyright (c) 2023
+ huskee
+
+
+
+ huge thanks to azisi from
+ hackerspace.gr for the idea ❤️
+
+
+
+
+ /* therminator */
last sensor update:
current data update interval (ms):
@@ -47,7 +67,7 @@
Sorry, your browser does not support inline SVG.
Recording CSV data to
+ >
Time left:
+
+
+ Channel C
+ label:
+
+
+
+ Channel D
+ label:
+
+
+
+
+
+ Channel E
+ label:
+
+
+
+ Channel F
+ label:
+
+
+
+
+
+ Channel G
+ label:
+
+
+
+ Channel H
+ label:
+
+
+
diff --git a/data/script.js b/data/script.js
index 0f3cfc0..7c082d5 100644
--- a/data/script.js
+++ b/data/script.js
@@ -1,7 +1,11 @@
+let isESP32;
fetch("/isESP32")
.then((res) => res.text())
.then((textResponse) => {
- let isESP32 = textResponse;
+ isESP32 = textResponse;
+ if (isESP32 == 0) {
+ document.querySelector("#chanGH").style.display = "none";
+ }
});
fetch("/int")
.then((res) => res.text())
@@ -9,42 +13,23 @@ fetch("/int")
document.querySelector("#updInt").innerText = textResponse;
});
+let lightbox = document.querySelector("#lightbox");
const canvasA = document.querySelector("#chartA");
const canvasB = document.querySelector("#chartB");
+const canvasC = document.querySelector("#chartC");
+const canvasD = document.querySelector("#chartD");
+const canvasE = document.querySelector("#chartE");
+const canvasF = document.querySelector("#chartF");
+const canvasG = document.querySelector("#chartG");
+const canvasH = document.querySelector("#chartH");
+
const chartUpdateInt = document.querySelector("#chartUpd").value;
document.querySelector("#stopRec").disabled = true;
let data;
let record = 0;
let filename;
-let csvData;
-let labelA, labelB;
-const chartAdata = {
- data: {
- labels: [],
- datasets: [
- {
- label: "Channel A temperature",
- backgroundColor: "red",
- borderColor: "red",
- data: [],
- },
- ],
- },
-};
-
-const chartBdata = {
- data: {
- labels: [],
- datasets: [
- {
- label: "Channel B temperature",
- backgroundColor: "blue",
- borderColor: "blue",
- data: [],
- },
- ],
- },
-};
+let csvData = [];
+let labelA, labelB, labelC, labelD, labelE, labelF, labelG, labelH;
const options = {
animation: false,
@@ -127,6 +112,100 @@ const chartB = new Chart(canvasB, {
options: options,
});
+const chartC = new Chart(canvasC, {
+ type: "line",
+ data: {
+ labels: [],
+ datasets: [
+ {
+ label: "Channel C temperature",
+ backgroundColor: "red",
+ borderColor: "red",
+ data: [],
+ },
+ ],
+ },
+ options: options,
+});
+
+const chartD = new Chart(canvasD, {
+ type: "line",
+ data: {
+ labels: [],
+ datasets: [
+ {
+ label: "Channel D temperature",
+ backgroundColor: "blue",
+ borderColor: "blue",
+ data: [],
+ },
+ ],
+ },
+ options: options,
+});
+const chartE = new Chart(canvasE, {
+ type: "line",
+ data: {
+ labels: [],
+ datasets: [
+ {
+ label: "Channel E temperature",
+ backgroundColor: "red",
+ borderColor: "red",
+ data: [],
+ },
+ ],
+ },
+ options: options,
+});
+
+const chartF = new Chart(canvasF, {
+ type: "line",
+ data: {
+ labels: [],
+ datasets: [
+ {
+ label: "Channel F temperature",
+ backgroundColor: "blue",
+ borderColor: "blue",
+ data: [],
+ },
+ ],
+ },
+ options: options,
+});
+const chartG = new Chart(canvasG, {
+ type: "line",
+ data: {
+ labels: [],
+ datasets: [
+ {
+ label: "Channel G temperature",
+ backgroundColor: "red",
+ borderColor: "red",
+ data: [],
+ },
+ ],
+ },
+ options: options,
+});
+
+const chartH = new Chart(canvasH, {
+ type: "line",
+ data: {
+ labels: [],
+ datasets: [
+ {
+ label: "Channel H temperature",
+ backgroundColor: "blue",
+ borderColor: "blue",
+ data: [],
+ },
+ ],
+ },
+ options: options,
+});
+
var gateway = `ws://${window.location.hostname}/ws`;
var websocket;
// Init web socket when the page loads
@@ -217,8 +296,51 @@ setInterval(function () {
} else {
document.querySelector("#error-tempB").innerText = "";
}
+ if (isNaN(data["tempC"])) {
+ document.querySelector("#error-tempC").innerHTML =
+ "" + data["tempC"] + " ";
+ } else {
+ document.querySelector("#error-tempC").innerText = "";
+ }
+ if (isNaN(data["tempD"])) {
+ document.querySelector("#error-tempD").innerHTML =
+ "" + data["tempD"] + " ";
+ } else {
+ document.querySelector("#error-tempD").innerText = "";
+ }
+ if (isNaN(data["tempE"])) {
+ document.querySelector("#error-tempE").innerHTML =
+ "" + data["tempE"] + " ";
+ } else {
+ document.querySelector("#error-tempE").innerText = "";
+ }
+ if (isNaN(data["tempF"])) {
+ document.querySelector("#error-tempF").innerHTML =
+ "" + data["tempF"] + " ";
+ } else {
+ document.querySelector("#error-tempF").innerText = "";
+ }
+ if (isNaN(data["tempG"])) {
+ document.querySelector("#error-tempG").innerHTML =
+ "" + data["tempG"] + " ";
+ } else {
+ document.querySelector("#error-tempG").innerText = "";
+ }
+ if (isNaN(data["tempH"])) {
+ document.querySelector("#error-tempH").innerHTML =
+ "" + data["tempH"] + " ";
+ } else {
+ document.querySelector("#error-tempH").innerText = "";
+ }
+
addData(chartA, data["time"], data["tempA"]);
addData(chartB, data["time"], data["tempB"]);
+ addData(chartC, data["time"], data["tempC"]);
+ addData(chartD, data["time"], data["tempD"]);
+ addData(chartE, data["time"], data["tempE"]);
+ addData(chartF, data["time"], data["tempF"]);
+ addData(chartG, data["time"], data["tempG"]);
+ addData(chartH, data["time"], data["tempH"]);
}, chartUpdateInt);
function escapeValue(value) {
@@ -269,7 +391,49 @@ function startRecording() {
document.querySelector("#filename").innerText = filename;
labelA = document.querySelector("#labelA").value;
labelB = document.querySelector("#labelB").value;
- csvData = ['time,"' + labelA + '","' + labelB + '"'];
+ labelC = document.querySelector("#labelC").value;
+ labelD = document.querySelector("#labelD").value;
+ labelE = document.querySelector("#labelE").value;
+ labelF = document.querySelector("#labelF").value;
+ labelG = document.querySelector("#labelG").value;
+ labelH = document.querySelector("#labelH").value;
+ if (isESP32 == 1) {
+ csvData = [
+ 'time,"' +
+ labelA +
+ '","' +
+ labelB +
+ '","' +
+ labelC +
+ '","' +
+ labelD +
+ '","' +
+ labelE +
+ '","' +
+ labelF +
+ '","' +
+ labelG +
+ '","' +
+ labelH +
+ '"',
+ ];
+ } else {
+ csvData = [
+ 'time,"' +
+ labelA +
+ '","' +
+ labelB +
+ '","' +
+ labelC +
+ '","' +
+ labelD +
+ '","' +
+ labelE +
+ '","' +
+ labelF +
+ '"',
+ ];
+ }
record = 1;
document.querySelector("#startRec").disabled = true;
document.querySelector("#stopRec").disabled = false;
@@ -284,7 +448,8 @@ function startRecording() {
}
timer = timer * 60000;
let timeleft = timer / 1000;
- let disp = document.querySelector("#timeLeft");
+ let disp = document.querySelector("#timeLeftCtr");
+ document.querySelector("#timeLeft").style.display = "inherit";
startTimer(timeleft, disp);
setTimeout(stopRecording, timer);
}
@@ -360,3 +525,12 @@ document.querySelector("#timedRec").onchange = function () {
document.querySelector("#timedSettings").style.display = "none";
}
};
+
+function showCredits() {
+ lightbox.style.visibility = "visible";
+ lightbox.style.opacity = "1"
+}
+function hideCredits() {
+ lightbox.style.visibility = "hidden";
+ lightbox.style.opacity = "0"
+}
diff --git a/data/style.css b/data/style.css
index 8584884..5410087 100644
--- a/data/style.css
+++ b/data/style.css
@@ -6,6 +6,7 @@ body {
background-color: rgb(50, 50, 50);
color: white;
padding: 1rem 2rem;
+ position: relative;
}
#du,
#cu {
@@ -42,7 +43,8 @@ button[disabled] {
display: none;
}
-#record, #recInfo {
+#record,
+#recInfo {
display: inline-grid;
grid-template-columns: auto 1fr;
grid-gap: 10px;
@@ -62,7 +64,13 @@ button[disabled] {
padding: 0 9.5rem;
}
#error-tempA,
-#error-tempB {
+#error-tempB,
+#error-tempC,
+#error-tempD,
+#error-tempE,
+#error-tempF,
+#error-tempG,
+#error-tempH {
color: red !important;
}
@@ -74,9 +82,45 @@ button[disabled] {
}
@keyframes blink {
50% {
- opacity: 0.0;
+ opacity: 0;
}
}
.blinking {
animation: blink 1s step-start 0s infinite;
+}
+#lightbox {
+ position: fixed;
+ z-index: 999;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ visibility: hidden;
+ opacity: 0;
+}
+#lightbox #credits {
+ user-select: none;
+ width: auto;
+ height: auto;
+ object-fit: cover;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background-color: rgb(50, 50, 50);
+ border: 3px solid black;
+ padding: 15px;
+}
+
+#closebtn {
+ position: absolute;
+ top: 0;
+ right: 0;
+}
+
+#timeLeft {
+ display: none;
}
\ No newline at end of file
diff --git a/src/main.cpp b/src/main.cpp
index 1ab37f9..57c0895 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -2,9 +2,9 @@
#include
#include
#ifdef ESP32
- #include
+#include
#else
- #include
+#include
#endif
#include
#include
@@ -14,16 +14,33 @@
// sensor chip select pin definitions
#ifdef ESP32
- #define CS_A 22
- #define CS_B 21
+#define CS_A 22
+#define CS_B 21
+#define CS_C 17
+#define CS_D 16
+#define CS_E 27
+#define CS_F 14
+#define CS_G 12
+#define CS_H 13
#else
- #define CS_A D2
- #define CS_B D3
+#define CS_A D0
+#define CS_B D1
+#define CS_C D2
+#define CS_D D3
+#define CS_E D4
+#define CS_F D7
#endif
-
Adafruit_MAX31855 thermocoupleA(CS_A);
Adafruit_MAX31855 thermocoupleB(CS_B);
+Adafruit_MAX31855 thermocoupleC(CS_C);
+Adafruit_MAX31855 thermocoupleD(CS_D);
+Adafruit_MAX31855 thermocoupleE(CS_E);
+Adafruit_MAX31855 thermocoupleF(CS_F);
+#ifdef ESP32
+Adafruit_MAX31855 thermocoupleG(CS_G);
+Adafruit_MAX31855 thermocoupleH(CS_H);
+#endif
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");
@@ -61,6 +78,14 @@ String getSensorReadings() {
readings["time"] = String(lastTime);
readings["tempA"] = checkSensor(thermocoupleA);
readings["tempB"] = checkSensor(thermocoupleB);
+ readings["tempC"] = checkSensor(thermocoupleC);
+ readings["tempD"] = checkSensor(thermocoupleD);
+ readings["tempE"] = checkSensor(thermocoupleE);
+ readings["tempF"] = checkSensor(thermocoupleF);
+#ifdef ESP32
+ readings["tempG"] = checkSensor(thermocoupleG);
+ readings["tempH"] = checkSensor(thermocoupleH);
+#endif
String jsonString = JSON.stringify(readings);
return jsonString;
}
@@ -81,9 +106,9 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
Serial.printf("%s\n", sensorReadings.c_str());
notifyClients(sensorReadings);
}
- // check if message is an update interval change
+ // check if message is an update interval change
if (message.startsWith("u")) {
- // if it is, strip the first character, convert to int and update timerDelay variable
+ // if it is, strip the first character, convert to int and update timerDelay variable
String uVal = (message.substring(message.indexOf("u") + 1, message.length()));
int uInt = uVal.toInt();
timerDelay = uInt;
@@ -117,12 +142,12 @@ void initSerial() {
Serial.print("serial init\n");
}
void initSPI() {
- #ifdef ESP32
- SPIClass hspi(HSPI);
- hspi.begin();
- #else
- SPI.begin();
- #endif
+#ifdef ESP32
+ SPIClass hspi(HSPI);
+ hspi.begin();
+#else
+ SPI.begin();
+#endif
Serial.printf("SPI init\n");
Serial.printf("using pins:\nPOCI: %d\tCLK: %d\nCS_A: %d\tCS_B: %d\n", MISO, SCK, CS_A, CS_B);
}
@@ -130,12 +155,12 @@ void initLittleFS() {
LittleFS.begin();
Serial.printf("LittleFS init\n");
}
-void initSensor() {
- if (!thermocoupleA.begin() | !thermocoupleB.begin()) {
- Serial.printf("sensor init error\n");
+void initSensor(Adafruit_MAX31855 &sensor, const char *id) {
+ if (!sensor.begin()) {
+ Serial.printf("sensor %s init error\n", id);
while (true) delay(10);
}
- Serial.printf("sensor init\n");
+ Serial.printf("sensor %s init\n", id);
}
void initWiFi() {
WiFi.mode(WIFI_AP_STA);
@@ -154,21 +179,24 @@ void initWebServer() {
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(LittleFS, "/index.html", "text/html");
});
+ server.on("/about", HTTP_GET, [](AsyncWebServerRequest *request) {
+ request->send(LittleFS, "/about.html", "text/html");
+ });
server.onNotFound([](AsyncWebServerRequest *request) {
request->send(404, "text/plain", "404 Not Found");
});
server.on("/int", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send_P(200, "text/plain", String(timerDelay).c_str());
});
- #ifdef ESP32
- server.on("/isESP32", HTTP_GET, [](AsyncWebServerRequest *request) {
- request->send(200, "text/plain", String(true));
- });
- #else
- server.on("/isESP32", HTTP_GET, [](AsyncWebServerRequest *request) {
- request->send(200, "text/plain", String(false));
- });
- #endif
+#ifdef ESP32
+ server.on("/isESP32", HTTP_GET, [](AsyncWebServerRequest *request) {
+ request->send(200, "text/plain", String(true));
+ });
+#else
+ server.on("/isESP32", HTTP_GET, [](AsyncWebServerRequest *request) {
+ request->send(200, "text/plain", String(false));
+ });
+#endif
server.serveStatic("/", LittleFS, "/");
server.begin();
Serial.printf("webserver init\n");
@@ -178,7 +206,16 @@ void setup() {
initSerial();
initSPI();
initLittleFS();
- initSensor();
+ initSensor(thermocoupleA, "A");
+ initSensor(thermocoupleB, "B");
+ initSensor(thermocoupleC, "C");
+ initSensor(thermocoupleD, "D");
+ initSensor(thermocoupleE, "E");
+ initSensor(thermocoupleF, "F");
+#ifdef ESP32
+ initSensor(thermocoupleG, "G");
+ initSensor(thermocoupleH, "H");
+#endif
initWiFi();
initWebSocket();
initWebServer();