Compare commits

...

No commits in common. "main" and "feature/views" have entirely different histories.

63 changed files with 914 additions and 2103 deletions

42
.gitignore vendored
View File

@ -1,42 +0,0 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
.kotlin
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
lib/
logs/
data/
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

10
.idea/.gitignore generated vendored
View File

@ -1,10 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

1
.idea/.name generated
View File

@ -1 +0,0 @@
Virtueller Gesundheitsassistent

7
.idea/encodings.xml generated
View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

View File

@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

15
.idea/misc.xml generated
View File

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.12 (gesundheitsassistent)" />
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="ms-17" project-jdk-type="JavaSDK" />
</project>

6
.idea/vcs.xml generated
View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

18
README.md Normal file
View File

@ -0,0 +1,18 @@
Virtueller Gesundheitsassistent
Überblick
Dieses Projekt implementiert einen virtuellen Gesundheits- und Fahrassistenten
auf Basis von JavaFX. Die Anwendung visualisiert den aktuellen Zustand des
Fahrers (z.B. Müdigkeit, Ablenkung, Stress) und integriert:
- ein ML-Modell, das Zustände klassifiziert
- eine JavaFX-GUI mit mehreren Designvarianten
- OpenCV für Kamera-Preview
- Unreal Engine Pixel Streaming für einen sprechenden Avatar
- WebSocket-Kommunikation zur Steuerung des Avatars
Features
Architektur
Services
Views & UI-Struktur
Java-Version & Build
MQTT & Datenfluss
OpenCV

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>

123
pom.xml
View File

@ -1,62 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>efi.projekt</groupId>
<groupId>vassistent</groupId>
<artifactId>Virtueller_Gesundheitsassistent</artifactId> <artifactId>Virtueller_Gesundheitsassistent</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
<properties> <properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
<dependencies> <dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>13</version>
</dependency>
<dependency> <dependency>
<groupId>org.eclipse.paho</groupId> <groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId> <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.5</version> <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>
<dependency> <dependency>
<groupId>jakarta.websocket</groupId> <groupId>jakarta.websocket</groupId>
<artifactId>jakarta.websocket-client-api</artifactId> <artifactId>jakarta.websocket-client-api</artifactId>
<version>2.2.0</version> <version>2.2.0</version>
<type>jar</type>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.openpnp</groupId>
<artifactId>junit-jupiter-api</artifactId> <artifactId>opencv</artifactId>
<version>5.10.2</version> <version>4.12.0</version>
<scope>test</scope> <type>jar</type>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>2.0.2-beta</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>me.friwi</groupId>
<artifactId>jcefmaven</artifactId>
<version>141.0.10</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.xerial</groupId> <groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId> <artifactId>sqlite-jdbc</artifactId>
<version>3.51.2.0</version> <version>3.51.1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jfree</groupId>
<artifactId>jfreechart</artifactId>
<version>1.5.4</version>
</dependency>
<dependency>
<groupId>com.formdev</groupId>
<artifactId>flatlaf</artifactId>
<version>2.6</version>
</dependency> </dependency>
</dependencies> </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> </project>

View File

@ -0,0 +1,84 @@
package efi.projekt.virtueller_gesundheitsassistent;
import efi.projekt.virtueller_gesundheitsassistent.model.AppState;
import efi.projekt.virtueller_gesundheitsassistent.service.DataPersistenceService;
import efi.projekt.virtueller_gesundheitsassistent.service.EvaluationService;
import efi.projekt.virtueller_gesundheitsassistent.service.MqttClientService;
import efi.projekt.virtueller_gesundheitsassistent.service.StatisticsService;
import efi.projekt.virtueller_gesundheitsassistent.service.UnrealWebSocketService;
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 {
private final AppState appState = new AppState();
// Services
private DataPersistenceService persistenceService;
private StatisticsService statisticsService;
private EvaluationService evaluationService;
private UnrealWebSocketService unrealService;
private MqttClientService mqttService;
@Override
public void start(Stage primaryStage) throws IOException {
// =========================
// Services initialisieren
// =========================
// Unreal-WebService
unrealService = new UnrealWebSocketService("127.0.0.1");
// MQTT-Service
mqttService = new MqttClientService();
// Datenbank initialisieren
persistenceService = new DataPersistenceService();
// Statistik-Service
statisticsService = new StatisticsService();
// Evaluationsservice
evaluationService = new EvaluationService(statisticsService, appState, unrealService);
// =========================
// UI laden
// =========================
FXMLLoader loader = new FXMLLoader(
getClass().getResource(
"/efi/projekt/virtueller_gesundheitsassistent/view/MainView.fxml"
)
);
Scene scene = new Scene(loader.load(), 1400, 900);
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(args);
}
}

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,26 @@
package efi.projekt.virtueller_gesundheitsassistent.model;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
/**
*
* @author naumueller
*/
public class AppState {
private final ObjectProperty<ProblemLevel> problemLevel =
new SimpleObjectProperty<>(ProblemLevel.NONE);
public ObjectProperty<ProblemLevel> problemLevelProperty() {
return problemLevel;
}
public ProblemLevel getProblemLevel() {
return problemLevel.get();
}
public void setProblemLevel(ProblemLevel level) {
problemLevel.set(level);
}
}

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,12 @@
package efi.projekt.virtueller_gesundheitsassistent.model;
/**
*
* @author naumueller
*/
public enum ProblemLevel {
NONE,
WARNING,
HIGH,
DISASTER
}

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,55 @@
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package efi.projekt.virtueller_gesundheitsassistent.service;
import efi.projekt.virtueller_gesundheitsassistent.util.Logger;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.LocalDateTime;
/**
*
* @author naumueller
*/
public class DataPersistenceService {
private static final String DB_URL = "jdbc:sqlite:data/health.db";
public DataPersistenceService() {
init();
}
private void init() {
try (
Connection c = DriverManager.getConnection(DB_URL); Statement s = c.createStatement()) {
s.execute("""
CREATE TABLE IF NOT EXISTS binary_event (
id INTEGER PRIMARY KEY AUTOINCREMENT,
value INTEGER NOT NULL,
timestamp DATETIME NOT NULL
)
""");
Logger.info("DB", "Datenbank initialisiert");
} catch (SQLException e) {
Logger.error("DB", "Datenbank initialisierung fehlgeschlagen", e);
}
}
public void store(int value) {
try (Connection c = DriverManager.getConnection(DB_URL); PreparedStatement ps = c.prepareStatement("INSERT INTO binary_event (value, timestamp) VALUES (?, ?)")) {
ps.setInt(1, value);
ps.setString(2, LocalDateTime.now().toString());
ps.executeUpdate();
} catch (SQLException e) {
Logger.error("DB", "Database Insert fehlgeschlagen", e);
}
}
}

View File

