Add support for multiple sensors and credit popup

This commit is contained in:
huskee 2023-12-24 19:23:51 +02:00
parent 1925493a7e
commit e9be1cd43e
6 changed files with 393 additions and 67 deletions

View file

@ -1,3 +1,12 @@
# therminator # therminator
ESP8266 + MAX31855 thermocouple thermometer with web interface ### 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)

BIN
data/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -4,10 +4,30 @@
<head> <head>
<title>therminator</title> <title>therminator</title>
<link rel="stylesheet" type="text/css" href="/style.css" /> <link rel="stylesheet" type="text/css" href="/style.css" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
</head> </head>
<body> <body>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<h1>/* therminator */</h1> <div id="lightbox">
<div id="credits">
<span id="closebtn" onclick="hideCredits()">[X]</span>
<img src="favicon.ico" />
<h1>/* therminator */</h1>
<h3>a modular ESP8266/ESP32+MAX31855 logging thermometer</h3>
<br />
<p>
licensed under the MIT license copyright (c) 2023
<a href="https://huskee.gay">huskee</a>
</p>
<br />
<p>
huge thanks to azisi from
<a href="https://hackerspace.gr">hackerspace.gr</a> for the idea ❤️
</p>
</div>
</div>
<h1 style="user-select: none" onclick="showCredits()">/* therminator */</h1>
<p>last sensor update: <span id="time"></span></p> <p>last sensor update: <span id="time"></span></p>
<div id="du"> <div id="du">
<p>current data update interval (ms): <span id="updInt"></span></p> <p>current data update interval (ms): <span id="updInt"></span></p>
@ -47,7 +67,7 @@
Sorry, your browser does not support inline SVG. Sorry, your browser does not support inline SVG.
</svg> </svg>
<p>Recording CSV data to <span id="filename"></span></p></span <p>Recording CSV data to <span id="filename"></span></p></span
><span id="timeLeft"></span> ><span id="timeLeft">Time left: <span id="timeLeftCtr"></span></span>
</div> </div>
<div id="timedCheck"> <div id="timedCheck">
<input type="checkbox" id="timedRec" /><label for="timedRec" <input type="checkbox" id="timedRec" /><label for="timedRec"
@ -73,6 +93,48 @@
<canvas id="chartB"></canvas> <canvas id="chartB"></canvas>
</td> </td>
</tr> </tr>
<tr>
<td class="chart-container">
<h2>Channel C <span id="error-tempC"></span></h2>
<label for="labelC">label: </label
><input type="text" value="tempC" id="labelC" />
<canvas id="chartC"></canvas>
</td>
<td class="chart-container">
<h2>Channel D <span id="error-tempD"></span></h2>
<label for="labelD">label: </label
><input type="text" value="tempD" id="labelD" />
<canvas id="chartD"></canvas>
</td>
</tr>
<tr>
<td class="chart-container">
<h2>Channel E <span id="error-tempE"></span></h2>
<label for="labelE">label: </label
><input type="text" value="tempE" id="labelE" />
<canvas id="chartE"></canvas>
</td>
<td class="chart-container">
<h2>Channel F <span id="error-tempF"></span></h2>
<label for="labelF">label: </label
><input type="text" value="tempF" id="labelF" />
<canvas id="chartF"></canvas>
</td>
</tr>
<tr id="chanGH">
<td class="chart-container">
<h2>Channel G <span id="error-tempG"></span></h2>
<label for="labelG">label: </label
><input type="text" value="tempG" id="labelG" />
<canvas id="chartG"></canvas>
</td>
<td class="chart-container">
<h2>Channel H <span id="error-tempH"></span></h2>
<label for="labelH">label: </label
><input type="text" value="tempH" id="labelH" />
<canvas id="chartH"></canvas>
</td>
</tr>
</table> </table>
<script src="script.js"></script> <script src="script.js"></script>
</body> </body>

View file

