Took 2 hours 20 minutes
This commit is contained in:
Tobias Hopp 2023-02-13 11:22:21 +01:00
parent bdd0a9f4f6
commit b05b111140
16 changed files with 201 additions and 78 deletions

View File

@ -199,6 +199,7 @@ Programmed by Tobias Hopp" >/etc/motd
echo "[Service] echo "[Service]
ExecStart=/usr/sbin/dhcpcd -q" >/etc/systemd/system/dhcpcd.service.d/wait.conf ExecStart=/usr/sbin/dhcpcd -q" >/etc/systemd/system/dhcpcd.service.d/wait.conf
chown itender:itender -R /home/itender/ chown itender:itender -R /home/itender/
echo "Installation finished!" echo "Installation finished!"

View File

@ -26,7 +26,7 @@
"@types/rpi-ws281x-native": "^1.0.0", "@types/rpi-ws281x-native": "^1.0.0",
"@types/serialport": "^8.0.2", "@types/serialport": "^8.0.2",
"@types/sharp": "^0.31.1", "@types/sharp": "^0.31.1",
"axios": "^1.2.0", "axios": "^1.3.2",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"debug": "^4.3.4", "debug": "^4.3.4",

BIN
public/static/large.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

BIN
public/static/normal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

BIN
public/static/shot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

BIN
public/static/small.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@ -80,6 +80,8 @@ body {
background-position: center; background-position: center;
background-size: auto 83%; background-size: auto 83%;
margin: auto auto 2%; margin: auto auto 2%;
color:white;
font-size: 1.5em;
} }
.water::before { .water::before {
@ -136,5 +138,6 @@ body {
} }
#main_fillTxt { #main_fillTxt {
margin-bottom: 3%; margin-bottom: 1.3%;
margin-top:1.2%;
} }

14
public_key.pem Normal file
View File

@ -0,0 +1,14 @@
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA3x3RpWBFx0LdBmW2Dspz
s5rigcjZLUVP9U8fJrtSqG79EmSXSNBOrNJpJnokWEDmNjXvHSCXpzuAGOQkYqbs
Z6o8g+OTK4LPd3J0IZeo7Y8NGerb15mXttR6wvEMmusFtp5J/wm7XYzUQADlvgKc
cgbi0+/A0Vf7jCmzRPsw/foKPh6UiElsvZJTzzCzuADohb53U9aIerx2akhR1YnN
2I/kgxhJ0ro+HZule0bEbJ7ZdDvhNMnXdNyaiotpb34q8EByjfhI663pvXAorFu4
9Yiejl3SfI9/e9xhh7Y6MWMFAVzSv3TTIZMbmjX22fAffK8nO4SbAdGBrCM2k2dE
7HURS9/3iAgBFQcLFA6OS2HKX8FjfExv7pc9b5ROPlcbcJ2jFAOue7ZMcNQVByqa
vA7PF+9lydCNOyHfRo2OTkqZRljIad27p92mX049U2AvBfODoHTvWSwVy7/3DTPd
HWdGFvV5dbazE25NmwjEcJ50sXLhPXv9rzij3mxY7j1c6bVd+6v7Dds7jUYsbE6o
MCnaetSRMITGohfhtwvS4kbt4pGOzZ73T/XRfdmR5bnWubx5bgwgaBMhAJnUF346
0uJnYY/ij+bCa+NJpUCegoudQ2PPmMxcTLs527EGbNFyUXfogLbzqr5XUOJIvgHK
sfaW7BSbcB4xTPvfDuLIhA8CAwEAAQ==
-----END PUBLIC KEY-----

32
src/Encrypter.ts Normal file
View File

@ -0,0 +1,32 @@
import crypto from "crypto";
export class Encrypter {
private readonly algorithm: string;
private readonly key: Buffer;
constructor(encryptionKey) {
this.algorithm = "aes-192-cbc";
this.key = crypto.scryptSync(encryptionKey, "salt", 24);
}
public encrypt(clearText) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(this.algorithm, this.key, iv);
const encrypted = cipher.update(clearText, "utf8", "hex");
return [
encrypted + cipher.final("hex"),
Buffer.from(iv).toString("hex"),
].join("|");
}
public decrypt(encryptedText) {
const [encrypted, iv] = encryptedText.split("|");
if (!iv) throw new Error("IV not found");
const decipher = crypto.createDecipheriv(
this.algorithm,
this.key,
Buffer.from(iv, "hex")
);
return decipher.update(encrypted, "hex", "utf8") + decipher.final("utf8");
}
}

