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
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>
<title>therminator</title>
<link rel="stylesheet" type="text/css" href="/style.css" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
</head>
<body>
<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>
<div id="du">
<p>current data update interval (ms): <span id="updInt"></span></p>
@ -47,7 +67,7 @@
Sorry, your browser does not support inline SVG.
</svg>
<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 id="timedCheck">
<input type="checkbox" id="timedRec" /><label for="timedRec"
@ -73,6 +93,48 @@
<canvas id="chartB"></canvas>
</td>
</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>
<script src="script.js"></script>
</body>

View file

@ -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 =
"<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(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"
}

View file

@ -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;
}

View file

@ -2,9 +2,9 @@
#include <Arduino.h>
#include <Arduino_JSON.h>
#ifdef ESP32
#include <WiFi.h>
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#include <ESP8266WiFi.h>
#endif
#include <ESPAsyncWebServer.h>
#include <ESPAsyncWiFiManager.h>
@ -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();