Add preliminary ESP32 support

This commit is contained in:
huskee 2023-12-23 19:49:32 +02:00
parent f90f45b401
commit 53abc53ade
5 changed files with 252 additions and 73 deletions

View file

@ -13,11 +13,7 @@
<p>current data update interval (ms): <span id="updInt"></span></p>
<p>
set data update interval (ms):
<select
id="upd"
onchange="setUpdateInterval();"
onfocus="this.selectedIndex = -1;"
>
<select id="upd" onchange="setUpdateInterval();">
<option>10</option>
<option>20</option>
<option>50</option>
@ -32,11 +28,7 @@
<p>current chart update interval (ms): <span id="cupdInt"></span></p>
<p>
set chart update interval (ms):
<select
id="chartUpd"
onchange="location.reload()"
onfocus="this.selectedIndex = -1;"
>
<select id="chartUpd" onchange="location.reload()">
<option>200</option>
<option selected>500</option>
<option>1000</option>
@ -44,22 +36,38 @@
</p>
</div>
<br />
<div id="record">
<button id="startRec" onclick="startRecording()">Start recording</button>
<button id="stopRec" onclick="stopRecording()">Stop recording</button>
<span id="recInfo"
><svg class="blinking" width="20px" height="20px">
<circle cx="10" cy="10" r="7" fill="red" />
Sorry, your browser does not support inline SVG.
</svg>
<p>Recording CSV data to <span id="filename"></span> </p
></span><span id="timeLeft"></span>
</div>
<div id="timedCheck">
<input type="checkbox" id="timedRec" /><label for="timedRec"
>Timed recording</label
>
</div>
<span id="timedSettings">
<label for="timer">Stop recording after (min): </label
><input type="text" id="timer" />
</span>
<table>
<tr>
<td class="chart-container">
<h2>Channel A:</h2>
<h2>Channel A <span id="error-tempA"></span></h2>
<label for="labelA">label: </label
><input type="text" value="tempA" id="labelA" />
<div id="error-tempA"></div>
<canvas id="chartA"></canvas>
</td>
<td class="chart-container">
<h2>Channel B:</h2>
<h2>Channel B <span id="error-tempB"></span></h2>
<label for="labelB">label: </label
><input type="text" value="tempB" id="labelB" />
<div id="error-tempB"></div>
<canvas id="chartB"></canvas>
</td>
</tr>

View file

