commit 5b072329d32b3357379f809c350e3c4e23f705ce Author: naumueller <> Date: Tue Feb 3 20:20:56 2026 +0100 Changes due to new requirements diff --git a/nbactions.xml b/nbactions.xml new file mode 100644 index 0000000..a0cb38e --- /dev/null +++ b/nbactions.xml @@ -0,0 +1,40 @@ + + + + run + + jar + + + clean + javafx:run + + + + debug + + clean + javafx:run@ide-debug + + + true + + + + profile + + clean + javafx:run@ide-profile + + + + CUSTOM-jlink + jlink + + clean + + compile + javafx:jlink + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..119a984 --- /dev/null +++ b/pom.xml @@ -0,0 +1,106 @@ + + 4.0.0 + efi.projekt + Virtueller_Gesundheitsassistent + 1.0-SNAPSHOT + + UTF-8 + + + + org.openjfx + javafx-controls + 13 + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + 1.2.5 + jar + + + org.openjfx + javafx-web + 21.0.9 + jar + + + org.openjfx + javafx-fxml + 21.0.9 + jar + + + jakarta.websocket + jakarta.websocket-client-api + 2.2.0 + jar + + + org.openpnp + opencv + 4.12.0 + jar + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + 21 + + + + org.openjfx + javafx-maven-plugin + 0.0.4 + + efi.projekt.virtueller_gesundheitsassistent.App + + + + + + default-cli + + + + + debug + + + + + + + + + ide-debug + + + + + + + + + ide-profile + + + + + + + + + + + + + + + diff --git a/src/main/java/efi/projekt/virtueller_gesundheitsassistent/App.java b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/App.java new file mode 100644 index 0000000..9feade9 --- /dev/null +++ b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/App.java @@ -0,0 +1,44 @@ +package efi.projekt.virtueller_gesundheitsassistent; + +import java.io.IOException; +import javafx.application.Application; +import static javafx.application.Application.launch; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; + + +/** + * JavaFX App + * @author naumueller + */ +public class App extends Application { + + @Override + public void start(Stage primaryStage) throws IOException { + // Lade FXML + FXMLLoader loader = new FXMLLoader(getClass().getResource("/efi/projekt/virtueller_gesundheitsassistent/view/FxView.fxml")); + Parent root = loader.load(); + + // Erzeuge Model & ViewModel + + Scene scene = new Scene(root, 1280, 720); + + primaryStage.setTitle("Virtueller Gesundheitsassistent"); + primaryStage.setScene(scene); + primaryStage.show(); + } + + private static Parent loadFXML(String fxml) throws IOException { + FXMLLoader fxmlLoader = new FXMLLoader( + App.class.getResource("/efi/projekt/virtueller_gesundheitsassistent/view/" + fxml + ".fxml") + ); + return fxmlLoader.load(); + } + + public static void main(String[] args) { + launch(); + } + +} \ No newline at end of file diff --git a/src/main/java/efi/projekt/virtueller_gesundheitsassistent/SystemInfo.java b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/SystemInfo.java new file mode 100644 index 0000000..5fd2dac --- /dev/null +++ b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/SystemInfo.java @@ -0,0 +1,13 @@ +package efi.projekt.virtueller_gesundheitsassistent; + +public class SystemInfo { + + public static String javaVersion() { + return System.getProperty("java.version"); + } + + public static String javafxVersion() { + return System.getProperty("javafx.version"); + } + +} \ No newline at end of file diff --git a/src/main/java/efi/projekt/virtueller_gesundheitsassistent/controller/CameraController.java b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/controller/CameraController.java new file mode 100644 index 0000000..cca2019 --- /dev/null +++ b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/controller/CameraController.java @@ -0,0 +1,34 @@ +package efi.projekt.virtueller_gesundheitsassistent.controller; + +import efi.projekt.virtueller_gesundheitsassistent.service.CameraService; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.scene.image.ImageView; + +/** + * + * @author naumueller + */ +public class CameraController { + + @FXML private ImageView cameraView; + + private final CameraService cameraService = new CameraService(); + + @FXML + private void initialize() { + start(); + } + + @FXML + private void start() { + cameraService.start(image -> + Platform.runLater(() -> cameraView.setImage(image)) + ); + } + + @FXML + private void stopCamera() { + cameraService.stop(); + } +} diff --git a/src/main/java/efi/projekt/virtueller_gesundheitsassistent/model/AssistantState.java b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/model/AssistantState.java new file mode 100644 index 0000000..963e25b --- /dev/null +++ b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/model/AssistantState.java @@ -0,0 +1,34 @@ +package efi.projekt.virtueller_gesundheitsassistent.model; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; + +/** + * + * @author naumueller + */ +public class AssistantState { + + private final ObjectProperty classification = + new SimpleObjectProperty<>(ClassificationType.NORMAL); + + private final BooleanProperty speaking = + new SimpleBooleanProperty(false); + + private final BooleanProperty monitoring = + new SimpleBooleanProperty(true); + + public ObjectProperty classificationProperty() { + return classification; + } + + public BooleanProperty speakingProperty() { + return speaking; + } + + public BooleanProperty monitoringProperty() { + return monitoring; + } +} diff --git a/src/main/java/efi/projekt/virtueller_gesundheitsassistent/model/Classification.java b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/model/Classification.java new file mode 100644 index 0000000..9384379 --- /dev/null +++ b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/model/Classification.java @@ -0,0 +1,11 @@ +package efi.projekt.virtueller_gesundheitsassistent.model; + +/** + * + * @author naumueller + */ +public record Classification ( + String label, + String text, + String mqttValue +) {} diff --git a/src/main/java/efi/projekt/virtueller_gesundheitsassistent/model/ClassificationType.java b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/model/ClassificationType.java new file mode 100644 index 0000000..1c6da2f --- /dev/null +++ b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/model/ClassificationType.java @@ -0,0 +1,13 @@ +package efi.projekt.virtueller_gesundheitsassistent.model; + +/** + * + * @author naumueller + */ +public enum ClassificationType { + NORMAL, + MUEDIGKEIT, + ABLENKUNG, + STRESS, + REAKTION +} diff --git a/src/main/java/efi/projekt/virtueller_gesundheitsassistent/service/CameraService.java b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/service/CameraService.java new file mode 100644 index 0000000..0f06c8c --- /dev/null +++ b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/service/CameraService.java @@ -0,0 +1,69 @@ +package efi.projekt.virtueller_gesundheitsassistent.service; + +import efi.projekt.virtueller_gesundheitsassistent.util.Logger; +import java.io.ByteArrayInputStream; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import javafx.scene.image.Image; +import org.opencv.core.Core; +import org.opencv.core.Mat; +import org.opencv.core.MatOfByte; +import org.opencv.imgcodecs.Imgcodecs; +import org.opencv.videoio.VideoCapture; + +/** + * + * @author naumueller + */ +public class CameraService { + + static { + System.loadLibrary(Core.NATIVE_LIBRARY_NAME); + } + + private VideoCapture capture; + private ScheduledExecutorService timer; + + public void start(java.util.function.Consumer frameConsumer) { + if (capture != null && capture.isOpened()) { + return; + } + + capture = new VideoCapture(0); + + if (!capture.isOpened()) { + Logger.error("CAMERA", "Kamera konnte nicht geoeffnet werden"); + return; + } + + Runnable frameGrabber = () -> { + Mat frame = new Mat(); + if (capture.read(frame)) { + Image image = matToImage(frame); + frameConsumer.accept(image); + } + }; + + timer = Executors.newSingleThreadScheduledExecutor(); + timer.scheduleAtFixedRate(frameGrabber, 0, 33, TimeUnit.MILLISECONDS); + + Logger.info("CAMERA", "Kamera gestartet"); + } + + public void stop() { + if (timer != null && !timer.isShutdown()) { + timer.shutdown(); + } + if (capture != null && capture.isOpened()) { + capture.release(); + } + Logger.info("CAMERA", "Kamera gestoppt"); + } + + private Image matToImage(Mat frame) { + MatOfByte buffer = new MatOfByte(); + Imgcodecs.imencode(".png", frame, buffer); + return new Image(new ByteArrayInputStream(buffer.toArray())); + } +} diff --git a/src/main/java/efi/projekt/virtueller_gesundheitsassistent/service/ClassificationService.java b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/service/ClassificationService.java new file mode 100644 index 0000000..57811b4 --- /dev/null +++ b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/service/ClassificationService.java @@ -0,0 +1,37 @@ +package efi.projekt.virtueller_gesundheitsassistent.service; + +import efi.projekt.virtueller_gesundheitsassistent.model.ClassificationType; +import efi.projekt.virtueller_gesundheitsassistent.util.Logger; +import javafx.application.Platform; + +/** + * + * @author naumueller + */ +public class ClassificationService { + + private static final String TOPIC = "Text"; + + public ClassificationService(MqttClientService mqtt) { + + mqtt.subscribe(TOPIC, payload -> { + + ClassificationType type = switch (payload) { + case "0" -> ClassificationType.NORMAL; + case "1" -> ClassificationType.MUEDIGKEIT; + case "2" -> ClassificationType.ABLENKUNG; + case "3" -> ClassificationType.REAKTION; + case "4" -> ClassificationType.STRESS; + default -> ClassificationType.NORMAL; + }; + + Logger.info("STATE", "Neue Klassifizierung: " + type); + + Platform.runLater(() -> + StateService.getState() + .classificationProperty() + .set(type) + ); + }); + } +} diff --git a/src/main/java/efi/projekt/virtueller_gesundheitsassistent/service/MqttClientService.java b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/service/MqttClientService.java new file mode 100644 index 0000000..134aefa --- /dev/null +++ b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/service/MqttClientService.java @@ -0,0 +1,121 @@ +package efi.projekt.virtueller_gesundheitsassistent.service; + +import efi.projekt.virtueller_gesundheitsassistent.util.Logger; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; +import org.eclipse.paho.client.mqttv3.*; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; + +/** + * + * @author naumueller + */ +public class MqttClientService implements MqttCallback { + + private static final String BROKER_URL = "tcp://localhost:1883"; + private static final String CLIENT_ID = "JavaClientPublisherSubscriber"; + + private final Map> topicListeners = + new ConcurrentHashMap<>(); + + private MqttClient client; + + public MqttClientService() { + try { + client = new MqttClient( + BROKER_URL, + CLIENT_ID, + new MemoryPersistence() + ); + client.setCallback(this); + + MqttConnectOptions options = new MqttConnectOptions(); + options.setCleanSession(true); + options.setAutomaticReconnect(true); + + Logger.info("MQTT", "Verbinde mit Broker " + BROKER_URL); + client.connect(options); + Logger.info("MQTT", "Verbindung hergestellt"); + + } catch (MqttException e) { + Logger.error("MQTT", "Fehler beim Verbinden", e); + } + } + + public void disconnect() { + try { + if (client != null && client.isConnected()) { + client.disconnect(); + Logger.info("MQTT", "Verbindung getrennt"); + } + } catch (MqttException e) { + Logger.error("MQTT", "Fehler beim Trennen", e); + } + } + + public void subscribe(String topic, Consumer listener) { + topicListeners.put(topic, listener); + try { + client.subscribe(topic); + Logger.info("MQTT", "Topic abonniert: " + topic); + } catch (MqttException e) { + Logger.error("MQTT", "Subscribe fehlgeschlagen: " + topic, e); + } + } + + public void publish(String topic, String message, int qos) { + try { + MqttMessage mqttMessage = + new MqttMessage(message.getBytes(StandardCharsets.UTF_8)); + mqttMessage.setQos(qos); + + client.publish(topic, mqttMessage); + + Logger.debug( + "MQTT", + "Publish -> Topic=" + topic + ", QoS=" + qos + ", Payload=" + message + ); + } catch (MqttException e) { + Logger.error("MQTT", "Publish fehlgeschlagen", e); + } + } + + @Override + public void connectionLost(Throwable cause) { + Logger.warn( + "MQTT", + "Verbindung verloren: " + cause.getMessage() + ); + } + + @Override + public void messageArrived(String topic, MqttMessage message) throws Exception { + String payload = + new String(message.getPayload(), StandardCharsets.UTF_8); + + Logger.debug( + "MQTT", + "Nachricht empfangen -> Topic=" + topic + ", Payload=" + payload + ); + + Consumer listener = topicListeners.get(topic); + if (listener != null) { + listener.accept(payload); + } else { + Logger.warn( + "MQTT", + "Keine Listener für Topic: " + topic + ); + } + } + + @Override + public void deliveryComplete(IMqttDeliveryToken token) { + Logger.debug( + "MQTT", + "Delivery complete, MessageId=" + token.getMessageId() + ); + } +} diff --git a/src/main/java/efi/projekt/virtueller_gesundheitsassistent/service/PixelStreamingService.java b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/service/PixelStreamingService.java new file mode 100644 index 0000000..755cd39 --- /dev/null +++ b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/service/PixelStreamingService.java @@ -0,0 +1,33 @@ +package efi.projekt.virtueller_gesundheitsassistent.service; + +import javafx.scene.Node; +import javafx.scene.web.WebView; + +/** + * + * @author naumueller + */ +public class PixelStreamingService { + private final WebView webView; + + public PixelStreamingService(String url) { + webView = new WebView(); + webView.getEngine().load(url); + } + + public Node getView() { + return webView; + } + + public void reload() { + webView.getEngine().reload(); + } + + public void stop() { + webView.setVisible(false); + } + + public void start() { + webView.setVisible(true); + } +} \ No newline at end of file diff --git a/src/main/java/efi/projekt/virtueller_gesundheitsassistent/service/StateService.java b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/service/StateService.java new file mode 100644 index 0000000..ea8db4e --- /dev/null +++ b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/service/StateService.java @@ -0,0 +1,18 @@ +package efi.projekt.virtueller_gesundheitsassistent.service; + +import efi.projekt.virtueller_gesundheitsassistent.model.AssistantState; + +/** + * + * @author naumueller + */ +public class StateService { + + private static final AssistantState STATE = new AssistantState(); + + private StateService() {} + + public static AssistantState getState() { + return STATE; + } +} diff --git a/src/main/java/efi/projekt/virtueller_gesundheitsassistent/service/UnrealWebSocketService.java b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/service/UnrealWebSocketService.java new file mode 100644 index 0000000..169fbe3 --- /dev/null +++ b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/service/UnrealWebSocketService.java @@ -0,0 +1,100 @@ +package efi.projekt.virtueller_gesundheitsassistent.service; + +import efi.projekt.virtueller_gesundheitsassistent.util.Logger; +import java.net.URI; +import java.util.concurrent.atomic.AtomicBoolean; +import jakarta.websocket.*; + + +/** + * + * @author naumueller + */ +@ClientEndpoint +public class UnrealWebSocketService { + + private Session session; + private final URI serverUri; + private final AtomicBoolean connected = new AtomicBoolean(false); + + + public UnrealWebSocketService(String serverUrl) { + this.serverUri = URI.create(serverUrl); + connect(); + } + + private void connect() { + try { + WebSocketContainer container = + ContainerProvider.getWebSocketContainer(); + + Logger.info("UNREAL", "Verbinde zu " + serverUri); + container.connectToServer(this, serverUri); + + } catch (Exception e) { + Logger.error("UNREAL", "WebSocket-Verbindung fehlgeschlagen", e); + } + } + + public boolean isConnected() { + return connected.get(); + } + + public void disconnect() { + try { + if (session != null && session.isOpen()) { + session.close(); + Logger.info("UNREAL", "WebSocket-Verbindung geschlossen"); + } + } catch (Exception e) { + Logger.error("UNREAL", "Fehler beim Schließen der Verbindung", e); + } + } + + @OnOpen + public void onOpen(Session session) { + this.session = session; + connected.set(true); + Logger.info("UNREAL", "WebSocket verbunden"); + } + + @OnClose + public void onClose(Session session, CloseReason reason) { + connected.set(false); + Logger.warn("UNREAL", "WebSocket geschlossen: " + reason.getReasonPhrase()); + } + + @OnError + public void onError(Session session, Throwable throwable) { + connected.set(false); + Logger.error("UNREAL", "WebSocket-Fehler", throwable); + } + + @OnMessage + public void onMessage(String message) { + Logger.debug("UNREAL", "Nachricht empfangen: " + message); + } + + public void speak(String text) { + if (!connected.get()) { + Logger.warn("UNREAL", "Nicht verbunden! Text wird nicht gesendet!"); + return; + } + + String json = buildSpeakMessage(text); + + session.getAsyncRemote().sendText(json); + Logger.info("UNREAL", "Sende Speak-Command: " + text); + } + + public String buildSpeakMessage(String text) { + return "{" + + "\"type\":\"speak\"," + + "\"text\":\"" + escape(text) + "\"" + + "}"; + } + + public String escape(String text) { + return text.replace("\"", "\\\""); + } +} diff --git a/src/main/java/efi/projekt/virtueller_gesundheitsassistent/util/Logger.java b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/util/Logger.java new file mode 100644 index 0000000..34f7fbd --- /dev/null +++ b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/util/Logger.java @@ -0,0 +1,81 @@ +package efi.projekt.virtueller_gesundheitsassistent.util; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * + * @author naumueller + */ +public class Logger { + public enum Level { + DEBUG, + INFO, + WARN, + ERROR + } + + private static Level currentLevel = Level.DEBUG; + + private static final DateTimeFormatter FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + private Logger() { + // Verhindert Instanziierung + } + + public static void setLevel(Level level) { + currentLevel = level; + } + + public static void debug(String source, String message) { + log(Level.DEBUG, source, message, null); + } + + public static void info(String source, String message) { + log(Level.INFO, source, message, null); + } + + public static void warn(String source, String message) { + log(Level.WARN, source, message, null); + } + + public static void error(String source, String message) { + log(Level.ERROR, source, message, null); + } + + public static void error(String source, String message, Throwable throwable) { + log(Level.ERROR, source, message, throwable); + } + + public static synchronized void log( + Level level, + String source, + String message, + Throwable throwable + ) { + // Log-Level filtern + if (level.ordinal() < currentLevel.ordinal()) { + return; + } + + String timestamp = LocalDateTime.now().format(FORMATTER); + + String logLine = String.format( + "[%s] [%s] [%s] %s", + timestamp, + level, + source, + message + ); + + if (level == Level.ERROR) { + System.err.println(logLine); + if (throwable != null) { + throwable.printStackTrace(System.err); + } + } else { + System.out.println(logLine); + } + } +} diff --git a/src/main/java/efi/projekt/virtueller_gesundheitsassistent/view/MainView.fxml b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/view/MainView.fxml new file mode 100644 index 0000000..11c8fc4 --- /dev/null +++ b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/view/MainView.fxml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/efi/projekt/virtueller_gesundheitsassistent/view/components/AvatarView.fxml b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/view/components/AvatarView.fxml new file mode 100644 index 0000000..18ac1b3 --- /dev/null +++ b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/view/components/AvatarView.fxml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/main/java/efi/projekt/virtueller_gesundheitsassistent/view/components/CameraView.fxml b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/view/components/CameraView.fxml new file mode 100644 index 0000000..fa30902 --- /dev/null +++ b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/view/components/CameraView.fxml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/main/java/efi/projekt/virtueller_gesundheitsassistent/view/designs/CompactView.fxml b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/view/designs/CompactView.fxml new file mode 100644 index 0000000..18ac1b3 --- /dev/null +++ b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/view/designs/CompactView.fxml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/main/java/efi/projekt/virtueller_gesundheitsassistent/view/designs/DashboardView.fxml b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/view/designs/DashboardView.fxml new file mode 100644 index 0000000..18ac1b3 --- /dev/null +++ b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/view/designs/DashboardView.fxml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/main/java/efi/projekt/virtueller_gesundheitsassistent/view/designs/ImmersiveView.fxml b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/view/designs/ImmersiveView.fxml new file mode 100644 index 0000000..18ac1b3 --- /dev/null +++ b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/view/designs/ImmersiveView.fxml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/main/java/efi/projekt/virtueller_gesundheitsassistent/view/designs/MinimalView.fxml b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/view/designs/MinimalView.fxml new file mode 100644 index 0000000..18ac1b3 --- /dev/null +++ b/src/main/java/efi/projekt/virtueller_gesundheitsassistent/view/designs/MinimalView.fxml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java new file mode 100644 index 0000000..1b6dc33 --- /dev/null +++ b/src/main/java/module-info.java @@ -0,0 +1,14 @@ +module efi.projekt.virtueller_gesundheitsassistent { + requires javafx.controls; + requires javafx.fxml; + requires java.base; + requires org.eclipse.paho.client.mqttv3; + requires javafx.web; + requires jakarta.websocket.client; + requires opencv; + + + opens efi.projekt.virtueller_gesundheitsassistent to javafx.fxml; + + exports efi.projekt.virtueller_gesundheitsassistent; +}