43
src/ErrorHandler.ts Normal file
View File

@ -0,0 +1,43 @@
import axios from "axios";
import {Encrypter} from "./Encrypter";
export class ErrorHandler {
public static sendError(error: InternalError) {
let encrypter = new Encrypter("N50LtuKpzOvxp44vaYBFXBQo1tubTY");
return new Promise<void>((resolve, reject) => {
let encrypted = Buffer.from(encrypter.encrypt(error.toJson())).toString("base64");
axios.post('https://itender.iif.li/report/send', encrypted, {headers: {"Content-Type": "text/plain", Accept: "application/json"}} ).then(res => {
if( res.status != 200 )
reject();
else
return resolve();
console.log("Error report was sent to iTender Manager");
}).catch((e) => reject(e));
});
}
}
export class InternalError {
private readonly message: string;
private readonly stack: string;
private readonly name: string;
constructor(message: string, stack: string|undefined, name?: string) {
this.message = message;
this.stack = stack ? stack : "";
this.name = name ? name : "";
}
public toJson(): string {
return JSON.stringify({
message: this.message,
stack: this.stack,
name: this.name,
date: new Date().toDateString()
})
}
}

View File

@ -9,7 +9,6 @@ import {iTender} from "./iTender";
import debug from "debug"; import debug from "debug";
import {ArduinoProxyPayload} from "./ArduinoProxyPayload"; import {ArduinoProxyPayload} from "./ArduinoProxyPayload";
import {ArduinoProxyPayloadType} from "./ArduinoProxyPayloadType"; import {ArduinoProxyPayloadType} from "./ArduinoProxyPayloadType";
import {ArduinoProxy} from "./ArduinoProxy";
import {ContainerHelper} from "./ContainerHelper"; import {ContainerHelper} from "./ContainerHelper";
const isPI = require("detect-rpi"); const isPI = require("detect-rpi");
@ -17,15 +16,16 @@ const isPI = require("detect-rpi");
const log = debug("itender:mix"); const log = debug("itender:mix");
export class Mixer { export class Mixer {
static get currentJob(): IJob {
return this._currentJob;
}
/** /**
* Timers for the job, for the pumps etc. * Timers for the job, for the pumps etc.
* @private * @private
*/ */
private static _jobTimers: NodeJS.Timeout[] = []; private static _jobTimers: NodeJS.Timeout[] = [];
/**
* Checks if the job has finished every 500ms
* @private
*/
private static _jobEndCheckInterval: NodeJS.Timer;
/** /**
* The current itender job * The current itender job
@ -33,12 +33,9 @@ export class Mixer {
*/ */
private static _currentJob: IJob; private static _currentJob: IJob;
/** static get currentJob(): IJob {
* Checks if the job has finished every 500ms return this._currentJob;
* @private }
*/
private static _jobEndCheckInterval: NodeJS.Timer;
/** /**
* Start the internal fill method, a sub-method of the onReceiveFill method * Start the internal fill method, a sub-method of the onReceiveFill method
@ -124,14 +121,13 @@ export class Mixer {
await x.container.save(); await x.container.save();
} }
this._jobTimers.splice(this._jobTimers.indexOf(timer),1); this._jobTimers.splice(this._jobTimers.indexOf(timer), 1);
}, waitTime); }, waitTime);
this._jobTimers.push(timer); this._jobTimers.push(timer);
} }
this._jobEndCheckInterval = setInterval(async () => { this._jobEndCheckInterval = setInterval(async () => {
if (this._jobTimers.length != 0) if (this._jobTimers.length != 0)
return; return;
@ -153,47 +149,52 @@ export class Mixer {
/** /**
* Cancel the fill instantly * Cancel the fill instantly
*/ */
static async cancelFill() { static cancelFill() {
if (!this._currentJob || iTender.status != iTenderStatus.FILLING) return new Promise<void>(async (resolve, reject) => {
return; if (!this._currentJob || iTender.status != iTenderStatus.FILLING)
return resolve();
clearInterval(this._jobEndCheckInterval); clearInterval(this._jobEndCheckInterval);
this._currentJob.successful = false; this._currentJob.successful = false;
this._currentJob.endAt = new Date(); this._currentJob.endAt = new Date();
await this._currentJob.save(); 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 {
if (jobIngredient.container.useProxy) {
let payload = new ArduinoProxyPayload(ArduinoProxyPayloadType.SET_PIN, {
pin: jobIngredient.container.pumpPin,
mode: "DIGITAL",
"value": 0
});
await payload.send();
} else {
await MyGPIO.write(jobIngredient.container.pumpPin, false);
}
} catch (e) {
for (let timer of this._jobTimers) {
// Clears all the ongoing stop timers
clearTimeout(timer);
} }
// ToDo v2 calc for (let jobIngredient of this._currentJob.amounts) {
let container: IContainer = jobIngredient.container; // stop pump pin
let deltaStartStop = (this._currentJob.endAt.getTime() - this._currentJob.startedAt.getTime()) / 1000; try {
if (jobIngredient.container.useProxy) {
let payload = new ArduinoProxyPayload(ArduinoProxyPayloadType.SET_PIN, {
pin: jobIngredient.container.pumpPin,
mode: "DIGITAL",
"value": 0
});
await payload.send();
} else {
await MyGPIO.write(jobIngredient.container.pumpPin, false);
}
} catch (e) {
// füllmenge - ( ( (stopp-start) / 1000 ) * ( sekunden100ml / 100 ) ) }
container.filled = container.filled - (deltaStartStop * (iTender.secondsPer100ml / 100)) // V2: Near the current fill value based on time values from delta start stop
container.save().then(); // ToDo v2 calc
} let container: IContainer = jobIngredient.container;
let deltaStartStop = (this._currentJob.endAt.getTime() - this._currentJob.startedAt.getTime()) / 1000;
// füllmenge - ( ( (stopp-start) / 1000 ) * ( sekunden100ml / 100 ) )
container.filled = container.filled - (deltaStartStop * (iTender.secondsPer100ml / 100)) // V2: Near the current fill value based on time values from delta start stop
container.save().then();
}
setTimeout(() => {
iTender.setStatus(iTenderStatus.READY);
resolve();
}, 1000);
});
iTender.setStatus(iTenderStatus.READY);
} }
} }

