import {iTenderStatus} from "./iTenderStatus"; import Container from "./database/Container"; import {IContainer} from "./database/IContainer"; import Drink from "./database/Drink"; import {IDrink} from "./database/IDrink"; import debug from "debug"; import {WebSocketHandler} from "./WebSocketHandler"; import {IJob} from "./database/IJob"; import {Utils} from "./Utils"; import {WebSocketPayload} from "./WebSocketPayload"; import {WebSocketEvent} from "./WebSocketEvent"; import Job from "./database/Job"; import {IIngredient} from "./database/IIngredient"; import Ingredient from "./database/Ingredient"; import {clearInterval} from "timers"; import {RejectReason} from "./RejectReason"; import axios from "axios"; import GPIO from "rpi-gpio"; import {MyGPIO} from "./MyGPIO"; import {SensorHelper} from "./SensorHelper"; import {SensorType} from "./SensorType"; 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; static get drinks(): IDrink[] { return this._drinks; } /** * The current job of the itender */ static get currentJob(): IJob | null { return this._currentJob; } private static _status: iTenderStatus = iTenderStatus.STARTING; private static _currentJob: IJob | null = null; private static _jobCheckInterval: NodeJS.Timer; private static _internetConnection: boolean = false; private static _jobTimers: NodeJS.Timeout[] = []; /** * Returns true if internet connection is active */ static get internetConnection(): boolean { return this._internetConnection; } private static _drinks: IDrink[]; static setStatus(status: iTenderStatus) { this._status = status; if (WebSocketHandler.ws && WebSocketHandler.ws.readyState == 1) WebSocketHandler.sendStatus().then().catch(console.error); log("Status is now " + status); } static get status(): iTenderStatus { 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 { return new Promise(async (resolve, reject) => { mixLog("Receiving fill"); let drink = await Drink.findById(data.drink).populate("ingredients.type"); if (!drink) { reject(); return; } const job = new Job(); let amounts: { ingredient: IIngredient, amount: number, container?: IContainer }[] = []; job.completeAmount = 0; if (data.amounts) { for (let x of data.amounts) { let ingredient = await Ingredient.findById(x.ingredient); if (!ingredient) continue; amounts.push({ingredient: ingredient, amount: x.amount}); job.completeAmount += x.amount; } } else if (data.amount) { let sum = 0; for (let x of (drink.ingredients as { type: IIngredient, amount: number }[])) { sum += x.amount; } let factor = sum / data.amount; for (let x of (drink.ingredients as { type: IIngredient, amount: number }[])) { amounts.push({ingredient: x.type, amount: x.amount * factor}) job.completeAmount += x.amount; } } else { for (let x of (drink.ingredients as { type: IIngredient, amount: number }[])) { amounts.push({ingredient: x.type, amount: x.amount}); job.completeAmount += x.amount; } } job.estimatedTime = 0; let tolerance = 3; for (let x of amounts) { let c = await Container.findOne({$and: [{enabled: true}, {filled: {$gt: x.amount + tolerance}}, {content: x.ingredient}]}); if (!c) { mixLog("Not enough ingredients!"); reject(RejectReason.NOT_ENOUGH_INGREDIENTS); return; } x.container = c; let estimated = iTender.secondsPer100ml / 100 * x.amount; if (job.estimatedTime < estimated) job.estimatedTime = estimated; } console.log(amounts); job.drink = drink job.amounts = amounts as { ingredient: IIngredient, amount: number, container: IContainer }[]; if (job.estimatedTime < 0.5) { job.estimatedTime = 1; } await job.save() resolve(job); await this.startFill(job); }); } /** * 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"}]); mixLog("New fill job " + job.drink.name + " will take " + job.estimatedTime + "s"); this._currentJob = job; iTender.setStatus(iTenderStatus.FILLING); for (let x of job.amounts) { // Start pump here try { await MyGPIO.setup(x.container.pumpPin, GPIO.DIR_OUT) await MyGPIO.write(x.container.pumpPin, true); } catch (e) { if (isPI()) { log("[ERROR] GPIO I/O Error " + e); // Todo error handling to user await iTender.cancelFill(); return; } else { log("[WARNING] GPIO I/O Error, but it's normal cause you are not on raspberry"); } } let waitTime = (iTender.secondsPer100ml as number) / 100 * x.amount * 1000; mixLog(`Starting output of pump ${x.container.pumpPin}`); //mixLog(x.ingredient + " takes " + (waitTime / 1000) + "s for " + x.amount + "ml"); let timer = setTimeout(async () => { // Remove from list of timers let arr: NodeJS.Timer[] = []; for (let i = 0; i < this._jobTimers.length; i++) { if (this._jobTimers[i] != timer) arr.push(this._jobTimers[i]); } mixLog(`Stopping output of pump ${x.container.pumpPin}`); // Stop pump here try { await MyGPIO.write(x.container.pumpPin, false); } catch (e) { if (isPI()) { log("[ERROR] GPIO I/O Error " + e); await iTender.cancelFill(); return; } else { log("[WARNING] GPIO I/O Error, but it's normal cause you are not on raspberry"); } } if (x.container.sensorType == SensorType.NONE) { // V2: Manual measuring x.container.filled = x.container.filled - x.amount; await x.container.save(); } this._jobTimers = arr; }, waitTime); this._jobTimers.push(timer); } iTender._jobCheckInterval = setInterval(async () => { if (this._jobTimers.length != 0) return; clearInterval(iTender._jobCheckInterval); job.endAt = new Date(); job.successful = true; await job.save(); mixLog("Job successful"); setTimeout(() => iTender.setStatus(iTenderStatus.READY), 3000) }, 500); } /** * Cancel the fill */ static async cancelFill() { if (!this._currentJob || this.status != iTenderStatus.FILLING) return; clearInterval(this._jobCheckInterval); this._currentJob.successful = false; this._currentJob.endAt = new Date(); await this._currentJob.save(); for (let timer of this._jobTimers) { // Clears all the ongoing stop timers clearTimeout(timer); } for (let jobIngredient of this._currentJob.amounts) { // stop pump pin try { await MyGPIO.write(jobIngredient.container.pumpPin, false); } catch (e) { } // ToDo let container: IContainer = jobIngredient.container; let deltaStartStop = (this._currentJob.endAt.getTime() - this._currentJob.startedAt.getTime()) / 1000; container.filled = container.filled - ( jobIngredient.amount * (deltaStartStop / ((jobIngredient.amount / 100) * iTender.secondsPer100ml)) ); // V2: Near the current fill value based on time values from delta start stop // todo fixme container.save().then(); } iTender.setStatus(iTenderStatus.READY); } /** * Measure all containers based on their sensor values */ static measureContainers(): Promise { log("Measuring containers..."); return new Promise(async resolve => { for (let c of (await Container.find({}))) { if (c.sensorType != SensorType.NONE) { let weight = SensorHelper.measure(c); if (!weight) { await WebSocketHandler.send(new WebSocketPayload(WebSocketEvent.ERROR, "Ein Sensor hat beim Austarieren einen ungültigen Wert zurückgegeben.
Dies weist auf eine Fehlkonfiguration oder kaputten Sensor hin.
Aus Sicherheitsgründen wurde der Sensor für diesen Behälter deaktiviert.")); continue; } // V2: New calculation method c.filled = weight - c.sensorDelta; // V2: Testing await c.save(); } } log("Containers measured!"); resolve(); let payload = new WebSocketPayload(WebSocketEvent.CONTAINERS, (await Container.find())); await WebSocketHandler.send(payload); }); } /** * Refresh the drinks to the local variable * Check which drinks can be done, based on the current container ingredients */ static refreshDrinks(): Promise { log("Refreshing drinks..."); return new Promise(async resolve => { this._drinks = []; for (let d of (await Drink.find().populate("ingredients.type"))) { let drinkAccept = true; for (let i of d.ingredients) { let c = await Container.findOne({content: i.type}); if (!c) { drinkAccept = false; break; } } if (drinkAccept) { this._drinks.push(d); } } resolve(); log("Drinks refreshed!"); await WebSocketHandler.sendDrinks(); }); } public static async autoCheckup() { setInterval(async () => { log("Auto Checkup"); if (!this._currentJob) return; // Check if startedTime plus 2 mins smaller than now if (this._currentJob.startedAt.getTime() + 1000 * 60 * 2 <= Date.now()) { // Job can be declared as stuck! this._currentJob.successful = false; this._currentJob.endAt = new Date(); await this._currentJob.save(); this._currentJob = null; this.setStatus(iTenderStatus.READY); } }, 30000); } public static async checkNetwork() { this._internetConnection = await Utils.checkInternet(); } static refreshFromServer(): Promise { return new Promise(async (resolve, reject) => { iTender.setStatus(iTenderStatus.DOWNLOADING) log("Refreshing drinks from server..."); 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(); for (let local of localIngredients) { let found = false; for (let remote of serverIngredients) { if (local.name == remote.name) { found = true; break; } } if (!found) { await Ingredient.deleteOne({"_id": local._id}); for (let c of (await Container.find({content: local._id}))) { c.content = undefined; c.save(); } } } for (let remote of serverIngredients) { let ingredient = await Ingredient.findById(remote._id); if (!ingredient) ingredient = new Ingredient(); ingredient._id = remote._id; ingredient.name = remote.name; await ingredient.save(); } 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"); let localDrinks = await Drink.find(); for (let local of localDrinks) { let found = false; for (let remote of serverDrinks) { if (local.name == remote.name) { found = true; break; } } if (!found) { Utils.deleteImage(local._id); await Drink.deleteOne({"_id": local._id}); } } for (let remote of serverDrinks) { let drink = await Drink.findById(remote._id); if (!drink) drink = new Drink(); drink._id = remote._id; drink.name = remote.name; drink.ingredients = remote.ingredients; await drink.save(); // Download thumbnail if (!Utils.checkForImage(remote._id)) { let url = "https://itender.iif.li/images/" + remote._id + ".png"; try { await Utils.downloadImage(url, "./public/images/" + drink._id + ".png") log("Drink " + remote.name + "'s Thumbnail downloaded"); } catch (e) { log("Drink " + remote.name + " failed to download thumbnail! (" + url + ") | " + e); } } } } catch (e) { console.error("Could not refresh drinks " + e); await WebSocketHandler.send(new WebSocketPayload(WebSocketEvent.ERROR, "Beim Aktualisieren der Getränke ist ein Netzwerk-Fehler aufgetreten.
Bitte später erneut versuchen!")); } iTender.setStatus(iTenderStatus.READY); resolve(); await iTender.refreshDrinks(); }); } private static interval; public static toggleTare(state: boolean) { clearInterval(iTender.interval); if (state) this.interval = setInterval(async () => { await this.measureContainers(); }, 500); } }