475 lines
16 KiB
TypeScript
475 lines
16 KiB
TypeScript
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<IJob> {
|
|
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<void> {
|
|
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.<br>Dies weist auf eine Fehlkonfiguration oder kaputten Sensor hin.<br>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<void> {
|
|
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<void> {
|
|
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.<br>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);
|
|
}
|
|
} |