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]
ExecStart=/usr/sbin/dhcpcd -q" >/etc/systemd/system/dhcpcd.service.d/wait.conf
chown itender:itender -R /home/itender/
echo "Installation finished!"

View File

@ -26,7 +26,7 @@
"@types/rpi-ws281x-native": "^1.0.0",
"@types/serialport": "^8.0.2",
"@types/sharp": "^0.31.1",
"axios": "^1.2.0",
"axios": "^1.3.2",
"buffer": "^6.0.3",
"cookie-parser": "^1.4.6",
"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-size: auto 83%;
margin: auto auto 2%;
color:white;
font-size: 1.5em;
}
.water::before {
@ -136,5 +138,6 @@ body {
}
#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 {ArduinoProxyPayload} from "./ArduinoProxyPayload";
import {ArduinoProxyPayloadType} from "./ArduinoProxyPayloadType";
import {ArduinoProxy} from "./ArduinoProxy";
import {ContainerHelper} from "./ContainerHelper";
const isPI = require("detect-rpi");
@ -17,15 +16,16 @@ const isPI = require("detect-rpi");
const log = debug("itender:mix");
export class Mixer {
static get currentJob(): IJob {
return this._currentJob;
}
/**
* Timers for the job, for the pumps etc.
* @private
*/
private static _jobTimers: NodeJS.Timeout[] = [];
/**
* Checks if the job has finished every 500ms
* @private
*/
private static _jobEndCheckInterval: NodeJS.Timer;
/**
* The current itender job
@ -33,12 +33,9 @@ export class Mixer {
*/
private static _currentJob: IJob;
/**
* Checks if the job has finished every 500ms
* @private
*/
private static _jobEndCheckInterval: NodeJS.Timer;
static get currentJob(): IJob {
return this._currentJob;
}
/**
* Start the internal fill method, a sub-method of the onReceiveFill method
@ -124,14 +121,13 @@ export class Mixer {
await x.container.save();
}
this._jobTimers.splice(this._jobTimers.indexOf(timer),1);
this._jobTimers.splice(this._jobTimers.indexOf(timer), 1);
}, waitTime);
this._jobTimers.push(timer);
}
this._jobEndCheckInterval = setInterval(async () => {
if (this._jobTimers.length != 0)
return;
@ -153,47 +149,52 @@ export class Mixer {
/**
* Cancel the fill instantly
*/
static async cancelFill() {
if (!this._currentJob || iTender.status != iTenderStatus.FILLING)
return;
static cancelFill() {
return new Promise<void>(async (resolve, reject) => {
if (!this._currentJob || iTender.status != iTenderStatus.FILLING)
return resolve();
clearInterval(this._jobEndCheckInterval);
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 {
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) {
clearInterval(this._jobEndCheckInterval);
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);
}
// ToDo v2 calc
let container: IContainer = jobIngredient.container;
let deltaStartStop = (this._currentJob.endAt.getTime() - this._currentJob.startedAt.getTime()) / 1000;
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) {
// 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 {MyGPIO} from "./MyGPIO";
import {ContainerHelper} from "./ContainerHelper";
import {Mixer} from "./Mixer";
import {ArduinoProxy} from "./ArduinoProxy";
import path from "path";
import {ErrorHandler, InternalError} from "./ErrorHandler";
const log = debug("itender:server");
@ -21,6 +22,18 @@ const wsApp = new WebsocketApp();
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 () => {
try {
log("Starting...");
@ -28,13 +41,11 @@ global.appRoot = path.resolve(__dirname);
await Database.connect();
if( Settings.get("arduino_proxy_enabled") as boolean )
{
if (Settings.get("arduino_proxy_enabled") as boolean) {
try {
await ArduinoProxy.connect();
} catch( e )
{
Settings.set("arduino_proxy_enabled",false);
} catch (e) {
Settings.set("arduino_proxy_enabled", false);
Settings.setupDone = false;
log("Force iTender to setup, because proxy not connected!");
}
@ -88,18 +99,18 @@ function init(): Promise<void> {
await iTender.refreshFromServer();
}
}, 1000 * 15);
log("1");
log("1/4");
// Containers
//await iTender.refreshContainers();
await ContainerHelper.measureContainers();
log("2");
log("2/4");
// Drinks
await iTender.refreshDrinks();
log("3");
log("3/4");
// Start auto checkup for stuck jobs
await iTender.autoCheckup();
log("4");
log("4/4");
resolve();
});
}

View File

@ -20,6 +20,7 @@ import {promisify} from "util";
import Drink from "../../database/Drink";
import path from "path";
import {Utils} from "../../Utils";
import {ErrorHandler, InternalError} from "../../ErrorHandler";
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"));
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>"));
let error = new InternalError("Update request from user-interface failed while executing update script", result.stderr, result.code);
await ErrorHandler.sendError(error);
} catch (e) {
console.error(e);
let error = e as { code: number, killed: boolean, cmd: string };
let error = e as { code: number, killed: boolean, cmd: string, stderr: string };
let msg = "";
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));
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;
}

View File

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

View File

@ -5,6 +5,10 @@ html
meta(name="viewport" content="width=device-width, initial-scale=1.0")
link(rel='stylesheet', href='/stylesheets/reset.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")
body
div#blockPanel

View File

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