View File

@ -9,9 +9,10 @@ import {Settings} from "./Settings";
import Drink from "./database/Drink"; import Drink from "./database/Drink";
import {MyGPIO} from "./MyGPIO"; import {MyGPIO} from "./MyGPIO";
import {ContainerHelper} from "./ContainerHelper"; import {ContainerHelper} from "./ContainerHelper";
import {Mixer} from "./Mixer";
import {ArduinoProxy} from "./ArduinoProxy"; import {ArduinoProxy} from "./ArduinoProxy";
import path from "path"; import path from "path";
import {ErrorHandler, InternalError} from "./ErrorHandler";
const log = debug("itender:server"); const log = debug("itender:server");
@ -21,6 +22,18 @@ const wsApp = new WebsocketApp();
global.appRoot = path.resolve(__dirname); global.appRoot = path.resolve(__dirname);
process.on("uncaughtException", (error) => {
let iError = new InternalError("UncaughtException: " + error.message, error.stack, error.name);
ErrorHandler.sendError(iError).then().catch((e) => console.error("Error report could not been sent!\n" +e)).then(() => process.exit(255));
});
process.on("unhandledRejection", (reason, promise) => {
let iError = new InternalError("UnhandledRejection: " + reason, promise.toString());
ErrorHandler.sendError(iError).then().catch((e) => console.error("Error report could not been sent!\n" +e)).then(() => process.exit(255));
});
(async () => { (async () => {
try { try {
log("Starting..."); log("Starting...");
@ -28,13 +41,11 @@ global.appRoot = path.resolve(__dirname);
await Database.connect(); await Database.connect();
if( Settings.get("arduino_proxy_enabled") as boolean ) if (Settings.get("arduino_proxy_enabled") as boolean) {
{
try { try {
await ArduinoProxy.connect(); await ArduinoProxy.connect();
} catch( e ) } catch (e) {
{ Settings.set("arduino_proxy_enabled", false);
Settings.set("arduino_proxy_enabled",false);
Settings.setupDone = false; Settings.setupDone = false;
log("Force iTender to setup, because proxy not connected!"); log("Force iTender to setup, because proxy not connected!");
} }
@ -88,18 +99,18 @@ function init(): Promise<void> {
await iTender.refreshFromServer(); await iTender.refreshFromServer();
} }
}, 1000 * 15); }, 1000 * 15);
log("1");
log("1/4");
// Containers // Containers
//await iTender.refreshContainers(); //await iTender.refreshContainers();
await ContainerHelper.measureContainers(); await ContainerHelper.measureContainers();
log("2"); log("2/4");
// Drinks // Drinks
await iTender.refreshDrinks(); await iTender.refreshDrinks();
log("3"); log("3/4");
// Start auto checkup for stuck jobs // Start auto checkup for stuck jobs
await iTender.autoCheckup(); await iTender.autoCheckup();
log("4"); log("4/4");
resolve(); resolve();
}); });
} }