@ -1,7 +1,11 @@
let isESP32;
fetch("/isESP32") fetch("/isESP32")
.then((res) => res.text()) .then((res) => res.text())
.then((textResponse) => { .then((textResponse) => {
let isESP32 = textResponse; isESP32 = textResponse;
if (isESP32 == 0) {
document.querySelector("#chanGH").style.display = "none";
}
}); });
fetch("/int") fetch("/int")
.then((res) => res.text()) .then((res) => res.text())
@ -9,42 +13,23 @@ fetch("/int")
document.querySelector("#updInt").innerText = textResponse; document.querySelector("#updInt").innerText = textResponse;
}); });
let lightbox = document.querySelector("#lightbox");
const canvasA = document.querySelector("#chartA"); const canvasA = document.querySelector("#chartA");
const canvasB = document.querySelector("#chartB"); 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; const chartUpdateInt = document.querySelector("#chartUpd").value;
document.querySelector("#stopRec").disabled = true; document.querySelector("#stopRec").disabled = true;
let data; let data;
let record = 0; let record = 0;
let filename; let filename;
let csvData; let csvData = [];
let labelA, labelB; let labelA, labelB, labelC, labelD, labelE, labelF, labelG, labelH;
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: [],
},
],
},
};
const options = { const options = {
animation: false, animation: false,
@ -127,6 +112,100 @@ const chartB = new Chart(canvasB, {
options: options, 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 gateway = `ws://${window.location.hostname}/ws`;
var websocket; var websocket;
// Init web socket when the page loads // Init web socket when the page loads
@ -217,8 +296,51 @@ setInterval(function () {
} else { } else {
document.querySelector("#error-tempB").innerText = ""; document.querySelector("#error-tempB").innerText = "";
} }
if (isNaN(data["tempC"])) {
document.querySelector("#error-tempC").innerHTML =
"<h3>" + data["tempC"] + "</h3>";
} else {
document.querySelector("#error-tempC").innerText = "";
}
if (isNaN(data["tempD"])) {
document.querySelector("#error-tempD").innerHTML =
"<h3>" + data["tempD"] + "</h3>";
} else {
document.querySelector("#error-tempD").innerText = "";
}
if (isNaN(data["tempE"])) {
document.querySelector("#error-tempE").innerHTML =
"<h3>" + data["tempE"] + "</h3>";
} else {
document.querySelector("#error-tempE").innerText = "";
}
if (isNaN(data["tempF"])) {
document.querySelector("#error-tempF").innerHTML =
"<h3>" + data["tempF"] + "</h3>";
} else {
document.querySelector("#error-tempF").innerText = "";
}
if (isNaN(data["tempG"])) {
document.querySelector("#error-tempG").innerHTML =
"<h3>" + data["tempG"] + "</h3>";
} else {
document.querySelector("#error-tempG").innerText = "";
}
if (isNaN(data["tempH"])) {
document.querySelector("#error-tempH").innerHTML =
"<h3>" + data["tempH"] + "</h3>";
} else {
document.querySelector("#error-tempH").innerText = "";
}
addData(chartA, data["time"], data["tempA"]); addData(chartA, data["time"], data["tempA"]);
addData(chartB, data["time"], data["tempB"]); 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); }, chartUpdateInt);
function escapeValue(value) { function escapeValue(value) {
@ -269,7 +391,49 @@ function startRecording() {
document.querySelector("#filename").innerText = filename; document.querySelector("#filename").innerText = filename;
labelA = document.querySelector("#labelA").value; labelA = document.querySelector("#labelA").value;
labelB = document.querySelector("#labelB").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; record = 1;
document.querySelector("#startRec").disabled = true; document.querySelector("#startRec").disabled = true;
document.querySelector("#stopRec").disabled = false; document.querySelector("#stopRec").disabled = false;
@ -284,7 +448,8 @@ function startRecording() {
} }
timer = timer * 60000; timer = timer * 60000;
let timeleft = timer / 1000; let timeleft = timer / 1000;
let disp = document.querySelector("#timeLeft"); let disp = document.querySelector("#timeLeftCtr");
document.querySelector("#timeLeft").style.display = "inherit";
startTimer(timeleft, disp); startTimer(timeleft, disp);
setTimeout(stopRecording, timer); setTimeout(stopRecording, timer);
} }
@ -360,3 +525,12 @@ document.querySelector("#timedRec").onchange = function () {
document.querySelector("#timedSettings").style.display = "none"; 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"
}

View file

@ -6,6 +6,7 @@ body {
background-color: rgb(50, 50, 50); background-color: rgb(50, 50, 50);
color: white; color: white;
padding: 1rem 2rem; padding: 1rem 2rem;
position: relative;
} }
#du, #du,
#cu { #cu {
@ -42,7 +43,8 @@ button[disabled] {
display: none; display: none;
} }
#record, #recInfo { #record,
#recInfo {
display: inline-grid; display: inline-grid;
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
grid-gap: 10px; grid-gap: 10px;
@ -62,7 +64,13 @@ button[disabled] {
padding: 0 9.5rem; padding: 0 9.5rem;
} }
#error-tempA, #error-tempA,
#error-tempB { #error-tempB,
#error-tempC,
#error-tempD,
#error-tempE,
#error-tempF,
#error-tempG,
#error-tempH {
color: red !important; color: red !important;
} }
@ -74,9 +82,45 @@ button[disabled] {
} }
@keyframes blink { @keyframes blink {
50% { 50% {
opacity: 0.0; opacity: 0;
} }
} }
.blinking { .blinking {
animation: blink 1s step-start 0s infinite; 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;
} }

View file

