write doc

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

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;
}
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();*/
setTimeout( () => {
if( txt )
{
txt.innerHTML = txt.innerHTML + "<br><br>Der Vorgang dauert länger als gewöhnlich.<br>Überprüfe deine Internetverbindung!"
}
}, 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) {