@ -1,8 +1,7 @@
const canvasA = document.getElementById("chartA");
const canvasB = document.getElementById("chartB");
const chartUpdateInt = document.getElementById("chartUpd").value;
document.getElementById("stopRec").disabled = true;
const canvasA = document.querySelector("#chartA");
const canvasB = document.querySelector("#chartB");
const chartUpdateInt = document.querySelector("#chartUpd").value;
document.querySelector("#stopRec").disabled = true;
let data;
let record = 0;
let filename;
@ -86,11 +85,11 @@ const options = {
fetch("/int")
.then((res) => res.text())
.then((textResponse) => {
document.getElementById("updInt").innerText = textResponse;
document.querySelector("#updInt").innerText = textResponse;
});
document.getElementById("cupdInt").innerText = chartUpdateInt;
document.querySelector("#cupdInt").innerText = chartUpdateInt;
document.querySelector("#recInfo").style.display = "none";
const chartA = new Chart(canvasA, {
type: "line",
data: {
@ -137,7 +136,7 @@ function getReadings() {
websocket.send("getReadings");
}
function setUpdateInterval() {
let updateInt = document.getElementById("upd").value;
let updateInt = document.querySelector("#upd").value;
websocket.send("u" + updateInt);
location.reload();
}
@ -146,7 +145,7 @@ function getUpdateInterval() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("upd").value = this.responseText;
document.querySelector("#upd").value = this.responseText;
}
};
xhttp.open("GET", "/int", true);
@ -170,6 +169,7 @@ function onOpen(event) {
function onClose(event) {
console.log("Connection closed");
setTimeout(initWebSocket, 2000);
csvData.length = 0;
}
function addData(chart, label, data) {
@ -192,7 +192,7 @@ function onMessage(event) {
//console.log(event.data);
data = JSON.parse(event.data);
let timestamp = new Date();
document.getElementById("time").innerHTML = timestamp.getTime();
document.querySelector("#time").innerHTML = timestamp.getTime();
data["time"] = timestamp.getTime();
if (record == 1) {
csvData.push(createCSV(data));
@ -201,16 +201,16 @@ function onMessage(event) {
setInterval(function () {
if (isNaN(data["tempA"])) {
document.getElementById("error-tempA").innerHTML =
document.querySelector("#error-tempA").innerHTML =
"<h3>" + data["tempA"] + "</h3>";
} else {
document.getElementById("error-tempA").innerText = "";
document.querySelector("#error-tempA").innerText = "";
}
if (isNaN(data["tempB"])) {
document.getElementById("error-tempB").innerHTML =
document.querySelector("#error-tempB").innerHTML =
"<h3>" + data["tempB"] + "</h3>";
} else {
document.getElementById("error-tempB").innerText = "";
document.querySelector("#error-tempB").innerText = "";
}
addData(chartA, data["time"], data["tempA"]);
addData(chartB, data["time"], data["tempB"]);
@ -257,15 +257,61 @@ function createCSV(object) {
}
function startRecording() {
let timedRec = document.querySelector("#timedRec").checked;
date = new Date();
let filedate = date.toISOString().slice(0, -5).replaceAll(":", "");
filename = "therminator-" + filedate + ".csv";
labelA = document.getElementById("labelA").value;
labelB = document.getElementById("labelB").value;
document.querySelector("#filename").innerText = filename;
labelA = document.querySelector("#labelA").value;
labelB = document.querySelector("#labelB").value;
csvData = ['time,"' + labelA + '","' + labelB + '"'];
record = 1;
document.getElementById("startRec").disabled = true;
document.getElementById("stopRec").disabled = false;
document.querySelector("#startRec").disabled = true;
document.querySelector("#stopRec").disabled = false;
document.querySelector("#recInfo").style.display = "inline-grid";
document.querySelector("#timedSettings").style.display = "none";
document.querySelector("#timedCheck").style.display = "none";
if (timedRec) {
let timer = document.querySelector("#timer").value;
if (timer == 0) {
alert("Recording time must be non-zero");
return;
}
timer = timer * 60000;
let timeleft = timer / 1000;
let disp = document.querySelector("#timeLeft");
startTimer(timeleft, disp);
setTimeout(stopRecording, timer);
}
}
function startTimer(duration, display) {
var timer = duration, minutes, seconds;
setInterval(function () {
minutes = parseInt(timer / 60, 10);
seconds = parseInt(timer % 60, 10);
minutes = minutes < 10 ? "0" + minutes : minutes;
seconds = seconds < 10 ? "0" + seconds : seconds;
display.textContent = minutes + ":" + seconds;
if (--timer < 0) {
timer = duration;
}
}, 1000);
}
function fancyTimeFormat(e) {
const h = Math.floor(e / 3600)
.toString()
.padStart(2, "0"),
m = Math.floor((e % 3600) / 60)
.toString()
.padStart(2, "0"),
s = Math.floor(e % 60)
.toString()
.padStart(2, "0");
return h + ":" + m + ":" + s;
}
function stopRecording() {
record = 0;
@ -277,7 +323,33 @@ function stopRecording() {
link.setAttribute("download", filename);
document.querySelector("body").append(link);
link.click();
csvData.length = 1;
document.getElementById("startRec").disabled = false;
document.getElementById("stopRec").disabled = true;
csvData.length = 0;
document.querySelector("#startRec").disabled = false;
document.querySelector("#stopRec").disabled = true;
document.querySelector("#recInfo").style.display = "none";
document.querySelector("#timedSettings").style.display = "none";
document.querySelector("#timedCheck").style.display = "inline-block";
document.querySelector("#timedRec").checked = false;
document.querySelector("#timeLeft").style.display = "none";
}
const beforeUnloadHandler = (event) => {
// Recommended
event.preventDefault();
// Included for legacy support, e.g. Chrome/Edge < 119
event.returnValue = true;
};
onbeforeunload = function () {
if (record) {
return "You are still recording data! Are you sure you want to leave?";
}
};
document.querySelector("#timedRec").onchange = function () {
if (this.checked) {
document.querySelector("#timedSettings").style.display = "inherit";
} else {
document.querySelector("#timedSettings").style.display = "none";
}
};

View file

@ -1,8 +1,10 @@
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap");
* {
font-family: "IBM Plex Mono", monospace;
}
body {
background-color: rgb(50, 50, 50);
color: white;
font-family: "IBM Plex Mono", monospace;
padding: 1rem 2rem;
}
#du,
@ -10,7 +12,55 @@ body {
display: inline-block;
padding-right: 3rem;
}
button {
background-color: white;
border: 1px rgb(0, 0, 0);
color: rgb(0, 0, 0);
text-align: center;
text-decoration: none;
display: inline-block;
}
input {
background-color: rgb(70, 70, 70);
border: none;
color: white;
padding: 5px 0;
padding-left: 5px;
font-size: 15px;
}
select {
border: 1px black;
background-color: white;
color: black;
padding: 5px;
}
button:disabled,
button[disabled] {
display: none;
}
#record, #recInfo {
display: inline-grid;
grid-template-columns: auto 1fr;
grid-gap: 10px;
align-items: center;
}
#timedCheck {
display: inline-block;
}
#timedCheck label {
padding-left: 8px;
}
#timedSettings {
display: none;
padding: 0 9.5rem;
}
#error-tempA,
#error-tempB {
color: red !important;
@ -22,3 +72,11 @@ body {
width: 40vw;
padding: 1em;
}
@keyframes blink {
50% {
opacity: 0.0;
}
}
.blinking {
animation: blink 1s step-start 0s infinite;
}

View file

@ -21,3 +21,18 @@ lib_deps =
Arduino_JSON
ESPAsyncWiFiManager
monitor_filters = esp8266_exception_decoder, colorize
[env:lolin32]
platform = espressif32
board = lolin32
board_build.filesystem = littlefs
framework = arduino
lib_deps =
adafruit/Adafruit MAX31855 library
Wire
SPI
ESPAsyncTCP
zeed/ESP Async WebServer
Arduino_JSON
ESPAsyncWiFiManager
monitor_filters = esp32_exception_decoder, colorize

View file

@ -1,15 +1,18 @@
#include <Adafruit_MAX31855.h>
#include <Arduino.h>
#include <Arduino_JSON.h>
#ifdef ESP32
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif
#include <ESPAsyncWebServer.h>
#include <ESPAsyncWiFiManager.h>
#include <LittleFS.h>
#include <SPI.h>
#include <pinout.h>
#include <chrono>
// sensor chip select pin definitions
#define CS_A D2
#define CS_B D3
@ -24,9 +27,14 @@ AsyncWiFiManager wm(&server, &dns);
JSONVar readings;
// data acquisition functions
unsigned long int lastTime = 0;
unsigned long int timerDelay = 500;
String checkSensor(Adafruit_MAX31855 &sensor) {
/*
return sensor value if it's numerical. if it isn't,
check for errors and return current error.
*/
if (isnan(sensor.readCelsius())) {
uint8_t err = sensor.readError();
if (err & MAX31855_FAULT_OPEN) {
@ -42,13 +50,16 @@ String checkSensor(Adafruit_MAX31855 &sensor) {
return String(sensor.readCelsius());
}
String getSensorReadings() {
/*
return sensor readings along with timestamp in JSON format
*/
readings["time"] = String(lastTime);
readings["tempA"] = checkSensor(thermocoupleA);
readings["tempB"] = checkSensor(thermocoupleB);
String jsonString = JSON.stringify(readings);
return jsonString;
}
// websocket functions
void notifyClients(String text) {
ws.textAll(text);
}
@ -58,14 +69,16 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
data[len] = 0;
String message = (char *)data;
// Check if the message is "getReadings"
// check if message is "getReadings"
if (strcmp((char *)data, "getReadings") == 0) {
// if it is, send current sensor readings
// if it is, send current readings to server
String sensorReadings = getSensorReadings();
Serial.printf("%s\n", sensorReadings.c_str());
notifyClients(sensorReadings);
}
// 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
String uVal = (message.substring(message.indexOf("u") + 1, message.length()));
int uInt = uVal.toInt();
timerDelay = uInt;
@ -91,53 +104,66 @@ void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType
break;
}
}
// initialization functions
void initSerial() {
Serial.begin(115200);
while (!Serial) delay(1);
delay(5000); // prevent garbage on serial output
Serial.print("serial init\n");
}
void initSPI() {
SPI.begin();
Serial.printf("SPI init\n");
}
void initLittleFS() {
LittleFS.begin();
Serial.printf("LittleFS init\n");
}
void initSensor() {
if (!thermocoupleA.begin() | !thermocoupleB.begin()) {
Serial.printf("sensor init error\n");
while (true) delay(10);
}
Serial.printf("sensor init\n");
}
void initWiFi() {
WiFi.mode(WIFI_AP_STA);
WiFi.setHostname("therminator");
Serial.printf("wifi init\n");
wm.autoConnect("therminator");
Serial.printf("hostname: %s\n", String(WiFi.getHostname()).c_str());
}
void initWebSocket() {
ws.onEvent(onEvent);
server.addHandler(&ws);
Serial.printf("websocket init\n");
}
void setup() {
Serial.begin(9600);
while (!Serial) delay(1);
delay(5000); // prevent garbage on serial output
Serial.println("serial init");
LittleFS.begin();
Serial.printf("LittleFS init\n");
SPI.begin();
Serial.println("SPI init");
Serial.println("MAX31855 test");
delay(500);
Serial.println("sensor init");
if (!thermocoupleA.begin() | !thermocoupleB.begin()) {
Serial.println("sensor init error");
while (true) delay(10);
}
WiFi.mode(WIFI_AP_STA);
Serial.printf("wifi init\n");
wm.autoConnect("therminator");
Serial.printf("connected! ip addr: ");
Serial.println(WiFi.localIP());
initWebSocket();
void initWebServer() {
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(LittleFS, "/index.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());
});
server.serveStatic("/", LittleFS, "/");
server.begin();
Serial.printf("webserver init\n");
}
void setup() {
initSerial();
initSPI();
initLittleFS();
initSensor();
initWiFi();
initWebSocket();
initWebServer();
}
void loop() {
if ((millis() - lastTime) > timerDelay) {
String sensorReadings = getSensorReadings();