@ -2,9 +2,9 @@
#include <Arduino.h> #include <Arduino.h>
#include <Arduino_JSON.h> #include <Arduino_JSON.h>
#ifdef ESP32 #ifdef ESP32
#include <WiFi.h> #include <WiFi.h>
#else #else
#include <ESP8266WiFi.h> #include <ESP8266WiFi.h>
#endif #endif
#include <ESPAsyncWebServer.h> #include <ESPAsyncWebServer.h>
#include <ESPAsyncWiFiManager.h> #include <ESPAsyncWiFiManager.h>
@ -14,16 +14,33 @@
// sensor chip select pin definitions // sensor chip select pin definitions
#ifdef ESP32 #ifdef ESP32
#define CS_A 22 #define CS_A 22
#define CS_B 21 #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 #else
#define CS_A D2 #define CS_A D0
#define CS_B D3 #define CS_B D1
#define CS_C D2
#define CS_D D3
#define CS_E D4
#define CS_F D7
#endif #endif
Adafruit_MAX31855 thermocoupleA(CS_A); Adafruit_MAX31855 thermocoupleA(CS_A);
Adafruit_MAX31855 thermocoupleB(CS_B); 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); AsyncWebServer server(80);
AsyncWebSocket ws("/ws"); AsyncWebSocket ws("/ws");
@ -61,6 +78,14 @@ String getSensorReadings() {
readings["time"] = String(lastTime); readings["time"] = String(lastTime);
readings["tempA"] = checkSensor(thermocoupleA); readings["tempA"] = checkSensor(thermocoupleA);
readings["tempB"] = checkSensor(thermocoupleB); 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); String jsonString = JSON.stringify(readings);
return jsonString; return jsonString;
} }
@ -81,9 +106,9 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
Serial.printf("%s\n", sensorReadings.c_str()); Serial.printf("%s\n", sensorReadings.c_str());
notifyClients(sensorReadings); notifyClients(sensorReadings);
} }
// check if message is an update interval change // check if message is an update interval change
if (message.startsWith("u")) { 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())); String uVal = (message.substring(message.indexOf("u") + 1, message.length()));
int uInt = uVal.toInt(); int uInt = uVal.toInt();
timerDelay = uInt; timerDelay = uInt;
@ -117,12 +142,12 @@ void initSerial() {
Serial.print("serial init\n"); Serial.print("serial init\n");
} }
void initSPI() { void initSPI() {
#ifdef ESP32 #ifdef ESP32
SPIClass hspi(HSPI); SPIClass hspi(HSPI);
hspi.begin(); hspi.begin();
#else #else
SPI.begin(); SPI.begin();
#endif #endif
Serial.printf("SPI init\n"); 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); 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(); LittleFS.begin();
Serial.printf("LittleFS init\n"); Serial.printf("LittleFS init\n");
} }
void initSensor() { void initSensor(Adafruit_MAX31855 &sensor, const char *id) {
if (!thermocoupleA.begin() | !thermocoupleB.begin()) { if (!sensor.begin()) {
Serial.printf("sensor init error\n"); Serial.printf("sensor %s init error\n", id);
while (true) delay(10); while (true) delay(10);
} }
Serial.printf("sensor init\n"); Serial.printf("sensor %s init\n", id);
} }
void initWiFi() { void initWiFi() {
WiFi.mode(WIFI_AP_STA); WiFi.mode(WIFI_AP_STA);
@ -154,21 +179,24 @@ void initWebServer() {
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(LittleFS, "/index.html", "text/html"); 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) { server.onNotFound([](AsyncWebServerRequest *request) {
request->send(404, "text/plain", "404 Not Found"); request->send(404, "text/plain", "404 Not Found");
}); });
server.on("/int", HTTP_GET, [](AsyncWebServerRequest *request) { server.on("/int", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send_P(200, "text/plain", String(timerDelay).c_str()); request->send_P(200, "text/plain", String(timerDelay).c_str());
}); });
#ifdef ESP32 #ifdef ESP32
server.on("/isESP32", HTTP_GET, [](AsyncWebServerRequest *request) { server.on("/isESP32", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", String(true)); request->send(200, "text/plain", String(true));
}); });
#else #else
server.on("/isESP32", HTTP_GET, [](AsyncWebServerRequest *request) { server.on("/isESP32", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", String(false)); request->send(200, "text/plain", String(false));
}); });
#endif #endif
server.serveStatic("/", LittleFS, "/"); server.serveStatic("/", LittleFS, "/");
server.begin(); server.begin();
Serial.printf("webserver init\n"); Serial.printf("webserver init\n");
@ -178,7 +206,16 @@ void setup() {
initSerial(); initSerial();
initSPI(); initSPI();
initLittleFS(); 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(); initWiFi();
initWebSocket(); initWebSocket();
initWebServer(); initWebServer();