mirror of
https://github.com/vincentvdwal/crust-craft.git
synced 2026-08-18 17:26:39 +02:00
switch to svelte and chart.js
This commit is contained in:
+27
-5
@@ -1,8 +1,30 @@
|
|||||||
.pio
|
|
||||||
.vscode
|
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
src/secrets.h
|
# Output
|
||||||
|
.output
|
||||||
|
.vercel
|
||||||
|
.netlify
|
||||||
|
.wrangler
|
||||||
|
/.svelte-kit
|
||||||
|
/build
|
||||||
|
/data
|
||||||
|
|
||||||
data
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env.test
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
|
|
||||||
|
.pio
|
||||||
|
.vscode/.browse.c_cpp.db*
|
||||||
|
.vscode/c_cpp_properties.json
|
||||||
|
.vscode/launch.json
|
||||||
|
.vscode/ipch
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Package Managers
|
||||||
|
package-lock.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
yarn.lock
|
||||||
|
bun.lock
|
||||||
|
bun.lockb
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
/static/
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100,
|
||||||
|
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.svelte",
|
||||||
|
"options": {
|
||||||
|
"parser": "svelte"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tailwindStylesheet": "./src/app.css"
|
||||||
|
}
|
||||||
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"platformio.platformio-ide"
|
||||||
|
],
|
||||||
|
"unwantedRecommendations": [
|
||||||
|
"ms-vscode.cpptools-extension-pack"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import prettier from 'eslint-config-prettier';
|
||||||
|
import { includeIgnoreFile } from '@eslint/compat';
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import svelte from 'eslint-plugin-svelte';
|
||||||
|
import globals from 'globals';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import ts from 'typescript-eslint';
|
||||||
|
import svelteConfig from './svelte.config.js';
|
||||||
|
|
||||||
|
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
|
||||||
|
|
||||||
|
export default ts.config(
|
||||||
|
includeIgnoreFile(gitignorePath),
|
||||||
|
js.configs.recommended,
|
||||||
|
...ts.configs.recommended,
|
||||||
|
...svelte.configs.recommended,
|
||||||
|
prettier,
|
||||||
|
...svelte.configs.prettier,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: { ...globals.browser, ...globals.node }
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
|
||||||
|
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
|
||||||
|
'no-undef': 'off'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
extraFileExtensions: ['.svelte'],
|
||||||
|
parser: ts.parser,
|
||||||
|
svelteConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
-48
@@ -8,55 +8,7 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<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">
|
|
||||||
Sensor 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_small.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="300" id="target_temperature">
|
|
||||||
</input> <label for="target_temperature">
|
|
||||||
°C
|
|
||||||
</p>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
Generated
+4377
-1047
File diff suppressed because it is too large
Load Diff
+50
-12
@@ -1,14 +1,52 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"name": "crust-craft",
|
||||||
"highcharts": "^12.3.0",
|
"private": true,
|
||||||
"typescript": "^5.9.2"
|
"version": "0.0.1",
|
||||||
},
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
},
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
"devDependencies": {
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"vite": "^7.1.3"
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
}
|
"format": "prettier --write .",
|
||||||
|
"lint": "prettier --check . && eslint ."
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/compat": "^1.2.5",
|
||||||
|
"@eslint/js": "^9.18.0",
|
||||||
|
"@sveltejs/adapter-static": "^3.0.8",
|
||||||
|
"@sveltejs/kit": "^2.22.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||||
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"eslint": "^9.18.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-svelte": "^3.0.0",
|
||||||
|
"globals": "^16.0.0",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"prettier-plugin-svelte": "^3.3.3",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
|
"svelte": "^5.0.0",
|
||||||
|
"svelte-check": "^4.0.0",
|
||||||
|
"tailwindcss": "^4.0.0",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"typescript-eslint": "^8.20.0",
|
||||||
|
"vite": "^7.0.4",
|
||||||
|
"vite-plugin-devtools-json": "^1.0.0"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"esbuild"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@cubejs-client/core": "^1.3.67",
|
||||||
|
"chart.js": "^4.5.0",
|
||||||
|
"chartjs-adapter-date-fns": "^3.0.0",
|
||||||
|
"chartjs-plugin-annotation": "^3.1.0",
|
||||||
|
"date-fns": "^4.1.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+2645
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,8 @@
|
|||||||
|
@import 'tailwindcss';
|
||||||
|
@plugin '@tailwindcss/forms';
|
||||||
|
@plugin '@tailwindcss/typography';
|
||||||
|
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
font-family:
|
font-family:
|
||||||
@@ -36,7 +41,7 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.graph {
|
.graph {
|
||||||
height: 570px;
|
height: 400px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -158,22 +163,25 @@ p {
|
|||||||
|
|
||||||
.onoffswitch-inner:before {
|
.onoffswitch-inner:before {
|
||||||
content: "ON";
|
content: "ON";
|
||||||
padding-left: 10px;
|
|
||||||
background-color: #C43535;
|
background-color: #C43535;
|
||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
|
padding-right: 8px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.onoffswitch-inner:after {
|
.onoffswitch-inner:after {
|
||||||
content: "OFF";
|
content: "OFF";
|
||||||
padding-right: 10px;
|
padding-left: 10px;
|
||||||
|
|
||||||
background-color: #EEEEEE;
|
background-color: #EEEEEE;
|
||||||
color: #999999;
|
color: #999999;
|
||||||
text-align: right;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.onoffswitch-switch {
|
.onoffswitch-switch {
|
||||||
display: block;
|
display: block;
|
||||||
width: 18px;
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
margin: 6px;
|
margin: 6px;
|
||||||
background: #FFFFFF;
|
background: #FFFFFF;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -194,6 +202,7 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings {
|
.settings {
|
||||||
|
margin-bottom: 200px;
|
||||||
margin-top: 50px;
|
margin-top: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,3 +211,20 @@ p {
|
|||||||
font-size: larger;
|
font-size: larger;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mode_span::first-letter {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart div {
|
||||||
|
font: 10px sans-serif;
|
||||||
|
background-color: steelblue;
|
||||||
|
text-align: right;
|
||||||
|
padding: 3px;
|
||||||
|
margin: 1px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
Vendored
+13
@@ -0,0 +1,13 @@
|
|||||||
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1 @@
|
|||||||
|
// place files you want to import through the `$lib` alias in this folder.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export const pad = (n: string | number, amount = 2) => {
|
||||||
|
if (amount === 2) {
|
||||||
|
return ('0' + n).slice(-2);
|
||||||
|
} else {
|
||||||
|
return ('00' + n).slice(-3);
|
||||||
|
}
|
||||||
|
};
|
||||||
+45
-5
@@ -34,8 +34,10 @@ float setUnderShoot = 20;
|
|||||||
float derivedOverShoot = targetTemp;
|
float derivedOverShoot = targetTemp;
|
||||||
float derivedUnderShoot = targetTemp;
|
float derivedUnderShoot = targetTemp;
|
||||||
unsigned long lastSwitch = 0;
|
unsigned long lastSwitch = 0;
|
||||||
unsigned long switchDelay = 30000; // 15s
|
unsigned long autoSwitchDelay = 20000; // 20s
|
||||||
bool autoSwitch = true;
|
unsigned long pwmSwitchDelay = 5000; // 5s
|
||||||
|
|
||||||
|
String mode = "off";
|
||||||
|
|
||||||
// Get Sensor Readings and return JSON object
|
// Get Sensor Readings and return JSON object
|
||||||
String getSensorReadings()
|
String getSensorReadings()
|
||||||
@@ -56,6 +58,8 @@ String getSensorReadings()
|
|||||||
readings["target_temp"] = targetTemp;
|
readings["target_temp"] = targetTemp;
|
||||||
readings["derived_overshoot"] = derivedOverShoot;
|
readings["derived_overshoot"] = derivedOverShoot;
|
||||||
readings["derived_undershoot"] = derivedUnderShoot;
|
readings["derived_undershoot"] = derivedUnderShoot;
|
||||||
|
readings["mode"] = mode;
|
||||||
|
|
||||||
serializeJson(readings, jsonString);
|
serializeJson(readings, jsonString);
|
||||||
return jsonString;
|
return jsonString;
|
||||||
}
|
}
|
||||||
@@ -131,6 +135,25 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len)
|
|||||||
Serial.print(targetTemp);
|
Serial.print(targetTemp);
|
||||||
Serial.printf("°C");
|
Serial.printf("°C");
|
||||||
}
|
}
|
||||||
|
if (message.startsWith("setMode"))
|
||||||
|
{
|
||||||
|
if (message.indexOf("auto_switch") > 0)
|
||||||
|
{
|
||||||
|
mode = "auto_switch";
|
||||||
|
}
|
||||||
|
else if (message.indexOf("pwm") > 0)
|
||||||
|
{
|
||||||
|
mode = "pwm";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mode = "off";
|
||||||
|
digitalWrite(relay, LOW);
|
||||||
|
lastSwitch = millis();
|
||||||
|
}
|
||||||
|
Serial.printf("\nMode set to ");
|
||||||
|
Serial.print(mode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +184,7 @@ void initWebSocket()
|
|||||||
|
|
||||||
void regulateRelais()
|
void regulateRelais()
|
||||||
{
|
{
|
||||||
if (autoSwitch)
|
if (mode == "auto_switch")
|
||||||
{
|
{
|
||||||
float sinceLastSwitch = millis() - lastSwitch;
|
float sinceLastSwitch = millis() - lastSwitch;
|
||||||
float fiveMinutesInMillis = 5 * 60 * 1000;
|
float fiveMinutesInMillis = 5 * 60 * 1000;
|
||||||
@@ -176,7 +199,7 @@ void regulateRelais()
|
|||||||
float overShootCorrectedTarget = derivedOverShoot = targetTemp - dirivedOvershoot;
|
float overShootCorrectedTarget = derivedOverShoot = targetTemp - dirivedOvershoot;
|
||||||
float underShootCorrectedTarget = derivedUnderShoot = targetTemp + dirivedUndershoot;
|
float underShootCorrectedTarget = derivedUnderShoot = targetTemp + dirivedUndershoot;
|
||||||
|
|
||||||
if ((millis() - lastSwitch) > switchDelay)
|
if ((millis() - lastSwitch) > autoSwitchDelay)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (temperature > overShootCorrectedTarget) // > 280°C
|
if (temperature > overShootCorrectedTarget) // > 280°C
|
||||||
@@ -189,7 +212,7 @@ void regulateRelais()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((millis() - lastSwitch) > switchDelay)
|
if ((millis() - lastSwitch) > autoSwitchDelay)
|
||||||
{
|
{
|
||||||
if (temperature < underShootCorrectedTarget) // < 320°C
|
if (temperature < underShootCorrectedTarget) // < 320°C
|
||||||
{
|
{
|
||||||
@@ -202,6 +225,23 @@ void regulateRelais()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mode == "pwm")
|
||||||
|
{
|
||||||
|
if ((millis() - lastSwitch) > pwmSwitchDelay)
|
||||||
|
{
|
||||||
|
if (digitalRead(relay) == HIGH)
|
||||||
|
{
|
||||||
|
digitalWrite(relay, LOW);
|
||||||
|
lastSwitch = millis();
|
||||||
|
}
|
||||||
|
else if (digitalRead(relay) == LOW)
|
||||||
|
{
|
||||||
|
digitalWrite(relay, HIGH);
|
||||||
|
lastSwitch = millis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setup()
|
void setup()
|
||||||
|
|||||||
+138
-181
@@ -1,17 +1,9 @@
|
|||||||
import Highcharts, { type DashStyleValue } from 'highcharts/highstock'
|
|
||||||
|
|
||||||
import 'highcharts/modules/accessibility'
|
import * as d3 from "d3";
|
||||||
|
|
||||||
|
|
||||||
import './styles.css'
|
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 gateway = `ws://${window.location.hostname}/ws`;
|
||||||
let ws: WebSocket;
|
let ws: WebSocket;
|
||||||
@@ -20,39 +12,41 @@ const now = new Date()
|
|||||||
|
|
||||||
let container: HTMLElement | null = document.querySelector('#plot')
|
let container: HTMLElement | null = document.querySelector('#plot')
|
||||||
|
|
||||||
|
let mode = 'off'
|
||||||
let relais = 0
|
|
||||||
let relaisData = [
|
|
||||||
[now.getTime(), 0]
|
|
||||||
]
|
|
||||||
|
|
||||||
let temp = 300;
|
let temp = 300;
|
||||||
|
let relais = 0
|
||||||
let targetTemp = 300
|
let targetTemp = 300
|
||||||
let setOverShoot = 30
|
let setOverShoot = 20
|
||||||
let setUnderShoot = 30
|
let setUnderShoot = 20
|
||||||
let targetTempOverShoot = 300
|
let targetTempOverShoot = targetTemp
|
||||||
let targetTempUnderShoot = 300
|
let targetTempUnderShoot = targetTemp
|
||||||
|
|
||||||
let lastSwitch = new Date()
|
let lastSwitch = new Date()
|
||||||
let lastSwitchDuration = 0
|
let lastSwitchDuration = 0
|
||||||
|
|
||||||
const oven = document.querySelector('#oven')
|
const oven = document.querySelector('#oven')
|
||||||
const temperature = document.querySelector('#temperature')
|
const temperature = document.querySelector('#temperature')
|
||||||
const relaisSwitch = document.querySelector('#relais_switch')
|
const relaisSwitch = document.querySelector('#relais_switch')
|
||||||
const relaisSwitchInput: HTMLInputElement | null = document.querySelector('#relais_switch_input')
|
const relaisSwitchInput: HTMLInputElement | null = document.querySelector('#relais_switch_input')
|
||||||
const targetTempInput: HTMLInputElement | null = document.querySelector('#target_temperature')
|
const targetTempInput: HTMLInputElement | null = document.querySelector('#target_temperature')
|
||||||
|
const modeSpan: HTMLInputElement | null = document.querySelector('#mode_span')
|
||||||
|
const modeSelector: HTMLInputElement | null = document.querySelector('#mode_selector')
|
||||||
|
const prepareDownload: HTMLElement | null = document.querySelector('#prepare_download')
|
||||||
|
const downloadCSVButton: HTMLElement | null = document.querySelector('#download_csv')
|
||||||
|
|
||||||
let spoofInterval = 10
|
let spoofInterval = 250
|
||||||
let speedFactor = 250 / spoofInterval
|
let speedFactor = 250 / spoofInterval
|
||||||
const switchDelay = 60 / speedFactor * 1000
|
const switchDelay = (100 / speedFactor) * 1000
|
||||||
|
|
||||||
let maxValues = 4 * 60 * 60 // 60 minutes (4 values/sec)
|
let maxValues = 4 * 60 * 60 // 60 minutes (4 values/sec)
|
||||||
|
|
||||||
let hs: Highcharts.StockChart;
|
|
||||||
let hsOptions: Highcharts.Options
|
|
||||||
|
|
||||||
let temperatureData = [
|
let temperatureData = [
|
||||||
[now.getTime(), temp]
|
[now.getTime(), temp]
|
||||||
]
|
]
|
||||||
|
let relaisData = [
|
||||||
|
[now.getTime(), 0]
|
||||||
|
]
|
||||||
|
|
||||||
const getRelaisBands = () => {
|
const getRelaisBands = () => {
|
||||||
let relaisBands = []
|
let relaisBands = []
|
||||||
@@ -76,134 +70,61 @@ const getRelaisBands = () => {
|
|||||||
return relaisBands
|
return relaisBands
|
||||||
}
|
}
|
||||||
|
|
||||||
const dashStyle: DashStyleValue = 'Dash'
|
|
||||||
const getTargetLines = () => {
|
// set the dimensions and margins of the graph
|
||||||
return [
|
const margin = { top: 10, right: 30, bottom: 30, left: 60 };
|
||||||
{ value: targetTemp, width: 3, dashStyle: dashStyle },
|
const width = 900 - margin.left - margin.right;
|
||||||
{ value: targetTempOverShoot, width: 1, dashStyle: dashStyle, color: "red" },
|
const height = 500 - margin.top - margin.bottom;
|
||||||
{ value: targetTempUnderShoot, width: 1, dashStyle: dashStyle, color: "red" }
|
let svg: d3.Selection<SVGGElement, unknown, null, undefined>;
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const createChart = () => {
|
const createChart = () => {
|
||||||
hsOptions = {
|
|
||||||
chart: {
|
|
||||||
height: '650px',
|
|
||||||
animation: {
|
|
||||||
duration: 250,
|
|
||||||
easing: 'linear'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
credits: {
|
// append the svg object to the body of the page
|
||||||
enabled: false
|
svg = d3.select(container)
|
||||||
},
|
.append("svg")
|
||||||
|
.attr("width", width + margin.left + margin.right)
|
||||||
|
.attr("height", height + margin.top + margin.bottom)
|
||||||
|
.append("g")
|
||||||
|
.attr("transform",
|
||||||
|
"translate(" + margin.left + "," + margin.top + ")");
|
||||||
|
|
||||||
// title: {
|
// Declare the x (horizontal position) scale.
|
||||||
// text: 'Temperature',
|
const x = d3.scaleUtc(d3.extent(aapl, d => d.date), [margin.left, width - margin.right]);
|
||||||
// align: 'left',
|
|
||||||
// },
|
|
||||||
|
|
||||||
rangeSelector: {
|
// Declare the y (vertical position) scale.
|
||||||
enabled: false,
|
const y = d3.scaleLinear([0, d3.max(aapl, d => d.close)], [height - margin.bottom, margin.top]);
|
||||||
inputDateFormat: '%H:%M:%S'
|
|
||||||
},
|
|
||||||
|
|
||||||
yAxis: {
|
// Declare the line generator.
|
||||||
title: {
|
const line = d3.line()
|
||||||
text: '°C'
|
.x(d => x(d.date))
|
||||||
},
|
.y(d => y(d.close));
|
||||||
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: {
|
// Add the x-axis.
|
||||||
type: 'datetime',
|
svg.append("g")
|
||||||
tickInterval: 30 * 1000, // 30 seconds
|
.attr("transform", `translate(0,${height - margin.bottom})`)
|
||||||
labels: {
|
.call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0));
|
||||||
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(),
|
|
||||||
|
|
||||||
},
|
// Add the y-axis, remove the domain line, add grid lines and a label.
|
||||||
|
svg.append("g")
|
||||||
|
.attr("transform", `translate(${margin.left},0)`)
|
||||||
|
.call(d3.axisLeft(y).ticks(height / 40))
|
||||||
|
.call(g => g.select(".domain").remove())
|
||||||
|
.call(g => g.selectAll(".tick line").clone()
|
||||||
|
.attr("x2", width - margin.left - margin.right)
|
||||||
|
.attr("stroke-opacity", 0.1))
|
||||||
|
.call(g => g.append("text")
|
||||||
|
.attr("x", -margin.left)
|
||||||
|
.attr("y", 10)
|
||||||
|
.attr("fill", "currentColor")
|
||||||
|
.attr("text-anchor", "start")
|
||||||
|
.text("↑ Daily close ($)"));
|
||||||
|
|
||||||
legend: {
|
// Append a path for the line.
|
||||||
enabled: false,
|
svg.append("path")
|
||||||
},
|
.attr("fill", "none")
|
||||||
|
.attr("stroke", "steelblue")
|
||||||
tooltip: {
|
.attr("stroke-width", 1.5)
|
||||||
formatter: function () {
|
.attr("d", line(aapl));
|
||||||
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 = () => {
|
const onLoad = () => {
|
||||||
@@ -214,7 +135,9 @@ const onLoad = () => {
|
|||||||
createChart()
|
createChart()
|
||||||
|
|
||||||
relaisSwitch?.addEventListener('click', switchRelais)
|
relaisSwitch?.addEventListener('click', switchRelais)
|
||||||
|
modeSelector?.addEventListener('change', changeMode)
|
||||||
targetTempInput?.addEventListener('change', changeTargetTemp)
|
targetTempInput?.addEventListener('change', changeTargetTemp)
|
||||||
|
prepareDownload?.addEventListener('click', downloadCSV)
|
||||||
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
@@ -234,13 +157,13 @@ const onLoad = () => {
|
|||||||
temp = temp + Math.random() * 0.5 - 0.25
|
temp = temp + Math.random() * 0.5 - 0.25
|
||||||
}
|
}
|
||||||
|
|
||||||
let shootFactor = Math.min(((((d.getTime() - lastSwitch.getTime()) / speedFactor) - ((lastSwitchDuration * 0.5) / speedFactor))) / ((3 * 60 * 1000) / speedFactor), 1) // 5mins
|
let shootFactor = Math.min(((((d.getTime() - lastSwitch.getTime()) / speedFactor) - ((lastSwitchDuration * 0.5) / speedFactor))) / ((5 * 60 * 1000) / speedFactor), 1) // 5mins
|
||||||
shootFactor = shootFactor
|
shootFactor = shootFactor
|
||||||
|
|
||||||
if (relais == 0) {
|
if (relais == 0) {
|
||||||
temp = temp - 0.8 * shootFactor
|
temp = temp - 0.3 * shootFactor
|
||||||
} else {
|
} else {
|
||||||
temp = temp + 1 * shootFactor
|
temp = temp + 0.3 * shootFactor
|
||||||
}
|
}
|
||||||
|
|
||||||
let mod = temp % 0.25
|
let mod = temp % 0.25
|
||||||
@@ -253,7 +176,6 @@ const onLoad = () => {
|
|||||||
if (temperature)
|
if (temperature)
|
||||||
temperature.innerHTML = temp.toFixed(2);
|
temperature.innerHTML = temp.toFixed(2);
|
||||||
|
|
||||||
temperatureData.push([d.getTime(), temp])
|
|
||||||
|
|
||||||
let derivedShootFactor = Math.min(((d.getTime() - lastSwitch.getTime()) / speedFactor) / ((3 * 60 * 1000) / speedFactor), 1) // 5mins
|
let derivedShootFactor = Math.min(((d.getTime() - lastSwitch.getTime()) / speedFactor) / ((3 * 60 * 1000) / speedFactor), 1) // 5mins
|
||||||
|
|
||||||
@@ -266,47 +188,47 @@ const onLoad = () => {
|
|||||||
targetTempOverShoot = targetTemp - dirivedOvershoot;
|
targetTempOverShoot = targetTemp - dirivedOvershoot;
|
||||||
targetTempUnderShoot = targetTemp + dirivedUndershoot;
|
targetTempUnderShoot = targetTemp + dirivedUndershoot;
|
||||||
|
|
||||||
if ((d.getTime() - lastSwitch.getTime()) > switchDelay) {
|
if (mode === 'auto_switch') {
|
||||||
if (temp > targetTempOverShoot) // > 270 °C
|
if ((d.getTime() - lastSwitch.getTime()) > switchDelay) {
|
||||||
{
|
if (temp > targetTempOverShoot) // > 270 °C
|
||||||
if (relais == 1) {
|
{
|
||||||
relais = 0;
|
if (relais == 1) {
|
||||||
oven?.classList.remove('on')
|
relais = 0;
|
||||||
relaisSwitch?.classList.remove('on')
|
oven?.classList.remove('on')
|
||||||
if (relaisSwitchInput)
|
relaisSwitch?.classList.remove('on')
|
||||||
relaisSwitchInput.checked = false
|
if (relaisSwitchInput)
|
||||||
|
relaisSwitchInput.checked = false
|
||||||
|
|
||||||
targetTempOverShoot = targetTemp
|
targetTempOverShoot = targetTemp
|
||||||
targetTempUnderShoot = targetTemp
|
targetTempUnderShoot = targetTemp
|
||||||
lastSwitchDuration = d.getTime() - lastSwitch.getTime()
|
lastSwitchDuration = d.getTime() - lastSwitch.getTime()
|
||||||
lastSwitch = new Date()
|
lastSwitch = new Date()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if ((d.getTime() - lastSwitch.getTime()) > switchDelay) {
|
||||||
if ((d.getTime() - lastSwitch.getTime()) > switchDelay) {
|
if (temp < targetTempUnderShoot) // < 310°C
|
||||||
if (temp < targetTempUnderShoot) // < 310°C
|
{
|
||||||
{
|
if (relais == 0) {
|
||||||
if (relais == 0) {
|
relais = 1;
|
||||||
relais = 1;
|
oven?.classList.add('on')
|
||||||
oven?.classList.add('on')
|
relaisSwitch?.classList.add('on')
|
||||||
relaisSwitch?.classList.add('on')
|
if (relaisSwitchInput)
|
||||||
if (relaisSwitchInput)
|
relaisSwitchInput.checked = true
|
||||||
relaisSwitchInput.checked = true
|
|
||||||
|
|
||||||
targetTempOverShoot = targetTemp
|
targetTempOverShoot = targetTemp
|
||||||
targetTempUnderShoot = targetTemp
|
targetTempUnderShoot = targetTemp
|
||||||
lastSwitchDuration = d.getTime() - lastSwitch.getTime()
|
lastSwitchDuration = d.getTime() - lastSwitch.getTime()
|
||||||
lastSwitch = new Date()
|
lastSwitch = new Date()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
temperatureData.push([d.getTime(), temp])
|
||||||
relaisData.push([d.getTime(), relais])
|
relaisData.push([d.getTime(), relais])
|
||||||
|
|
||||||
hs.xAxis[0].update({ plotBands: getRelaisBands() })
|
//update chart
|
||||||
hs.yAxis[0].update({ plotLines: getTargetLines() })
|
|
||||||
hs.series[0].setData(temperatureData, true)
|
|
||||||
}, spoofInterval)
|
}, spoofInterval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -382,11 +304,14 @@ const onMessage = (event: MessageEvent) => {
|
|||||||
targetTempOverShoot = myObj[key]
|
targetTempOverShoot = myObj[key]
|
||||||
} else if (key === 'derived_undershoot') {
|
} else if (key === 'derived_undershoot') {
|
||||||
targetTempUnderShoot = myObj[key]
|
targetTempUnderShoot = myObj[key]
|
||||||
|
} else if (key === 'mode') {
|
||||||
|
mode = myObj[key]
|
||||||
|
if (modeSpan) {
|
||||||
|
modeSpan.innerText = mode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hs.xAxis[0].update({ plotBands: getRelaisBands() })
|
|
||||||
hs.yAxis[0].update({ plotLines: getTargetLines() })
|
|
||||||
hs.series[0].setData(temperatureData, true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,3 +353,35 @@ const changeTargetTemp = (e: Event) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const changeMode = (e: Event) => {
|
||||||
|
const target = e.target as HTMLTextAreaElement;
|
||||||
|
let value = target?.value
|
||||||
|
console.log('Mode set to ' + value)
|
||||||
|
if (!import.meta.env.DEV) {
|
||||||
|
ws.send("setMode: " + value);
|
||||||
|
} else {
|
||||||
|
mode = value
|
||||||
|
if (modeSpan) {
|
||||||
|
modeSpan.innerText = mode + " ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadCSV = () => {
|
||||||
|
let csvContent = 'Time,Temp,Relais\n'
|
||||||
|
|
||||||
|
console.log(temperatureData.length, relaisData.length)
|
||||||
|
|
||||||
|
for (let [i, temp] of temperatureData.entries()) {
|
||||||
|
|
||||||
|
csvContent += `${temp[0]},${temp[1]},${relaisData[i] ? relaisData[i][1] : ''}\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8,' })
|
||||||
|
const objUrl = URL.createObjectURL(blob)
|
||||||
|
if (downloadCSVButton) {
|
||||||
|
downloadCSVButton.classList.remove('hidden')
|
||||||
|
downloadCSVButton.setAttribute('href', objUrl)
|
||||||
|
downloadCSVButton.setAttribute('download', 'data.csv')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import '../app.css';
|
||||||
|
|
||||||
|
|
||||||
|
import favicon from '$lib/assets/favicon.ico';
|
||||||
|
|
||||||
|
|
||||||
|
let { children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<link rel="icon" href={favicon} />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
{@render children?.()}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const prerender = true
|
||||||
@@ -0,0 +1,522 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
import Chart from 'chart.js/auto';
|
||||||
|
import 'chartjs-adapter-date-fns';
|
||||||
|
|
||||||
|
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||||
|
|
||||||
|
import { nl } from 'date-fns/locale';
|
||||||
|
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
|
import { pad } from '$lib/utils';
|
||||||
|
import { page } from '$app/state';
|
||||||
|
|
||||||
|
let gateway = `ws://${page.url.host}/ws`;
|
||||||
|
let ws: WebSocket;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
let mode = 'off';
|
||||||
|
let temp = 300;
|
||||||
|
let relais = 0;
|
||||||
|
let targetTemp = 300;
|
||||||
|
let setOverShoot = 20;
|
||||||
|
let setUnderShoot = 20;
|
||||||
|
let targetTempOverShoot = targetTemp;
|
||||||
|
let targetTempUnderShoot = targetTemp;
|
||||||
|
|
||||||
|
let lastSwitch = new Date();
|
||||||
|
let lastSwitchDuration = 0;
|
||||||
|
|
||||||
|
let canvas: HTMLCanvasElement;
|
||||||
|
|
||||||
|
let pauseGraphUpdate = false;
|
||||||
|
let spoofInterval = 250;
|
||||||
|
let speedFactor = 250 / spoofInterval;
|
||||||
|
const switchDelay = (30 / speedFactor) * 1000;
|
||||||
|
const pwmDelay = (5 / speedFactor) * 1000;
|
||||||
|
|
||||||
|
let maxValues = 4 * 60 * 60; // 60 minutes (4 values/sec)
|
||||||
|
|
||||||
|
let temperatureData = [];
|
||||||
|
let relaisData = [];
|
||||||
|
|
||||||
|
let downloadCSVButton: HTMLElement;
|
||||||
|
let modeSpan: HTMLElement;
|
||||||
|
|
||||||
|
const getRelaisBands = () => {
|
||||||
|
let relaisBands = {};
|
||||||
|
let active = false;
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
for (let r of relaisData) {
|
||||||
|
let key = 'relais' + count;
|
||||||
|
if (r[1] === 1 && !active) {
|
||||||
|
active = true;
|
||||||
|
relaisBands[key] = {
|
||||||
|
type: 'box',
|
||||||
|
backgroundColor: 'rgba(255, 0, 0, 0.1)',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 0, 0, 0.5)',
|
||||||
|
drawTime: 'beforeDraw',
|
||||||
|
|
||||||
|
xMax: relaisData[relaisData.length - 1][0] + 10,
|
||||||
|
xMin: r[0]
|
||||||
|
// label: {
|
||||||
|
// display: true,
|
||||||
|
// content: 'Heat',
|
||||||
|
// position: {
|
||||||
|
// x: 'center',
|
||||||
|
// y: 'start'
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (r[1] === 0 && active) {
|
||||||
|
relaisBands[key]['xMax'] = r[0];
|
||||||
|
active = false;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return relaisBands;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAnnotations = () => {
|
||||||
|
return {
|
||||||
|
annotations: {
|
||||||
|
limitGreen: {
|
||||||
|
type: 'box',
|
||||||
|
drawTime: 'beforeDraw',
|
||||||
|
backgroundColor: 'rgba(0, 255, 133, 0.25)',
|
||||||
|
borderWidth: 0,
|
||||||
|
yMax: 0,
|
||||||
|
yMin: 400
|
||||||
|
},
|
||||||
|
limitOrange: {
|
||||||
|
type: 'box',
|
||||||
|
drawTime: 'beforeDraw',
|
||||||
|
backgroundColor: 'rgba(255, 138, 0 ,0.25)',
|
||||||
|
borderWidth: 0,
|
||||||
|
yMax: 400,
|
||||||
|
yMin: 500
|
||||||
|
},
|
||||||
|
limitRed: {
|
||||||
|
type: 'box',
|
||||||
|
drawTime: 'beforeDraw',
|
||||||
|
|
||||||
|
backgroundColor: 'rgba(255, 0, 0, 0.25)',
|
||||||
|
borderWidth: 0,
|
||||||
|
yMax: 500,
|
||||||
|
yMin: 600
|
||||||
|
},
|
||||||
|
targetTemp: {
|
||||||
|
type: 'line',
|
||||||
|
drawTime: 'beforeDraw',
|
||||||
|
borderColor: 'black',
|
||||||
|
borderWidth: 2,
|
||||||
|
scaleID: 'y',
|
||||||
|
value: targetTemp,
|
||||||
|
borderDash: [6, 6],
|
||||||
|
borderDashOffset: 0,
|
||||||
|
label: {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
display: true,
|
||||||
|
color: 'rgba(0,0,0, 1)',
|
||||||
|
content: 'Target temp',
|
||||||
|
position: 'center',
|
||||||
|
yAdjust: -9
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...getRelaisBands()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let chart: Chart;
|
||||||
|
Chart.register(annotationPlugin);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
initWebSocket();
|
||||||
|
|
||||||
|
chart = new Chart(canvas, {
|
||||||
|
type: 'line',
|
||||||
|
options: {
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
mode: 'nearest',
|
||||||
|
intersect: false,
|
||||||
|
animation: false
|
||||||
|
},
|
||||||
|
annotation: getAnnotations()
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'time',
|
||||||
|
time: {
|
||||||
|
tooltipFormat: 'HH:mm:ss',
|
||||||
|
|
||||||
|
displayFormats: {
|
||||||
|
millisecond: 'HH:mm:ss.S',
|
||||||
|
second: 'HH:mm:ss',
|
||||||
|
minute: 'HH:mm',
|
||||||
|
hour: 'HH:mm'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
adapters: {
|
||||||
|
date: {
|
||||||
|
locale: nl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
title: 'Temperature C',
|
||||||
|
min: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
labels: temperatureData.map((row) => row[0]),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Temperature',
|
||||||
|
data: temperatureData.map((row) => row[1]),
|
||||||
|
fill: true,
|
||||||
|
tension: 0.25
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
// spoof some test data
|
||||||
|
setInterval(() => {
|
||||||
|
if (temperatureData.length > maxValues) {
|
||||||
|
temperatureData.shift();
|
||||||
|
relaisData.shift();
|
||||||
|
}
|
||||||
|
let d = new Date();
|
||||||
|
|
||||||
|
if (temp < targetTemp / 2) {
|
||||||
|
temp = temp + Math.random() * 0.5 - 0.125;
|
||||||
|
} else if (temp < targetTemp / (3 / 2)) {
|
||||||
|
temp = temp + Math.random() * 0.5 - 0.15;
|
||||||
|
} else {
|
||||||
|
temp = temp + Math.random() * 0.5 - 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
let shootFactor = Math.min(
|
||||||
|
((d.getTime() - lastSwitch.getTime()) / speedFactor -
|
||||||
|
(lastSwitchDuration * 0.25) / speedFactor) /
|
||||||
|
((3 * 60 * 1000) / speedFactor),
|
||||||
|
1
|
||||||
|
); // 5mins
|
||||||
|
shootFactor = shootFactor;
|
||||||
|
|
||||||
|
if (relais == 0) {
|
||||||
|
temp = temp - 0.3 * shootFactor;
|
||||||
|
} else {
|
||||||
|
temp = temp + 0.3 * shootFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mod = temp % 0.25;
|
||||||
|
if (mod > 0.125) {
|
||||||
|
temp = temp - mod + 0.25;
|
||||||
|
} else {
|
||||||
|
temp = temp - mod;
|
||||||
|
}
|
||||||
|
|
||||||
|
let derivedShootFactor = Math.min(
|
||||||
|
(d.getTime() - lastSwitch.getTime()) / speedFactor / ((3 * 60 * 1000) / speedFactor),
|
||||||
|
1
|
||||||
|
); // 5mins
|
||||||
|
|
||||||
|
let overshoot = setOverShoot * derivedShootFactor; // derive over 5 mins -> 1 min eq. 3°C, 5mins eq. 15°C
|
||||||
|
let dirivedOvershoot = Math.min(overshoot, setOverShoot);
|
||||||
|
|
||||||
|
let undershoot = setUnderShoot * derivedShootFactor; // derive over 5 min -> 1 min eq. 3°C, 5mins eq. 15°C
|
||||||
|
let dirivedUndershoot = Math.min(undershoot, setUnderShoot);
|
||||||
|
|
||||||
|
targetTempOverShoot = targetTemp - dirivedOvershoot;
|
||||||
|
targetTempUnderShoot = targetTemp + dirivedUndershoot;
|
||||||
|
|
||||||
|
if (mode === 'auto_switch') {
|
||||||
|
if (d.getTime() - lastSwitch.getTime() > switchDelay) {
|
||||||
|
if (temp > targetTempOverShoot) {
|
||||||
|
// > 270 °C
|
||||||
|
if (relais == 1) {
|
||||||
|
relais = 0;
|
||||||
|
|
||||||
|
targetTempOverShoot = targetTemp;
|
||||||
|
targetTempUnderShoot = targetTemp;
|
||||||
|
lastSwitchDuration = d.getTime() - lastSwitch.getTime();
|
||||||
|
lastSwitch = new Date();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (d.getTime() - lastSwitch.getTime() > switchDelay) {
|
||||||
|
if (temp < targetTempUnderShoot) {
|
||||||
|
// < 310°C
|
||||||
|
if (relais == 0) {
|
||||||
|
relais = 1;
|
||||||
|
|
||||||
|
targetTempOverShoot = targetTemp;
|
||||||
|
targetTempUnderShoot = targetTemp;
|
||||||
|
lastSwitchDuration = d.getTime() - lastSwitch.getTime();
|
||||||
|
lastSwitch = new Date();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === 'pwm') {
|
||||||
|
if (d.getTime() - lastSwitch.getTime() > pwmDelay) {
|
||||||
|
if (relais) {
|
||||||
|
relais = 0;
|
||||||
|
lastSwitch = new Date();
|
||||||
|
} else if (!relais) {
|
||||||
|
relais = 1;
|
||||||
|
lastSwitch = new Date();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
temperatureData.push([d.getTime(), temp]);
|
||||||
|
relaisData.push([d.getTime(), relais]);
|
||||||
|
|
||||||
|
if (!pauseGraphUpdate) {
|
||||||
|
chart.options.plugins.annotation = getAnnotations();
|
||||||
|
chart.data.labels.push(d.getTime());
|
||||||
|
chart.data.datasets.forEach((dataset) => {
|
||||||
|
dataset.data.push(temp);
|
||||||
|
});
|
||||||
|
chart.update('none');
|
||||||
|
}
|
||||||
|
}, spoofInterval);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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]]);
|
||||||
|
temp = myObj[key];
|
||||||
|
} else if (key === 'relais') {
|
||||||
|
if (relaisData.length > maxValues) {
|
||||||
|
relaisData.shift();
|
||||||
|
}
|
||||||
|
relaisData.push([d.getTime(), myObj[key]]);
|
||||||
|
if (myObj[key]) {
|
||||||
|
relais = 1;
|
||||||
|
} else {
|
||||||
|
relais = 0;
|
||||||
|
}
|
||||||
|
} else if (key === 'target_temp') {
|
||||||
|
targetTemp = myObj[key];
|
||||||
|
} else if (key === 'derived_overshoot') {
|
||||||
|
targetTempOverShoot = myObj[key];
|
||||||
|
} else if (key === 'derived_undershoot') {
|
||||||
|
targetTempUnderShoot = myObj[key];
|
||||||
|
} else if (key === 'mode') {
|
||||||
|
mode = myObj[key];
|
||||||
|
modeSpan.innerText = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pauseGraphUpdate) {
|
||||||
|
chart.options.plugins.annotation = getAnnotations();
|
||||||
|
chart.data.labels.push(d.getTime());
|
||||||
|
chart.data.datasets.forEach((dataset) => {
|
||||||
|
dataset.data.push(temp);
|
||||||
|
});
|
||||||
|
chart.update('none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchRelais = () => {
|
||||||
|
console.log('Send switch relais');
|
||||||
|
if (!import.meta.env.DEV) {
|
||||||
|
ws.send('switchRelais');
|
||||||
|
} else {
|
||||||
|
// spoof some test data
|
||||||
|
let d = new Date();
|
||||||
|
lastSwitchDuration = d.getTime() - lastSwitch.getTime();
|
||||||
|
lastSwitch = new Date();
|
||||||
|
if (!relais) {
|
||||||
|
relais = 1;
|
||||||
|
} else {
|
||||||
|
relais = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeTargetTemp = (e: Event) => {
|
||||||
|
const target = e.target as HTMLTextAreaElement;
|
||||||
|
let value = Number(target?.value);
|
||||||
|
console.log('Changed target temp to ' + value);
|
||||||
|
if (!import.meta.env.DEV) {
|
||||||
|
ws.send('setTargetTemp: ' + pad(value, 3));
|
||||||
|
} else {
|
||||||
|
targetTemp = value;
|
||||||
|
targetTempOverShoot = value;
|
||||||
|
targetTempUnderShoot = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeMode = (e: Event) => {
|
||||||
|
const target = e.target as HTMLTextAreaElement;
|
||||||
|
let value = target?.value;
|
||||||
|
console.log('Mode set to ' + value);
|
||||||
|
if (!import.meta.env.DEV) {
|
||||||
|
ws.send('setMode: ' + value);
|
||||||
|
} else {
|
||||||
|
mode = value;
|
||||||
|
modeSpan.innerText = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const prepareDownloadCSV = () => {
|
||||||
|
let csvContent = 'Time,Temp,Relais\n';
|
||||||
|
|
||||||
|
for (let [i, temp] of temperatureData.entries()) {
|
||||||
|
csvContent += `${temp[0]},${temp[1]},${relaisData[i] ? relaisData[i][1] : ''}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8,' });
|
||||||
|
const objUrl = URL.createObjectURL(blob);
|
||||||
|
if (downloadCSVButton) {
|
||||||
|
downloadCSVButton.classList.remove('hidden');
|
||||||
|
downloadCSVButton.setAttribute('href', objUrl);
|
||||||
|
downloadCSVButton.setAttribute('download', 'data.csv');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="nav">
|
||||||
|
<div class="nav-title">CrustCraft</div>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="card-grid">
|
||||||
|
<div class="card">
|
||||||
|
<p class="card-title">Sensor Temperature</p>
|
||||||
|
<p class="reading">{temp.toFixed(2)} °C</p>
|
||||||
|
</div>
|
||||||
|
<button class="card relais" onclick={switchRelais}>
|
||||||
|
<p>Relais</p>
|
||||||
|
<div class="onoffswitch">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="onoffswitch"
|
||||||
|
class="onoffswitch-checkbox h-0"
|
||||||
|
id="relais_switch_input"
|
||||||
|
tabindex="0"
|
||||||
|
checked={relais}
|
||||||
|
/>
|
||||||
|
<label class="onoffswitch-label" for="relais_switch_input">
|
||||||
|
<span class="onoffswitch-inner"></span>
|
||||||
|
<span class="onoffswitch-switch"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="oven {relais ? 'on' : ''}" id="oven">
|
||||||
|
<div class="oven-wrapper">
|
||||||
|
<div class="oven-img-wrapper">
|
||||||
|
<img src="/oven_small.png" alt="Oven" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="graph">
|
||||||
|
<div class="plot-wrapper w-[800px]">
|
||||||
|
<div id="plot"></div>
|
||||||
|
<canvas bind:this={canvas} id="acquisitions"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-grid settings">
|
||||||
|
<div class="card">
|
||||||
|
<p class="card-title">Target temperature</p>
|
||||||
|
<p class="reading">
|
||||||
|
<input
|
||||||
|
class="min-w-[88px] rounded"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="600"
|
||||||
|
placeholder="300"
|
||||||
|
id="target_temperature"
|
||||||
|
onchange={changeTargetTemp}
|
||||||
|
/>
|
||||||
|
<label for="target_temperature"> °C </label>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<p class="card-title">
|
||||||
|
Mode: <span bind:this={modeSpan} class="mode" id="mode_span">Manual</span>
|
||||||
|
</p>
|
||||||
|
<p class="reading">
|
||||||
|
<select class="rounded" name="modes" id="mode_selector" onchange={changeMode}>
|
||||||
|
<option value="auto_switch">Auto Switch</option>
|
||||||
|
<option value="pwm">PWM</option>
|
||||||
|
<option value="off" selected>Manual</option>
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card flex cursor-pointer gap-2">
|
||||||
|
<button
|
||||||
|
class="cursor-pointer rounded border p-2"
|
||||||
|
onclick={() => (pauseGraphUpdate = !pauseGraphUpdate)}>Pause Graph</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="cursor-pointer rounded border p-2"
|
||||||
|
id="prepare_download"
|
||||||
|
onclick={prepareDownloadCSV}>Prepare Download</button
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
bind:this={downloadCSVButton}
|
||||||
|
class="hidden cursor-pointer rounded border p-2"
|
||||||
|
id="download_csv">Download CSV</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
Before Width: | Height: | Size: 517 KiB After Width: | Height: | Size: 517 KiB |
|
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 216 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
# allow crawling everything by default
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-static';
|
||||||
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
// Consult https://svelte.dev/docs/kit/integrations
|
||||||
|
// for more information about preprocessors
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
kit: {
|
||||||
|
adapter: adapter({
|
||||||
|
pages: 'data',
|
||||||
|
assets: 'data',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
+17
-27
@@ -1,29 +1,19 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
"target": "ES2022",
|
"compilerOptions": {
|
||||||
"useDefineForClassFields": true,
|
"allowJs": true,
|
||||||
"module": "ESNext",
|
"checkJs": true,
|
||||||
"lib": [
|
"esModuleInterop": true,
|
||||||
"ES2022",
|
"forceConsistentCasingInFileNames": true,
|
||||||
"DOM",
|
"resolveJsonModule": true,
|
||||||
"DOM.Iterable"
|
"skipLibCheck": true,
|
||||||
],
|
"sourceMap": true,
|
||||||
"skipLibCheck": true,
|
"strict": true,
|
||||||
/* Bundler mode */
|
"moduleResolution": "bundler"
|
||||||
"moduleResolution": "bundler",
|
}
|
||||||
"allowImportingTsExtensions": true,
|
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||||
"verbatimModuleSyntax": true,
|
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||||
"moduleDetection": "force",
|
//
|
||||||
"noEmit": true,
|
// To make changes to top-level options such as include and exclude, we recommend extending
|
||||||
/* Linting */
|
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
|
||||||
"strict": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"erasableSyntaxOnly": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedSideEffectImports": true
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
|
import devtoolsJson from 'vite-plugin-devtools-json';
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
build: {
|
plugins: [tailwindcss(), sveltekit(), devtoolsJson()]
|
||||||
outDir: 'data',
|
|
||||||
chunkSizeWarningLimit: 1000
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user