don't know what I did, I did a lot

Took 4 hours 33 minutes
This commit is contained in:
2025-05-21 10:27:20 +02:00
parent 64d211d9e8
commit bb60ed7a0c
45 changed files with 1829 additions and 590 deletions

1
server/.env Normal file
View File

@ -0,0 +1 @@
DEBUG=itender:*

View File

@ -1,15 +1,16 @@
{
"name": "itender",
"name": "itender-server",
"version": "2.2.8",
"private": true,
"author": "Tobias Hopp <tobi@gaminggeneration.de>",
"license": "UNLICENSED",
"main": "dist/main.js",
"scripts": {
"start": "node --trace-warnings ./dist/main.js",
"compile": "tsc && webpack",
"compileStart": "yarn run compile && yarn start",
"watchTS": "tsc --watch",
"watchWP": "webpack --watch",
"watch": "nodemon -w src/ -e ts src/main.ts",
"doc": "typedoc"
},
"dependencies": {
@ -19,6 +20,7 @@
"cookie-parser": "^1.4.6",
"debug": "^4.3.4",
"detect-rpi": "^1.4.0",
"dotenv": "^16.5.0",
"express": "5.0.0",
"express-ws": "^5.0.2",
"hc-sr04": "^0.0.1",

View File

@ -14,51 +14,84 @@ export class ContainerHelper {
/**
* Measure all containers based on their sensor values
*/
static measureContainers(): Promise<void> {
static async measureContainers() {
log("Measuring containers...");
return new Promise(async resolve => {
for (let c of (await Container.find({}))) {
if (c.sensorType != SensorType.NONE) {
for (let c of (await Container.find({}))) {
if (c.sensorType != SensorType.NONE) {
let weight;
try {
weight = await SensorHelper.measureRaw(c);
} catch (e) {
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
/*
(WEIGHT_VAL - NO_WEIGHT_VAL) / Sensitivitätsfaktor = Gewicht in Gramm
(WEIGHT_VAL - NO_WEIGHT_VAL) / (100g_val /100) ist die Formel zur Berechnung des Gewichts in Gramm nach der Kalibrierung mit einem 100 Gramm Gewicht.
100g_val /100 gibt den Sensitivitätsfaktor an, der angibt, wie viel sich der Sensorwert ändert, wenn sich das Gewicht um ein Gramm ändert.
Durch die Division von 100g_val durch 100 wird der Sensitivitätsfaktor berechnet, und durch die Division von (WEIGHT_VAL - NO_WEIGHT_VAL) durch den Sensitivitätsfaktor wird das Gewicht in Gramm berechnet.
Beispiel:
(WEIGHT_VAL - NO_WEIGHT_VAL) / (100g_val /100) = Gewicht in Gramm
(2400 - 2000) / (2450 /100) = 80 Gramm
*/
let newFilled = weight - c.sensorDelta / iTender.sensitivityFactor;
if (newFilled <= 3 && c.filled != -1) {
c.filled = -1;
// Container is empty!
} else {
// Container > 2
c.filled = newFilled;
}
await c.save();
let weight: number;
try {
weight = await SensorHelper.measureRaw(c);
} catch (e) {
log(e);
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;
}
}
log("Containers measured!");
resolve();
await WebSocketHandler.sendContainers();
});
// V2: New calculation method
/*
(WEIGHT_VAL - NO_WEIGHT_VAL) / Sensitivitätsfaktor = Gewicht in Gramm
(WEIGHT_VAL - NO_WEIGHT_VAL) / (100g_val /100) ist die Formel zur Berechnung des Gewichts in Gramm nach der Kalibrierung mit einem 100 Gramm Gewicht.
100g_val /100 gibt den Sensitivitätsfaktor an, der angibt, wie viel sich der Sensorwert ändert, wenn sich das Gewicht um ein Gramm ändert.
Durch die Division von 100g_val durch 100 wird der Sensitivitätsfaktor berechnet, und durch die Division von (WEIGHT_VAL - NO_WEIGHT_VAL) durch den Sensitivitätsfaktor wird das Gewicht in Gramm berechnet.
Beispiel:
(WEIGHT_VAL - NO_WEIGHT_VAL) / (100g_val /100) = Gewicht in Gramm
(2400 - 2000) / (2450 /100) = 80 Gramm
*/
let newFilled = weight - c.sensorDelta / iTender.sensitivityFactor;
if (newFilled <= 3 && c.filled != -1) {
c.filled = -1;
// Container is empty!
} else {
// Container > 2
c.filled = newFilled;
}
await c.save();
}
}
await WebSocketHandler.sendContainers();
log("Containers measured!");
}
static async tare() {
// Start TARE
for (let c of await Container.find({})) {
if (c.sensorType != SensorType.NONE) {
c.sensorTare = 0;
await c.save();
}
}
let timeouts: Array<NodeJS.Timeout> = [];
async function measureAndSave() {
await SensorHelper.measureAllRaw();
for (let c of await Container.find({})) {
if (c.sensorType != SensorType.NONE) {
c.sensorTare += c.rawData;
}
}
}
timeouts.push(setTimeout(measureAndSave, 500));
timeouts.push(setTimeout(measureAndSave, 1000));
timeouts.push(setTimeout(measureAndSave, 2000));
timeouts.push(setTimeout(measureAndSave, 3000));
await Promise.all(timeouts);
for (let c of await Container.find({})) {
if (c.sensorType != SensorType.NONE) {
c.sensorTare = c.sensorTare / 4;
await c.save();
}
}
}
}

View File

@ -1,6 +1,6 @@
export enum RequestType {
CONTAINERS = "CONTAINERS",
INGREDIENTS = "INGREDIENTS",
CONTAINERS = "CONTAINERS", // duplicated, try to use from event
INGREDIENTS = "INGREDIENTS", // duplicated, try to use from event
STATS = "STATS",
JOB = "JOB",
STARTFILL = "STARTFILL",

View File

@ -1,10 +1,14 @@
/**
* File for backend
*/
export enum WebSocketEvent {
STATUS= "STATUS",
DRINKS = "DRINKS",
CONTAINERS = "CONTAINERS",
INGREDIENTS = "INGREDIENTS",
CONTAINER_UPDATE = "CONTAINER_UPDATE",
CONFIG = "CONFIG",
TARE = "TARE",
SETUP = "SETUP",
REQUEST = "REQUEST",
RESPONSE = "RESPONSE",

View File

@ -12,29 +12,26 @@ import debug from "debug";
const log = debug("itender:WShandler");
export class WebSocketHandler {
private static _ws: WebSocket|any;
private static _ws: WebSocket | any;
static get ws(): WebSocket {
return this._ws;
}
static set ws(value: WebSocket|any) {
static set ws(value: WebSocket | any) {
this._ws = value;
}
public static send(payload: WebSocketPayload): Promise<void> {
return new Promise(async (resolve, reject) => {
try {
if (this.ws && this.ws.readyState == 1) {
log("Sending " + payload.event);
await this.ws.send(payload.toString());
resolve();
}
} catch (e) {
public static send(payload: WebSocketPayload) {
try {
if (this.ws && this.ws.readyState == 1) {
log("Sending " + payload.event);
this.ws.send(payload.toString());
}
});
} catch {
log("There was no open ws client, so no data sent (" + payload.event + ")");
}
}
public static answerRequest(type: RequestType, data: any) {
@ -45,70 +42,68 @@ export class WebSocketHandler {
}
public static sendStatus() {
return new Promise(resolve => {
let payload = new WebSocketPayload(WebSocketEvent.STATUS, {status: iTender.status});
WebSocketHandler.send(payload).then(resolve);
});
log("Sending status " + iTender.status);
let payload = new WebSocketPayload(WebSocketEvent.STATUS, {status: iTender.status});
WebSocketHandler.send(payload);
}
static sendRunningConfig() {
return new Promise(resolve => {
let payload = new WebSocketPayload(WebSocketEvent.CONFIG, Settings.json);
WebSocketHandler.send(payload).then(resolve);
});
let payload = new WebSocketPayload(WebSocketEvent.CONFIG, Settings.json);
WebSocketHandler.send(payload)
}
static sendStats() {
return new Promise(async resolve => {
static async sendStats() {
let counts: any[] = [];
let drinks = await Drink.find();
for (let drink of drinks) {
console.log(drink._id);
console.log( (await Job.countDocuments( )) );
let count = await Job.countDocuments({drink: drink._id});
console.log(count);
let counts: any[] = [];
let drinks = await Drink.find();
for (let drink of drinks) {
console.log(drink._id);
console.log((await Job.countDocuments()));
let count = await Job.countDocuments({drink: drink._id});
console.log(count);
counts.push([drink, count]);
}
counts.push([drink, count]);
}
counts = counts.sort((a, b) => {
if (a[1] > b[1])
return -1;
else if (a[1] < b[1])
return 1;
else
return 0;
});
let stats = {
"drinks_finished": (await Job.countDocuments({successful: true})),
"drink_most": (counts.length == 0) ? "Keiner" : counts[0][0].name,
"count_ingredients": (await Ingredient.countDocuments()),
"count_cocktails": (await Drink.countDocuments())
};
let payload = new WebSocketPayload(WebSocketEvent.RESPONSE, {
type: RequestType.STATS,
data: stats
});
WebSocketHandler.send(payload).then(resolve);
counts = counts.sort((a, b) => {
if (a[1] > b[1])
return -1;
else if (a[1] < b[1])
return 1;
else
return 0;
});
let stats = {
"drinks_finished": (await Job.countDocuments({successful: true})),
"drink_most": (counts.length == 0) ? "Keiner" : counts[0][0].name,
"count_ingredients": (await Ingredient.countDocuments()),
"count_cocktails": (await Drink.countDocuments())
};
let payload = new WebSocketPayload(WebSocketEvent.RESPONSE, {
type: RequestType.STATS,
data: stats
});
WebSocketHandler.send(payload)
}
static sendContainers() {
return new Promise(async resolve => {
let payload = new WebSocketPayload(WebSocketEvent.CONTAINERS, (await Container.find().populate("content")));
WebSocketHandler.send(payload).then(resolve);
})
public static async sendContainers() {
let payload = new WebSocketPayload(WebSocketEvent.CONTAINERS, (await Container.find().sort({"slot": 1}).populate("content")));
WebSocketHandler.send(payload);
}
public static async sendIngredients() {
let payload = new WebSocketPayload(WebSocketEvent.INGREDIENTS, (await Ingredient.find().sort({"name": 1})));
WebSocketHandler.send(payload);
}
static sendDrinks() {
return new Promise(async resolve => {
let payload = new WebSocketPayload(WebSocketEvent.DRINKS, iTender.drinks);
WebSocketHandler.send(payload).then(resolve);
})
let payload = new WebSocketPayload(WebSocketEvent.DRINKS, iTender.drinks);
WebSocketHandler.send(payload);
}
}

View File

@ -1,5 +1,4 @@
import {WebSocketEvent} from "./WebSocketEvent";
import {Buffer} from "buffer";
export class WebSocketPayload {
set event(value: WebSocketEvent) {

View File

@ -13,7 +13,7 @@ export const ContainerSchema = new Mongoose.Schema<IContainer>({
content: {type: mongoose.Types.ObjectId, ref: "Ingredient"},
sensorDelta: Number, // V2: Now sensorDelta - Differenz, welche beim Einstellen der Zutat aus Gewicht(Sensor) - Volumen errechnet wird
sensorTare: Number, // V2: Now sensorTare
useProxy: { type: Boolean, default: false },
useProxy: {type: Boolean, default: false},
filled: Number,
enabled: {type: Boolean, default: false},
});

View File

@ -28,6 +28,7 @@ export interface IContainer extends mongoose.Document {
rawData: number;
pumpPin: number;
// Filled up to?
filled: number;
enabled: boolean;
}

View File

@ -6,7 +6,7 @@ export interface IDrink extends mongoose.Document {
name: string;
// Ingredients
ingredients: { type: IIngredient, amount: Number }[];
ingredients: { type: IIngredient, amount: number }[];
}

View File

@ -15,6 +15,7 @@ import Ingredient from "./database/Ingredient";
import {RejectReason} from "./RejectReason";
import axios from "axios";
import {Mixer} from "./Mixer";
import mongoose from "mongoose";
const log = debug("itender:station");
@ -97,12 +98,12 @@ export class iTender {
* also calculates the amount of time, the drink will need to be done
* @param data
*/
static onReceiveFill(data: { drink: IDrink, amounts?: { ingredient: String, amount: number }[], amount?: number }): Promise<IJob> {
static onReceiveFill(data: { drink: IDrink, amounts?: { ingredient: mongoose.Types.ObjectId, 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();
reject(new Error("Drink could not be found"));
return;
}
@ -145,7 +146,7 @@ export class iTender {
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);
reject(new Error(RejectReason.NOT_ENOUGH_INGREDIENTS));
return;
}
x.container = c;
@ -166,8 +167,6 @@ export class iTender {
await Mixer.startFill(job);
});
}

View File

@ -1,4 +1,7 @@
import {App} from "./App";
require('dotenv').config();
console.log("iTender - Node " + process.version);
import debug from "debug";
import {WebsocketApp} from "./WebsocketApp";
import {Database} from "./database/Database";
@ -12,14 +15,10 @@ import {ContainerHelper} from "./ContainerHelper";
import {ArduinoProxy} from "./ArduinoProxy";
import path from "path";
import {ErrorHandler, InternalError} from "./ErrorHandler";
//import {LEDHandler} from "./LEDHandler";
import {AppMaintenance} from "./maintenance/AppMaintenance";
const log = debug("itender:server");
const maintenance = new AppMaintenance();
const app = new App();
const wsApp = new WebsocketApp();
@ -46,22 +45,22 @@ process.on("unhandledRejection", (reason, promise) => {
if (Settings.get("arduino_proxy_enabled") as boolean) {
try {
await ArduinoProxy.connect();
} catch (e) {
} catch {
Settings.set("arduino_proxy_enabled", false);
Settings.setupDone = false;
log("Force iTender to setup, because proxy not connected!");
}
}
//await test();
/*//await test();
try {
await maintenance.listen();
//await maintenance.listen();
} catch( e )
{
log("Could not start maintenance web app");
console.error(e);
}
}*/
await app.listen();
//await app.listen();
await wsApp.listen();

View File

@ -22,6 +22,7 @@ import {ErrorHandler, InternalError} from "../../ErrorHandler";
import {NextFunction, Request} from "express";
import WebsocketContainers from "./WebsocketContainers";
import WebsocketContainerUpdate from "./WebsocketContainerUpdate";
import {ContainerHelper} from "../../ContainerHelper";
const exec = promisify(require('child_process').exec)
@ -38,11 +39,12 @@ export async function handleWebSocket(ws: WebSocket | any, req: Request, next: N
await WebSocketHandler.sendRunningConfig();
await WebSocketHandler.sendContainers();
await WebSocketHandler.sendIngredients();
await WebSocketHandler.sendStatus();
await WebSocketHandler.sendDrinks();
ws.on('message', async (raw, bool) => {
ws.on('message', async (raw: any, bool: any) => {
try {
let msg = WebSocketPayload.parseFromBase64Json(raw);
// If message is null, close the socket because it could not be decompiled
@ -79,11 +81,9 @@ export async function handleWebSocket(ws: WebSocket | any, req: Request, next: N
case WebSocketEvent.SETUP: {
if ((msg.data as boolean)) {
iTender.setStatus(iTenderStatus.SETUP);
} else {
if (Settings.setupDone) {
iTender.setStatus(iTenderStatus.READY);
await WebSocketHandler.sendRunningConfig();
}
} else if (Settings.setupDone) {
iTender.setStatus(iTenderStatus.READY);
await WebSocketHandler.sendRunningConfig();
}
await WebSocketHandler.sendContainers();
break;
@ -103,11 +103,11 @@ export async function handleWebSocket(ws: WebSocket | any, req: Request, next: N
break;
}
case RequestType.CONTAINERS: {
WebSocketHandler.answerRequest(msg.data["type"] as RequestType, (await Container.find().sort({"slot": 1}).populate("content")));
await WebSocketHandler.sendContainers();
break;
}
case RequestType.INGREDIENTS: {
WebSocketHandler.answerRequest(msg.data["type"] as RequestType, (await Ingredient.find().sort({"name": 1})));
await WebSocketHandler.sendIngredients();
break;
}
case RequestType.STARTFILL: {
@ -154,8 +154,8 @@ export async function handleWebSocket(ws: WebSocket | any, req: Request, next: N
if (conf["arduino_proxy_enabled"]) {
try {
ArduinoProxy.disconnect();
} catch (e) {
} catch {
//ignored
}
try {
await ArduinoProxy.connect();
@ -189,50 +189,15 @@ export async function handleWebSocket(ws: WebSocket | any, req: Request, next: N
}
case RequestType.TARE: {
let type = msg.data["type"];
// Start TARE
let success = true;
for (let c of await Container.find({})) {
if (c.sensorType != SensorType.NONE) {
c.sensorTare = 0;
await c.save();
}
try {
await ContainerHelper.tare()
WebSocketHandler.answerRequest(type, {success: true, msg: "OK"});
} catch (e) {
console.error(e);
WebSocketHandler.answerRequest(type, {success: false, msg: "failed" + e});
}
let timeouts: Array<NodeJS.Timeout> = [];
async function measureAndSafe() {
try {
await SensorHelper.measureAllRaw();
for (let c of await Container.find({})) {
if (c.sensorType != SensorType.NONE) {
c.sensorTare += c.rawData;
}
}
} catch (e) {
WebSocketHandler.answerRequest(type, {success: false, msg: e});
success = false;
for (let t of timeouts)
clearTimeout(t);
}
}
timeouts.push(setTimeout(measureAndSafe, 500));
timeouts.push(setTimeout(measureAndSafe, 1000));
timeouts.push(setTimeout(measureAndSafe, 2000));
timeouts.push(setTimeout(measureAndSafe, 3000));
setTimeout(async () => {
if (success) {
for (let c of await Container.find({})) {
if (c.sensorType != SensorType.NONE) {
c.sensorTare = c.sensorTare / 4;
await c.save();
}
}
WebSocketHandler.answerRequest(type, {success: true, msg: "OK"});
}
}, 4000);
break;
}
@ -269,8 +234,7 @@ export async function handleWebSocket(ws: WebSocket | any, req: Request, next: N
case RequestType.INFO: {
let nets = os.networkInterfaces();
let net = nets["wlan0"];
if (!net)
net = nets["wlp0s20f3"];
net ??= nets["wlp0s20f3"];
let ipAddr: string = "";
if (net)
@ -284,7 +248,7 @@ export async function handleWebSocket(ws: WebSocket | any, req: Request, next: N
try {
wifi = (await exec("iwgetid")).stdout
uptime = (await exec("uptime -p")).stdout;
} catch (e) {
} catch {
}
wifi = wifi.substring(wifi.indexOf('"') + 1, wifi.length - 2);

View File

@ -9,7 +9,6 @@ import {IContainer} from "../database/IContainer";
import {SensorType} from "../SensorType";
import {RequestType} from "../RequestType";
export class Setup {
public static arduinoProxyCheckboxes: HTMLInputElement[] = [];
@ -277,7 +276,7 @@ Die Gewichtssensoren werden beim Bestätigen austariert<br><br>Zum fortfahren Ta
ul = document.createElement("ul");
modal.addContent(ul);
let tareInterval: NodeJS.Timer | number = 0;
let tareInterval: NodeJS.Timeout | number = 0;
let btn = document.createElement("button");
btn.classList.add("btn", "btn-primary");
@ -439,9 +438,9 @@ Mindestens ein Sensor konnte nicht kalibriert werden.<br>${data.msg}<br>`;
buttonCalibrateSeconds.innerText = "Füllzeit Kalibrieren";
containerDiv.append(buttonCalibrateSeconds);
buttonCalibrateSeconds.onclick = () => {
/* WebWebSocketHandler.send(new WebSocketPayload()).then(() => {
/* WebWebSocketHandler.send(new WebSocketPayload()).then(() => {
});*/
});*/
let modal = new Modal("calibration", "Kalibrierung");
let txt = document.createElement("p");
txt.innerHTML = `Während der Kalibrierung wird der Behälter solange entleert, bis die 100ml erreicht wurden.<br>

View File

@ -156,7 +156,7 @@ function setupOnClickEvents() {
}
let wsHandler;
let wsHandler: WebWebSocketHandler;
function connect(): Promise<void> {
return new Promise(resolve => {

View File

@ -24,6 +24,7 @@
"./src/"
],
"exclude": [
"../node_modules"
"../node_modules",
"./src/web"
]
}

View File

@ -1008,6 +1008,11 @@ doctypes@^1.1.0:
resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9"
integrity sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==
dotenv@^16.5.0:
version "16.5.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.5.0.tgz#092b49f25f808f020050051d1ff258e404c78692"
integrity sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==
dunder-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"