mirror of
https://github.com/vincentvdwal/crust-craft.git
synced 2026-08-18 09:26:37 +02:00
359 lines
9.6 KiB
TypeScript
359 lines
9.6 KiB
TypeScript
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 relais = 0
|
|
let relaisData = [
|
|
[now.getTime(), 0]
|
|
]
|
|
|
|
let temp = 300;
|
|
let targetTemp = 300
|
|
let targetTempOverShoot = 300
|
|
let targetTempUnderShoot = 300
|
|
|
|
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
|
|
|
|
let temperatureData = [
|
|
[now.getTime(), temp]
|
|
]
|
|
|
|
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 getTargetLines = () => {
|
|
return [
|
|
{ value: targetTemp, width: 3, dashStyle: dashStyle },
|
|
{ value: targetTempOverShoot, width: 1, dashStyle: dashStyle, color: "red" },
|
|
{ value: targetTempUnderShoot, width: 1, dashStyle: dashStyle, color: "red" }
|
|
]
|
|
}
|
|
|
|
const createChart = () => {
|
|
hsOptions = {
|
|
chart: {
|
|
height: '650px',
|
|
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: getTargetLines()
|
|
},
|
|
|
|
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: getTargetLines() })
|
|
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
|
|
}
|
|
} else if (key === 'target_temp') {
|
|
targetTemp = myObj[key]
|
|
} else if (key === 'derived_overshoot') {
|
|
targetTempOverShoot = myObj[key]
|
|
} else if (key === 'derived_undershoot') {
|
|
targetTempUnderShoot = myObj[key]
|
|
}
|
|
|
|
hs.xAxis[0].update({ plotBands: getRelaisBands() })
|
|
hs.yAxis[0].update({ plotLines: getTargetLines() })
|
|
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 ' + value)
|
|
if (!import.meta.env.DEV) {
|
|
ws.send("setTargetTemp: " + pad(value, 3));
|
|
}
|
|
}
|
|
|