itender/src/iTender.ts
Tobias Hopp 6446896565 Update v1.0.0
Took 1 hour 1 minute
2022-11-23 20:50:08 +01:00

395 lines
13 KiB
TypeScript

import {iTenderStatus} from "./iTenderStatus";
import Container from "./database/Container";
import {HCSR04} from "hc-sr04";
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 {HX711} from "./HX711";
import {SensorType} from "./SensorType";
import Job from "./database/Job";
import {IIngredient} from "./database/IIngredient";
import Ingredient from "./database/Ingredient";
import {clearInterval} from "timers";
import {RejectReason} from "./RejectReason";
import {Settings} from "./Settings";
import GPIO from "rpi-gpio";
import axios from "axios";
const log = debug("itender:station");
const mixLog = debug("itender:mix");
export class iTender {
static get containers(): { container: IContainer, sensor: HCSR04 | HX711, pump: null }[] {
return this._containers;
}
static get drinks(): IDrink[] {
return this._drinks;
}
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;
static get internetConnection(): boolean {
return this._internetConnection;
}
/**
* @Deprecated
* @private
*/
private static _containers: { container: IContainer, sensor: HCSR04 | HX711, pump: null }[] = [];
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;
}
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 }[] = [];
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});
}
} 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})
}
} else {
for (let x of (drink.ingredients as { type: IIngredient, amount: number }[])) {
amounts.push({ingredient: x.type, amount: x.amount});
}
}
job.estimatedTime = 0;
let tolerance = 5;
for (let x of amounts) {
let c = await Container.findOne({$and: [{enabled: true}, {filled: {$gt: x.amount + tolerance}}]});
if (!c) {
mixLog("Not enough ingredients!");
reject(RejectReason.NOT_ENOUGH_INGREDIENTS);
return;
}
x.container = c;
let estimated = Settings.get("secondsPer100ml") as number / 100 * x.amount;
if (job.estimatedTime < estimated)
job.estimatedTime = estimated;
}
job.drink = drink
job.amounts = amounts as { ingredient: IIngredient, amount: number, container: IContainer }[];
await job.save()
resolve(job);
await this.startFill(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);
let timers: NodeJS.Timeout[] = [];
for (let x of job.amounts) {
// Start pump here
await GPIO.setup(x.container.pumpPin, GPIO.DIR_OUT);
await GPIO.write(x.container.pumpPin, true);
let waitTime = (Settings.get("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(() => {
// Stop pump here
GPIO.write(x.container.pumpPin, false);
// Remove from list of timers
let arr: NodeJS.Timer[] = [];
for (let i = 0; i < timers.length; i++) {
if (timers[i] != timer)
arr.push(timers[i]);
}
mixLog(`Stopping output of pump ${x.container.pumpPin}`)
timers = arr;
}, waitTime);
timers.push(timer);
}
iTender._jobCheckInterval = setInterval(async () => {
if (timers.length != 0)
return;
clearInterval(iTender._jobCheckInterval);
job.endAt = new Date();
job.successful = true;
await job.save();
mixLog("Job successful");
iTender.setStatus(iTenderStatus.READY);
}, 500);
}
static cancelFill() {
if (!this._currentJob)
return;
clearInterval(this._jobCheckInterval);
this._currentJob.successful = false;
this._currentJob.endAt = new Date();
this._currentJob.save();
for (let x of this._currentJob.amounts) {
// stop pump pin
//x.container.pumpPin
GPIO.write(x.container.pumpPin, false);
}
iTender.setStatus(iTenderStatus.READY);
}
static measureContainers(): Promise<void> {
log("Measuring containers...");
return new Promise(async resolve => {
for (let c of (await Container.find({}))) {
try {
let rand = Math.random() * 5;
if (c.sensorType == SensorType.ULTRASOUND) {
let sensor = new HCSR04(c.sensorPin1, c.sensorPin2);
c.rawData = sensor.distance();
c.rawData = rand;
} else if (c.sensorType == SensorType.LOADCELL) {
let sensor = new HX711(c.sensorPin1, c.sensorPin2);
c.rawData = sensor.measure();
c.rawData = rand;
}
if (c.sensorFilledMax && c.sensorFilledMin) {
c.filled = c.rawData * c.sensorFilledMax / 100;
}
} catch (e) {
c.filled = -1;
c.rawData = 0;
}
await c.save();
}
log("Containers measured!");
resolve();
let payload = new WebSocketPayload(WebSocketEvent.CONTAINERS, false, (await Container.find()));
await WebSocketHandler.send(payload);
});
}
static refreshDrinks(): Promise<void> {
this.setStatus(iTenderStatus.REFRESHING);
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);
}
}
log("Drinks refreshed!");
iTender.setStatus(iTenderStatus.READY);
await WebSocketHandler.sendDrinks();
resolve();
});
}
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[];
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.findOne({name: remote.name});
if (!ingredient)
ingredient = new Ingredient();
ingredient.name = remote.name;
await ingredient.save();
}
const requestDrinks = await axios.get("https://itender.iif.li/api/drinks");
let serverDrinks = requestDrinks.data as IDrink[];
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.findOne({name: remote.name});
if (!drink)
drink = new Drink();
drink.name = remote.name;
drink.ingredients = remote.ingredients;
await drink.save();
// Download thumbnail
Utils.downloadImage("https://itender.iif.li/images/" + remote._id + ".png", "./public/images/" + drink._id + ".png").then(filepath => {
log("Drink " + remote.name + "'s Thumbnail downloaded to " + filepath);
}).catch(e => {
log("Drink " + remote.name + " failed to download thumbnail!\n" + e);
});
}
} catch (e) {
console.error(e);
}
iTender.setStatus(iTenderStatus.READY);
resolve();
iTender.refreshDrinks();
});
}
private static interval;
public static toggleTare(state: boolean) {
clearInterval(iTender.interval);
if (state)
this.interval = setInterval(async () => {
await this.measureContainers();
}, 500);
}
}