update
Took 4 hours 31 minutes
45
doc/Notes.md
@ -26,14 +26,49 @@ Was haben wir bereits am iTender Projekt gemacht?
|
||||
<img src="./Screenshot_Model1.1_BackDownLeft.png" width="50%">
|
||||
|
||||
#### Neues 3D-Modell
|
||||
Folgt.
|
||||
<img src="./Screenshot_Model1.2_Front.png" width="50%">
|
||||
<img src="./Screenshot_Model1.2_Back.png" width="50%">
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
## Umsetzung
|
||||
|
||||
#### Webseiten-Skizzen
|
||||
|
||||
## Das Programm
|
||||
|
||||
#### Aufbau
|
||||
- Das Programm ist aufgebaut in eine Client-Seite und eine Server-Seite
|
||||
- Diese sind zur Sicherheit des Geräts voneinander getrennt
|
||||
- Server und Client kommunizieren über einen WebSocket, welchen man sich als eine Art Chat-Kanal vorstellen kann
|
||||
- Client und Server haben bestimmte Status, ein Status ist beispielsweise READY oder FILLING
|
||||
- Die Oberfläche ist sowohl über das Display, aber auch über ein Tablet steuerbar
|
||||
- Oberfläche sendet Befehle an den Server → Server verarbeitet und gibt ggfs. eine Antwort
|
||||
|
||||
#### Fotos des Webinterfaces (Stand 21.11)
|
||||
|
||||
<strong>Main</strong>
|
||||
<br>
|
||||
Die Main Pane ist der Hauptteil und direkt die Einstiegsseite des iTenders<br>
|
||||
Hier können Getränke ausgewählt werden, welche dann "gemacht" werden
|
||||
<img src="./v1Main.png">
|
||||
<br><br>
|
||||
<strong>Menu</strong><br>
|
||||
Das Menü ist das Navigationsherz, von hier aus können alle anderen Panels erreicht werden<br>
|
||||
<img src="./v1Menu.png">
|
||||
<br><br>
|
||||
<strong>Containers</strong><br>
|
||||
Hier können die Behälter inhalte aktualisiert werden<br>
|
||||
Man wählt die "Zutat" aus und danach wie voll der Behälter nun ist<br>
|
||||
In der Regel kann das auch automatisch eingemessen werden, wenn alle Sensoren eingestellt sind<br>
|
||||
<br>Trotzdem sollte das hier eingestellt werden
|
||||
<img src="./v1Containers.png">
|
||||
<br><br>
|
||||
<strong>Fill</strong><br>
|
||||
Einfach ein "Popup" welches anzeigt dass das Getränk gefüllt wird
|
||||
<img src="./v1Fill.png">
|
||||
<br><br>
|
||||
<strong>Setup</strong><br>
|
||||
Das Setup ist das erste menü was nach dem ersten einrichten erscheint<br>
|
||||
es dient zur Grundkonfiguration
|
||||
<img src="./v1Setup.png">
|
||||
<br><br>
|
||||
|
||||
|
||||
|
BIN
doc/Screenshot_Model1.2_Back.png
Normal file
After Width: | Height: | Size: 162 KiB |
BIN
doc/Screenshot_Model1.2_Front.png
Normal file
After Width: | Height: | Size: 270 KiB |
33
doc/autostart.config
Normal file
@ -0,0 +1,33 @@
|
||||
#
|
||||
# These things are run when an Openbox X Session is started.
|
||||
# You may place a similar script in $HOME/.config/openbox/autostart
|
||||
# to run user-specific things.
|
||||
#
|
||||
|
||||
# If you want to use GNOME config tools...
|
||||
#
|
||||
#if test -x /usr/lib/aarch64-linux-gnu/gnome-settings-daemon >/dev/null; then
|
||||
# /usr/lib/aarch64-linux-gnu/gnome-settings-daemon &
|
||||
#elif which gnome-settings-daemon >/dev/null 2>&1; then
|
||||
# gnome-settings-daemon &
|
||||
#fi
|
||||
|
||||
# If you want to use XFCE config tools...
|
||||
#
|
||||
#xfce-mcs-manager &
|
||||
|
||||
xset s off
|
||||
xset s noblank
|
||||
xset -dpms
|
||||
|
||||
setxkbmap -option terminate:ctrl_alt_bksp
|
||||
|
||||
# Start Chromium in kiosk mode
|
||||
sed -i 's/"exited_cleanly":false/"exited_cleanly":true/' ~/.config/chromium/'Local State'
|
||||
sed -i 's/"exited_cleanly":false/"exited_cleanly":true/; s/"exit_type":"[^"]\+"/"exit_type":"Normal"/' ~/.config/chromium/Default/Preferences
|
||||
|
||||
/usr/bin/chromium-browser --disable-infobars --kiosk --incognito --disable-pinch --overscroll-history-navigation=0 http://192.168.1.186:3000/
|
||||
|
||||
|
||||
|
||||
|
19
doc/installPi.sh
Normal file
@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
apt update
|
||||
apt install
|
||||
|
||||
apt install --no-install-recommends xserver-xorg x11-xserver-utils xinit openbox -y
|
||||
apt purge nodejs npm -y
|
||||
curl -fsSL https://deb.nodesource.com/setup_19.x | sudo bash -
|
||||
apt install -y nodejs
|
||||
apt install gcc g++ make -y
|
||||
curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | sudo tee /usr/share/keyrings/yarnkey.gpg >/dev/null
|
||||
echo "deb [signed-by=/usr/share/keyrings/yarnkey.gpg] https://dl.yarnpkg.com/debian stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
|
||||
apt update
|
||||
apt install yarn git cmake make chromium-browser unclutter -y
|
||||
apt upgrade -y
|
||||
echo "allowed_users=anybody" >/etc/X11/Xwrapper.config
|
||||
cp autostart.config /etc/xdg/openbox/autostart
|
||||
|
||||
reboot now
|
3
doc/start.sh
Normal file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
/usr/bin/startx /usr/bin/chromium-browser --kiosk --incognito --disable-pinch --overscroll-history-navigation=0 http://192.168.1.186:3000/
|
BIN
doc/v1Containers.png
Normal file
After Width: | Height: | Size: 89 KiB |
BIN
doc/v1Fill.png
Normal file
After Width: | Height: | Size: 107 KiB |
BIN
doc/v1Main.png
Normal file
After Width: | Height: | Size: 181 KiB |
BIN
doc/v1Menu.png
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
doc/v1Setup.png
Normal file
After Width: | Height: | Size: 135 KiB |
BIN
doc/v1Stats.png
Normal file
After Width: | Height: | Size: 92 KiB |
Before Width: | Height: | Size: 3.7 MiB After Width: | Height: | Size: 3.7 MiB |
@ -6,7 +6,8 @@
|
||||
border-radius: 2px;
|
||||
cursor: none !important;
|
||||
font-weight: 500;
|
||||
margin-right: 2%;
|
||||
margin-right: 1%;
|
||||
margin-left: 1%;
|
||||
transition: 0.2s all;
|
||||
|
||||
}
|
||||
|
@ -23,7 +23,7 @@
|
||||
grid-row-gap: 4%;
|
||||
text-align: center;
|
||||
border-radius: 30px 10px 30px;
|
||||
color: black;
|
||||
color: white;
|
||||
/*box-shadow: 3px 3px 3px;*/
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.3), 0 6px 20px 0 rgba(0, 0, 0, 0.29);
|
||||
transition: 0.4s;
|
||||
@ -55,4 +55,78 @@
|
||||
grid-column: span 1;
|
||||
grid-row: span 1;
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* Water animation */
|
||||
body {
|
||||
margin: 50px;
|
||||
}
|
||||
|
||||
.water {
|
||||
position: relative;
|
||||
width: 130px;
|
||||
height: 150px;
|
||||
background-color: #23417B;
|
||||
box-shadow: inset 0 0 80px #18566D;
|
||||
clip-path: polygon(0 0, 100% 0, 85% 100%, 15% 100%);
|
||||
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: auto 83%;
|
||||
margin: auto auto 3%;
|
||||
}
|
||||
|
||||
.water::before {
|
||||
content: "";
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background-color: #ececec;
|
||||
position: absolute;
|
||||
top: -90%;
|
||||
left: -50%;
|
||||
border-radius: 40%;
|
||||
animation: animWater 10s linear infinite, animFillIn 15s linear forwards;
|
||||
}
|
||||
|
||||
.water::after {
|
||||
content: "";
|
||||
width: 204%;
|
||||
height: 204%;
|
||||
background-color: #ececec80;
|
||||
position: absolute;
|
||||
top: -90%;
|
||||
left: -52%;
|
||||
border-radius: 40%;
|
||||
animation: animWater 10s linear infinite, animFillIn 15s linear forwards;
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
.waterCancel {
|
||||
background-color: red;
|
||||
transition: background-color 1s;
|
||||
}
|
||||
|
||||
@keyframes animFillIn {
|
||||
0% {
|
||||
top: -90%;
|
||||
}
|
||||
100% {
|
||||
top: -190%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animWater {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#main_fillTxt {
|
||||
margin-bottom: 3%;
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
#menu {
|
||||
padding-left: 5%;
|
||||
padding-right: 5%;
|
||||
padding-top: 10%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, calc(100% / 2));
|
||||
grid-template-columns: repeat(2, calc(95% / 2));
|
||||
grid-template-rows: repeat(2, calc(95% / 2));
|
||||
grid-gap: 2% 2%;
|
||||
color: white;
|
||||
|
@ -12,15 +12,23 @@
|
||||
/*cursor: none !important;*/
|
||||
}
|
||||
|
||||
|
||||
ul {
|
||||
list-style: circle;
|
||||
}
|
||||
|
||||
|
||||
html * {
|
||||
/*cursor: none !important*/
|
||||
-webkit-user-select: none; /* Safari */
|
||||
-ms-user-select: none; /* IE 10 and IE 11 */
|
||||
user-select: none; /* Standard syntax */
|
||||
cursor: none;
|
||||
}
|
||||
|
||||
|
||||
html *::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@ -39,6 +47,7 @@ body {
|
||||
user-select: none; /* Standard syntax */
|
||||
}
|
||||
|
||||
|
||||
h1 {
|
||||
font-size: 1.74em;
|
||||
font-weight: 500;
|
||||
@ -63,7 +72,9 @@ h1 {
|
||||
font-size: 2em;
|
||||
padding-right: 3px;
|
||||
padding-left: 3px;
|
||||
background-color: #167fcc;
|
||||
background-color: #167FCC;
|
||||
box-shadow: inset 11px 45px 50px 3px rgba(181, 15, 15, 0.66);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -102,14 +113,15 @@ h1 {
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 9%;
|
||||
background-color: #167fcc;
|
||||
background-color: #167FCC;
|
||||
box-shadow: inset 11px -45px 50px 3px rgba(223, 12, 42, 0.66);
|
||||
}
|
||||
|
||||
|
||||
#overlay #bottom #menuBtn {
|
||||
height: 100%;
|
||||
width: 10%;
|
||||
background-color: #21212d;
|
||||
background-color: #21212D;
|
||||
padding: 1px;
|
||||
font-size: 1.5em;
|
||||
border: 0;
|
||||
@ -117,13 +129,17 @@ h1 {
|
||||
color: white;
|
||||
float: left;
|
||||
transition: all 0.4s;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
|
||||
#menuBtn:hover {
|
||||
background-color: #313147 !important;
|
||||
}
|
||||
|
||||
|
||||
#menuBtn:disabled {
|
||||
background-color: #777 !important;
|
||||
background-color: #777777 !important;
|
||||
color: #C1C1C1 !important;
|
||||
}
|
||||
|
||||
@ -143,9 +159,10 @@ h1 {
|
||||
right: 0;
|
||||
height: 82%;
|
||||
color: white;
|
||||
background-color: #0e1f31;
|
||||
background-color: #0E1F31;
|
||||
}
|
||||
|
||||
|
||||
.pane {
|
||||
height: 100%;
|
||||
padding: 1% 2%;
|
||||
@ -155,9 +172,15 @@ h1 {
|
||||
scroll-behavior: smooth;
|
||||
color: white;
|
||||
/*animation: showPane 0.3s forwards;*/
|
||||
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
}
|
||||
|
||||
|
||||
.pane::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@keyframes showPane {
|
||||
0% {
|
||||
display: none;
|
||||
@ -171,11 +194,14 @@ h1 {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.hiddenPane {
|
||||
transition: 0.4s;
|
||||
display: none !important;
|
||||
/*animation: hidePane 0.4s forwards;*/
|
||||
}
|
||||
|
||||
|
||||
@keyframes hidePane {
|
||||
0% {
|
||||
opacity: 1;
|
||||
@ -190,6 +216,7 @@ h1 {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#settings {
|
||||
display: none;
|
||||
background-color: red;
|
||||
|
@ -2,4 +2,5 @@ export enum RequestType {
|
||||
CONTAINERS = "CONTAINERS",
|
||||
INGREDIENTS = "INGREDIENTS",
|
||||
STATS = "STATS",
|
||||
JOB = "JOB",
|
||||
}
|
@ -7,5 +7,7 @@ export enum WebSocketEvent {
|
||||
TARE = "TARE",
|
||||
SETUP = "SETUP",
|
||||
REQUEST = "REQUEST",
|
||||
RESPONSE = "RESPONSE"
|
||||
RESPONSE = "RESPONSE",
|
||||
FILL = "FILL",
|
||||
CANCEL = "CANCEL"
|
||||
}
|
@ -43,6 +43,11 @@ router.ws('/', async (ws, req, next) => {
|
||||
log(msg);
|
||||
|
||||
switch (msg.event) {
|
||||
case WebSocketEvent.FILL : {
|
||||
iTender.setStatus(iTenderStatus.FILLING);
|
||||
break;
|
||||
}
|
||||
|
||||
case WebSocketEvent.TARE: {
|
||||
if (msg.data["state"] == true) {
|
||||
iTender.toggleTare(true);
|
||||
|
@ -2,6 +2,9 @@ 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";
|
||||
|
||||
export class WebHandler {
|
||||
static get currentPane(): Pane {
|
||||
@ -38,6 +41,13 @@ export class WebHandler {
|
||||
drinkImg.src = "/images/" + drink.name + ".png";
|
||||
drinkName.innerText = drink.name;
|
||||
|
||||
|
||||
|
||||
drinkEle.onclick = () => {
|
||||
let payload = new WebSocketPayload(WebSocketEvent.FILL, false, {drink: drink._id });
|
||||
WebWebSocketHandler.send(payload);
|
||||
}
|
||||
|
||||
/*
|
||||
let ingredients = "<ul style='list-style-type: disc;'>";
|
||||
for (let i of drink.ingredients) {
|
||||
|
@ -7,6 +7,7 @@ import {WebHandler} from "./WebHandler";
|
||||
import {Setup} from "./Setup";
|
||||
import {Pane} from "./Pane";
|
||||
import {RequestType} from "../RequestType";
|
||||
import {IDrink} from "../database/IDrink";
|
||||
|
||||
export class WebWebSocketHandler {
|
||||
private static socket: WebSocket;
|
||||
@ -61,8 +62,8 @@ export class WebWebSocketHandler {
|
||||
switch (status) {
|
||||
case iTenderStatus.READY: {
|
||||
Modal.close("start");
|
||||
Modal.close("refreshing");
|
||||
Modal.close("setup");
|
||||
Modal.close("fill");
|
||||
WebHandler.openPane(Pane.MAIN);
|
||||
(document.getElementById("menuBtn") as HTMLButtonElement).disabled = false;
|
||||
break;
|
||||
@ -88,7 +89,52 @@ export class WebWebSocketHandler {
|
||||
case iTenderStatus.SETUP: {
|
||||
Modal.close("start");
|
||||
Setup.openSetup();
|
||||
break;
|
||||
}
|
||||
case iTenderStatus.FILLING: {
|
||||
|
||||
let modal = new Modal("fill", "Getränk wird ausgegeben");
|
||||
let header = document.createElement("h2");
|
||||
header.innerText = "";
|
||||
|
||||
modal.addContent(header);
|
||||
|
||||
let txt = document.createElement("p");
|
||||
txt.innerHTML = `Dein Cocktail wird gerade zubereitet`;
|
||||
txt.id = "main_fillTxt";
|
||||
|
||||
let waterAnimDiv = document.createElement("div");
|
||||
waterAnimDiv.classList.add("water");
|
||||
modal.addContent(txt);
|
||||
|
||||
modal.addContent(waterAnimDiv);
|
||||
|
||||
let cancelBtn = document.createElement("button");
|
||||
cancelBtn.classList.add("btn", "btn-danger");
|
||||
cancelBtn.innerText = "Abbrechen";
|
||||
cancelBtn.disabled = true;
|
||||
setTimeout(() => {
|
||||
cancelBtn.disabled = false;
|
||||
}, 1000);
|
||||
cancelBtn.onclick = () => {
|
||||
cancelBtn.disabled = true;
|
||||
txt.innerHTML = "Der Vorgang wird abgebrochen...";
|
||||
waterAnimDiv.classList.add("waterCancel");
|
||||
|
||||
WebWebSocketHandler.send(new WebSocketPayload(WebSocketEvent.CANCEL));
|
||||
};
|
||||
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();
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
break;
|
||||
}
|
||||
|