Changes due to new requirements

This commit is contained in:
naumueller 2026-02-03 20:20:56 +01:00
commit 5b072329d3
23 changed files with 857 additions and 0 deletions

40
nbactions.xml Normal file
View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<actions>
<action>
<actionName>run</actionName>
<packagings>
<packaging>jar</packaging>
</packagings>
<goals>
<goal>clean</goal>
<goal>javafx:run</goal>
</goals>
</action>
<action>
<actionName>debug</actionName>
<goals>
<goal>clean</goal>
<goal>javafx:run@ide-debug</goal>
</goals>
<properties>
<jpda.listen>true</jpda.listen>
</properties>
</action>
<action>
<actionName>profile</actionName>
<goals>
<goal>clean</goal>
<goal>javafx:run@ide-profile</goal>
</goals>
</action>
<action>
<actionName>CUSTOM-jlink</actionName>
<displayName>jlink</displayName>
<goals>
<goal>clean</goal>
<!-- compile not needed with javafx-maven-plugin v0.0.5 -->
<goal>compile</goal>
<goal>javafx:jlink</goal>
</goals>
</action>
</actions>

106
pom.xml Normal file
View File

@ -0,0 +1,106 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>efi.projekt</groupId>
<artifactId>Virtueller_Gesundheitsassistent</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>13</version>
</dependency>
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.5</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-web</artifactId>
<version>21.0.9</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>21.0.9</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>jakarta.websocket</groupId>
<artifactId>jakarta.websocket-client-api</artifactId>
<version>2.2.0</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.openpnp</groupId>
<artifactId>opencv</artifactId>
<version>4.12.0</version>
<type>jar</type>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<release>21</release>
</configuration>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.4</version>
<configuration>
<mainClass>efi.projekt.virtueller_gesundheitsassistent.App</mainClass>
</configuration>
<executions>
<execution>
<!-- Default configuration for running -->
<!-- Usage: mvn clean javafx:run -->
<id>default-cli</id>
</execution>
<execution>
<!-- Configuration for manual attach debugging -->
<!-- Usage: mvn clean javafx:run@debug -->
<id>debug</id>
<configuration>
<options>
<option>-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=localhost:8000</option>
</options>
</configuration>
</execution>
<execution>
<!-- Configuration for automatic IDE debugging -->
<id>ide-debug</id>
<configuration>
<options>
<option>-agentlib:jdwp=transport=dt_socket,server=n,address=${jpda.address}</option>
</options>
</configuration>
</execution>
<execution>
<!-- Configuration for automatic IDE profiling -->
<id>ide-profile</id>
<configuration>
<options>
<option>${profiler.jvmargs.arg1}</option>
<option>${profiler.jvmargs.arg2}</option>
<option>${profiler.jvmargs.arg3}</option>
<option>${profiler.jvmargs.arg4}</option>
<option>${profiler.jvmargs.arg5}</option>
</options>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

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

View File

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

View File

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

View File

@ -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<ClassificationType> classification =
new SimpleObjectProperty<>(ClassificationType.NORMAL);
private final BooleanProperty speaking =
new SimpleBooleanProperty(false);
private final BooleanProperty monitoring =
new SimpleBooleanProperty(true);
public ObjectProperty<ClassificationType> classificationProperty() {
return classification;
}
public BooleanProperty speakingProperty() {
return speaking;
}
public BooleanProperty monitoringProperty() {
return monitoring;
}
}

View File

@ -0,0 +1,11 @@
package efi.projekt.virtueller_gesundheitsassistent.model;
/**
*
* @author naumueller
*/
public record Classification (
String label,
String text,
String mqttValue
) {}

View File

@ -0,0 +1,13 @@
package efi.projekt.virtueller_gesundheitsassistent.model;
/**
*
* @author naumueller
*/
public enum ClassificationType {
NORMAL,
MUEDIGKEIT,
ABLENKUNG,
STRESS,
REAKTION
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Tab?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.layout.AnchorPane?>
<TabPane xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1">
<tabs>
<Tab text="Minimal">
<fx:include source="designs/MinimalView.fxml"/>
</Tab>
<Tab text="Immersive">
<fx:include source="designs/ImmersiveView.fxml"/>
</Tab>
<Tab text="Compact">
<fx:include source="designs/CompactView.fxml"/>
</Tab>
<Tab text="Dashboard">
<fx:include source="designs/DashboardView.fxml"/>
</Tab>
</tabs>
</TabPane>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml/1">
</AnchorPane>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.StackPane?>
<StackPane xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1" fx:controller="efi.projekt.virtueller_gesundheitsassistent.controller.CameraController">
<children>
<ImageView fx:id="cameraView" fitHeight="240.0" fitWidth="320.0" pickOnBounds="true" preserveRatio="true" />
</children>
</StackPane>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml/1">
</AnchorPane>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml/1">
</AnchorPane>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml/1">
</AnchorPane>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml/1">
</AnchorPane>

View File

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