write doc

Took 2 hours 59 minutes
This commit is contained in:
Tobias Hopp 2022-11-29 20:22:42 +01:00
parent 82ab47e9fd
commit 7d9dfca62e
11 changed files with 323 additions and 159 deletions

View File

@ -5,4 +5,7 @@
- [ ] Fix Fehler, wenn keine Getränke hinzugefügt worden sind
- [ ] Schriftarten Lokal machen
- [x] Probleme beim Laden der Container im Setup
-
- [ ] Container option "Auto tare" hinzufügen
- Heißt, sobald ein Inhalt in einen Container eingestellt wird und das Volumen angegeben ist, werden die Sensoren auf den aktuellen Füllstand als 100% gesetzt
- [ ] Nach Speichern des Setups und bei Einmessen auf Schließen, Setup verlassen
- [ ]

View File

@ -1,4 +1,5 @@
# Notes und kleine Dokumentation
Was haben wir bereits am iTender Projekt gemacht?
<hr>
@ -6,6 +7,7 @@ Was haben wir bereits am iTender Projekt gemacht?
## Konzept-Erstellung
#### Ideen
- Grund-Ideen
- Smarten Cocktail-Mischer
- 4 Getränke Behälter (mit Saft, Sirup oder Likör bzw. Schnapps)
@ -13,7 +15,8 @@ Was haben wir bereits am iTender Projekt gemacht?
- Raspberry Pi als Prozessoreinheit
- Display in der Front mit Benutzeroberfläche
- Automatisches filtern von Getränken, je nachdem welche "Zutaten" in den Behältern sind
- Messung der aktuellen Füllmenge der Behälter, basierend auf Gewicht (mittels Wägezelle) oder Abstand zur Wasseroberfläche (mittels Ultraschall-Sensor)
- Messung der aktuellen Füllmenge der Behälter, basierend auf Gewicht (mittels Wägezelle) oder Abstand zur
Wasseroberfläche (mittels Ultraschall-Sensor)
- Nice to have
- LED-Stripes für schöne Beleuchtung, basierend auf dem aktuellen Status der Maschine
@ -22,10 +25,12 @@ Was haben wir bereits am iTender Projekt gemacht?
- Kühlung der Container mittels Peltierelement und Lüftern
#### Erstes 3D-Modell
<img src="./Screenshot_Model1.1_FrontTopRight.png" width="50%">
<img src="./Screenshot_Model1.1_BackDownLeft.png" width="50%">
#### Neues 3D-Modell
<img src="./Screenshot_Model1.2_Front.png" width="50%">
<img src="./Screenshot_Model1.2_Back.png" width="50%">
@ -35,12 +40,23 @@ Was haben wir bereits am iTender Projekt gemacht?
## Das Programm
#### Aufbau
- Das Programm ist aufgebaut in eine Client-Seite und eine Server-Seite
- Diese sind zur Sicherheit des Geräts voneinander getrennt
- Server und Client kommunizieren über einen WebSocket, welchen man sich als eine Art Chat-Kanal vorstellen kann
- Client und Server haben bestimmte Status, ein Status ist beispielsweise READY oder FILLING
- Die Oberfläche ist sowohl über das Display, aber auch über ein Tablet steuerbar
- Oberfläche sendet Befehle an den Server → Server verarbeitet und gibt ggfs. eine Antwort
- Oberfläche sendet Befehle an den Server → Server verarbeitet und gibt ggf. eine Antwort
- Bei jedem start wird der Status des Netzwerkes erfasst
- Sollte Netzwerk zu verfügung stehen, versucht iTender die Getränke vom Hauptserver (im Internet) zu aktualisieren
- Somit kommen auch beim bestehenden Produkt immer neue Getränke und mögliche Zutaten dazu
- Nach jedem mischen und alle 5 minuten werden die Füllstände der Behälter erfasst
- Danach werden auch die verfügbaren Cocktails berechnet, diese werden dann im Webinterface angezeigt
#### Code fakten
- Inzwischen hat der Programmiercode 14492 Zeilen
- In Stunden wurde das Projekt (Stand 29.11) ~80 Stunden programmiert
#### Fotos des Webinterfaces (Stand 21.11)
@ -48,27 +64,55 @@ Was haben wir bereits am iTender Projekt gemacht?
<br>
Die Main Pane ist der Hauptteil und direkt die Einstiegsseite des iTenders<br>
Hier können Getränke ausgewählt werden, welche dann "gemacht" werden
<img src="./v1Main.png">
<img src="./v1Main.png" width="50%">
<br><br>
<strong>Menu</strong><br>
Das Menü ist das Navigationsherz, von hier aus können alle anderen Panels erreicht werden<br>
<img src="./v1Menu.png">
<img src="./v1Menu.png" width="50%">
<br><br>
<strong>Containers</strong><br>
Hier können die Behälter inhalte aktualisiert werden<br>
Man wählt die "Zutat" aus und danach wie voll der Behälter nun ist<br>
In der Regel kann das auch automatisch eingemessen werden, wenn alle Sensoren eingestellt sind<br>
<br>Trotzdem sollte das hier eingestellt werden
<img src="./v1Containers.png">
<img src="./v1Containers.png" width="50%">
<br><br>
<strong>Fill</strong><br>
Einfach ein "Popup" welches anzeigt dass das Getränk gefüllt wird
<img src="./v1Fill.png">
<img src="./v1Fill.png" width="50%">
<br><br>
<strong>Setup</strong><br>
Das Setup ist das erste menü was nach dem ersten einrichten erscheint<br>
es dient zur Grundkonfiguration
<img src="./v1Setup.png">
<img src="./v1Setup.png" width="50%">
<br><br>
<hr>
<br><br>
### Erklärung der Dateien und Modulen
| Context | Datei | Beschreibung |
|-----------|---------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|
| iTender | main | Die Hauptdatei, sie ist der Einstiegspunkt des Programms, von hier aus wird die Datenbank verbunden und der Webserver gestartet |
| iTender | MyGPIO | Die eigene GPIO Library von Tobias Hopp. Sie dient zum Steuern von GPIO-Pins am Raspberry Pi |
| iTender | Utils | Hier befinden sich einige schnelle Funktionen, welche des öfteren von mehreren Methoden im Programm genutzt werden |
| iTender | RejectReason, SensorType, RequestType | Ein paar Klassen welche Enums (sogenannte feste Platzhalter für Variablen) bereitstellen |
| iTender | SensorType | Die Art des Sensors, aktuell sind Ultraschallsensoren und Wäge sensoren unterstützt |
| Webserver | App | Die App ist die Instanz des Webservers, welcher für die Oberfläche genutzt wird, dieser wird vom iTender selbst, sowie etwaigen anderen Geräten aufgerufen |
| Webserver | WebsocketApp | Ähnlich wie die normale App, nur spezifisch für die Direktverbindung zwischen Oberfläche und Server |
| iTender | LEDHandler | Übernimmt die Steuerung der WS2812b LEDs |
| iTender | Category | Kategorie des Getränks |
| iTender | Settings | Die Einstellungen des iTenders, werden gespeichert in der config.json |
| Webseite | error.pug, index.pug, layout.pug | Die statische Webseite für den iTender, diese Oberfläche liest die JavaScript Dateien ein und verbindet sich dann mit dem WebSocket (mit dem iTender) |
| Compiler | dist/ Ordner | Hier sind alle kompilierten Dateien zu finden |
| Webserver | web/main | Der Einstiegspunkt für die Weboberfläche, von hier aus wird mit dem WebSocket verbunden |
| | | |

