mirror of
https://github.com/vincentvdwal/crust-craft.git
synced 2026-08-18 17:26:39 +02:00
initial commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
.pio
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
src/secrets.h
|
||||||
|
|
||||||
|
data
|
||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>CrustCraft</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" type="image/ico" href="favicon.ico">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="nav">
|
||||||
|
<div class="nav-title">CrustCraft</div>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="card-grid">
|
||||||
|
<div class="card">
|
||||||
|
<p class="card-title">
|
||||||
|
Temperature</p>
|
||||||
|
<p class="reading"><span id="temperature"></span> °C</p>
|
||||||
|
</div>
|
||||||
|
<div class="card relais" id="relais_switch">
|
||||||
|
<p>Relais</p>
|
||||||
|
<div class="onoffswitch">
|
||||||
|
<input type="checkbox" name="onoffswitch" class="onoffswitch-checkbox" id="relais_switch_input"
|
||||||
|
tabindex="0">
|
||||||
|
<label class="onoffswitch-label" for="relais_switch_input">
|
||||||
|
<span class="onoffswitch-inner"></span>
|
||||||
|
<span class="onoffswitch-switch"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="oven" id="oven">
|
||||||
|
<div class="oven-wrapper">
|
||||||
|
<div class="oven-img-wrapper">
|
||||||
|
<img src="/oven.png">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="graph">
|
||||||
|
<div class="plot-wrapper">
|
||||||
|
<div id="plot"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-grid settings">
|
||||||
|
<div class="card">
|
||||||
|
<p class="card-title">
|
||||||
|
Target temperature</p>
|
||||||
|
<p class="reading"><input type="number" min="0" max="600" placeholder="350" id="target_temperature">
|
||||||
|
</input> <label for="target_temperature">
|
||||||
|
°C
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Generated
+1049
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"highcharts": "^12.3.0",
|
||||||
|
"typescript": "^5.9.2"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^7.1.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
; PlatformIO Project Configuration File
|
||||||
|
;
|
||||||
|
; Build options: build flags, source filter
|
||||||
|
; Upload options: custom upload port, speed and extra flags
|
||||||
|
; Library options: dependencies, extra library storages
|
||||||
|
; Advanced options: extra scripting
|
||||||
|
;
|
||||||
|
; Please visit documentation for the other options and examples
|
||||||
|
; https://docs.platformio.org/page/projectconf.html
|
||||||
|
|
||||||
|
[env:esp32doit-devkit-v1]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32doit-devkit-v1
|
||||||
|
|
||||||
|
framework = arduino
|
||||||
|
lib_compat_mode = strict
|
||||||
|
lib_ldf_mode = chain
|
||||||
|
|
||||||
|
lib_deps =
|
||||||
|
adafruit/MAX6675 library@^1.1.2
|
||||||
|
bblanchon/ArduinoJson@^7.4.2
|
||||||
|
ayushsharma82/ElegantOTA@^3.1.7
|
||||||
|
ESP32Async/AsyncTCP
|
||||||
|
ESP32Async/ESPAsyncWebServer
|
||||||
|
|
||||||
|
board_build.filesystem = littlefs
|
||||||
|
|
||||||
|
build_flags =
|
||||||
|
-D ELEGANTOTA_USE_ASYNC_WEBSERVER=1
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 517 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 216 KiB |
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
declare module '*.css';
|
||||||
+190
@@ -0,0 +1,190 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <AsyncTCP.h>
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
#include "LittleFS.h"
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include "max6675.h"
|
||||||
|
#include <ElegantOTA.h>
|
||||||
|
|
||||||
|
#include "secrets.h"
|
||||||
|
|
||||||
|
// Create AsyncWebServer object on port 80
|
||||||
|
AsyncWebServer server(80);
|
||||||
|
|
||||||
|
// Create a WebSocket object
|
||||||
|
AsyncWebSocket ws("/ws");
|
||||||
|
|
||||||
|
// Json Variable to Hold Sensor Readings
|
||||||
|
JsonDocument readings;
|
||||||
|
String jsonString;
|
||||||
|
|
||||||
|
// Timer variables
|
||||||
|
unsigned long lastTime = 0;
|
||||||
|
unsigned long timerDelay = 250;
|
||||||
|
|
||||||
|
int relay = 21;
|
||||||
|
int thermoDO = 19;
|
||||||
|
int thermoCS = 23;
|
||||||
|
int thermoCLK = 5;
|
||||||
|
|
||||||
|
MAX6675 thermocouple(thermoCLK, thermoCS, thermoDO);
|
||||||
|
|
||||||
|
int targetTemp = 300;
|
||||||
|
int overShoot = 10;
|
||||||
|
int underShoot = 5;
|
||||||
|
unsigned long lastSwitch = 0;
|
||||||
|
unsigned long switchDelay = 60000;
|
||||||
|
bool autoSwitch = true;
|
||||||
|
|
||||||
|
// Get Sensor Readings and return JSON object
|
||||||
|
String getSensorReadings()
|
||||||
|
{
|
||||||
|
readings["temperature"] = thermocouple.readCelsius();
|
||||||
|
if (digitalRead(relay) == HIGH)
|
||||||
|
{
|
||||||
|
readings["relais"] = 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
readings["relais"] = 0;
|
||||||
|
}
|
||||||
|
serializeJson(readings, jsonString);
|
||||||
|
return jsonString;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize LittleFS
|
||||||
|
void initLittleFS()
|
||||||
|
{
|
||||||
|
if (!LittleFS.begin(true))
|
||||||
|
{
|
||||||
|
Serial.println("An error has occurred while mounting LittleFS");
|
||||||
|
}
|
||||||
|
Serial.println("LittleFS mounted successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize WiFi
|
||||||
|
void initWiFi()
|
||||||
|
{
|
||||||
|
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
WiFi.begin(ssid, password);
|
||||||
|
Serial.print("Connecting to WiFi ..");
|
||||||
|
while (WiFi.status() != WL_CONNECTED)
|
||||||
|
{
|
||||||
|
Serial.print('.');
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
Serial.println(WiFi.localIP());
|
||||||
|
}
|
||||||
|
|
||||||
|
void notifyClients(String sensorReadings)
|
||||||
|
{
|
||||||
|
ws.textAll(sensorReadings);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleWebSocketMessage(void *arg, uint8_t *data, size_t len)
|
||||||
|
{
|
||||||
|
AwsFrameInfo *info = (AwsFrameInfo *)arg;
|
||||||
|
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT)
|
||||||
|
{
|
||||||
|
data[len] = 0;
|
||||||
|
String message = (char *)data;
|
||||||
|
if (strcmp((char *)data, "getReadings") == 0)
|
||||||
|
{
|
||||||
|
String sensorReadings = getSensorReadings();
|
||||||
|
notifyClients(sensorReadings);
|
||||||
|
};
|
||||||
|
if (strcmp((char *)data, "switchRelais") == 0)
|
||||||
|
{
|
||||||
|
Serial.printf("\nSwitch Relais");
|
||||||
|
if (digitalRead(relay) == HIGH)
|
||||||
|
{
|
||||||
|
digitalWrite(relay, LOW);
|
||||||
|
lastSwitch = millis();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
digitalWrite(relay, HIGH);
|
||||||
|
lastSwitch = millis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (message.startsWith("setTargetTemp"))
|
||||||
|
{
|
||||||
|
String target = message.substring(15, 18);
|
||||||
|
targetTemp = target.toInt();
|
||||||
|
Serial.printf("\nTarget temp set to ");
|
||||||
|
Serial.print(targetTemp);
|
||||||
|
Serial.printf("°C");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case WS_EVT_CONNECT:
|
||||||
|
Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
|
||||||
|
break;
|
||||||
|
case WS_EVT_DISCONNECT:
|
||||||
|
Serial.printf("WebSocket client #%u disconnected\n", client->id());
|
||||||
|
break;
|
||||||
|
case WS_EVT_DATA:
|
||||||
|
handleWebSocketMessage(arg, data, len);
|
||||||
|
break;
|
||||||
|
case WS_EVT_PONG:
|
||||||
|
case WS_EVT_ERROR:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void initWebSocket()
|
||||||
|
{
|
||||||
|
ws.onEvent(onEvent);
|
||||||
|
server.addHandler(&ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
Serial.begin(9600);
|
||||||
|
pinMode(relay, OUTPUT);
|
||||||
|
|
||||||
|
initWiFi();
|
||||||
|
initLittleFS();
|
||||||
|
initWebSocket();
|
||||||
|
|
||||||
|
// Web Server Root URL
|
||||||
|
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||||
|
{ request->send(LittleFS, "/index.html", "text/html"); });
|
||||||
|
|
||||||
|
server.serveStatic("/", LittleFS, "/");
|
||||||
|
|
||||||
|
ElegantOTA.begin(&server);
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
server.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
if ((millis() - lastTime) > timerDelay)
|
||||||
|
{
|
||||||
|
String sensorReadings = getSensorReadings();
|
||||||
|
notifyClients(sensorReadings);
|
||||||
|
if (((millis() - lastSwitch) > switchDelay) && thermocouple.readCelsius() >= (targetTemp - overShoot)) // >= 285°C
|
||||||
|
{
|
||||||
|
digitalWrite(relay, LOW);
|
||||||
|
lastSwitch = millis();
|
||||||
|
}
|
||||||
|
if (((millis() - lastSwitch) > switchDelay) && thermocouple.readCelsius() < (targetTemp - underShoot)) // < 270°C
|
||||||
|
{
|
||||||
|
digitalWrite(relay, HIGH);
|
||||||
|
lastSwitch = millis();
|
||||||
|
}
|
||||||
|
lastTime = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.cleanupClients();
|
||||||
|
ElegantOTA.loop();
|
||||||
|
}
|
||||||
+345
@@ -0,0 +1,345 @@
|
|||||||
|
import Highcharts, { type DashStyleValue } from 'highcharts/highstock'
|
||||||
|
|
||||||
|
import 'highcharts/modules/accessibility'
|
||||||
|
|
||||||
|
import './styles.css'
|
||||||
|
|
||||||
|
export const pad = (n: string | number, amount = 2) => {
|
||||||
|
if (amount == 2) {
|
||||||
|
return ('0' + n).slice(-2);
|
||||||
|
} else {
|
||||||
|
return ('00' + n).slice(-3);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
let gateway = `ws://${window.location.hostname}/ws`;
|
||||||
|
let ws: WebSocket;
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
let container: HTMLElement | null = document.querySelector('#plot')
|
||||||
|
let temperatureData = [
|
||||||
|
[now.getTime(), 25]
|
||||||
|
]
|
||||||
|
|
||||||
|
let relais = 0
|
||||||
|
let relaisData = [
|
||||||
|
[now.getTime(), 0]
|
||||||
|
]
|
||||||
|
|
||||||
|
let temp = 25;
|
||||||
|
let targetTemp = 350
|
||||||
|
|
||||||
|
const oven = document.querySelector('#oven')
|
||||||
|
const temperature = document.querySelector('#temperature')
|
||||||
|
const relaisSwitch = document.querySelector('#relais_switch')
|
||||||
|
const relaisSwitchInput: HTMLInputElement | null = document.querySelector('#relais_switch_input')
|
||||||
|
const targetTempInput: HTMLInputElement | null = document.querySelector('#target_temperature')
|
||||||
|
|
||||||
|
let maxValues = 4 * 60 * 60 // 60 minutes (4 values/sec)
|
||||||
|
|
||||||
|
let hs: Highcharts.StockChart;
|
||||||
|
let hsOptions: Highcharts.Options
|
||||||
|
|
||||||
|
const getRelaisBands = () => {
|
||||||
|
let relaisBands = []
|
||||||
|
let active = false;
|
||||||
|
|
||||||
|
for (let r of relaisData) {
|
||||||
|
if (r[1] === 1 && !active) {
|
||||||
|
active = true
|
||||||
|
relaisBands.push({
|
||||||
|
from: r[0],
|
||||||
|
to: relaisData[relaisData.length - 1][0] + 1000,
|
||||||
|
color: 'rgba(255, 0, 0, 0.25)'
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (r[1] === 0 && active) {
|
||||||
|
relaisBands[relaisBands.length - 1]['to'] = r[0]
|
||||||
|
active = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return relaisBands
|
||||||
|
}
|
||||||
|
|
||||||
|
const dashStyle: DashStyleValue = 'Dash'
|
||||||
|
const getTargetLine = () => {
|
||||||
|
return [
|
||||||
|
{ value: targetTemp, width: 3, dashStyle: dashStyle }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const createChart = () => {
|
||||||
|
hsOptions = {
|
||||||
|
chart: {
|
||||||
|
animation: {
|
||||||
|
duration: 250,
|
||||||
|
easing: 'linear'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
credits: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
|
||||||
|
title: {
|
||||||
|
text: 'Temperature',
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
|
||||||
|
rangeSelector: {
|
||||||
|
enabled: false,
|
||||||
|
inputDateFormat: '%H:%M:%S'
|
||||||
|
},
|
||||||
|
|
||||||
|
yAxis: {
|
||||||
|
title: {
|
||||||
|
text: '°C'
|
||||||
|
},
|
||||||
|
opposite: false,
|
||||||
|
min: 0,
|
||||||
|
max: 600,
|
||||||
|
tickInterval: 100,
|
||||||
|
plotBands: [{
|
||||||
|
from: 0,
|
||||||
|
to: 250,
|
||||||
|
color: 'rgba(69, 171, 255, 0.34)'
|
||||||
|
}, {
|
||||||
|
from: 250,
|
||||||
|
to: 420,
|
||||||
|
color: 'rgba(0, 255, 60, 0.34)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 420,
|
||||||
|
to: 530,
|
||||||
|
color: 'rgba(255, 162, 0, 0.4)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 530,
|
||||||
|
to: 600,
|
||||||
|
color: 'rgba(255, 0, 0, 0.4)'
|
||||||
|
}],
|
||||||
|
plotLines: getTargetLine()
|
||||||
|
},
|
||||||
|
|
||||||
|
xAxis: {
|
||||||
|
type: 'datetime',
|
||||||
|
tickInterval: 30 * 1000, // 30 seconds
|
||||||
|
labels: {
|
||||||
|
rotation: -45,
|
||||||
|
formatter: (f) => {
|
||||||
|
let d = new Date(f.pos)
|
||||||
|
return pad(d.getHours()) + ":" + pad(d.getMinutes()) + ":" + pad(d.getSeconds())
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
fontSize: '0.55em'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plotBands: getRelaisBands(),
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
legend: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
tooltip: {
|
||||||
|
formatter: function () {
|
||||||
|
let d = new Date(this.x)
|
||||||
|
return [pad(d.getHours()) + ":" + pad(d.getMinutes()) + ":" + pad(d.getSeconds()), `<b>${this.y?.toFixed(1)}°C</b>`]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
plotOptions: {
|
||||||
|
series: {
|
||||||
|
animation: false,
|
||||||
|
tooltip: {
|
||||||
|
valueDecimals: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
enablePolling: true,
|
||||||
|
dataRefreshRate: 0.5
|
||||||
|
},
|
||||||
|
|
||||||
|
series: [{
|
||||||
|
type: 'areaspline',
|
||||||
|
name: 'Temperature',
|
||||||
|
data: temperatureData
|
||||||
|
}],
|
||||||
|
|
||||||
|
responsive: {
|
||||||
|
rules: [{
|
||||||
|
condition: {
|
||||||
|
maxWidth: 1000
|
||||||
|
},
|
||||||
|
chartOptions: {
|
||||||
|
legend: {
|
||||||
|
layout: 'horizontal',
|
||||||
|
align: 'center',
|
||||||
|
verticalAlign: 'bottom'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (container && hsOptions)
|
||||||
|
hs = Highcharts.stockChart(container, hsOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onLoad = () => {
|
||||||
|
initWebSocket();
|
||||||
|
|
||||||
|
|
||||||
|
if (container) {
|
||||||
|
createChart()
|
||||||
|
|
||||||
|
relaisSwitch?.addEventListener('click', switchRelais)
|
||||||
|
targetTempInput?.addEventListener('change', changeTargetTemp)
|
||||||
|
|
||||||
|
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
// spoof some test data
|
||||||
|
setInterval(() => {
|
||||||
|
if (temperatureData.length > maxValues) {
|
||||||
|
temperatureData.shift()
|
||||||
|
relaisData.shift()
|
||||||
|
}
|
||||||
|
let d = new Date()
|
||||||
|
|
||||||
|
if (temp < 150) {
|
||||||
|
temp = temp + Math.random() * 4 - 1
|
||||||
|
} else if (temp < 20) {
|
||||||
|
temp = temp + Math.random() * 4 - 1.3
|
||||||
|
} else if (temp < targetTemp) {
|
||||||
|
temp = temp + Math.random() * 4 - 1.5
|
||||||
|
} else if (temp >= targetTemp) {
|
||||||
|
temp = temp + Math.random() * 3 - 1.6
|
||||||
|
}
|
||||||
|
if (relais) {
|
||||||
|
temp = temp + 0.2
|
||||||
|
} else {
|
||||||
|
temp = temp - 0.1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (temperature)
|
||||||
|
temperature.innerHTML = temp.toFixed(2);
|
||||||
|
temperatureData.push([d.getTime(), temp])
|
||||||
|
relaisData.push([d.getTime(), relais])
|
||||||
|
|
||||||
|
|
||||||
|
hs.xAxis[0].update({ plotBands: getRelaisBands() })
|
||||||
|
hs.yAxis[0].update({ plotLines: getTargetLine() })
|
||||||
|
hs.series[0].setData(temperatureData, true)
|
||||||
|
}, 250)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
window.addEventListener('load', onLoad);
|
||||||
|
|
||||||
|
const getReadings = () => {
|
||||||
|
ws.send("getReadings");
|
||||||
|
}
|
||||||
|
|
||||||
|
const initWebSocket = () => {
|
||||||
|
if (!import.meta.env.DEV) {
|
||||||
|
console.log('Trying to open a WebSocket connection…');
|
||||||
|
ws = new WebSocket(gateway);
|
||||||
|
ws.onopen = onOpen;
|
||||||
|
ws.onclose = onClose;
|
||||||
|
ws.onmessage = onMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When websocket is established, call the getReadings() function
|
||||||
|
const onOpen = () => {
|
||||||
|
console.log('Connection opened');
|
||||||
|
getReadings();
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
console.log('Connection closed');
|
||||||
|
setTimeout(initWebSocket, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function that receives the message from the ESP32 with the readings
|
||||||
|
const onMessage = (event: MessageEvent) => {
|
||||||
|
let myObj = JSON.parse(event.data);
|
||||||
|
let keys = Object.keys(myObj);
|
||||||
|
|
||||||
|
for (var i = 0; i < keys.length; i++) {
|
||||||
|
let key = keys[i];
|
||||||
|
let element = document.getElementById(key)
|
||||||
|
if (element) {
|
||||||
|
element.innerHTML = myObj[key].toFixed(2);
|
||||||
|
}
|
||||||
|
let d = new Date()
|
||||||
|
if (key === 'temperature') {
|
||||||
|
if (temperatureData.length > maxValues) {
|
||||||
|
temperatureData.shift()
|
||||||
|
}
|
||||||
|
temperatureData.push([d.getTime(), myObj[key]])
|
||||||
|
|
||||||
|
} else if (key === 'relais') {
|
||||||
|
if (relaisData.length > maxValues) {
|
||||||
|
relaisData.shift()
|
||||||
|
}
|
||||||
|
relaisData.push([d.getTime(), myObj[key]])
|
||||||
|
if (myObj[key]) {
|
||||||
|
oven?.classList.add('on')
|
||||||
|
relaisSwitch?.classList.add('on')
|
||||||
|
if (relaisSwitchInput)
|
||||||
|
relaisSwitchInput.checked = true
|
||||||
|
} else {
|
||||||
|
oven?.classList.remove('on')
|
||||||
|
relaisSwitch?.classList.remove('on')
|
||||||
|
if (relaisSwitchInput)
|
||||||
|
relaisSwitchInput.checked = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hs.xAxis[0].update({ plotBands: getRelaisBands() })
|
||||||
|
hs.yAxis[0].update({ plotLines: getTargetLine() })
|
||||||
|
hs.series[0].setData(temperatureData, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const switchRelais = () => {
|
||||||
|
console.log('Send switch relais')
|
||||||
|
if (!import.meta.env.DEV) {
|
||||||
|
ws.send("switchRelais");
|
||||||
|
} else {
|
||||||
|
// spoof some test data
|
||||||
|
if (!relais) {
|
||||||
|
relais = 1
|
||||||
|
oven?.classList.add('on')
|
||||||
|
relaisSwitch?.classList.add('on')
|
||||||
|
if (relaisSwitchInput)
|
||||||
|
relaisSwitchInput.checked = true
|
||||||
|
} else {
|
||||||
|
relais = 0
|
||||||
|
oven?.classList.remove('on')
|
||||||
|
|
||||||
|
relaisSwitch?.classList.remove('on')
|
||||||
|
if (relaisSwitchInput)
|
||||||
|
relaisSwitchInput.checked = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeTargetTemp = (e: Event) => {
|
||||||
|
const target = e.target as HTMLTextAreaElement;
|
||||||
|
let value = Number(target?.value)
|
||||||
|
targetTemp = value
|
||||||
|
console.log('Changed target temp to ' + targetTemp)
|
||||||
|
if (!import.meta.env.DEV) {
|
||||||
|
ws.send("setTargetTemp: " + pad(targetTemp, 3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// Replace with your network credentials
|
||||||
|
const char *ssid = "WIFI_SSID";
|
||||||
|
const char *password = "WIFI_PWD";
|
||||||
+204
@@ -0,0 +1,204 @@
|
|||||||
|
html,
|
||||||
|
body {
|
||||||
|
font-family:
|
||||||
|
ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||||
|
'Noto Color Emoji';
|
||||||
|
margin: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
width: full;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-title {
|
||||||
|
font-size: xx-large;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-grid {
|
||||||
|
margin-top: 15px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph {
|
||||||
|
height: 310px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plot-wrapper {
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 25%;
|
||||||
|
padding: 1em;
|
||||||
|
min-height: 120px;
|
||||||
|
box-shadow: 2px 2px 12px 1px rgba(140, 140, 140, .5);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reading {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@property --ovenColor1 {
|
||||||
|
syntax: '<color>';
|
||||||
|
initial-value: rgba(255, 0, 0, 0);
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@property --ovenColor2 {
|
||||||
|
syntax: '<color>';
|
||||||
|
initial-value: rgba(255, 0, 0, 0);
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oven {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oven-wrapper {
|
||||||
|
background: radial-gradient(var(--ovenColor1), var(--ovenColor2), rgba(255, 0, 0, 0), rgba(255, 0, 0, 0));
|
||||||
|
width: 50%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: visible;
|
||||||
|
padding: 30px;
|
||||||
|
transition: --ovenColor1 3s, --ovenColor2 3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oven.on .oven-wrapper {
|
||||||
|
--ovenColor1: rgb(255, 60, 0);
|
||||||
|
--ovenColor2: rgba(255, 60, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oven .oven-img-wrapper {
|
||||||
|
width: 65%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oven img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relais {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onoffswitch {
|
||||||
|
pointer-events: none;
|
||||||
|
margin-top: 10px;
|
||||||
|
position: relative;
|
||||||
|
width: 90px;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onoffswitch-checkbox {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onoffswitch-label {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid #999999;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onoffswitch-inner {
|
||||||
|
display: block;
|
||||||
|
width: 200%;
|
||||||
|
margin-left: -100%;
|
||||||
|
transition: margin 0.3s ease-in 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onoffswitch-inner:before,
|
||||||
|
.onoffswitch-inner:after {
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
width: 50%;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 30px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: white;
|
||||||
|
font-family: Trebuchet, Arial, sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onoffswitch-inner:before {
|
||||||
|
content: "ON";
|
||||||
|
padding-left: 10px;
|
||||||
|
background-color: #C43535;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onoffswitch-inner:after {
|
||||||
|
content: "OFF";
|
||||||
|
padding-right: 10px;
|
||||||
|
background-color: #EEEEEE;
|
||||||
|
color: #999999;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onoffswitch-switch {
|
||||||
|
display: block;
|
||||||
|
width: 18px;
|
||||||
|
margin: 6px;
|
||||||
|
background: #FFFFFF;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 56px;
|
||||||
|
border: 2px solid #999999;
|
||||||
|
border-radius: 20px;
|
||||||
|
transition: all 0.3s ease-in 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onoffswitch-checkbox:checked+.onoffswitch-label .onoffswitch-inner {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onoffswitch-checkbox:checked+.onoffswitch-label .onoffswitch-switch {
|
||||||
|
right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings {
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings input {
|
||||||
|
max-width: 50px;
|
||||||
|
font-size: larger;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly DEV: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": [
|
||||||
|
"ES2022",
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
outDir: 'data',
|
||||||
|
chunkSizeWarningLimit: 1000
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user