View File

@ -20,6 +20,7 @@ import {promisify} from "util";
import Drink from "../../database/Drink"; import Drink from "../../database/Drink";
import path from "path"; import path from "path";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
import {ErrorHandler, InternalError} from "../../ErrorHandler";
const exec = promisify(require('child_process').exec) const exec = promisify(require('child_process').exec)
@ -307,9 +308,10 @@ router.ws('/', async (ws, req, next) => {
let result = await exec(path.join(global.appRoot, "/../update.sh")); let result = await exec(path.join(global.appRoot, "/../update.sh"));
if (result.stderr) if (result.stderr)
await WebSocketHandler.send(new WebSocketPayload(WebSocketEvent.ERROR, "Der iTender konnte das Update nicht installieren.<br>Möglicherweise ist die Internetverbindung nicht ausreichend oder das Update enthält Fehler.<br>")); await WebSocketHandler.send(new WebSocketPayload(WebSocketEvent.ERROR, "Der iTender konnte das Update nicht installieren.<br>Möglicherweise ist die Internetverbindung nicht ausreichend oder das Update enthält Fehler.<br>"));
let error = new InternalError("Update request from user-interface failed while executing update script", result.stderr, result.code);
await ErrorHandler.sendError(error);
} catch (e) { } catch (e) {
console.error(e); let error = e as { code: number, killed: boolean, cmd: string, stderr: string };
let error = e as { code: number, killed: boolean, cmd: string };
let msg = ""; let msg = "";
if (error.code == 127) if (error.code == 127)
@ -319,6 +321,9 @@ router.ws('/', async (ws, req, next) => {
await WebSocketHandler.send(new WebSocketPayload(WebSocketEvent.ERROR, "Der iTender konnte das Update nicht installieren.<br><br>" + msg)); await WebSocketHandler.send(new WebSocketPayload(WebSocketEvent.ERROR, "Der iTender konnte das Update nicht installieren.<br><br>" + msg));
log("Could not execute update.sh"); log("Could not execute update.sh");
let iE = new InternalError("Update request from user-interface failed while executing update script", error.stderr, error.code + "");
await ErrorHandler.sendError(iE);
} }
break; break;
} }

View File

