V2: Remap many stuff
Took 3 hours 0 minutes
3
.gitignore
vendored
@ -3,4 +3,5 @@
|
||||
/node_modules
|
||||
/public/images/
|
||||
/config.json
|
||||
/yarn-error.log
|
||||
/yarn-error.log
|
||||
/docs/
|
||||
|
@ -2,6 +2,7 @@
|
||||
* Official proxy code for iTender GPIO Communication
|
||||
**/
|
||||
#include <ArduinoJson.h>
|
||||
#include "HX711.h"
|
||||
|
||||
// Define the size of the JSON buffer
|
||||
#define JSON_BUFFER_SIZE 256
|
||||
@ -10,17 +11,16 @@
|
||||
StaticJsonDocument<JSON_BUFFER_SIZE> incomingJson;
|
||||
|
||||
// Create a JSON object for outgoing messages
|
||||
DynamicJsonDocument<JSON_BUFFER_SIZE> outgoingJson;
|
||||
DynamicJsonDocument outgoingJson(JSON_BUFFER_SIZE);
|
||||
|
||||
void setup() {
|
||||
// Initialize serial communication
|
||||
Serial.begin(9600);
|
||||
}
|
||||
|
||||
void(* resetFunc) (void) = 0; //declare reset function @ address 0
|
||||
void (*resetFunc)(void) = 0; //declare reset function @ address 0
|
||||
|
||||
void loop() {
|
||||
// Wait for a new line on the serial console
|
||||
if (Serial.available()) {
|
||||
// Read the incoming JSON message
|
||||
DeserializationError error = deserializeJson(incomingJson, Serial);
|
||||
@ -29,43 +29,74 @@ void loop() {
|
||||
} else {
|
||||
// Extract the "type" and "data" fields from the JSON object
|
||||
String id = incomingJson["id"];
|
||||
String type = incomingJson["type"];
|
||||
int type = incomingJson["type"];
|
||||
JsonVariant data = incomingJson["data"];
|
||||
|
||||
// Create a nested object in the root object
|
||||
JsonObject outgoingData = outgoingJson.to<JsonObject>().createNestedObject("data");
|
||||
|
||||
outgoingData["success"] = true;
|
||||
outgoingData["success"] = true;
|
||||
|
||||
// Handle the message based on the "type" field
|
||||
switch (type) {
|
||||
case "ACK":
|
||||
// Handle ACK message
|
||||
break;
|
||||
case "SET_PIN":
|
||||
case 1:
|
||||
// Handle SET_PIN message
|
||||
pinMode((int)data["pin"], OUTPUT);
|
||||
if (data["mode"] == "DIGITAL") {
|
||||
digitalWrite((int)data["pin"], data["value"]);
|
||||
} else {
|
||||
analogWrite((int)data["pin"], (data["value"] == 255) ? HIGH : LOW);
|
||||
}
|
||||
break;
|
||||
case "GET_VAL":
|
||||
case 2:
|
||||
// Handle GET_VAL message
|
||||
pinMode((int)data["pin"], INPUT);
|
||||
int val;
|
||||
if (data["mode"] == "DIGITAL") {
|
||||
val = digitalRead((int)data["pin"]);
|
||||
} else {
|
||||
val = analogRead((int)data["pin"]);
|
||||
}
|
||||
break;
|
||||
case "GET_SENSOR":
|
||||
case 3:
|
||||
// Handle GET_SENSOR message
|
||||
|
||||
/*
|
||||
(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
|
||||
*/
|
||||
// HX711 circuit wiring
|
||||
const int LOADCELL_DATA_PIN = (int)data["pin_data"];
|
||||
const int LOADCELL_CLOCK_PIN = (int)data["pin_clock"];
|
||||
HX711 scale;
|
||||
scale.begin(LOADCELL_DATA_PIN, LOADCELL_CLOCK_PIN);
|
||||
|
||||
// Get the weight value from the scale
|
||||
long weight_val = scale.get_units();
|
||||
outgoingData["value"] = weight_val;
|
||||
break;
|
||||
case "RESTART":
|
||||
case 4:
|
||||
resetFunc(); //call reset
|
||||
break;
|
||||
default:
|
||||
// Handle unknown message type
|
||||
outgoingData[""] = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
// Prepare the outgoing JSON message
|
||||
outgoingJson["id"] = id;
|
||||
outgoingJson["type"] = type;
|
||||
outgoingJson["data"] = "";
|
||||
outgoingJson["type"] = 0;
|
||||
outgoingJson["data"] = outgoingData;
|
||||
|
||||
// Send the outgoing JSON message
|
||||
serializeJson(outgoingJson, Serial);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
/**
|
||||
|
||||
#include "HX711.h"
|
||||
|
||||
@ -7,37 +6,45 @@ const int LOADCELL_SCK_PIN = 3;
|
||||
|
||||
HX711 scale;
|
||||
|
||||
int minus = 0;
|
||||
long scale_factor = 0;
|
||||
long no_weight = 0;
|
||||
|
||||
void setup() {
|
||||
void tare() {
|
||||
// put your setup code here, to run once:
|
||||
Serial.begin(9600);
|
||||
delay(1000);
|
||||
Serial.println("TARE");
|
||||
Serial.println("Alle Gewichte entfernen...");
|
||||
|
||||
scale.begin(LOADCELL_DOUT_PIN, LOADCELL_SCK_PIN);
|
||||
scale.set_scale(429.6159259259259);
|
||||
scale.tare();
|
||||
delay(4000);
|
||||
Serial.println("[Measureing]");
|
||||
|
||||
Serial.print("Null-Gewicht: ");
|
||||
no_weight = scale.get_units(3);
|
||||
Serial.println(no_weight);
|
||||
|
||||
delay(1000);
|
||||
minus=abs(scale.get_units(5));
|
||||
// scale.set_scale();
|
||||
// scale.tare();
|
||||
// Serial.println("TARE OK - PLACE WEIGHT");
|
||||
//delay(5000);
|
||||
Serial.println("100g Gewicht platzieren...");
|
||||
delay(4000);
|
||||
|
||||
Serial.print("100g-Gewicht: ");
|
||||
scale_factor = scale.get_units(3);
|
||||
Serial.println(scale_factor);
|
||||
|
||||
scale_factor = scale_factor / 100;
|
||||
Serial.print("Skalierungsfaktor: ");
|
||||
Serial.println(scale_factor);
|
||||
|
||||
delay(2000);
|
||||
}
|
||||
|
||||
int len = 0;
|
||||
long sum = 0;
|
||||
|
||||
void loop() {
|
||||
if (scale.is_ready()) {
|
||||
long data = scale.get_units(5) + minus;
|
||||
Serial.println(data);
|
||||
|
||||
}
|
||||
|
||||
void tare_loop() {
|
||||
delay(2000);
|
||||
long val = scale.get_units(1);
|
||||
// (WEIGHT_VAL - NO_WEIGHT_VAL) / (100g_val /100) = Gewicht in Gramm
|
||||
Serial.println( ( val - no_weight ) / scale_factor );
|
||||
}
|
||||
|
||||
|
||||
**/
|
||||
|
68
doc/Notes.md
@ -1,68 +0,0 @@
|
||||
# ### Achtung! \###
|
||||
Diese Datei ist nicht mehr aktuell.<br>
|
||||
Bitte nutze die neue Wiki unter [https://git.gaminggeneration.de/tobiash/itender/wiki](https://git.gaminggeneration.de/tobiash/itender/wiki) (Wiki des iTender Projekts)
|
||||
|
||||
<br><br><br><br><br><br><hr><br><br><br><br><br>
|
||||
|
||||
|
||||
|
||||
# Notes und kleine Dokumentation
|
||||
|
||||
Was haben wir bereits am iTender Projekt gemacht?
|
||||
|
||||
<hr>
|
||||
|
||||
## Konzept-Erstellung
|
||||
|
||||
#### Ideen
|
||||
|
||||
- Grund-Ideen
|
||||
- Smarten Cocktail-Mischer
|
||||
- 4 Getränke Behälter (mit Saft, Sirup oder Likör bzw. Schnapps)
|
||||
- 4 Pumpen (Peristaltik Pumpe)
|
||||
- Raspberry Pi als Prozessoreinheit
|
||||
- Display in der Front mit Benutzeroberfläche
|
||||
- Automatisches filtern von Getränken, je nachdem welche "Zutaten" in den Behältern sind
|
||||
- Messung der aktuellen Füllmenge der Behälter, basierend auf Gewicht (mittels Wägezelle) oder Abstand zur
|
||||
Wasseroberfläche (mittels Ultraschall-Sensor)
|
||||
|
||||
- Nice to have
|
||||
- LED-Stripes für schöne Beleuchtung, basierend auf dem aktuellen Status der Maschine
|
||||
- Extra Schlauch für weitere außenstehende Getränke
|
||||
- Mit Bier-Fass Adapter?
|
||||
- Kühlung der Container mittels Peltierelement und Lüftern
|
||||
|
||||
#### Erstes 3D-Modell
|
||||
|
||||
<img src="./Screenshot_Model1.1_FrontTopRight.png" width="50%">
|
||||
<img src="./Screenshot_Model1.1_BackDownLeft.png" width="50%">
|
||||
|
||||
#### Neues 3D-Modell
|
||||
|
||||
<img src="./Screenshot_Model1.2_Front.png" width="50%">
|
||||
<img src="./Screenshot_Model1.2_Back.png" width="50%">
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
## Das Programm
|
||||
|
||||
#### Aufbau
|
||||
|
||||
- Das Programm des iTenders ist getrennt in 3 Teile
|
||||
- iTender Basis
|
||||
- Die Basis besteht aus der Kommunikation zwischen den Pumpen, LEDs und jeglicher Hardware
|
||||
- Außerdem gibt es Timer, automatische Events, Prüfung und aktualisierung von Getränken etc.
|
||||
- Sie übernimmt z.B. das Starten vom Getränke-Füllen, stoppen sowie Berechnen von den Zutaten für ein Getränk
|
||||
- Dieser Teil arbeitet seh eng mit dem Websocket-Server zusammen, um so auf Events vom Endnutzer zu reagieren.
|
||||
- iTender Webserver
|
||||
- Der Webserver ist einfach ein statischer Webserver welche Dateien "serviert", die dann von der Oberfläche geladen werden können.
|
||||
- Der Client-Browser oder iTender-Display lädt dann diese Seite, erstmal passiert dann noch nichts
|
||||
- iTender Websocket-Server
|
||||
- Der Websocket-Server dient zur eigentlichen Live-Kommunikation zwischen Client und Gerät.
|
||||
- Der Server und die Webseite (Endgerät), bauen eine Ende-zu-Ende Verbindung auf
|
||||
- Die Kommunikation zwischen Client und Websocket-Server basiert auf JSON (Javascript-Objekt-Notation).
|
||||
- Da der Websocket in früheren Client-Versionen anfällig für Fehler beim Übertragen von nicht alphabetischen Zeichen war, wird der gesendete Inhalt noch in Base64-Kodiert.
|
||||
- Base64 dient dazu, die Daten binär zu kodieren, um sie auf der Gegenstelle wieder zu entkodieren.
|
||||
- Außerdem wird beim übertragen eine Checksumme mitgegeben, um bei Fehlerhaften Paketen ein neues anzufragen.
|
||||
|
Before Width: | Height: | Size: 102 KiB |
Before Width: | Height: | Size: 108 KiB |
Before Width: | Height: | Size: 162 KiB |
Before Width: | Height: | Size: 270 KiB |
@ -1,33 +0,0 @@
|
||||
#
|
||||
# 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/
|
||||
|
||||
|
||||
|
||||
|
220
doc/installPi.sh
@ -1,220 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "Please run as root!"
|
||||
exit
|
||||
fi
|
||||
|
||||
echo "Creating user if not exists"
|
||||
useradd -p $(openssl passwd -1 iTender2022) itender || true
|
||||
|
||||
echo "Updating indexes"
|
||||
apt update
|
||||
|
||||
echo "Installing xserver xinit openbox ufw xserver-xorg x11 unclutter make chromium-browser crontab cmake g++ gcc and git..."
|
||||
apt install --no-install-recommends ufw xserver-xorg x11-xserver-utils xinit openbox -y || exit
|
||||
apt install git gcc g++ make cmake chromium-browser unclutter iptables cron -y || exit
|
||||
echo "Try to uninstall node and npm... (Can fail)"
|
||||
apt purge node -y || true
|
||||
apt purge npm -y || true
|
||||
|
||||
echo "Setup xserver..."
|
||||
# XServer
|
||||
echo "allowed_users=anybody" >/etc/X11/Xwrapper.config
|
||||
#no-uncomment cp autostart.config /etc/xdg/openbox/autostart
|
||||
|
||||
echo "Adding apt keys..."
|
||||
# Keys and stuff ---
|
||||
# Nodejs
|
||||
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash
|
||||
|
||||
# Yarn
|
||||
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
|
||||
|
||||
# MongoDB
|
||||
wget -qO - https://www.mongodb.org/static/pgp/server-4.4.asc | sudo apt-key add -
|
||||
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/4.4 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.4.list
|
||||
|
||||
# End Keys and stuff ---
|
||||
|
||||
echo "Updating firewall..."
|
||||
# Firewall
|
||||
ufw allow ssh
|
||||
ufw allow 3000/tcp
|
||||
ufw allow 3015/tcp
|
||||
ufw --force enable
|
||||
|
||||
echo "Updating indexes..."
|
||||
# Final update
|
||||
apt update
|
||||
echo "Installing mongodb and yarn..."
|
||||
apt install nodejs yarn mongodb-org -y
|
||||
apt upgrade -y
|
||||
|
||||
# V2: Arduino CLI
|
||||
echo "Installing arduino-cli..."
|
||||
sudo -u itender mkdir -p /home/itender/bin
|
||||
sudo -u itender sh -c 'curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/home/itender/bin/ sh'
|
||||
sudo -u itender /home/itender/bin/arduino-cli config init
|
||||
sudo -u itender /home/itender/bin/arduino-cli core update-index || true
|
||||
sudo -u itender /home/itender/bin/arduino-cli lib search ArduinoJson || true
|
||||
sudo -u itender /home/itender/bin/arduino-cli lib install ArduinoJson || true
|
||||
|
||||
|
||||
|
||||
echo "Installing autostart..."
|
||||
# Autostart
|
||||
cat <<EOT >/etc/xdg/openbox/autostart
|
||||
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://127.0.0.1:3000/" &
|
||||
EOT
|
||||
|
||||
echo "Setting to console autologin..."
|
||||
raspi-config nonint do_boot_behaviour B2
|
||||
|
||||
echo "Installing bashrc"
|
||||
echo "clear" >>/home/itender/.bashrc
|
||||
echo "[[ -z \$DISPLAY && \$XDG_VTNR -eq 1 ]] && startx -- -nocursor >/dev/null 2>&1" >>/home/itender/.bashrc
|
||||
#
|
||||
#echo "Installing start.sh"
|
||||
#cat <<EOT >>/home/itender/start.sh
|
||||
##!/bin/bash
|
||||
#cd /home/itender/ || exit
|
||||
#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
|
||||
#
|
||||
#address="localhost"
|
||||
#
|
||||
#echo "Waiting 5 seconds to start chromium..."
|
||||
#sleep 5
|
||||
#/usr/bin/chromium-browser --disable-infobars --kiosk --incognito --disable-pinch --overscroll-history-navigation=0 "http://\${address}:3000/"
|
||||
##/usr/bin/startx /usr/bin/chromium-browser --kiosk --incognito --disable-pinch --overscroll-history-navigation=0 http://192.168.1.186:3000/
|
||||
#EOT
|
||||
#chmod +x /home/itender/start.sh
|
||||
|
||||
DIR="/home/itender/itender/"
|
||||
if [ -d "$DIR" ]; then
|
||||
# Take action if $DIR exists. #
|
||||
cd "$DIR" || exit
|
||||
echo "Updating..."
|
||||
git pull
|
||||
else
|
||||
echo "Cloning..."
|
||||
sudo -u itender git config --global credential.helper store
|
||||
git config --global credential.helper store
|
||||
git clone "https://tobiash:!IwedwrimmVeudiweN!@git.gaminggeneration.de/tobiash/itender.git" --quiet
|
||||
fi
|
||||
cd "$DIR" || exit
|
||||
yarn install
|
||||
echo "Compiling..."
|
||||
yarn run compile
|
||||
|
||||
echo "Updating Cron..."
|
||||
# Add line to cron
|
||||
echo "@reboot sudo chmod g+rw /dev/tty?" >/tmp/currentCron
|
||||
#echo "@reboot cd /home/itender/itender/ && /usr/bin/yarn run start &" >> /tmp/currentCron
|
||||
chown itender:itender /tmp/currentCron
|
||||
#install new cron file
|
||||
sudo -u itender crontab /tmp/currentCron
|
||||
|
||||
echo "Installing systemd service..."
|
||||
cat <<EOT >/etc/systemd/system/itender.service
|
||||
[Unit]
|
||||
Description=iTender App
|
||||
After=network.target mongod.service
|
||||
StartLimitIntervalSec=1
|
||||
StartLimitBurst=1000
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Restart=always
|
||||
RestartSec=1s
|
||||
User=itender
|
||||
ExecStartPre=sleep 3
|
||||
WorkingDirectory=/home/itender/itender/
|
||||
ExecStart=/usr/bin/yarn run start
|
||||
StandardOutput=append:/var/log/itender.log
|
||||
StandardError=append:/var/log/itender.log
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
EOT
|
||||
#sh -c "git pull --quiet || true"
|
||||
|
||||
|
||||
echo "Activating systemctl daemons..."
|
||||
systemctl daemon-reload
|
||||
systemctl enable mongod
|
||||
systemctl enable itender
|
||||
|
||||
echo "Backing up /boot/config.txt to /root/config.txt.bak"
|
||||
cp /boot/config.txt /root/config.txt.bak
|
||||
echo "Updating Boot config..."
|
||||
if ! grep -w "hdmi_group=2" /boot/config.txt; then
|
||||
echo "hdmi_group=2" >>/boot/config.txt
|
||||
fi
|
||||
if ! grep -w "hdmi_mode=87" /boot/config.txt; then
|
||||
echo "hdmi_mode=87" >>/boot/config.txt
|
||||
fi
|
||||
if ! grep -w "hdmi_cvt 1024 600 60 0 0 0 0" /boot/config.txt; then
|
||||
echo "hdmi_cvt 1024 600 60 0 0 0 0" >>/boot/config.txt
|
||||
fi
|
||||
if ! grep -w "hdmi_drive=1" /boot/config.txt; then
|
||||
echo "hdmi_drive=1" >>/boot/config.txt
|
||||
fi
|
||||
if ! grep -w "disable_splash=1" /boot/config.txt; then
|
||||
echo "disable_splash=1" >>/boot/config.txt
|
||||
fi
|
||||
if ! grep -w "vc4-fkms-v3d" /boot/config.txt; then
|
||||
sed -i 's/dtoverlay=vc4-kms-v3d/dtoverlay=vc4-fkms-v3d,disable-bt/' /boot/config.txt
|
||||
fi
|
||||
if ! grep -w "boot_delay=0" /boot/config.txt; then
|
||||
echo "boot_delay=0" >>/boot/config.txt
|
||||
fi
|
||||
if ! grep -w "over_voltage=6" /boot/config.txt; then
|
||||
echo "over_voltage=6" >>/boot/config.txt
|
||||
fi
|
||||
if ! grep -w "arm_freq=1300" /boot/config.txt; then
|
||||
echo "arm_freq=1300" >>/boot/config.txt
|
||||
fi
|
||||
if ! grep -w "gpu_freq=700" /boot/config.txt; then
|
||||
echo "gpu_freq=700" >>/boot/config.txt
|
||||
fi
|
||||
|
||||
|
||||
echo "Setting no-logo..."
|
||||
systemctl disable getty@tty1.service
|
||||
|
||||
if ! grep -w "logo.nologo" /boot/cmdline.txt; then
|
||||
echo "Backing up /boot/cmdline.txt to /root/cmdline.txt.bak"
|
||||
cp /boot/cmdline.txt /root/cmdline.txt.bak
|
||||
sed -i '1 s_$_ loglevel=3 logo.nologo disable\_splash=1 splash quiet plymouth.ignore-serial-consoles logo.nologo vt.global\_cursor_default=0_' /boot/cmdline.txt
|
||||
#cp /tmp/cmdline.txt /boot/cmdline.txt
|
||||
sed -i "1 s|$| vt.global_cursor_default=0|" /boot/cmdline.txt
|
||||
sed -i '1 i\avoid_warnings=1' /boot/config.txt
|
||||
sed -i 's/console=tty0/console=tty3/' /boot/cmdline.txt
|
||||
fi
|
||||
|
||||
echo "iTender© 2022-2023
|
||||
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/
|
||||
adduser itender gpio
|
||||
adduser itender sudo
|
||||
|
||||
echo "Installation finished!"
|
||||
|
||||
reboot now
|
23
doc/start.sh
@ -1,23 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
exit
|
||||
cd /home/itender/ || exit
|
||||
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
|
||||
|
||||
#address=""
|
||||
#if ping -c1 -W1 192.168.1.186; then
|
||||
# address="192.168.1.186"
|
||||
#fi
|
||||
#if ping -c1 -W1 192.168.208.15; then
|
||||
# address="192.168.208.15"
|
||||
#fi
|
||||
#if ping -c1 -W1 10.10.0.5; then
|
||||
# address="10.10.0.5"
|
||||
#fi
|
||||
address="localhost"
|
||||
|
||||
echo "Waiting 5 seconds to start chromium..."
|
||||
sleep 5
|
||||
/usr/bin/chromium-browser --disable-infobars --kiosk --incognito --disable-pinch --overscroll-history-navigation=0 "http://${address}:3000/"
|
||||
#/usr/bin/startx /usr/bin/chromium-browser --kiosk --incognito --disable-pinch --overscroll-history-navigation=0 http://192.168.1.186:3000/
|
Before Width: | Height: | Size: 89 KiB |
BIN
doc/v1Fill.png
Before Width: | Height: | Size: 107 KiB |
BIN
doc/v1Main.png
Before Width: | Height: | Size: 181 KiB |
BIN
doc/v1Menu.png
Before Width: | Height: | Size: 78 KiB |
BIN
doc/v1Setup.png
Before Width: | Height: | Size: 135 KiB |
BIN
doc/v1Stats.png
Before Width: | Height: | Size: 92 KiB |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "itender",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"private": true,
|
||||
"author": "Tobias Hopp <tobi@gaminggeneration.de>",
|
||||
"license": "UNLICENSED",
|
||||
@ -46,6 +46,8 @@
|
||||
"nodemon": "^2.0.20",
|
||||
"ts-loader": "^9.4.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typedoc": "^0.23.24",
|
||||
"typedoc-plugin-missing-exports": "^1.0.0",
|
||||
"typescript": "^4.8.4",
|
||||
"webpack": "^5.74.0",
|
||||
"webpack-cli": "^4.10.0"
|
||||
|
@ -12,7 +12,7 @@ export class ArduinoProxyPayload {
|
||||
private _id: string ="";
|
||||
|
||||
|
||||
constructor(type: ArduinoProxyPayloadType, data: any) {
|
||||
constructor(type: ArduinoProxyPayloadType, data: { pin?: number, value?: number|"HIGH"|"LOW", pin_data?: number, pin_clock?: number, mode?: "DIGITAL"|"ANALOG" } ) {
|
||||
this._type = type;
|
||||
this._data = data;
|
||||
}
|
||||
@ -21,7 +21,7 @@ export class ArduinoProxyPayload {
|
||||
return this._type;
|
||||
}
|
||||
|
||||
get data(): any {
|
||||
get data(): {value?: number} {
|
||||
return this._data;
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
export enum ArduinoProxyPayloadType {
|
||||
ACK="ACK",
|
||||
SET_PIN="SET_PIN",
|
||||
GET_VAL="GET_VAL",
|
||||
GET_SENSOR="GET_SENSOR",
|
||||
RESTART="RESTART",
|
||||
ACK = 0,
|
||||
SET_PIN = 1,
|
||||
GET_VAL = 2,
|
||||
GET_SENSOR = 3,
|
||||
RESTART = 4,
|
||||
}
|
65
src/ContainerHelper.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import Container from "./database/Container";
|
||||
import {SensorType} from "./SensorType";
|
||||
import {SensorHelper} from "./SensorHelper";
|
||||
import {WebSocketHandler} from "./WebSocketHandler";
|
||||
import {WebSocketPayload} from "./WebSocketPayload";
|
||||
import {WebSocketEvent} from "./WebSocketEvent";
|
||||
import debug from "debug";
|
||||
import {iTender} from "./iTender";
|
||||
|
||||
const log = debug("itender:container");
|
||||
|
||||
export class ContainerHelper {
|
||||
|
||||
/**
|
||||
* Measure all containers based on their sensor values
|
||||
*/
|
||||
static measureContainers(): Promise<void> {
|
||||
log("Measuring containers...");
|
||||
|
||||
return new Promise(async resolve => {
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
log("Containers measured!");
|
||||
resolve();
|
||||
|
||||
let payload = new WebSocketPayload(WebSocketEvent.CONTAINERS, (await Container.find()));
|
||||
await WebSocketHandler.send(payload);
|
||||
});
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ export class HX711 {
|
||||
private dataPin: number;
|
||||
|
||||
|
||||
constructor(clockPin: number, dataPin: number) {
|
||||
constructor(dataPin: number, clockPin: number) {
|
||||
this.clockPin = clockPin;
|
||||
this.dataPin = dataPin;
|
||||
}
|
||||
|
162
src/Mixer.ts
Normal file
@ -0,0 +1,162 @@
|
||||
import {IJob} from "./database/IJob";
|
||||
import {iTenderStatus} from "./iTenderStatus";
|
||||
import {MyGPIO} from "./MyGPIO";
|
||||
import GPIO from "rpi-gpio";
|
||||
import {SensorType} from "./SensorType";
|
||||
import {clearInterval} from "timers";
|
||||
import {IContainer} from "./database/IContainer";
|
||||
import {iTender} from "./iTender";
|
||||
import debug from "debug";
|
||||
|
||||
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[] = [];
|
||||
|
||||
/**
|
||||
* The current itender job
|
||||
* @private
|
||||
*/
|
||||
private static _currentJob: IJob;
|
||||
|
||||
/**
|
||||
* Checks if the job has finished every 500ms
|
||||
* @private
|
||||
*/
|
||||
private static _jobEndCheckInterval: NodeJS.Timer;
|
||||
|
||||
/**
|
||||
* Start the internal fill method, a sub-method of the onReceiveFill method
|
||||
* This method only gets executed if REALLY all is okay, it is the internal function
|
||||
* @param job
|
||||
*/
|
||||
|
||||
static async startFill(job: IJob) {
|
||||
job.startedAt = new Date();
|
||||
await job.populate([{path: "amounts.ingredient"}, {path: "amounts.container"}, {path: "drink"}]);
|
||||
log("New fill job " + job.drink.name + " will take " + job.estimatedTime + "s");
|
||||
|
||||
this._currentJob = job;
|
||||
iTender.setStatus(iTenderStatus.FILLING);
|
||||
|
||||
|
||||
for (let x of job.amounts) {
|
||||
|
||||
// Start pump here
|
||||
try {
|
||||
await MyGPIO.setup(x.container.pumpPin, GPIO.DIR_OUT)
|
||||
|
||||
await MyGPIO.write(x.container.pumpPin, true);
|
||||
} catch (e) {
|
||||
if (isPI()) {
|
||||
log("[ERROR] GPIO I/O Error " + e);
|
||||
// Todo error handling to user
|
||||
await this.cancelFill();
|
||||
return;
|
||||
} else {
|
||||
log("[WARNING] GPIO I/O Error, but it's normal cause you are not on raspberry");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let waitTime = (iTender.secondsPer100ml as number) / 100 * x.amount * 1000;
|
||||
log(`Starting output of pump ${x.container.pumpPin}`);
|
||||
//mixLog(x.ingredient + " takes " + (waitTime / 1000) + "s for " + x.amount + "ml");
|
||||
let timer = setTimeout(async () => {
|
||||
// Remove from list of timers
|
||||
let arr: NodeJS.Timer[] = [];
|
||||
for (let i = 0; i < this._jobTimers.length; i++) {
|
||||
if (this._jobTimers[i] != timer)
|
||||
arr.push(this._jobTimers[i]);
|
||||
}
|
||||
|
||||
log(`Stopping output of pump ${x.container.pumpPin}`);
|
||||
// Stop pump here
|
||||
try {
|
||||
await MyGPIO.write(x.container.pumpPin, false);
|
||||
} catch (e) {
|
||||
if (isPI()) {
|
||||
log("[ERROR] GPIO I/O Error " + e);
|
||||
await this.cancelFill();
|
||||
return;
|
||||
} else {
|
||||
log("[WARNING] GPIO I/O Error, but it's normal cause you are not on raspberry");
|
||||
}
|
||||
}
|
||||
|
||||
if (x.container.sensorType == SensorType.NONE) {
|
||||
// V2: Manual measuring
|
||||
x.container.filled = x.container.filled - x.amount;
|
||||
await x.container.save();
|
||||
}
|
||||
|
||||
this._jobTimers = arr;
|
||||
|
||||
}, waitTime);
|
||||
this._jobTimers.push(timer);
|
||||
}
|
||||
|
||||
this._jobEndCheckInterval = setInterval(async () => {
|
||||
|
||||
if (this._jobTimers.length != 0)
|
||||
return;
|
||||
|
||||
clearInterval(this._jobEndCheckInterval);
|
||||
job.endAt = new Date();
|
||||
job.successful = true;
|
||||
|
||||
await job.save();
|
||||
log("Job successful");
|
||||
setTimeout(() => iTender.setStatus(iTenderStatus.READY), 3000)
|
||||
|
||||
}, 500);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Cancel the fill instantly
|
||||
*/
|
||||
static async cancelFill() {
|
||||
if (!this._currentJob || iTender.status != iTenderStatus.FILLING)
|
||||
return;
|
||||
|
||||
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 {
|
||||
await MyGPIO.write(jobIngredient.container.pumpPin, false);
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
|
||||
// ToDo
|
||||
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();
|
||||
}
|
||||
|
||||
iTender.setStatus(iTenderStatus.READY);
|
||||
}
|
||||
}
|
@ -2,44 +2,97 @@ import {IContainer} from "./database/IContainer";
|
||||
import {SensorType} from "./SensorType";
|
||||
import {HX711} from "./HX711";
|
||||
import debug from "debug";
|
||||
import {ArduinoProxyPayload} from "./ArduinoProxyPayload";
|
||||
import {ArduinoProxyPayloadType} from "./ArduinoProxyPayloadType";
|
||||
import {ArduinoProxy} from "./ArduinoProxy";
|
||||
import Container from "./database/Container";
|
||||
|
||||
const log = debug("itender:sensor");
|
||||
|
||||
export class SensorHelper {
|
||||
|
||||
/**
|
||||
* Returns the current container weight
|
||||
* Returns the current raw container weight
|
||||
* @param container
|
||||
*/
|
||||
static measure(container: IContainer): number | null {
|
||||
if (container.sensorType == SensorType.LOADCELL) {
|
||||
try {
|
||||
// V2: Measure weight
|
||||
let sensor = new HX711(container.sensorPin1, container.sensorPin2);
|
||||
static measureRaw(container: IContainer): Promise<number> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (container.sensorType == SensorType.LOADCELL) {
|
||||
try {
|
||||
if (container.sensorProxy) {
|
||||
let payload = new ArduinoProxyPayload(ArduinoProxyPayloadType.GET_SENSOR, {
|
||||
pin_data: container.sensorPin1,
|
||||
pin_clock: container.sensorPin2
|
||||
});
|
||||
let val = await ArduinoProxy.sendRequest(payload);
|
||||
if (!val.data.value)
|
||||
return reject("");
|
||||
|
||||
container.rawData = sensor.measure();
|
||||
} catch (e) {
|
||||
log("Sensor (Weight cell) of container " + container._id + " is broken or has malfunction - Removing it!");
|
||||
container.sensorType = SensorType.NONE;
|
||||
container.save();
|
||||
return null;
|
||||
container.rawData = val.data.value;
|
||||
} else {
|
||||
let sensor = new HX711(container.sensorPin1, container.sensorPin2);
|
||||
container.rawData = sensor.measure();
|
||||
}
|
||||
} catch (e) {
|
||||
log("Sensor (Weight cell) of container " + container._id + " is broken or has malfunction - Removing it!");
|
||||
container.sensorType = SensorType.NONE;
|
||||
await container.save();
|
||||
return reject();
|
||||
}
|
||||
} else if (container.sensorType == SensorType.ULTRASOUND) {
|
||||
try {
|
||||
// V2: Measure weight
|
||||
let sensor = new HX711(container.sensorPin1, container.sensorPin2);
|
||||
|
||||
container.rawData = sensor.measure();
|
||||
|
||||
} catch (e) {
|
||||
log("Sensor (Ultrasound) of container " + container._id + " is broken or has malfunction - Removing it!");
|
||||
container.sensorType = SensorType.NONE;
|
||||
await container.save();
|
||||
return reject();
|
||||
}
|
||||
}
|
||||
} else if (container.sensorType == SensorType.ULTRASOUND) {
|
||||
try {
|
||||
// V2: Measure weight
|
||||
let sensor = new HX711(container.sensorPin1, container.sensorPin2);
|
||||
|
||||
container.rawData = sensor.measure();
|
||||
|
||||
} catch (e) {
|
||||
log("Sensor (Ultrasound) of container " + container._id + " is broken or has malfunction - Removing it!");
|
||||
container.sensorType = SensorType.NONE;
|
||||
container.save();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// todo Überprüfen ob hier Umrechnungen nötig sind. Soll in Gramm zurück gegeben werden
|
||||
return container.rawData;
|
||||
resolve(container.rawData);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* All raw values from the measurements get cleared
|
||||
*/
|
||||
public static clearAllRawMeasurements() {
|
||||
return new Promise<void>(async (resolve, reject) => {
|
||||
for (let c of (await Container.find({}))) {
|
||||
if (c.sensorType != SensorType.NONE) {
|
||||
c.rawData = -1;
|
||||
await c.save();
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* All containers will be measured
|
||||
*/
|
||||
public static measureAllRaw() {
|
||||
return new Promise<void>(async (resolve, reject) => {
|
||||
for (let c of (await Container.find({}))) {
|
||||
if (c.sensorType != SensorType.NONE) {
|
||||
let weight: number | null = c.rawData;
|
||||
try {
|
||||
weight = await SensorHelper.measureRaw(c);
|
||||
} catch (e) {
|
||||
return reject("Fehler Sensor (" + c.sensorPin1 + ", " + c.sensorPin2 + ") - Container " + c.slot + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -7,7 +7,7 @@ import {Settings} from "./Settings";
|
||||
export class Utils {
|
||||
public static checkInternet(): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
dns.resolve('gaminggeneration.de', (err) => {
|
||||
dns.resolve('itender.iif.li', (err) => {
|
||||
if (err)
|
||||
resolve(false);
|
||||
else
|
||||
|
@ -10,7 +10,14 @@ export interface IContainer extends mongoose.Document {
|
||||
sensorTare: number;
|
||||
// Sensor Type
|
||||
sensorType: SensorType;
|
||||
|
||||
/**
|
||||
* HX711 DATA-Pin
|
||||
*/
|
||||
sensorPin1: number;
|
||||
/**
|
||||
* HX711 CLOCK-Pin
|
||||
*/
|
||||
sensorPin2: number;
|
||||
sensorProxy: boolean
|
||||
rawData: number;
|
||||
|
285
src/iTender.ts
@ -12,43 +12,57 @@ import {WebSocketEvent} from "./WebSocketEvent";
|
||||
import Job from "./database/Job";
|
||||
import {IIngredient} from "./database/IIngredient";
|
||||
import Ingredient from "./database/Ingredient";
|
||||
import {clearInterval} from "timers";
|
||||
import {RejectReason} from "./RejectReason";
|
||||
import axios from "axios";
|
||||
import GPIO from "rpi-gpio";
|
||||
import {MyGPIO} from "./MyGPIO";
|
||||
import {SensorHelper} from "./SensorHelper";
|
||||
import {SensorType} from "./SensorType";
|
||||
import {Mixer} from "./Mixer";
|
||||
|
||||
|
||||
const isPI = require("detect-rpi");
|
||||
|
||||
const log = debug("itender:station");
|
||||
const mixLog = debug("itender:mix");
|
||||
const mixLog = debug("itender:mixer");
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The main class of the itender, here a located all main features of the system, like starting pumps, firing events and stuff
|
||||
*/
|
||||
export class iTender {
|
||||
private static secondsPer100ml: number = 35.3335;
|
||||
|
||||
|
||||
/**
|
||||
* How many seconds it takes to fill 100ml
|
||||
* @private
|
||||
*/
|
||||
static secondsPer100ml: number = 35.3335;
|
||||
|
||||
/**
|
||||
* Sensitivity-Factor of the hx711 scales
|
||||
* Calculated by (Value /100) Value needs to be measured by hx711 when a known weight is on the sensor,
|
||||
* in this example its 100g, so divide it by 100
|
||||
*/
|
||||
static sensitivityFactor: number = 1.0;
|
||||
|
||||
/**
|
||||
* Retrieve all drinks in cache
|
||||
*/
|
||||
static get drinks(): IDrink[] {
|
||||
return this._drinks;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current job of the itender
|
||||
*/
|
||||
static get currentJob(): IJob | null {
|
||||
return this._currentJob;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current internal status of itender
|
||||
* @private
|
||||
*/
|
||||
private static _status: iTenderStatus = iTenderStatus.STARTING;
|
||||
private static _currentJob: IJob | null = null;
|
||||
private static _jobCheckInterval: NodeJS.Timer;
|
||||
|
||||
|
||||
/**
|
||||
* Current internal connection-status boolean
|
||||
* @private
|
||||
*/
|
||||
private static _internetConnection: boolean = false;
|
||||
|
||||
private static _jobTimers: NodeJS.Timeout[] = [];
|
||||
|
||||
/**
|
||||
* Returns true if internet connection is active
|
||||
@ -57,8 +71,16 @@ export class iTender {
|
||||
return this._internetConnection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drinks in cache
|
||||
* @private
|
||||
*/
|
||||
private static _drinks: IDrink[];
|
||||
|
||||
/**
|
||||
* Sets the current itender status and sends it to the client
|
||||
* @param status
|
||||
*/
|
||||
static setStatus(status: iTenderStatus) {
|
||||
this._status = status;
|
||||
if (WebSocketHandler.ws && WebSocketHandler.ws.readyState == 1)
|
||||
@ -66,6 +88,9 @@ export class iTender {
|
||||
log("Status is now " + status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the internal status of itender app
|
||||
*/
|
||||
static get status(): iTenderStatus {
|
||||
return this._status;
|
||||
}
|
||||
@ -73,6 +98,8 @@ export class iTender {
|
||||
|
||||
/**
|
||||
* This method is fired if the user likes to mix a drink
|
||||
* It starts to calculate the ingredients and amounts of each ingredient
|
||||
* 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> {
|
||||
@ -142,176 +169,13 @@ export class iTender {
|
||||
await job.save()
|
||||
resolve(job);
|
||||
|
||||
await this.startFill(job);
|
||||
await Mixer.startFill(job);
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Start the internal fill method
|
||||
* @param 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);
|
||||
|
||||
|
||||
for (let x of job.amounts) {
|
||||
|
||||
// Start pump here
|
||||
try {
|
||||
await MyGPIO.setup(x.container.pumpPin, GPIO.DIR_OUT)
|
||||
|
||||
await MyGPIO.write(x.container.pumpPin, true);
|
||||
} catch (e) {
|
||||
if (isPI()) {
|
||||
log("[ERROR] GPIO I/O Error " + e);
|
||||
// Todo error handling to user
|
||||
await iTender.cancelFill();
|
||||
return;
|
||||
} else {
|
||||
log("[WARNING] GPIO I/O Error, but it's normal cause you are not on raspberry");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let waitTime = (iTender.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(async () => {
|
||||
// Remove from list of timers
|
||||
let arr: NodeJS.Timer[] = [];
|
||||
for (let i = 0; i < this._jobTimers.length; i++) {
|
||||
if (this._jobTimers[i] != timer)
|
||||
arr.push(this._jobTimers[i]);
|
||||
}
|
||||
|
||||
mixLog(`Stopping output of pump ${x.container.pumpPin}`);
|
||||
// Stop pump here
|
||||
try {
|
||||
await MyGPIO.write(x.container.pumpPin, false);
|
||||
} catch (e) {
|
||||
if (isPI()) {
|
||||
log("[ERROR] GPIO I/O Error " + e);
|
||||
await iTender.cancelFill();
|
||||
return;
|
||||
} else {
|
||||
log("[WARNING] GPIO I/O Error, but it's normal cause you are not on raspberry");
|
||||
}
|
||||
}
|
||||
|
||||
if (x.container.sensorType == SensorType.NONE) {
|
||||
// V2: Manual measuring
|
||||
x.container.filled = x.container.filled - x.amount;
|
||||
await x.container.save();
|
||||
}
|
||||
|
||||
this._jobTimers = arr;
|
||||
|
||||
}, waitTime);
|
||||
this._jobTimers.push(timer);
|
||||
}
|
||||
|
||||
iTender._jobCheckInterval = setInterval(async () => {
|
||||
|
||||
if (this._jobTimers.length != 0)
|
||||
return;
|
||||
|
||||
clearInterval(iTender._jobCheckInterval);
|
||||
job.endAt = new Date();
|
||||
job.successful = true;
|
||||
|
||||
await job.save();
|
||||
mixLog("Job successful");
|
||||
setTimeout(() => iTender.setStatus(iTenderStatus.READY), 3000)
|
||||
|
||||
}, 500);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Cancel the fill
|
||||
*/
|
||||
static async cancelFill() {
|
||||
if (!this._currentJob || this.status != iTenderStatus.FILLING)
|
||||
return;
|
||||
|
||||
clearInterval(this._jobCheckInterval);
|
||||
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 {
|
||||
await MyGPIO.write(jobIngredient.container.pumpPin, false);
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
|
||||
// ToDo
|
||||
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();
|
||||
}
|
||||
|
||||
iTender.setStatus(iTenderStatus.READY);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Measure all containers based on their sensor values
|
||||
*/
|
||||
static measureContainers(): Promise<void> {
|
||||
log("Measuring containers...");
|
||||
|
||||
return new Promise(async resolve => {
|
||||
for (let c of (await Container.find({}))) {
|
||||
if (c.sensorType != SensorType.NONE) {
|
||||
let weight = SensorHelper.measure(c);
|
||||
if (!weight) {
|
||||
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
|
||||
let newFilled = weight - c.sensorDelta;
|
||||
|
||||
if (newFilled <= 3 && c.filled != -1) {
|
||||
c.filled = -1;
|
||||
// Container is empty!
|
||||
} else {
|
||||
// Container > 2
|
||||
c.filled = newFilled;
|
||||
}
|
||||
|
||||
await c.save();
|
||||
}
|
||||
|
||||
}
|
||||
log("Containers measured!");
|
||||
resolve();
|
||||
|
||||
let payload = new WebSocketPayload(WebSocketEvent.CONTAINERS, (await Container.find()));
|
||||
await WebSocketHandler.send(payload);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Refresh the drinks to the local variable
|
||||
* Check which drinks can be done, based on the current container ingredients
|
||||
@ -343,28 +207,36 @@ export class iTender {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Automatic checkup (all 30 seconds)
|
||||
* When no current job is ongoing, skip
|
||||
* if current job is active, but the start time was more than 2mins ago, the job gets canceled
|
||||
*/
|
||||
public static async autoCheckup() {
|
||||
setInterval(async () => {
|
||||
log("Auto Checkup");
|
||||
if (!this._currentJob)
|
||||
if (!Mixer.currentJob)
|
||||
return;
|
||||
// Check if startedTime plus 2 mins smaller than now
|
||||
if (this._currentJob.startedAt.getTime() + 1000 * 60 * 2 <= Date.now()) {
|
||||
if (Mixer.currentJob.startedAt.getTime() + 1000 * 60 * 2 <= Date.now()) {
|
||||
// Job can be declared as stuck!
|
||||
this._currentJob.successful = false;
|
||||
this._currentJob.endAt = new Date();
|
||||
await this._currentJob.save();
|
||||
this._currentJob = null;
|
||||
this.setStatus(iTenderStatus.READY);
|
||||
await Mixer.cancelFill();
|
||||
}
|
||||
}, 30000);
|
||||
}, 1000 * 30);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks the internet connection
|
||||
*/
|
||||
public static async checkNetwork() {
|
||||
this._internetConnection = await Utils.checkInternet();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Refrehs drinks from the cloud (https://itender.iif.li)
|
||||
*/
|
||||
static refreshFromServer(): Promise<void> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
iTender.setStatus(iTenderStatus.DOWNLOADING)
|
||||
@ -431,7 +303,6 @@ export class iTender {
|
||||
Utils.deleteImage(local._id);
|
||||
await Drink.deleteOne({"_id": local._id});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -470,39 +341,5 @@ export class iTender {
|
||||
});
|
||||
}
|
||||
|
||||
public static clearAllRawMeasurements()
|
||||
{
|
||||
return new Promise<void>(async (resolve, reject) => {
|
||||
for (let c of (await Container.find({}))) {
|
||||
if (c.sensorType != SensorType.NONE) {
|
||||
c.rawData = -1;
|
||||
await c.save();
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
}
|
||||
|
||||
public static measureAllRaw() {
|
||||
return new Promise<void>(async (resolve, reject) => {
|
||||
for (let c of (await Container.find({}))) {
|
||||
if (c.sensorType != SensorType.NONE) {
|
||||
let weight : number | null = c.rawData;
|
||||
if( !c.sensorProxy )
|
||||
{
|
||||
// Check values
|
||||
weight = SensorHelper.measure(c);
|
||||
}
|
||||
|
||||
if (weight == null || weight > 1000 || weight < 0 ) { //fixme werte
|
||||
// Problem erkannt!
|
||||
return reject("Fehler Sensor (" + c.sensorPin1 + ", " + c.sensorPin2 + ") - Container " + c.slot + 1);
|
||||
}
|
||||
c.rawData = weight;
|
||||
await c.save();
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
}
|
||||
}
|
11
src/main.ts
@ -8,6 +8,8 @@ import {Utils} from "./Utils";
|
||||
import {Settings} from "./Settings";
|
||||
import Drink from "./database/Drink";
|
||||
import {MyGPIO} from "./MyGPIO";
|
||||
import {ContainerHelper} from "./ContainerHelper";
|
||||
import {Mixer} from "./Mixer";
|
||||
|
||||
const log = debug("itender:server");
|
||||
|
||||
@ -51,6 +53,9 @@ const wsApp = new WebsocketApp();
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* tst
|
||||
*/
|
||||
function init(): Promise<void> {
|
||||
|
||||
return new Promise(async resolve => {
|
||||
@ -69,7 +74,7 @@ function init(): Promise<void> {
|
||||
|
||||
// Containers
|
||||
//await iTender.refreshContainers();
|
||||
await iTender.measureContainers();
|
||||
await ContainerHelper.measureContainers();
|
||||
log("2");
|
||||
// Drinks
|
||||
await iTender.refreshDrinks();
|
||||
@ -90,11 +95,11 @@ function refresh(): Promise<void> {
|
||||
// Below are refreshments of containers / drinks
|
||||
|
||||
// If there is a current job, DO NOT REFRESH!
|
||||
if (iTender.currentJob)
|
||||
if (Mixer.currentJob)
|
||||
return;
|
||||
|
||||
//await iTender.refreshContainers(); Not needed because there is no change in containers?
|
||||
await iTender.measureContainers();
|
||||
await ContainerHelper.measureContainers();
|
||||
//await iTender.refreshDrinks(); Not needed because there is no change in drinks?
|
||||
});
|
||||
}
|
@ -12,6 +12,7 @@ import {RequestType} from "../../RequestType";
|
||||
import {IJob} from "../../database/IJob";
|
||||
import {SensorHelper} from "../../SensorHelper";
|
||||
import {IContainer} from "../../database/IContainer";
|
||||
import {Mixer} from "../../Mixer";
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
@ -80,11 +81,11 @@ router.ws('/', async (ws, req, next) => {
|
||||
container.volume = filled; // V2: Volume is now being updated after change of ingredient
|
||||
|
||||
if (container.sensorType != SensorType.NONE) {
|
||||
let raw = SensorHelper.measure(container);
|
||||
let raw = SensorHelper.measureRaw(container);
|
||||
if (!raw) {
|
||||
await WebSocketHandler.send(new WebSocketPayload(WebSocketEvent.ERROR, "Der 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."));
|
||||
} else {
|
||||
container.sensorDelta = raw - filled; // V2: Kalkuliere differenz zwischen Gewicht und gefülltem Inhalt // Todo Möglicherweise ist der "raw"-Wert nicht Gewicht
|
||||
container.sensorDelta = await raw - filled; // V2: Kalkuliere differenz zwischen Gewicht und gefülltem Inhalt // Todo Möglicherweise ist der "raw"-Wert nicht Gewicht
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,7 +150,7 @@ router.ws('/', async (ws, req, next) => {
|
||||
break;
|
||||
}
|
||||
case RequestType.JOB: {
|
||||
WebSocketHandler.answerRequest(msg.data["type"] as RequestType, iTender.currentJob);
|
||||
WebSocketHandler.answerRequest(msg.data["type"] as RequestType, Mixer.currentJob);
|
||||
break;
|
||||
}
|
||||
case RequestType.DOWNLOAD_DRINKS: {
|
||||
@ -158,7 +159,7 @@ router.ws('/', async (ws, req, next) => {
|
||||
break;
|
||||
}
|
||||
case RequestType.CHECK: {
|
||||
await iTender.clearAllRawMeasurements();
|
||||
await SensorHelper.clearAllRawMeasurements();
|
||||
|
||||
|
||||
let content : {error: boolean, msg: string} = {
|
||||
@ -174,7 +175,7 @@ router.ws('/', async (ws, req, next) => {
|
||||
}
|
||||
|
||||
// Check measurements
|
||||
await iTender.measureAllRaw();
|
||||
await SensorHelper.measureAllRaw();
|
||||
for( let c of await Container.find() )
|
||||
{
|
||||
if( c.sensorType != SensorType.NONE && c.rawData == -1 )
|
||||
@ -202,7 +203,7 @@ router.ws('/', async (ws, req, next) => {
|
||||
|
||||
async function measureAndSafe() {
|
||||
try {
|
||||
await iTender.measureAllRaw();
|
||||
await SensorHelper.measureAllRaw();
|
||||
for (let c of await Container.find({})) {
|
||||
if (c.sensorType != SensorType.NONE) {
|
||||
c.sensorTare += c.rawData;
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"pretty": true,
|
||||
"noImplicitAny": false,
|
||||
"sourceMap": true,
|
||||
"outDir": "dist",
|
||||
@ -19,10 +20,11 @@
|
||||
"node_modules/@types"
|
||||
]
|
||||
},
|
||||
|
||||
"include": [
|
||||
"./src/",
|
||||
"./src/"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
14
typedoc.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"entryPoints": [
|
||||
"src/main.ts",
|
||||
"src/iTender.ts",
|
||||
"src/web/main.ts",
|
||||
"src/WebsocketApp.ts",
|
||||
"src/App.ts",
|
||||
"src/routes/ws/websocketRoute.ts"
|
||||
],
|
||||
"out": "docs/",
|
||||
"readme": "README.md",
|
||||
"name": "iTender Documentation",
|
||||
"tsconfig": "./tsconfig.json"
|
||||
}
|
63
yarn.lock
@ -1430,6 +1430,13 @@ brace-expansion@^1.1.7:
|
||||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
||||
brace-expansion@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
|
||||
integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
|
||||
braces@^3.0.2, braces@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
|
||||
@ -2123,6 +2130,11 @@ json-schema-traverse@^0.4.1:
|
||||
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
|
||||
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
|
||||
|
||||
jsonc-parser@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76"
|
||||
integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==
|
||||
|
||||
jstransformer@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-1.0.0.tgz#ed8bf0921e2f3f1ed4d5c1a44f68709ed24722c3"
|
||||
@ -2165,11 +2177,21 @@ lru-cache@^6.0.0:
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
lunr@^2.3.9:
|
||||
version "2.3.9"
|
||||
resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1"
|
||||
integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==
|
||||
|
||||
make-error@^1.1.1:
|
||||
version "1.3.6"
|
||||
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
|
||||
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
|
||||
|
||||
marked@^4.2.5:
|
||||
version "4.2.12"
|
||||
resolved "https://registry.yarnpkg.com/marked/-/marked-4.2.12.tgz#d69a64e21d71b06250da995dcd065c11083bebb5"
|
||||
integrity sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==
|
||||
|
||||
media-typer@0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||
@ -2227,6 +2249,13 @@ minimatch@^3.1.2:
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimatch@^5.1.2:
|
||||
version "5.1.6"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
|
||||
integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
mongodb-connection-string-url@^2.5.4:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz#57901bf352372abdde812c81be47b75c6b2ec5cf"
|
||||
@ -2806,6 +2835,15 @@ shebang-regex@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
|
||||
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
|
||||
|
||||
shiki@^0.12.1:
|
||||
version "0.12.1"
|
||||
resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.12.1.tgz#26fce51da12d055f479a091a5307470786f300cd"
|
||||
integrity sha512-aieaV1m349rZINEBkjxh2QbBvFFQOlgqYTNtCal82hHj4dDZ76oMlQIX+C7ryerBTDiga3e5NfH6smjdJ02BbQ==
|
||||
dependencies:
|
||||
jsonc-parser "^3.2.0"
|
||||
vscode-oniguruma "^1.7.0"
|
||||
vscode-textmate "^8.0.0"
|
||||
|
||||
sift@16.0.1:
|
||||
version "16.0.1"
|
||||
resolved "https://registry.yarnpkg.com/sift/-/sift-16.0.1.tgz#e9c2ccc72191585008cf3e36fc447b2d2633a053"
|
||||
@ -2996,6 +3034,21 @@ type-is@~1.6.16:
|
||||
media-typer "0.3.0"
|
||||
mime-types "~2.1.24"
|
||||
|
||||
typedoc-plugin-missing-exports@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-1.0.0.tgz#7212a2cfaba7b48264df4b4110f3a5684b5c49a1"
|
||||
integrity sha512-7s6znXnuAj1eD9KYPyzVzR1lBF5nwAY8IKccP5sdoO9crG4lpd16RoFpLsh2PccJM+I2NASpr0+/NMka6ThwVA==
|
||||
|
||||
typedoc@^0.23.24:
|
||||
version "0.23.24"
|
||||
resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.23.24.tgz#01cf32c09f2c19362e72a9ce1552d6e5b48c4fef"
|
||||
integrity sha512-bfmy8lNQh+WrPYcJbtjQ6JEEsVl/ce1ZIXyXhyW+a1vFrjO39t6J8sL/d6FfAGrJTc7McCXgk9AanYBSNvLdIA==
|
||||
dependencies:
|
||||
lunr "^2.3.9"
|
||||
marked "^4.2.5"
|
||||
minimatch "^5.1.2"
|
||||
shiki "^0.12.1"
|
||||
|
||||
typescript@^4.8.4:
|
||||
version "4.9.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78"
|
||||
@ -3051,6 +3104,16 @@ void-elements@^3.1.0:
|
||||
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
|
||||
integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==
|
||||
|
||||
vscode-oniguruma@^1.7.0:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz#439bfad8fe71abd7798338d1cd3dc53a8beea94b"
|
||||
integrity sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==
|
||||
|
||||
vscode-textmate@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-8.0.0.tgz#2c7a3b1163ef0441097e0b5d6389cd5504b59e5d"
|
||||
integrity sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==
|
||||
|
||||
watchpack@^2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
|
||||
|