@ -0,0 +1,51 @@
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package efi.projekt.virtueller_gesundheitsassistent.service;
import efi.projekt.virtueller_gesundheitsassistent.model.AppState;
import efi.projekt.virtueller_gesundheitsassistent.model.ProblemLevel;
/**
*
* @author naumueller
*/
public class EvaluationService {
private final StatisticsService statisticsService;
private final AppState appState;
private final UnrealWebSocketService unrealService;
public EvaluationService(
StatisticsService statisticsService,
AppState appState,
UnrealWebSocketService unrealService
) {
this.statisticsService = statisticsService;
this.appState = appState;
this.unrealService = unrealService;
}
public void evaluate() {
double ratio = statisticsService.getRatio(10);
ProblemLevel level;
if (ratio >= 0.9) {
level = ProblemLevel.DISASTER;
} else if (ratio >= 0.8) {
level = ProblemLevel.HIGH;
} else if (ratio >= 0.5) {
level = ProblemLevel.WARNING;
} else {
level = ProblemLevel.NONE;
}
if (level != appState.getProblemLevel()) {
appState.setProblemLevel(level);
unrealService.speak(level.name());
}
}
}

View File

@ -1,18 +1,19 @@
package vassistent.service; package efi.projekt.virtueller_gesundheitsassistent.service;
import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import vassistent.model.AppState;
import vassistent.util.Logger;
import efi.projekt.virtueller_gesundheitsassistent.util.Logger;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer; 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 { public class MqttClientService implements MqttCallback {
private AppState appState;
private static final String BROKER_URL = "tcp://localhost:1883"; private static final String BROKER_URL = "tcp://localhost:1883";
private static final String CLIENT_ID = "JavaClientPublisherSubscriber"; private static final String CLIENT_ID = "JavaClientPublisherSubscriber";
@ -21,8 +22,7 @@ public class MqttClientService implements MqttCallback {
private MqttClient client; private MqttClient client;
public MqttClientService(AppState appState) { public MqttClientService() {
this.appState = appState;
try { try {
client = new MqttClient( client = new MqttClient(
BROKER_URL, BROKER_URL,
@ -37,7 +37,6 @@ public class MqttClientService implements MqttCallback {
Logger.info("MQTT", "Verbinde mit Broker " + BROKER_URL); Logger.info("MQTT", "Verbinde mit Broker " + BROKER_URL);
client.connect(options); client.connect(options);
appState.setMqttConnected(true);
Logger.info("MQTT", "Verbindung hergestellt"); Logger.info("MQTT", "Verbindung hergestellt");
} catch (MqttException e) { } catch (MqttException e) {
@ -89,8 +88,6 @@ public class MqttClientService implements MqttCallback {
"MQTT", "MQTT",
"Verbindung verloren: " + cause.getMessage() "Verbindung verloren: " + cause.getMessage()
); );
appState.setMqttConnected(false);
} }
@Override @Override
@ -98,15 +95,14 @@ public class MqttClientService implements MqttCallback {
String payload = String payload =
new String(message.getPayload(), StandardCharsets.UTF_8); new String(message.getPayload(), StandardCharsets.UTF_8);
//Logger.debug( Logger.debug(
// "MQTT", "MQTT",
// "Nachricht empfangen -> Topic=" + topic + ", Payload=" + payload "Nachricht empfangen -> Topic=" + topic + ", Payload=" + payload
//); );
Consumer<String> listener = topicListeners.get(topic); Consumer<String> listener = topicListeners.get(topic);
if (listener != null) { if (listener != null) {
listener.accept(payload); listener.accept(payload);
Logger.debug("MQTT", "Payload accepted");
} else { } else {
Logger.warn( Logger.warn(
"MQTT", "MQTT",
@ -122,6 +118,4 @@ public class MqttClientService implements MqttCallback {
"Delivery complete, MessageId=" + token.getMessageId() "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.AppState;
/**
*
* @author naumueller
*/
public class StateService {
private static final AppState STATE = new AppState();
private StateService() {}
public static AppState getState() {
return STATE;
}
}

View File

@ -0,0 +1,46 @@
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package efi.projekt.virtueller_gesundheitsassistent.service;
import efi.projekt.virtueller_gesundheitsassistent.util.Logger;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
*
* @author naumueller
*/
public class StatisticsService {
private static final String DB_URL = "jdbc:sqlite:data/health.db";
public double getRatio(int lastN) {
String query = """
SELECT AVG(value)
FROM (
SELECT value
FROM binary_event
ORDER BY timestamp DESC
LIMIT ?
)
""";
try (Connection c = DriverManager.getConnection(DB_URL); PreparedStatement ps = c.prepareStatement(query)) {
ps.setInt(1, lastN);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
return rs.getDouble(1);
}
} catch (SQLException e) {
Logger.error("Evaluation", "Couldn't get ratio.", e);
}
return 0.0;
}
}

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,15 @@
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;
requires java.sql;
opens efi.projekt.virtueller_gesundheitsassistent to javafx.fxml;
exports efi.projekt.virtueller_gesundheitsassistent;
}

View File

@ -1,58 +0,0 @@
package vassistent;
import vassistent.bootstrap.ApplicationContext;
import vassistent.bootstrap.ApplicationInitializer;
import vassistent.controller.AppWindowController;
import vassistent.service.*;
import vassistent.util.Logger;
import javax.swing.*;
import java.io.File;
public class App {
public static void main(String[] args) {
ApplicationInitializer.initLookAndFeel();
ApplicationContext context =
ApplicationInitializer.initialize();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
Logger.info("SYSTEM", "Programm wird beendet");
deleteDatabase();
Logger.shutdown();
}));
SwingUtilities.invokeLater(() -> {
new AppWindowController(context)
.createAndShowWindow();
});
}
private static void deleteDatabase() {
try {
File dbFile = new File("data/health.db");
if (dbFile.exists()) {
boolean deleted = dbFile.delete();
if (deleted) {
Logger.info("SYSTEM", "Datenbank gelöscht");
} else {
Logger.warn("SYSTEM", "Datenbank konnte nicht gelöscht werden");
}
}
} catch (Exception e) {
Logger.error("SYSTEM", "DB Löschung fehlgeschlagen", e);
}
}
}

View File

@ -1,81 +0,0 @@
package vassistent.bootstrap;
import vassistent.model.AppState;
import vassistent.service.*;
public class ApplicationContext {
private AppState appState;
private DataPersistenceService persistenceService;
private StatisticsService statisticsService;
private EvaluationService evaluationService;
private AnimationFileService unrealService;
private MqttClientService mqttService;
private BinaryEventService binaryEventService;
private ProcessManagerService processManagerService;
public void setAppState(AppState appState) {
this.appState = appState;
}
public void setPersistenceService(DataPersistenceService persistenceService) {
this.persistenceService = persistenceService;
}
public void setStatisticsService(StatisticsService statisticsService) {
this.statisticsService = statisticsService;
}
public void setEvaluationService(EvaluationService evaluationService) {
this.evaluationService = evaluationService;
}
public void setAnimationFileService(AnimationFileService unrealService) {
this.unrealService = unrealService;
}
public void setMqttService(MqttClientService mqttService) {
this.mqttService = mqttService;
}
public void setBinaryEventService(BinaryEventService binaryEventService) {
this.binaryEventService = binaryEventService;
}
public AppState getAppState() {
return appState;
}
public DataPersistenceService getPersistenceService() {
return persistenceService;
}
public StatisticsService getStatisticsService() {
return statisticsService;
}
public EvaluationService getEvaluationService() {
return evaluationService;
}
public AnimationFileService getAnimationFileService() {
return unrealService;
}
public MqttClientService getMqttService() {
return mqttService;
}
public BinaryEventService getBinaryEventService() {
return binaryEventService;
}
public ProcessManagerService getProcessManagerService() {
return processManagerService;
}
public void setProcessManagerService(ProcessManagerService processManagerService) {
this.processManagerService = processManagerService;
}
}

View File

@ -1,72 +0,0 @@
package vassistent.bootstrap;
import com.formdev.flatlaf.FlatDarkLaf;
import vassistent.model.AppState;
import vassistent.service.*;
import vassistent.util.ConfigLoader;
import javax.swing.*;
import java.util.Properties;
public class ApplicationInitializer {
private static Properties config =
ConfigLoader.loadProperties(
"config/application.properties"
);
public static ApplicationContext initialize() {
ApplicationContext context = new ApplicationContext();
// ===== Model State =====
context.setAppState(new AppState());
// ===== Infrastructure Services =====
context.setAnimationFileService(
new AnimationFileService()
);
context.setMqttService(new MqttClientService(context.getAppState()));
context.setPersistenceService(new DataPersistenceService());
context.setStatisticsService(
new StatisticsService(context.getPersistenceService())
);
context.setEvaluationService(
new EvaluationService(
context.getStatisticsService(),
context.getAppState(),
context.getAnimationFileService()
)
);
context.setBinaryEventService(
new BinaryEventService(
context.getPersistenceService(),
context.getEvaluationService()
)
);
context.getMqttService().subscribe(
config.getProperty("mqtt.topic"),
payload -> context.getBinaryEventService()
.handlePayload(payload)
);
context.setProcessManagerService(new ProcessManagerService(config));
context.getProcessManagerService().startProcesses();
return context;
}
public static void initLookAndFeel() {
try {
UIManager.setLookAndFeel(new FlatDarkLaf());
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@ -1,58 +0,0 @@
package vassistent.bootstrap;
import vassistent.service.MqttClientService;
import vassistent.service.ProcessManagerService;
import vassistent.util.Logger;
public class ApplicationShutdownManager {
private final ApplicationContext context;
public ApplicationShutdownManager(ApplicationContext context) {
this.context = context;
Runtime.getRuntime().addShutdownHook(
new Thread(this::shutdown)
);
}
public void shutdown() {
Logger.info("SHUTDOWN", "Shutdown Sequencing gestartet");
disconnectMqtt();
stopExternalProcesses();
}
private void disconnectMqtt() {
try {
MqttClientService mqtt = context.getMqttService();
if (mqtt != null) {
mqtt.disconnect();
}
Logger.info("SHUTDOWN", "MQTT getrennt");
} catch (Exception e) {
Logger.error("SHUTDOWN", "MQTT Shutdown Fehler", e);
}
}
private void stopExternalProcesses() {
try {
ProcessManagerService pm = context.getProcessManagerService();
if (pm != null) {
pm.shutdown();
}
Logger.info("SHUTDOWN", "Externe Prozesse beendet");
} catch (Exception e) {
Logger.error("SHUWTDOWN", "Process Shutdown Fehler", e);
}
}
}

View File

@ -1,110 +0,0 @@
package vassistent.controller;
import vassistent.bootstrap.ApplicationContext;
import vassistent.bootstrap.ApplicationShutdownManager;
import vassistent.model.AppState;
import vassistent.ui.AppWindow;
import vassistent.ui.DashboardView;
import vassistent.ui.PixelStreamingView;
import vassistent.util.Logger;
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
public class AppWindowController {
private final ApplicationContext context;
private final ApplicationShutdownManager shutdownManager;
private AppWindow window;
public AppWindowController(ApplicationContext context) {
this.context = context;
this.shutdownManager = new ApplicationShutdownManager(context);
subscribeAppState();
}
public void createAndShowWindow() {
window = new AppWindow();
DashboardView dashboardView = window.getDashboardView();
PixelStreamingView pixelStreamingView = window.getStreamingView();
DashboardController dashboardController =
new DashboardController(
context.getStatisticsService(),
dashboardView,
context.getAppState()
);
StreamingController previewController = new StreamingController(pixelStreamingView);
dashboardView.getReloadPixelStreamingViewButton()
.addActionListener(e ->
previewController.reloadStream("http://141.75.215.233")
);
dashboardView.getOpenFullscreenButton()
.addActionListener(e ->
window.getStreamingView().requestFocus());
window.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
shutdown();
}
});
window.setVisible(true);
}
private void shutdown() {
shutdownManager.shutdown();
if (window != null)
window.dispose();
System.exit(0);
}
private void subscribeAppState() {
Logger.info("Controller",
"Subscribe AppState Observer gestartet");
AppState state = context.getAppState();
state.addListener(this::onStateChanged);
Logger.info("Controller",
"AppState Observer registriert");
}
private void onStateChanged(AppState appState) {
if (window == null) {
Logger.warn("Controller",
"Window ist null, UI Update übersprungen");
return;
}
SwingUtilities.invokeLater(() -> {
window.updateProblemLevel(
appState.getProblemLevel().name()
);
window.updateMqttStatus(
appState.isMqttConnected()
);
Logger.debug("Controller",
"ProblemLevel UI aktualisiert → "
+ appState.getProblemLevel());
});
}
}

View File

@ -1,51 +0,0 @@
package vassistent.controller;
import vassistent.model.AppState;
import vassistent.model.RatioPoint;
import vassistent.service.StatisticsService;
import vassistent.ui.DashboardView;
import javax.swing.*;
import java.util.List;
public class DashboardController {
private final StatisticsService statisticsService;
private final DashboardView dashboardView;
private long lastDataVersion = -1;
public DashboardController(
StatisticsService statisticsService,
DashboardView dashboardView,
AppState appState
) {
this.statisticsService = statisticsService;
this.dashboardView = dashboardView;
appState.addListener(this::onStateChanged);
}
private void onStateChanged(AppState state) {
List<RatioPoint> points =
statisticsService.getLastNAverages(20);
if (points.isEmpty())
return;
double ratio = points.get(points.size() - 1).getRatio();
SwingUtilities.invokeLater(() -> {
dashboardView.updateChart(points);
dashboardView.updateProblemLevel(ratio);
});
}
public void loadChartData() {
List<RatioPoint> points = statisticsService.getLastNAverages(20);
dashboardView.updateChart(points);
}
}

View File

@ -1,20 +0,0 @@
package vassistent.controller;
import vassistent.ui.PixelStreamingView;
public class StreamingController {
private final PixelStreamingView view;
public StreamingController(PixelStreamingView view) {
this.view = view;
}
public void reloadStream(String url) {
view.loadStream(url);
}
public void toggleFullscreen() {
view.requestFocus();
}
}

View File

@ -1,44 +0,0 @@
package vassistent.model;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
public class AppState {
private long dataVersion = 0;
private boolean mqttConnected;
private ProblemLevel problemLevel = ProblemLevel.NONE;
private final List<Consumer<AppState>> listeners =
new ArrayList<>();
public void addListener(Consumer<AppState> listener) {
listeners.add(listener);
}
private void notifyListeners() {
listeners.forEach(l -> l.accept(this));
}
public ProblemLevel getProblemLevel() {
return problemLevel;
}
public void setProblemLevel(ProblemLevel problemLevel) {
this.problemLevel = problemLevel;
dataVersion++;
notifyListeners();
}
public boolean isMqttConnected() {
return mqttConnected;
}
public void setMqttConnected(boolean mqttConnected) {
if (this.mqttConnected != mqttConnected) {
this.mqttConnected = mqttConnected;
notifyListeners();
}
}
}

View File

@ -1,23 +0,0 @@
package vassistent.model;
import java.time.LocalDateTime;
public class DatabaseEntry {
private final LocalDateTime timestamp;
private final double value;
public DatabaseEntry(LocalDateTime timestamp, double value) {
this.timestamp = timestamp;
this.value = value;
}
public LocalDateTime getTimestamp() {
return timestamp;
}
public double getValue() {
return value;
}
}

View File

@ -1,8 +0,0 @@
package vassistent.model;
public enum ProblemLevel {
NONE,
WARNING,
HIGH,
DISASTER
}

View File

@ -1,23 +0,0 @@
package vassistent.model;
import java.time.LocalDateTime;
public class RatioPoint {
private final LocalDateTime timestamp;
private final double ratio;
public RatioPoint(LocalDateTime timestamp, double ratio) {
this.timestamp = timestamp;
this.ratio = ratio;
}
public LocalDateTime getTimestamp() {
return timestamp;
}
public double getRatio() {
return ratio;
}
}

View File

@ -1,50 +0,0 @@
package vassistent.service;
import vassistent.model.ProblemLevel;
import vassistent.util.Logger;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URI;
import java.util.concurrent.atomic.AtomicBoolean;
import jakarta.websocket.*;
public class AnimationFileService {
private static final String PATH = "C:\\Privat\\Dokumente\\Niklas_Aumueller\\TH\\MSY\\Semester_2\\animation.json";
public void wirteAnimationState(ProblemLevel level) {
String animation = mapLevelToAnimation(level);
File file = new File(PATH);
try {
String json = """
{
"animation": "%s"
}
""".formatted(animation);
try (FileWriter writer = new FileWriter(file, false)) {
writer.write(json);
}
Logger.info("ANIMATION FILE", "Animation json geschrieben");
} catch (IOException e) {
Logger.error("ANIMATION FILE", "Fehler beim Schreiben der Animation Datei", e);
}
}
private String mapLevelToAnimation(ProblemLevel level) {
return switch (level) {
case NONE -> "no";
case WARNING -> "warning";
case HIGH -> "high";
case DISASTER -> "disaster";
};
}
}

View File

@ -1,62 +0,0 @@
package vassistent.service;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import vassistent.util.Logger;
public class BinaryEventService {
private final DataPersistenceService persistenceService;
private final EvaluationService evaluationService;
private Integer lastId = null;
public BinaryEventService(
DataPersistenceService persistenceService,
EvaluationService evaluationService
) {
this.persistenceService = persistenceService;
this.evaluationService = evaluationService;
}
public synchronized void handlePayload(String payload) {
try {
JsonObject json = JsonParser.parseString(payload).getAsJsonObject();
if (!json.has("valid") || !json.get("valid").getAsBoolean()) {
Logger.warn("EVENT", "Payload ist nicht valid");
return;
}
if (!json.has("_id") || !json.has("prediction")) {
Logger.warn("EVENT", "Payload unvollständig");
return;
}
int id = json.get("_id").getAsInt();
if (lastId != null && lastId == id) {
Logger.warn("EVENT", "Payload ID bereits verarbeitet: " + id);
return;
}
lastId = id;
int prediction = json.get("prediction").getAsInt();
if (prediction != 0 && prediction != 1) {
Logger.warn("EVENT", "Ungültige Prediction: " + prediction);
return;
}
persistenceService.store(prediction);
evaluationService.evaluate();
//Logger.debug("EVENT", "Prediction verarbeitet: " + prediction);
} catch (Exception e) {
Logger.error("EVENT", "Payload Verarbeitung fehlgeschlagen: " + payload, e);
}
}
}

View File

@ -1,115 +0,0 @@
package vassistent.service;
import vassistent.model.DatabaseEntry;
import vassistent.util.Logger;
import java.io.File;
import java.sql.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class DataPersistenceService {
private static final String DB_FOLDER = "data";
private final String DB_URL;
public DataPersistenceService() {
DB_URL = "jdbc:sqlite:data/health.db";
createDatabaseFolder();
init();
}
// Für Tests
public DataPersistenceService(String DB_URL) {
this.DB_URL = DB_URL;
createDatabaseFolder();
init();
}
public void createDatabaseFolder() {
File folder = new File(DB_FOLDER);
if (!folder.exists()) {
boolean created = folder.mkdirs();
if (created) {
Logger.info("DB", "Datenbank-Ordner erstellt");
} else {
Logger.warn("DB", "Konnte DB-Ordner nicht erstellen");
}
}
}
private void init() {
try (
Connection c = DriverManager.getConnection(DB_URL); Statement s = c.createStatement()) {
s.execute("""
CREATE TABLE IF NOT EXISTS binary_event (
id INTEGER PRIMARY KEY AUTOINCREMENT,
value INTEGER NOT NULL,
timestamp DATETIME NOT NULL
)
""");
Logger.info("DB", "Datenbank initialisiert");
} catch (SQLException e) {
Logger.error("DB", "Datenbank initialisierung fehlgeschlagen", e);
}
}
public void store(int value) {
try (
Connection c = DriverManager.getConnection(DB_URL); PreparedStatement ps = c.prepareStatement(
"INSERT INTO binary_event (value, timestamp) VALUES (?, ?)"
)) {
ps.setInt(1, value);
ps.setString(2, LocalDateTime.now().toString());
ps.executeUpdate();
//Logger.debug("DB", "Database Insert erfolgreich: " + ps.toString());
} catch (SQLException e) {
Logger.error("DB", "Database Insert fehlgeschlagen", e);
}
}
public List<DatabaseEntry> getLastEntries(int count) {
List<DatabaseEntry> result = new ArrayList<>();
String query = """
SELECT value, timestamp
FROM binary_event
ORDER BY timestamp DESC
LIMIT ?
""";
try (Connection c = DriverManager.getConnection(DB_URL); PreparedStatement ps = c.prepareStatement(query)) {
ps.setInt(1, count);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
String ts = rs.getString("timestamp");
LocalDateTime timestamp =
LocalDateTime.parse(ts);
double value = rs.getDouble("value");
result.add(new DatabaseEntry(timestamp, value));
}
} catch (SQLException e) {
Logger.error("DB",
"Fehler beim Laden der letzten Einträge", e);
}
Collections.reverse(result);
return result;
}
public String getDbUrl() {
return DB_URL;
}
}

View File

@ -1,43 +0,0 @@
package vassistent.service;
import vassistent.model.AppState;
import vassistent.model.ProblemLevel;
public class EvaluationService {
private final StatisticsService statisticsService;
private final AppState appState;
private final AnimationFileService animationFileService;
public EvaluationService(
StatisticsService statisticsService,
AppState appState,
AnimationFileService animationFileService
) {
this.statisticsService = statisticsService;
this.appState = appState;
this.animationFileService = animationFileService;
}
public void evaluate() {
double ratio = statisticsService.getRatio(20);
ProblemLevel level = calculateLevel(ratio);
appState.setProblemLevel(level);
animationFileService.wirteAnimationState(level);
}
private ProblemLevel calculateLevel(Double ratio) {
if (ratio >= 0.9) {
return ProblemLevel.DISASTER;
} else if (ratio >= 0.8) {
return ProblemLevel.HIGH;
} else if (ratio >= 0.5) {
return ProblemLevel.WARNING;
} else {
return ProblemLevel.NONE;
}
}
}

View File

@ -1,146 +0,0 @@
package vassistent.service;
import vassistent.util.Logger;
import java.io.File;
import java.io.IOException;
import java.util.Properties;
public class ProcessManagerService {
private final Properties config;
private Process pythonProcess;
private Process unrealProcess;
private Process unrealSignallingProcess;
public ProcessManagerService(Properties config) {
this.config = config;
}
public void startProcesses() {
startPythonIfEnabled();
startUnrealIfEnabled();
}
private void startPythonIfEnabled() {
boolean enabled = Boolean.parseBoolean(
config.getProperty("mqtt_sim.enabled", "false")
);
if (!enabled) return;
try {
String script = config.getProperty("mqtt_sim.script");
String python = config.getProperty("python.path");
ProcessBuilder pb = new ProcessBuilder(python, script);
pb.redirectErrorStream(true);
pythonProcess = pb.start();
Logger.info("PROCESS", "Mqtt Simulator gestartet");
} catch (IOException e) {
Logger.error("PROCESS", "Mqtt Simulator Start fehlgeschlagen", e);
}
}
private void startUnrealIfEnabled() {
boolean enabled = Boolean.parseBoolean(config.getProperty("unreal.enabled", "false"));
if (!enabled) return;
try {
startSignallingServer();
startUnrealEngine();
} catch (IOException e) {
Logger.error("PROCESS", "Unreal Start fehlgeschlagen", e);
}
}
private void startSignallingServer() throws IOException {
String script =
config.getProperty("unreal.signalling_server.script");
if (script == null) return;
ProcessBuilder pb = new ProcessBuilder(
"cmd",
"/c",
script);
pb.redirectErrorStream(true);
unrealSignallingProcess = pb.start();
Logger.info("PROCESS",
"Unreal Signalling Server gestartet" + pb.command());
}
private void startUnrealEngine() throws IOException {
String exe =
config.getProperty("unreal.executable");
ProcessBuilder pb = new ProcessBuilder(
exe,
"-PixelStreamingURL=ws://127.0.0.1:8888",
"-RenderOffscreen",
"-NoSound"
);
pb.directory(new File(exe).getParentFile());
pb.redirectErrorStream(true);
unrealProcess = pb.start();
Logger.info("PROCESS",
"Unreal Engine gestartet" + pb.command());
}
public void shutdown() {
Logger.info("PROCESS", "Shutdown externe Prozesse gestartet");
terminateProcess(pythonProcess);
terminateProcess(unrealProcess);
terminateProcess(unrealSignallingProcess);
Logger.info("PROCESS", "Externe Prozesse beendet");
}
private void terminateProcess(Process process) {
if (process == null)
return;
try {
long pid = process.pid();
ProcessBuilder pb = new ProcessBuilder(
"taskkill",
"/PID",
String.valueOf(pid),
"/T",
"/F"
);
pb.start().waitFor();
Logger.info("PROCESS",
"Process Tree beendet → PID " + pid);
} catch (Exception e) {
Logger.error("PROCESS",
"Fehler beim Prozess Kill", e);
}
}
}

View File

@ -1,69 +0,0 @@
package vassistent.service;
import vassistent.model.DatabaseEntry;
import vassistent.model.RatioPoint;
import vassistent.util.Logger;
import java.sql.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
public class StatisticsService {
private final DataPersistenceService persistenceService;
public StatisticsService(DataPersistenceService persistenceService) {
this.persistenceService = persistenceService;
}
public double getRatio(int lastN) {
String query = """
SELECT AVG(value)
FROM (
SELECT value
FROM binary_event
ORDER BY id DESC
LIMIT ?
)
""";
try (Connection c = DriverManager.getConnection(persistenceService.getDbUrl()); PreparedStatement ps = c.prepareStatement(query)) {
ps.setInt(1, lastN);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
return rs.getDouble(1);
}
} catch (SQLException e) {
Logger.error("Evaluation", "Couldn't get ratio.", e);
}
return 0.0;
}
public List<RatioPoint> getLastNAverages(int lastN) {
List<DatabaseEntry> entries = persistenceService.getLastEntries(30);
List<RatioPoint> result = new ArrayList<>();
if (entries.isEmpty())
return result;
for (int i = 10; i < entries.size(); i++) {
double sum = 0;
for (int j = i - 10; j < i; j++) {
sum += entries.get(j).getValue();
}
double avg = sum / 10.0;
LocalDateTime timestamp = entries.get(i).getTimestamp();
result.add(new RatioPoint(timestamp, avg));
}
return result;
}
}

View File

@ -1,91 +0,0 @@
package vassistent.ui;
import vassistent.bootstrap.ApplicationContext;
import vassistent.controller.DashboardController;
import javax.swing.*;
import java.awt.*;
public class AppWindow extends JFrame {
private PixelStreamingView streamingView;
private DashboardView dashboardView;
private JTabbedPane tabs;
private JLabel mqttStatusLabel;
private JLabel problemLevelLabel;
public AppWindow() {
setTitle("Virtueller Gesundheitsassistent");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(1400, 850);
setLocationRelativeTo(null);
setLayout(new BorderLayout());
streamingView = new PixelStreamingView(
"http://localhost:80",
false,
false
);
dashboardView = new DashboardView();
tabs = createTabPane();
add(tabs, BorderLayout.CENTER);
add(createStatusBar(), BorderLayout.SOUTH);
}
// ---------------- Tabs ----------------
private JTabbedPane createTabPane() {
JTabbedPane tabs = new JTabbedPane();
JPanel streamingPanel = new JPanel(new BorderLayout());
streamingPanel.add(streamingView, BorderLayout.CENTER);
tabs.addTab("Avatar Streaming", streamingView);
tabs.addTab("Dashboard", dashboardView);
return tabs;
}
// ---------------- Status Bar ----------------
private JPanel createStatusBar() {
JPanel statusBar = new JPanel(new FlowLayout(FlowLayout.LEFT));
mqttStatusLabel = new JLabel("MQTT: Disconnected");
problemLevelLabel = new JLabel("Problem: NONE");
statusBar.add(mqttStatusLabel);
statusBar.add(new JLabel(" | "));
statusBar.add(problemLevelLabel);
return statusBar;
}
public void updateMqttStatus(boolean connected) {
mqttStatusLabel.setText("MQTT: " +
(connected ? "Connected" : "Disconnected"));
}
public void updateProblemLevel(String level) {
problemLevelLabel.setText("Problem: " + level);
}
public DashboardView getDashboardView() {
return dashboardView;
}
public PixelStreamingView getStreamingView() {
return streamingView;
}
public JTabbedPane getTabs() {
return tabs;
}
}

View File

@ -1,101 +0,0 @@
package vassistent.ui;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.data.time.Millisecond;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;
import vassistent.model.ProblemLevel;
import vassistent.model.RatioPoint;
import javax.swing.*;
import java.awt.*;
import java.sql.Timestamp;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
public class DashboardView extends JPanel {
private TimeSeries series;
private ProblemLevelBar levelBar;
private JButton reloadPixelStreamingViewButton;
private JButton openFullscreenButton;
public DashboardView() {
setLayout(new BorderLayout());
// ---------- TOP: Problem Level ----------
levelBar = new ProblemLevelBar();
add(levelBar, BorderLayout.NORTH);
// ---------- CENTER: Chart ----------
series = new TimeSeries("Ratio");
TimeSeriesCollection dataset = new TimeSeriesCollection(series);
JFreeChart chart = ChartFactory.createTimeSeriesChart(
"Ratio Verlauf",
"Zeit",
"Durchschnitt",
dataset
);
add(new ChartPanel(chart), BorderLayout.CENTER);
// ---------- SOUTH: Preview + Buttons ----------
add(createBottomPanel(), BorderLayout.SOUTH);
}
private JPanel createBottomPanel() {
JPanel panel = new JPanel(new BorderLayout());
JPanel buttonPanel = new JPanel(new FlowLayout());
reloadPixelStreamingViewButton = new JButton("Reload Preview");
openFullscreenButton = new JButton("Open Fullscreen");
buttonPanel.add(reloadPixelStreamingViewButton);
buttonPanel.add(openFullscreenButton);
panel.add(buttonPanel, BorderLayout.SOUTH);
return panel;
}
public void updateChart(List<RatioPoint> points) {
series.clear();
for (RatioPoint point : points) {
series.addOrUpdate(
new Millisecond(
Date.from(
point.getTimestamp()
.atZone(ZoneId.systemDefault())
.toInstant()
)
),
point.getRatio()
);
}
}
public void updateProblemLevel(double ratio) {
levelBar.setRatio(ratio);
}
public JButton getReloadPixelStreamingViewButton() {
return reloadPixelStreamingViewButton;
}
public JButton getOpenFullscreenButton() {
return openFullscreenButton;
}
}

View File

@ -1,55 +0,0 @@
package vassistent.ui;
import org.cef.CefApp;
import org.cef.CefClient;
import org.cef.CefSettings;
import org.cef.browser.CefBrowser;
import org.cef.handler.CefAppHandlerAdapter;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class PixelStreamingView extends JPanel {
private static final long serialVersionUID = -5570653778104813836L;
private final CefApp cefApp;
private final CefClient client;
private final CefBrowser browser;
private final Component browserUI_;
public PixelStreamingView(String startURL, boolean useOSR, boolean isTransparent) {
super(new BorderLayout());
CefApp.addAppHandler(new CefAppHandlerAdapter(null) {
@Override
public void stateHasChanged(CefApp.CefAppState state) {
if (state == CefApp.CefAppState.TERMINATED){
System.out.println("TERMINATED");
}
}
});
CefSettings settings = new CefSettings();
settings.windowless_rendering_enabled = useOSR;
cefApp = CefApp.getInstance(settings);
client = cefApp.createClient();
browser = client.createBrowser(startURL, useOSR, isTransparent);
browserUI_ = browser.getUIComponent();
add(browserUI_, BorderLayout.CENTER);
}
public void dispose() {
if (browser != null) browser.close(true);
if (client != null) client.dispose();
if (cefApp != null) cefApp.dispose();
}
public void loadStream(String url) {
browser.loadURL(url);
}
}

View File

@ -1,89 +0,0 @@
package vassistent.ui;
import vassistent.model.ProblemLevel;
import javax.swing.*;
import java.awt.*;
public class ProblemLevelBar extends JPanel {
private double ratio = 0.0;
private ProblemLevel level = ProblemLevel.NONE;
public ProblemLevelBar() {
setPreferredSize(new Dimension(100, 50));
}
public void setRatio(double ratio) {
this.ratio = Math.max(0.0, Math.min(1.0, ratio));
this.level = calculateLevel(this.ratio);
repaint();
}
private ProblemLevel calculateLevel(Double ratio) {
if (ratio >= 0.9) {
return ProblemLevel.DISASTER;
} else if (ratio >= 0.8) {
return ProblemLevel.HIGH;
} else if (ratio >= 0.5) {
return ProblemLevel.WARNING;
} else {
return ProblemLevel.NONE;
}
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
int width = getWidth();
int height = getHeight();
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
// --- Hintergrund ---
g2.setColor(Color.LIGHT_GRAY);
g2.fillRoundRect(0, 0, width, height, 20, 20);
// --- Gefüllter Bereich ---
int filledWidth = (int) (width * ratio);
Color baseColor = getColorForLevel(level);
Color darkerColor = baseColor.darker();
GradientPaint gradient = new GradientPaint(
0, 0, darkerColor,
filledWidth, 0, baseColor
);
g2.setPaint(gradient);
g2.fillRoundRect(0, 0, filledWidth, height, 20, 20);
// --- Text ---
String text = level.name() + " (" + (int)(ratio * 100) + "%)";
FontMetrics fm = g2.getFontMetrics();
int textWidth = fm.stringWidth(text);
g2.setColor(Color.BLACK);
g2.drawString(
text,
(width - textWidth) / 2,
(height + fm.getAscent()) / 2 - 3
);
}
private Color getColorForLevel(ProblemLevel level) {
switch (level) {
case DISASTER:
return new Color(200, 0, 0);
case HIGH:
return new Color(255, 140, 0);
case WARNING:
return new Color(255, 215, 0);
default:
return new Color(0, 170, 0);
}
}
}

View File

@ -1,28 +0,0 @@
package vassistent.util;
import java.io.InputStream;
import java.util.Properties;
public class ConfigLoader {
public ConfigLoader() {}
public static Properties loadProperties(String path) {
Properties props = new Properties();
try (InputStream is =
Logger.class.getClassLoader()
.getResourceAsStream(path)) {
if (is != null)
props.load(is);
} catch (Exception e) {
Logger.error("CONFIG",
"Properties Laden fehlgeschlagen", e);
}
return props;
}
}

View File

@ -1,199 +0,0 @@
package vassistent.util;
import java.io.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Properties;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class Logger {
public enum Level {
DEBUG, INFO, WARN, ERROR
}
private static Level currentLevel = Level.DEBUG;
private static boolean logToFile = true;
private static String logFilePath = "logs/application.log";
private static int maxFileSizeMB = 10;
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final BlockingQueue<String> logQueue =
new LinkedBlockingQueue<>();
static {
loadConfig();
startLogWriterThread();
}
private Logger() {}
// ================= CONFIG =================
private static void loadConfig() {
try (InputStream is = Logger.class.getClassLoader()
.getResourceAsStream("config/logger.properties")) {
if (is == null) return;
Properties props = new Properties();
props.load(is);
currentLevel =
Level.valueOf(
props.getProperty("logger.level", "DEBUG"));
logToFile =
Boolean.parseBoolean(
props.getProperty("logger.file.enabled", "true"));
logFilePath =
props.getProperty("logger.file", "logs/application.log");
maxFileSizeMB =
Integer.parseInt(
props.getProperty("logger.max.size.mb", "10"));
} catch (Exception e) {
System.err.println("Logger Config Fehler: " + e.getMessage());
}
}
// ================= API =================
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 t) {
log(Level.ERROR, source, message, t);
}
// ================= CORE =================
public static void log(Level level, String source, String message, Throwable throwable) {
if (level.ordinal() < currentLevel.ordinal()) return;
String timestamp =
LocalDateTime.now().format(FORMATTER);
StringBuilder sb = new StringBuilder();
sb.append(String.format("[%s] [%s] [%s] %s",
timestamp, level, source, message));
if (throwable != null) {
StringWriter sw = new StringWriter();
throwable.printStackTrace(new PrintWriter(sw));
sb.append("\n").append(sw);
}
logQueue.offer(sb.toString());
}
// ================= BACKGROUND WRITER =================
private static void startLogWriterThread() {
Thread writerThread = new Thread(() -> {
while (true) {
try {
String logEntry = logQueue.take();
if (logToFile) {
rotateIfNeeded();
File logFile = new File(logFilePath);
logFile.getParentFile().mkdirs();
try (FileWriter fw = new FileWriter(logFile, true)) {
fw.write(logEntry + "\n");
}
}
System.out.println(logEntry);
} catch (Exception e) {
System.err.println("Logger Writer Fehler: " + e.getMessage());
}
}
});
writerThread.setDaemon(true);
writerThread.start();
}
// ================= ROTATION =================
private static void rotateIfNeeded() {
try {
File file = new File(logFilePath);
if (!file.exists()) return;
long maxBytes =
maxFileSizeMB * 1024L * 1024L;
if (file.length() > maxBytes) {
String archiveName =
logFilePath.replace(".log",
"_" + System.currentTimeMillis() + ".log");
File archive = new File(archiveName);
boolean renamed = file.renameTo(archive);
if (renamed) {
System.out.println("Logger Rotation: " + archiveName);
}
}
} catch (Exception e) {
System.err.println("Logger Rotation Fehler: " + e.getMessage());
}
}
public static void shutdown() {
System.out.println("Logger Shutdown gestartet");
try {
Thread.sleep(500); // Warte auf Queue Flush
while (!logQueue.isEmpty()) {
String log = logQueue.poll();
if (log != null) {
System.out.println(log);
}
}
} catch (Exception e) {
System.err.println("Logger Shutdown Fehler: " + e.getMessage());
}
}
}

View File

@ -1,17 +0,0 @@
# ===== MODE =====
app.mode=test
# ===== PYTHON =====
python.path=C:\\Program Files\\PyManager\\python.exe
# ===== MQTT CLIENT =====
mqtt.topic=PREDICTION
# ===== MQTT SIMULATOR =====
mqtt_sim.enabled=true
mqtt_sim.script=src/main/resources/scripts/mqtt_simulator.py
# ===== UNREAL ENGINE =====
unreal.enabled=true
unreal.executable=C:\\Privat\\Dokumente\\Niklas_Aumueller\\TH\\MSY\\Semester_2\\Projektarbeit\\Windows\\Prototyp1.exe
unreal.signalling_server.script=C:\\Privat\\Dokumente\\Niklas_Aumueller\\TH\\MSY\\Semester_2\\Projektarbeit\\Windows\\Prototyp1\\Samples\\PixelStreaming\\WebServers\\SignallingWebServer\\platform_scripts\\cmd\\start_with_stun.bat

View File

@ -1,4 +0,0 @@
logger.level=DEBUG
logger.file.enabled=true
logger.file=logs/application.log
logger.max.size.mb=10

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

@ -1,85 +0,0 @@
import json
import paho.mqtt.client as mqtt
import sys
import random
import time
import logging
# ===== KONFIGURATION =====
BROKER = "127.0.0.1"
PORT = 1883
TOPIC = "PREDICTION"
USERNAME = None
PASSWORD = None
QOS = 0
INTERVAL_SECONDS = 10
# ==========================
# Logging konfigurieren (Console + Datei)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
handlers=[
logging.FileHandler("logs/mqtt_simulator.log"),
logging.StreamHandler(sys.stdout)
]
)
def on_connect(client, userdata, flags, rc):
if rc == 0:
logging.info("Erfolgreich mit Broker verbunden")
else:
logging.error(f"Verbindung fehlgeschlagen mit Code {rc}")
def main():
logging.info("Python script gestartet")
client = mqtt.Client()
if USERNAME and PASSWORD:
client.username_pw_set(USERNAME, PASSWORD)
client.on_connect = on_connect
try:
logging.info(f"Verbinde mit Broker {BROKER}:{PORT}")
client.connect(BROKER, PORT, 60)
client.loop_start()
logging.info("Starte kontinuierliches Senden...")
current_id = 1
while True:
payload = {
"valid": True,
"_id": current_id,
"prediction": random.randint(0, 1)
}
json_payload = json.dumps(payload)
client.publish(TOPIC, json_payload, qos=QOS)
logging.info(f"Gesendet an '{TOPIC}': {json_payload}")
current_id += 1
time.sleep(INTERVAL_SECONDS)
except KeyboardInterrupt:
logging.info("Publisher manuell beendet")
client.loop_stop()
client.disconnect()
sys.exit(0)
except Exception as e:
logging.exception("Unerwarteter Fehler")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -1,49 +0,0 @@
package vassistent.service;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
class BinaryEventServiceTest {
private DataPersistenceService persistenceService;
private EvaluationService evaluationService;
private BinaryEventService binaryEventService;
@BeforeEach
void setUp() {
persistenceService = mock(DataPersistenceService.class);
evaluationService = mock(EvaluationService.class);
binaryEventService = new BinaryEventService(persistenceService, evaluationService);
}
@AfterEach
void tearDown() {
persistenceService = null;
evaluationService = null;
binaryEventService = null;
}
@Test
void handlePayloadValid() {
binaryEventService.handlePayload("1");
verify(persistenceService).store(anyInt());
verify(evaluationService).evaluate();
}
@Test
void handlePayloadInvalidValue() {
binaryEventService.handlePayload("5");
verify(persistenceService, never()).store(anyInt());
verify(evaluationService, never()).evaluate();
}
@Test
void handlePayloadNonNumeric() {
binaryEventService.handlePayload("abc");
verify(persistenceService, never()).store(anyInt());
verify(evaluationService, never()).evaluate();
}
}

View File

@ -1,66 +0,0 @@
package vassistent.service;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import static org.junit.jupiter.api.Assertions.*;
class DataPersistenceServiceTest {
private DataPersistenceService service;
private static final String TEST_DB = "jdbc:sqlite:data/test_health.db";
private static final String DB_FILE = "data/test_health.db";
@BeforeEach
void setup() {
service = new DataPersistenceService(TEST_DB);
}
@AfterEach
void cleanup() {
deleteTestDatabase();
}
private void deleteTestDatabase() {
File db = new File(DB_FILE);
if (db.exists()) {
assertTrue(db.delete(), "Testdatenbank konnte nicht gelöscht werden");
}
}
@org.junit.jupiter.api.Test
void createDatabaseFolder() {
File folder = new File("data");
assertTrue(folder.exists(), "Datenbankordner sollte existieren");
}
@org.junit.jupiter.api.Test
void store() throws Exception {
service.store(1);
try (Connection c = DriverManager.getConnection(TEST_DB);
Statement s = c.createStatement();
ResultSet rs = s.executeQuery(
"SELECT COUNT(*) FROM binary_event")) {
assertTrue(rs.next());
int count = rs.getInt(1);
assertTrue(count > 0,
"Es sollte mindestens ein Eintrag existieren");
}
}
@org.junit.jupiter.api.Test
void getDbUrl() {
assertEquals(TEST_DB, service.getDbUrl());
}
}

View File

@ -1,49 +0,0 @@
package vassistent.service;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import vassistent.model.AppState;
import vassistent.model.ProblemLevel;
import static org.mockito.Mockito.*;
class EvaluationServiceTest {
private StatisticsService statisticsService;
private AppState appState;
private AnimationFileService unrealService;
private EvaluationService evaluationService;
@BeforeEach
void setUp() {
statisticsService = mock(StatisticsService.class);
appState = new AppState();
unrealService = mock(AnimationFileService.class);
evaluationService = new EvaluationService(statisticsService, appState, unrealService);
}
@org.junit.jupiter.api.Test
void tearDown() {
statisticsService = null;
appState = null;
unrealService = null;
evaluationService = null;
}
@org.junit.jupiter.api.Test
void testEvaluateDisasterLevel() {
when(statisticsService.getRatio(anyInt())).thenReturn(0.95);
evaluationService.evaluate();
Assertions.assertEquals(ProblemLevel.DISASTER, appState.getProblemLevel());
verify(unrealService).speak("DISASTER");
}
@Test
void testEvaluateNoChange() {
appState.setProblemLevel(ProblemLevel.NONE);
when(statisticsService.getRatio(anyInt())).thenReturn(0.1);
evaluationService.evaluate();
verify(unrealService, never()).speak(anyString());
}
}

View File

@ -1,74 +0,0 @@
package vassistent.service;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.sql.SQLException;
import static org.junit.jupiter.api.Assertions.*;
class StatisticsServiceTest {
private String DB_URL;
private DataPersistenceService persistenceService;
private StatisticsService statisticsService;
private File tempDbFile;
@BeforeEach
void setUp() {
try {
tempDbFile = File.createTempFile("testdb", ".db");
} catch (Exception e) {
throw new RuntimeException("Kann temporäre DB-Datei nicht erstellen", e);
}
DB_URL = "jdbc:sqlite:" + tempDbFile.getAbsolutePath();
persistenceService = new DataPersistenceService(DB_URL);
statisticsService = new StatisticsService(persistenceService);
}
@AfterEach
void tearDown() {
if (tempDbFile != null && tempDbFile.exists()) {
tempDbFile.delete();
}
}
@Test
void testGetRatioNoData() {
double ratio = statisticsService.getRatio(10);
assertEquals(0.0, ratio, "Ratio sollte 0 sein, wenn keine Daten vorhanden");
}
@Test
void testGetRatioSingleValue() throws SQLException {
persistenceService.store(1);
double ratio = statisticsService.getRatio(1);
assertEquals(1.0, ratio, "Ratio sollte 1 sein, wenn nur 1 gespeichert wurde");
}
@Test
void testGetRatioMultipleValues() {
persistenceService.store(1);
persistenceService.store(0);
persistenceService.store(1);
double ratio = statisticsService.getRatio(10);
assertEquals((1 + 0 + 1) / 3.0, ratio, 0.0001, "Ratio sollte Durchschnitt der letzten Werte sein");
}
@Test
void testGetRatioLimitLastN() {
persistenceService.store(1);
persistenceService.store(1);
persistenceService.store(0);
persistenceService.store(0);
persistenceService.store(1);
double ratio = statisticsService.getRatio(3);
assertEquals((1 + 0 + 0) / 3.0, ratio, 0.0001, "Ratio sollte Durchschnitt der letzten Werte sein");
}
}