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