Took 5 hours 35 minutes
This commit is contained in:
2022-11-23 18:39:47 +01:00
parent c933b9943d
commit 7307505175
17 changed files with 290 additions and 648 deletions

3
src/RejectReason.ts Normal file
View File

@ -0,0 +1,3 @@
export enum RejectReason {
NOT_ENOUGH_INGREDIENTS = "NOT_ENOUGH_INGREDIENTS"
}

View File

@ -3,4 +3,6 @@ export enum RequestType {
INGREDIENTS = "INGREDIENTS",
STATS = "STATS",
JOB = "JOB",
STARTFILL = "STARTFILL",
STOPFILL = "STOPFILL",
}

View File

@ -15,7 +15,6 @@ export const ContainerSchema = new Mongoose.Schema<IContainer>({
sensorFilledMin: Number,
filled: Number,
enabled: {type: Boolean, default: false},
autoDisabled: {type: Boolean, default: false}
});
const Container = mongoose.model<IContainer>('Container', ContainerSchema);

View File

@ -16,5 +16,4 @@ export interface IContainer extends mongoose.Document {
pumpPin: number;
filled: number;
enabled: boolean;
autoDisabled: boolean;
}

View File

@ -1,10 +1,14 @@
import mongoose from "mongoose";
import {IDrink} from "./IDrink";
import {IIngredient} from "./IIngredient";
import {IContainer} from "./IContainer";
export interface IJob extends mongoose.Document {
drink: IDrink;
amount: number;
amounts: { ingredient: IIngredient, amount: number, container: IContainer }[];
completeAmount: number
startedAt: Date;
endAt: Date;
estimatedTime : number;
successful: boolean;
}

View File

@ -4,9 +4,11 @@ import {IJob} from "./IJob";
export const JobSchema = new mongoose.Schema<IJob>({
drink: {type: mongoose.Types.ObjectId, ref: "Drink"},
amount: Number,
amounts: [{ingredient: {type: mongoose.Types.ObjectId, ref: "Ingredient"}, amount: Number, container: { type: mongoose.Types.ObjectId, ref: "Container" }}],
completeAmount: Number,
startedAt: Date,
endAt: Date,
estimatedTime: Number,
successful: Boolean
});

View File