@ -16,7 +16,7 @@ export class Fill {
modal.addContent(header); modal.addContent(header);
let txt = document.createElement("p"); let txt = document.createElement("p");
txt.innerHTML = `Der Cocktail wird gerade zubereitet`; txt.innerHTML = ``;
txt.id = "main_fillTxt"; txt.id = "main_fillTxt";
let waterAnimDiv = document.createElement("div"); let waterAnimDiv = document.createElement("div");
@ -68,30 +68,34 @@ export class Fill {
WebWebSocketHandler.request(RequestType.JOB).then((payload) => { WebWebSocketHandler.request(RequestType.JOB).then((payload) => {
let minus = -1; let minus = -1;
let job = payload.data as IJob; let job = payload.data as IJob;
ml.innerText = Math.floor((job.completeAmount / job.estimatedTime) * minus) + "ml"; ml.innerText = "0ml";
waterAnimDiv.style.setProperty("--fillTime", job.estimatedTime + "s"); waterAnimDiv.style.setProperty("--fillTime", job.estimatedTime + "s");
waterAnimDiv.style.backgroundImage = `url("/images/${job.drink._id}.png")`; waterAnimDiv.style.backgroundImage = `url("/images/${job.drink._id}.png")`;
txt.innerText = job.completeAmount + "ml";
header.innerText = job.drink.name; header.innerText = job.drink.name;
seconds.innerText = Math.floor(job.estimatedTime) + "s"; seconds.innerText = Math.floor(job.estimatedTime) + "s";
let last = 0; let last = 0;
function updateTimeAndMl()
{ function updateTimeAndMl() {
minus++; minus++;
if (minus + 1 > (job.estimatedTime as number)) { if (minus + 1 > (job.estimatedTime as number)) {
setTimeout(() => clearInterval(interval), 2000); clearInterval(interval);
} }
let iT = (Math.floor(job.estimatedTime as number - minus)); let iT = (Math.floor(job.estimatedTime as number - minus));
if (iT < 0) if (iT < 0)
iT = 0; iT = 0;
let eA = Math.floor((job.completeAmount / job.estimatedTime) * minus);
if (eA < 0) {
eA = 0;
}
seconds.innerText = iT + "s"; seconds.innerText = iT + "s";
let calc = Math.floor((job.completeAmount / job.estimatedTime) * minus); riseSlowlyUp(last, eA)
riseSlowlyUp(last, calc) last = eA;
last = calc;
} }
interval = setInterval(updateTimeAndMl, 1000); interval = setInterval(updateTimeAndMl, 1000);
updateTimeAndMl(); updateTimeAndMl();
@ -99,6 +103,7 @@ export class Fill {
setTimeout(() => { setTimeout(() => {
txt.innerHTML = "Bitte entnehme den Cocktail"; txt.innerHTML = "Bitte entnehme den Cocktail";
modal.title.innerHTML = "Cocktail fertig gestellt" modal.title.innerHTML = "Cocktail fertig gestellt"
ml.innerText = job.completeAmount + "ml";
cancelBtn.classList.add("btn-blendout"); cancelBtn.classList.add("btn-blendout");
waterAnimDiv.classList.add("waterFinished"); waterAnimDiv.classList.add("waterFinished");
@ -123,15 +128,19 @@ export class Fill {
div.style.gridTemplateRows = "100%"; div.style.gridTemplateRows = "100%";
div.style.gridTemplateColumns = "repeat(4,auto)"; div.style.gridTemplateColumns = "repeat(4,auto)";
div.style.marginTop = "5%"; div.style.marginTop = "5%";
div.style.marginBottom = "2%"; div.style.height = "50vh";
div.style.marginBottom = "-12%";
let sizes = [["shot", "Shot", 20], ["small", "Klein", 120], ["normal", "Normal", 200], ["large", "Groß", 300]]; let sizes = [["shot", "Shot", 20], ["small", "Klein", 120], ["normal", "Normal", 200], ["large", "Groß", 300]];
for (let s of sizes) { for (let s of sizes) {
let glass = document.createElement("div"); let glass = document.createElement("div");
/*glass.style.maxWidth = "50%"
glass.style.minWidth = "50%";*/
let img = document.createElement("img"); let img = document.createElement("img");
img.src = "/static/" + s[0] + ".png"; img.src = "/static/" + s[0] + ".png";
img.style.minHeight = "100%"; img.style.minHeight = "50%";
img.style.maxHeight = "100%"; img.style.maxHeight = "50%";
img.alt = "" + s[1]; img.alt = "" + s[1];
let bottom = document.createElement("p"); let bottom = document.createElement("p");

View File

@ -5,6 +5,10 @@ html
meta(name="viewport" content="width=device-width, initial-scale=1.0") meta(name="viewport" content="width=device-width, initial-scale=1.0")
link(rel='stylesheet', href='/stylesheets/reset.css') link(rel='stylesheet', href='/stylesheets/reset.css')
link(rel='stylesheet', href='/stylesheets/style.css') link(rel='stylesheet', href='/stylesheets/style.css')
link(rel='preload' as='image' href='/static/shot.png')
link(rel='preload' as='image' href='/static/small.png')
link(rel='preload' as='image' href='/static/normal.png')
link(rel='preload' as='image' href='/static/large.png')
meta(charset="UTF-8") meta(charset="UTF-8")
body body
div#blockPanel div#blockPanel

View File

@ -1363,10 +1363,10 @@ asynckit@^0.4.0:
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
axios@^1.2.0: axios@^1.3.2:
version "1.2.3" version "1.3.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.2.3.tgz#31a3d824c0ebf754a004b585e5f04a5f87e6c4ff" resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.2.tgz#7ac517f0fa3ec46e0e636223fd973713a09c72b3"
integrity sha512-pdDkMYJeuXLZ6Xj/Q5J3Phpe+jbGdsSzlQaFVkMQzRUL05+6+tetX8TV3p4HrU4kzuO9bt+io/yGQxuyxA/xcw== integrity sha512-1M3O703bYqYuPhbHeya5bnhpYVsDDRyQSabNja04mZtboLNSuZ4YrltestrLXfHgmzua4TpUqRiVKbiQuo2epw==
dependencies: dependencies:
follow-redirects "^1.15.0" follow-redirects "^1.15.0"
form-data "^4.0.0" form-data "^4.0.0"