Add support for multiple sensors and credit popup
This commit is contained in:
parent
1925493a7e
commit
e9be1cd43e
11
README.md
11
README.md
|
@ -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
BIN
data/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
|
@ -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>
|
||||
<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>
|
||||
|
|
238
data/script.js
238
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 =
|
||||
"<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"
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
73
src/main.cpp
73
src/main.cpp
|
@ -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;
|
||||
}
|
||||
|
@ -117,12 +142,12 @@ void initSerial() {
|
|||
Serial.print("serial init\n");
|
||||
}
|
||||
void initSPI() {
|
||||
#ifdef ESP32
|
||||
#ifdef ESP32
|
||||
SPIClass hspi(HSPI);
|
||||
hspi.begin();
|
||||
#else
|
||||
#else
|
||||
SPI.begin();
|
||||
#endif
|
||||
#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
|
||||
#ifdef ESP32
|
||||
server.on("/isESP32", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", String(true));
|
||||
});
|
||||
#else
|
||||
#else
|
||||
server.on("/isESP32", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", String(false));
|
||||
});
|
||||
#endif
|
||||
#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();
|
||||
|
|
Loading…
Reference in a new issue