@ -12,8 +12,16 @@ 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";
const log = debug("itender:station");
const mixLog = debug("itender:mix");
export class iTender {
static get containers(): { container: IContainer, sensor: HCSR04 | HX711, pump: null }[] {
@ -30,6 +38,7 @@ export class iTender {
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 {
@ -53,23 +62,136 @@ export class iTender {
return this._status;
}
static async startFill(data: { drink: IDrink, ingredients?: { id: String, amount: number }[], amount?: number }) {
// todo Fill method
let drink = await Drink.findById(data.drink);
if(!drink)
return;
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);
// todo
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);
// End
this.measureContainers().then();
// 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() {
// todo Stop fill method
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> {
@ -132,65 +254,17 @@ export class iTender {
}
log("Drinks refreshed!");
iTender.setStatus(iTenderStatus.READY);
await WebSocketHandler.sendDrinks();
resolve();
});
}
/**
* @Deprecated
*/
static refreshContainers(): Promise<void> {
log("Refreshing containers...");
this.setStatus(iTenderStatus.CALCULATING);
return new Promise(async resolve => {
let containers = await Container.find();
for (let c of containers) {
let i = 0;
let found = false;
for (let c2 of this._containers) {
if (c2.container._id == c._id) {
let sensor;
try {
//sensor = new HCSR04(c.sensorTrigger, c.sensorEcho);
} catch (e) {
}
this._containers[i] = {
container: c,
sensor: sensor,
pump: null
};
found = true;
break;
}
i++;
}
if (!found) {
let sensor;
try {
//sensor = new HCSR04(c.sensorTrigger, c.sensorEcho);
} catch (e) {
}
this._containers.push({
container: c,
sensor: sensor,
pump: null
});
}
}
log("Containers refreshed!");
this.measureContainers().then().catch(console.error);
resolve();
this.setStatus(iTenderStatus.READY);
});
}
public static async autoCheckup() {
setInterval(async () => {
log("Auto Checkup");
if (!this._currentJob)
return;
// Check if startedTime plus 2 mins smaller than now
@ -212,13 +286,12 @@ export class iTender {
static refreshFromServer(): Promise<void> {
return new Promise(async (resolve, reject) => {
let before = iTender._status;
iTender.setStatus(iTenderStatus.DOWNLOADING)
// todo
iTender.setStatus(iTenderStatus.READY);
resolve();
iTender.setStatus(before);
});
}

View File

@ -2,7 +2,6 @@ import {App} from "./App";
import debug from "debug";
import {WebsocketApp} from "./WebsocketApp";
import {Database} from "./database/Database";
import Ingredient from "./database/Ingredient";
import {iTender} from "./iTender";
import {iTenderStatus} from "./iTenderStatus";
import {Utils} from "./Utils";
@ -61,11 +60,14 @@ function init(): Promise<void> {
return new Promise(async resolve => {
iTender.setStatus(iTenderStatus.STARTING);
// Network
await iTender.checkNetwork();
if (iTender.internetConnection) {
await iTender.refreshFromServer();
}
setTimeout( async () => {
// Network
await iTender.checkNetwork();
if (iTender.internetConnection && iTender.status == iTenderStatus.READY) {
await iTender.refreshFromServer();
}
}, 1000 * 20 ) ;
// Containers
//await iTender.refreshContainers();
@ -81,6 +83,7 @@ function init(): Promise<void> {
function refresh(): Promise<void> {
return new Promise(async resolve => {
log("Refreshing...")
// Network
await iTender.checkNetwork();
@ -94,89 +97,4 @@ function refresh(): Promise<void> {
await iTender.measureContainers();
//await iTender.refreshDrinks(); Not needed because there is no change in drinks?
});
}
async function test() {
console.log("Testing fn");
/*
let cola = new Ingredient();
cola.name = "Cola";
cola.category = Category.SOFTDRINK;
await cola.save();
let sprite = new Ingredient();
sprite.name = "Sprite";
sprite.category = Category.SOFTDRINK;
await sprite.save();
let fanta = new Ingredient();
fanta.name = "Fanta";
fanta.category = Category.SOFTDRINK;
await fanta.save();
let drink = new Drink();
drink.name = "Fanta";
drink.category = Category.ALCOHOL_FREE;
drink.ingredients = [{type: fanta, amount: 200}];
await drink.save();
drink = new Drink();
drink.name = "Mezzo Mix";
drink.category = Category.ALCOHOL_FREE;
drink.ingredients = [{type: fanta, amount: 100}, {type: cola, amount: 100}];
await drink.save();*/
let ingredient = await Ingredient.findOne({name: "Fanta"});
if (!ingredient)
return;
/*let drink = new Drink();
drink.name = "Cola";
drink.ingredients = [{type: ingredient, amount: 200}];
await drink.save();*/
/* let drink = await Drink.findOne({name: "Cola"}).populate("ingredients.type");
if (!drink) return;
console.log(drink);*/
/*let container = new Container();
container.slot = 2;
container.volume = 750;
container.sensorEcho = 28;
container.sensorTrigger = 29;
container.content = ingredient;
container.sensorFilledMax = 2;
container.sensorFilledMin = 15;*/
//await container.save();
/* let container = await Container.findOne({slot: 1});
if (!container) return;
console.log(container);*/
//console.log(drink.ingredients)
/*let ingredient = new Ingredient();
ingredient.name = "Cola";
ingredient.category = Category.ALCOHOL_FREE;
await ingredient.save();
let ingredient2 = new Ingredient();
ingredient2.name = "Fanta";
ingredient2.category = Category.ALCOHOL_FREE;
await ingredient2.save();
let drink = new Drink();
drink.name = "Mezzo Mix";
drink.ingredients = [ {type: ingredient2, amount: 2}, { type: ingredient, amount: 10 } ];
await drink.save();*/
}

View File

@ -9,7 +9,7 @@ import {SensorType} from "../../SensorType";
import {Settings} from "../../Settings";
import Ingredient from "../../database/Ingredient";
import {RequestType} from "../../RequestType";
import {IDrink} from "../../database/IDrink";
import {IJob} from "../../database/IJob";
const express = require('express');
const router = express.Router();
@ -32,7 +32,6 @@ router.ws('/', async (ws, req, next) => {
await WebSocketHandler.sendDrinks();
ws.on('message', async (raw, bool) => {
let msg = WebSocketPayload.parseFromBase64Json(raw);
// If message is null, close the socket because it could not be decompiled
@ -41,14 +40,9 @@ router.ws('/', async (ws, req, next) => {
return;
}
log(msg);
switch (msg.event) {
case WebSocketEvent.FILL : {
iTender.startFill(msg.data as { drink: IDrink, ingredients?: { id: String, amount: number }[], amount?: number });
break;
}
case WebSocketEvent.TARE: {
if (msg.data["state"] == true) {
iTender.toggleTare(true);
@ -71,7 +65,6 @@ router.ws('/', async (ws, req, next) => {
container.sensorPin1 = c.sensor1;
container.sensorPin2 = c.sensor2;
container.enabled = true;
container.autoDisabled = true;
await container.save();
i++;
}
@ -89,7 +82,7 @@ router.ws('/', async (ws, req, next) => {
ingredient = undefined;
}
container.volume = parseInt(msg.data["volume"]);
container.filled = parseInt(msg.data["filled"]);
container.content = ingredient;
await container.save();
@ -101,7 +94,7 @@ router.ws('/', async (ws, req, next) => {
case WebSocketEvent.CONFIG: {
// ToDo
console.log("New Settings:", msg.data);
// Danach setup modus aus
// Danach setup modus us
for (const [key, value] of Object.entries(msg.data)) {
Settings.set(key, value);
}
@ -138,6 +131,20 @@ router.ws('/', async (ws, req, next) => {
WebSocketHandler.answerRequest(msg.data["type"] as RequestType, (await Ingredient.find().sort({"name": 1})));
break;
}
case RequestType.STARTFILL: {
let job: IJob | null = null;
try {
job = await iTender.onReceiveFill(msg.data["content"]);
} catch (e: any) {
console.error(e);
}
WebSocketHandler.answerRequest(msg.data["type"] as RequestType, {success: (!!job), job: job});
break;
}
case RequestType.JOB: {
WebSocketHandler.answerRequest(msg.data["type"] as RequestType, iTender.currentJob);
break;
}
}
break;

View File

@ -132,7 +132,7 @@ export class Containers {
let payload = new WebSocketPayload(WebSocketEvent.CONTAINER_UPDATE, false, {
container: selectContainer.value,
ingredient: (selectIngredient.value == "null") ? null : selectIngredient.value,
volume: volumeSlider.value
filled: volumeSlider.value
});
WebWebSocketHandler.send(payload).then(() => modal.close());
};

View File

@ -2,9 +2,11 @@ import {WebSocketPayload} from "../WebSocketPayload";
import {IDrink} from "../database/IDrink";
import {Pane} from "./Pane";
import {Setup} from "./Setup";
import {Modal} from "./Modal";
import {WebSocketEvent} from "../WebSocketEvent";
import {WebWebSocketHandler} from "./WebWebSocketHandler";
import {RequestType} from "../RequestType";
import {IJob} from "../database/IJob";
import {Modal} from "./Modal";
import {ButtonType} from "./ButtonType";
export class WebHandler {
static get currentPane(): Pane {
@ -42,10 +44,19 @@ export class WebHandler {
drinkName.innerText = drink.name;
drinkEle.onclick = () => {
let payload = new WebSocketPayload(WebSocketEvent.FILL, false, {drink: drink._id });
WebWebSocketHandler.send(payload);
WebWebSocketHandler.request(RequestType.STARTFILL, {drink: drink}).then((payload) => {
let data = payload.data.content as { success: boolean, job?: IJob };
if (!data.success) {
let modal = new Modal("fill", "Oh nein!");
let txt = document.createElement("p");
txt.innerHTML = `Es scheint so, als wäre nicht genug Inhalt in den Behältern, um den gewünschten Cocktail bereitzustellen.<br><br>Bitte wende dich an das Wartungspersonal.<br><br>`;
modal.addContent(txt);
modal.addButton(ButtonType.PRIMARY, "Schließen", () => modal.close());
modal.open();
}
});
}
/*

View File

@ -8,6 +8,7 @@ import {Setup} from "./Setup";
import {Pane} from "./Pane";
import {RequestType} from "../RequestType";
import {IDrink} from "../database/IDrink";
import {IJob} from "../database/IJob";
export class WebWebSocketHandler {
private static socket: WebSocket;
@ -64,7 +65,8 @@ export class WebWebSocketHandler {
Modal.close("start");
Modal.close("setup");
Modal.close("fill");
WebHandler.openPane(Pane.MAIN);
if( WebHandler.currentPane != Pane.MENU )
WebHandler.openPane(Pane.MAIN);
(document.getElementById("menuBtn") as HTMLButtonElement).disabled = false;
break;
}
@ -109,6 +111,11 @@ export class WebWebSocketHandler {
modal.addContent(waterAnimDiv);
let seconds = document.createElement("p");
seconds.innerText = "60s";
modal.addContent(seconds);
let cancelBtn = document.createElement("button");
cancelBtn.classList.add("btn", "btn-danger");
cancelBtn.innerText = "Abbrechen";
@ -125,13 +132,28 @@ export class WebWebSocketHandler {
};
modal.addContent(cancelBtn);
WebWebSocketHandler.request(RequestType.JOB).then((payload) => {
let drink = payload.data as IDrink;
waterAnimDiv.style.backgroundImage = "/images/" + drink.name + ".png";
header.innerText = drink.name;
});
modal.open();
modal.open().then( () => {
WebWebSocketHandler.request(RequestType.JOB).then((payload) => {
let job = payload.data.content as IJob;
waterAnimDiv.style.setProperty("--fillTime", job.estimatedTime + "s");
waterAnimDiv.style.backgroundImage = `url("/images/${job.drink.name}.png")`;
header.innerText = job.drink.name;
seconds.innerText = job.estimatedTime + "s";
let minus = 0;
let interval = setInterval( () => {
minus++;
if( minus+1 > (job.estimatedTime as number) )
{
clearInterval(interval);
}
seconds.innerText = (job.estimatedTime as number - minus) + "s";
}, 1000 )
});
} );
break;
}
@ -226,8 +248,9 @@ export class WebWebSocketHandler {
/**
* @return Promise<WebSocketPayload>
* @param type
* @param content
*/
public static request(type: RequestType): Promise<WebSocketPayload> {
public static request(type: RequestType, content: any = null ): Promise<WebSocketPayload> {
console.log("Request to " + type)
return new Promise(resolve => {
WebWebSocketHandler.registerForEvent(WebSocketEvent.RESPONSE, (payload) => {
@ -235,7 +258,7 @@ export class WebWebSocketHandler {
resolve(payload);
}
});
WebWebSocketHandler.send(new WebSocketPayload(WebSocketEvent.REQUEST, false, {type: type}));
WebWebSocketHandler.send(new WebSocketPayload(WebSocketEvent.REQUEST, false, {type: type, content: content}));
});
}