V2: Remap many stuff

Took 3 hours 0 minutes
This commit is contained in:
Tobias Hopp 2023-01-23 23:56:39 +01:00
parent 58a3df8111
commit c9a0ac68c4
32 changed files with 559 additions and 653 deletions

3
.gitignore vendored
View File

@ -3,4 +3,5 @@
/node_modules
/public/images/
/config.json
/yarn-error.log
/yarn-error.log
/docs/

View File

@ -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);
}
}
}
}

View File

@ -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 );
}
**/

View File

@ -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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

View File

@ -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/

View File

@ -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

View File

@ -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/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

View File

@ -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"

View File

@ -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;
}

View File

@ -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
View 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);
});
}
}

View File

@ -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
View 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);
}
}

View File

@ -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();
})
}
}

View File

@ -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

View File

@ -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;

View File

@ -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();
})
}
}

View File

@ -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?
});
}

View File

@ -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;

View File

@ -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
View 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"
}

View File

@ -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"