View File

@ -48,6 +48,32 @@ body {
user-select: none; /* Standard syntax */
}
#blockPanel {
z-index: 999;
position: fixed;
top:0;
left:0;
background-color: rgba(0,0,0,0.9);
width: 100vw;
height: 100vh;
;
transition: opacity 0.4s;;
}
.opacityOutDisplayNone {
animation: opacityOutDisplayNone 0.4s linear forwards;
}
@keyframes opacityOutDisplayNone {
0% {
opacity: 1;
}
100% {
opacity: 0;
height: 0;
}
}
h1 {
font-size: 1.74em;

View File

@ -42,6 +42,8 @@ export class Utils {
reject(new Error(`Request Failed With a Status Code: ${res.statusCode}`));
}
}).on("error", (e) => {
reject(new Error("Request failed " + e))
});
});
}

View File

@ -8,6 +8,6 @@ export enum WebSocketEvent {
SETUP = "SETUP",
REQUEST = "REQUEST",
RESPONSE = "RESPONSE",
FILL = "FILL",
CANCEL = "CANCEL"
CANCEL = "CANCEL",
ERROR = "ERROR",
}

View File

@ -17,7 +17,6 @@ import {IIngredient} from "./database/IIngredient";
import Ingredient from "./database/Ingredient";
import {clearInterval} from "timers";
import {RejectReason} from "./RejectReason";
import {Settings} from "./Settings";
import axios from "axios";
import GPIO from "rpi-gpio";
import {MyGPIO} from "./MyGPIO";
@ -27,6 +26,10 @@ const isPI = require("detect-rpi");
const log = debug("itender:station");
const mixLog = debug("itender:mix");
/**
* The main class of the itender, here a located all main features of the system, like starting pumps, firing events and stuff
*/
export class iTender {
private static secondsPer100ml: number = 35.3335;
@ -34,6 +37,9 @@ export class iTender {
return this._drinks;
}
/**
* The current job of the itender
*/
static get currentJob(): IJob | null {
return this._currentJob;
}
@ -43,6 +49,9 @@ export class iTender {
private static _jobCheckInterval: NodeJS.Timer;
private static _internetConnection: boolean = false;
/**
* Returns true if internet connection is active
*/
static get internetConnection(): boolean {
return this._internetConnection;
}
@ -60,6 +69,11 @@ export class iTender {
return this._status;
}
/**
* This method is fired if the user likes to mix a drink
* @param data
*/
static onReceiveFill(data: { drink: IDrink, amounts?: { ingredient: String, amount: number }[], amount?: number }): Promise<IJob> {
return new Promise(async (resolve, reject) => {
mixLog("Receiving fill");
@ -71,6 +85,7 @@ export class iTender {
const job = new Job();
let amounts: { ingredient: IIngredient, amount: number, container?: IContainer }[] = [];
job.completeAmount = 0;
if (data.amounts) {
@ -121,8 +136,7 @@ export class iTender {
console.log(amounts);
job.drink = drink
job.amounts = amounts as { ingredient: IIngredient, amount: number, container: IContainer }[];
if( job.estimatedTime < 0.5 )
{
if (job.estimatedTime < 0.5) {
job.estimatedTime = 1;
}
await job.save()
@ -134,6 +148,11 @@ export class iTender {
}
/**
* Start the internal fill method
* @param job
*/
static async startFill(job: IJob) {
job.startedAt = new Date();
await job.populate([{path: "amounts.ingredient"}, {path: "amounts.container"}, {path: "drink"}]);
@ -149,7 +168,7 @@ export class iTender {
try {
await MyGPIO.setup(x.container.pumpPin, GPIO.DIR_OUT)
await MyGPIO.write(x.container.pumpPin, true );
await MyGPIO.write(x.container.pumpPin, true);
} catch (e) {
if (isPI()) {
log("[ERROR] GPIO I/O Error " + e);
@ -177,7 +196,7 @@ export class iTender {
mixLog(`Stopping output of pump ${x.container.pumpPin}`);
// Stop pump here
try {
await MyGPIO.write(x.container.pumpPin, false );
await MyGPIO.write(x.container.pumpPin, false);
} catch (e) {
if (isPI()) {
log("[ERROR] GPIO I/O Error " + e);
@ -204,11 +223,15 @@ export class iTender {
job.successful = true;
await job.save();
mixLog("Job successful");
setTimeout( () => iTender.setStatus(iTenderStatus.READY), 3000 )
setTimeout(() => iTender.setStatus(iTenderStatus.READY), 3000)
}, 500);
}
/**
* Cancel the fill
*/
static async cancelFill() {
if (!this._currentJob || this.status != iTenderStatus.FILLING)
return;
@ -221,7 +244,7 @@ export class iTender {
for (let x of this._currentJob.amounts) {
// stop pump pin
try {
await MyGPIO.write(x.container.pumpPin, false );
await MyGPIO.write(x.container.pumpPin, false);
} catch (e) {
}
@ -231,6 +254,10 @@ export class iTender {
}
/**
* Measure all containers based on their sensor values
*/
static measureContainers(): Promise<void> {
log("Measuring containers...");
@ -269,6 +296,11 @@ export class iTender {
});
}
/**
* Refresh the drinks to the local variable
* Check which drinks can be done, based on the current container ingredients
*/
static refreshDrinks(): Promise<void> {
log("Refreshing drinks...");
return new Promise(async resolve => {
@ -326,6 +358,10 @@ export class iTender {
try {
const requestIngredients = await axios.get("https://itender.iif.li/api/ingredients");
let serverIngredients = requestIngredients.data as IIngredient[];
if (serverIngredients.length == 0) {
log("Got 0 ingredients from the server... aborting.");
throw new Error("Got 0 ingredients from the server, invalid");
}
log("Got " + serverIngredients.length + " ingredients from server");
let localIngredients = await Ingredient.find();
@ -362,6 +398,10 @@ export class iTender {
const requestDrinks = await axios.get("https://itender.iif.li/api/drinks");
let serverDrinks = requestDrinks.data as IDrink[];
if (serverDrinks.length == 0) {
log("Got 0 drinks from the server... aborting.");
throw new Error("Got 0 drinks from the server, invalid");
}
log("Got " + serverDrinks.length + " drinks from server");
@ -405,16 +445,16 @@ export class iTender {
log("Drink " + remote.name + " failed to download thumbnail! (" + url + ") | " + e);
}
}
}
} catch (e) {
console.error(e);
console.error("Could not refresh drinks " + e);
await WebSocketHandler.send(new WebSocketPayload(WebSocketEvent.ERROR, false, "Beim aktualisieren der Getränke ist ein Netzwerk-Fehler aufgetreten.<br>Bitte später erneut versuchen!"));
}
iTender.setStatus(iTenderStatus.READY);
resolve();
iTender.refreshDrinks();
await iTender.refreshDrinks();
});
}

View File

@ -8,6 +8,10 @@ import {WebSocketPayload} from "../WebSocketPayload";
import {WebSocketEvent} from "../WebSocketEvent";
export class Containers {
/**
* Open the menu for the container ingredient setup
*/
static openMenu() {
let modal = new Modal("containers", "Behälter aktualisieren");
let txt = document.createElement("p");
@ -24,6 +28,7 @@ export class Containers {
btnSave.disabled = true;
let containerVolumes: Record<any, number> = {};
let containers: Record<string, IContainer> = {};
let volume = document.createElement("span");
volume.innerText = "";
@ -35,6 +40,7 @@ export class Containers {
volumeSlider.style.visibility = "hidden";
volumeSlider.id = "containers_volumeSlider"
// When volume slider is changed
function onChange() {
volume.innerText = volumeSlider.value + " ml ";
txt.innerText = "Speichern zum abschließen"
@ -52,6 +58,8 @@ export class Containers {
let selectIngredient = document.createElement("select");
selectIngredient.style.visibility = "hidden";
selectIngredient.classList.add("input");
// When ingredient is changed
selectIngredient.onchange = () => {
if (selectIngredient.value == "null") {
volumeSlider.value = "0";
@ -78,27 +86,40 @@ export class Containers {
let selectContainer = document.createElement("select");
selectContainer.classList.add("input");
//let containers : IContainer[] = [];
selectContainer.onchange = () => {
// Enable select ingredient field and set max and min to the slider
selectIngredient.style.visibility = "visible";
volumeSlider.max = String(containerVolumes[selectContainer.value]);
volumeSlider.min = String(0);
volumeSlider.value = String(containerVolumes[selectContainer.value] / 2);
txt.innerText = "Ingredient des Behälters auswählen";
// When content of container is filled, preselect the ingredient selector
if (containers[selectContainer.value].content) {
selectIngredient.value = containers[selectContainer.value].content?._id;
let event = new Event('change', {bubbles: true});
selectIngredient.dispatchEvent(event);
}
}
selectContainer.append(nonSelect.cloneNode(true));
selectContainer.selectedIndex = 0;
WebWebSocketHandler.request(RequestType.CONTAINERS).then((payload) => {
for (let container of (payload.data["content"] as IContainer[])) {
containerVolumes[container._id] = container.volume;
let option = document.createElement("option");
option.value = container._id;
option.innerText = "Behälter Slot " + container.slot + "[" + (container.content && container.content.name ? container.content.name : "Kein Inhalt") + "]";
option.innerText = "Behälter Slot " + (container.slot+1) + "[" + (container.content && container.content.name ? container.content.name : "Kein Inhalt") + "]";
selectContainer.append(option);
containers[container._id] = container;
}
//containers = payload.data["content"] as IContainer[];
});
WebWebSocketHandler.request(RequestType.INGREDIENTS).then((payload) => {
for (let ingredient of (payload.data["content"] as IIngredient[])) {
@ -134,7 +155,14 @@ export class Containers {
ingredient: (selectIngredient.value == "null") ? null : selectIngredient.value,
filled: volumeSlider.value
});
WebWebSocketHandler.send(payload).then(() => modal.close());
WebWebSocketHandler.send(payload).then(() => {
selectContainer.value = "-1";
selectIngredient.value = "-1";
let event = new Event('change', {bubbles: true});
selectContainer.dispatchEvent(event);
selectIngredient.dispatchEvent(event);
});
};
modal.open();

101
src/web/Fill.ts Normal file
View File

@ -0,0 +1,101 @@
import {WebSocketPayload} from "../WebSocketPayload";
import {Modal} from "./Modal";
import {WebSocketEvent} from "../WebSocketEvent";
import {RequestType} from "../RequestType";
import {IJob} from "../database/IJob";
import {WebWebSocketHandler} from "./WebWebSocketHandler";
export class Fill {
static onFillEvent(payload: WebSocketPayload) {
let modal = new Modal("fill", "Cocktail wird zubereitet");
let header = document.createElement("h2");
header.innerText = "";
modal.addContent(header);
let txt = document.createElement("p");
txt.innerHTML = `Der Cocktail wird gerade zubereitet`;
txt.id = "main_fillTxt";
let waterAnimDiv = document.createElement("div");
waterAnimDiv.classList.add("water");
modal.addContent(txt);
modal.addContent(waterAnimDiv);
let seconds = document.createElement("span");
seconds.innerText = "60s";
seconds.style.marginRight = "3%";
modal.addContent(seconds);
let ml = document.createElement("span");
ml.innerText = "200ml";
modal.addContent(ml);
modal.addContent(document.createElement("br"));
modal.addContent(document.createElement("br"));
let cancelBtn = document.createElement("button");
cancelBtn.classList.add("btn", "btn-danger");
cancelBtn.innerText = "Abbrechen";
cancelBtn.disabled = true;
setTimeout(() => {
cancelBtn.disabled = false;
}, 1000);
cancelBtn.onclick = () => {
cancelBtn.disabled = true;
txt.innerHTML = "Der Vorgang wird abgebrochen...";
waterAnimDiv.classList.add("waterCancel");
WebWebSocketHandler.send(new WebSocketPayload(WebSocketEvent.CANCEL));
};
modal.addContent(cancelBtn);
function riseSlowlyUp(lastNumber: number, number: number) {
for (let i = lastNumber; i < number; i++) {
setTimeout(() => {
ml.innerText = Math.floor(i) + "ml";
}, (number - lastNumber / 1000) + i * 4);
}
}
modal.open().then(() => {
WebWebSocketHandler.request(RequestType.JOB).then((payload) => {
let minus = 0;
let job = payload.data.content as IJob;
ml.innerText = Math.floor((job.completeAmount / job.estimatedTime) * minus) + "ml";
waterAnimDiv.style.setProperty("--fillTime", job.estimatedTime + "s");
waterAnimDiv.style.backgroundImage = `url("/images/${job.drink._id}.png")`;
header.innerText = job.drink.name;
seconds.innerText = Math.floor(job.estimatedTime) + "s";
let last = 0;
let interval = setInterval(() => {
minus++;
if (minus + 1 > (job.estimatedTime as number)) {
clearInterval(interval);
}
seconds.innerText = (Math.floor(job.estimatedTime as number - minus)) + "s";
let calc = Math.floor((job.completeAmount / job.estimatedTime) * minus);
riseSlowlyUp(last, calc)
last = calc;
}, 1000);
setTimeout(() => {
txt.innerHTML = "Bitte entnehme den Cocktail";
modal.title.innerHTML = "Cocktail fertig gestellt"
cancelBtn.classList.add("btn-blendout");
waterAnimDiv.classList.add("waterFinished");
cancelBtn.onclick = () => {
modal.close();
}
}, job.estimatedTime * 1000);
});
});
}
}

View File

@ -1,10 +1,13 @@
import {ButtonType} from "./ButtonType";
export class Modal {
get title(): HTMLHeadingElement {
return this._title;
}
private static currentModalId: string | undefined = "";
private _title: string = "iTender";
private _title: HTMLHeadingElement;
private _id: string = "";
private _loader: boolean = false;
private _buttons: { type: string, content: string, onclick: Function }[] = [];
@ -19,11 +22,10 @@ export class Modal {
constructor(id, title: string) {
this._id = id;
this._title = title;
let t = document.createElement("h1");
t.innerText = title;
this._elements.push(t);
this._title = document.createElement("h1") as HTMLHeadingElement;
this._title.innerText = title;
this._elements.push(this._title);
}
public static isModalOpen(): boolean {
@ -91,6 +93,10 @@ export class Modal {
});
}
public setTitle(title) {
}
/**
* @param elements
* @param id

View File

@ -7,7 +7,7 @@ import {WebHandler} from "./WebHandler";
import {Setup} from "./Setup";
import {Pane} from "./Pane";
import {RequestType} from "../RequestType";
import {IJob} from "../database/IJob";
import {Fill} from "./Fill";
export class WebWebSocketHandler {
private static socket: WebSocket;
@ -29,8 +29,7 @@ export class WebWebSocketHandler {
}
public static registerForEvent(event: WebSocketEvent, fn: (payload: WebSocketPayload) => void) {
for( let e of WebWebSocketHandler.eventRegister )
{
for (let e of WebWebSocketHandler.eventRegister) {
if (e.fn == fn) {
console.log("Event fn already registered");
return;
@ -55,11 +54,27 @@ export class WebWebSocketHandler {
switch (payload.event) {
case WebSocketEvent.CONFIG: {
// Incoming WebSocketStatus
Setup.onConfigUpdate(payload);
break;
}
case WebSocketEvent.DRINKS: {
WebHandler.onDrinkUpdate(payload);
break;
}
case WebSocketEvent.ERROR: {
let modal = new Modal("error", "Aww crap!");
let txt = document.createElement("p");
txt.innerHTML = payload.data;
modal.addContent(txt);
modal.addContent(document.createElement("br"));
modal.addButton(ButtonType.PRIMARY, "Schließen", () => modal.close() );
modal.open();
break;
}
// Incoming WebSocketStatus
case WebSocketEvent.STATUS: {
let statusElement = document.getElementById("status");
if (statusElement)
@ -90,19 +105,16 @@ export class WebWebSocketHandler {
case iTenderStatus.DOWNLOADING: {
let modal = new Modal("download", "Aktualisieren");
let txt = document.createElement("p");
txt.innerHTML = `Einen Augenblick bitte<br>iTender aktualisiert die Getränke vom Server.`;
txt.innerHTML = `Einen Augenblick bitte<br>iTender aktualisiert die Getränke vom Server...`;
modal.addContent(txt);
modal.loader = true;
modal.open();
break;
setTimeout( () => {
if( txt )
{
txt.innerHTML = txt.innerHTML + "<br><br>Der Vorgang dauert länger als gewöhnlich.<br>Überprüfe deine Internetverbindung!"
}
case iTenderStatus.REFRESHING: {
/* let modal = new Modal("refreshing", "Aktualisieren...");
let txt = document.createElement("p");
txt.innerHTML = `Einen Augenblick bitte<br>iTender aktualisiert die Getränke...`;
modal.addContent(txt);
modal.loader = true;
modal.open();*/
}, 1000 * 15 )
break;
}
case iTenderStatus.SETUP: {
@ -111,111 +123,16 @@ export class WebWebSocketHandler {
break;
}
case iTenderStatus.FILLING: {
let modal = new Modal("fill", "Getränk wird ausgegeben");
let header = document.createElement("h2");
header.innerText = "";
modal.addContent(header);
let txt = document.createElement("p");
txt.innerHTML = `Dein Cocktail wird gerade zubereitet`;
txt.id = "main_fillTxt";
let waterAnimDiv = document.createElement("div");
waterAnimDiv.classList.add("water");
modal.addContent(txt);
modal.addContent(waterAnimDiv);
let seconds = document.createElement("span");
seconds.innerText = "60s";
seconds.style.marginRight = "3%";
modal.addContent(seconds);
let ml = document.createElement("span");
ml.innerText = "200ml";
modal.addContent(ml);
modal.addContent(document.createElement("br"));
modal.addContent(document.createElement("br"));
let cancelBtn = document.createElement("button");
cancelBtn.classList.add("btn", "btn-danger");
cancelBtn.innerText = "Abbrechen";
cancelBtn.disabled = true;
setTimeout(() => {
cancelBtn.disabled = false;
}, 1000);
cancelBtn.onclick = () => {
cancelBtn.disabled = true;
txt.innerHTML = "Der Vorgang wird abgebrochen...";
waterAnimDiv.classList.add("waterCancel");
WebWebSocketHandler.send(new WebSocketPayload(WebSocketEvent.CANCEL));
};
modal.addContent(cancelBtn);
function riseSlowlyUp(lastNumber:number, number: number) {
for (let i = lastNumber; i < number; i++) {
setTimeout(() => {
ml.innerText = i + "ml";
}, (number-lastNumber/1000)+i*4);
}
}
modal.open().then(() => {
WebWebSocketHandler.request(RequestType.JOB).then((payload) => {
let minus = 0;
let job = payload.data.content as IJob;
ml.innerText = Math.floor((job.completeAmount / job.estimatedTime) * minus) + "ml";
waterAnimDiv.style.setProperty("--fillTime", job.estimatedTime + "s");
waterAnimDiv.style.backgroundImage = `url("/images/${job.drink._id}.png")`;
header.innerText = job.drink.name;
seconds.innerText = job.estimatedTime + "s";
let last = 0;
let interval = setInterval(() => {
minus++;
if (minus + 1 > (job.estimatedTime as number)) {
clearInterval(interval);
}
seconds.innerText = (Math.floor(job.estimatedTime as number - minus)) + "s";
let calc = Math.floor((job.completeAmount / job.estimatedTime) * minus);
riseSlowlyUp(last, calc)
last = calc;
//ml.innerText = + "ml";
}, 1000);
setTimeout(() => {
txt.innerHTML = "Bitte entnehme den Cocktail!";
/*cancelBtn.classList.add("btn-primary");
cancelBtn.classList.remove("btn-danger");
cancelBtn.innerText = "Schließen";*/
cancelBtn.classList.add("btn-blendout");
waterAnimDiv.classList.add("waterFinished");
cancelBtn.onclick = () => {
modal.close();
}
//setTimeout(() => modal.close(), 1000 * 4.5);
}, job.estimatedTime * 1000);
});
});
Fill.onFillEvent(payload);
break;
}
default: {
console.log("Unknown to handle " + status);
}
}
break;
}
case WebSocketEvent.DRINKS: {
WebHandler.onDrinkUpdate(payload);
break;
}
}
}
@ -223,12 +140,8 @@ export class WebWebSocketHandler {
private onOpen(event) {
console.log("[WS] Connected", event);
/*let connectionElement = document.getElementById("right");
if (connectionElement) {
connectionElement.innerText = "Verbunden";
connectionElement.style.color = "green";
}*/
const blockPanel = document.getElementById("blockPanel") as HTMLDivElement;
blockPanel.classList.add("opacityOutDisplayNone");
}
private onClose(event) {

View File

@ -7,6 +7,7 @@ html
link(rel='stylesheet', href='/stylesheets/style.css')
meta(charset="UTF-8")
body
div#blockPanel
div.modal#modal
div.modal-content#modal-content
div.modalInnerContent#modalInnerContent