JavaDoc comments added

This commit is contained in:
Niklas Aumueller 2026-03-05 15:58:01 +01:00
parent 31b81cde1b
commit 3cf2cf9288
29 changed files with 864 additions and 21 deletions

148
readme.md Normal file
View File

@ -0,0 +1,148 @@
# Virtueller Gesundheitsassistent
Java desktop application that receives binary health predictions via MQTT, stores them in SQLite, computes rolling risk ratios, and visualizes the result in a dashboard with an avatar streaming view.
## Features
- Swing desktop UI with two tabs:
- `Avatar Streaming` (embedded JCEF browser, default `http://localhost`)
- `Dashboard` (time series chart + problem level bar)
- MQTT client subscriber (`tcp://localhost:1883`) for prediction events
- SQLite persistence (`data/health.db`)
- Rolling ratio evaluation and automatic risk level classification:
- `NONE` for `< 0.5`
- `WARNING` for `>= 0.5`
- `HIGH` for `>= 0.8`
- `DISASTER` for `>= 0.9`
- Writes animation state JSON for Unreal integration
- Optional external process startup:
- Python MQTT simulator
- Unreal/Pixel Streaming scripts
- Custom async logger with file output + rotation
## Tech Stack
- Java 17
- Maven
- Swing + FlatLaf
- JFreeChart
- Eclipse Paho MQTT
- SQLite JDBC
- JCEF (`jcefmaven`)
- JUnit 5 + Mockito (tests)
## Runtime Flow
1. App initializes look and feel and services.
2. MQTT subscriber listens on topic from config (`mqtt.topic`).
3. Incoming JSON payloads are validated in `BinaryEventService`.
4. `prediction` (`0`/`1`) is stored in SQLite.
5. `EvaluationService` computes ratio over last 20 values.
6. `AppState` updates notify UI controllers.
7. Dashboard chart and status bar refresh.
8. Animation state file is written for Unreal consumption.
## Payload Format
Incoming MQTT payload must be JSON like:
```json
{
"valid": true,
"_id": 123,
"prediction": 0
}
```
## Prerequisites
- JDK 17
- Maven 3.9+
- Local MQTT broker on `localhost:1883` (or adjust code/config)
- Windows environment if using bundled process scripts/paths
- Optional: Python (if MQTT simulator should be auto-started)
- Optional: Unreal + Pixel Streaming setup (path-based integration)
## Configuration
Main config: `src/main/resources/config/application.properties`
- `app.mode`: current mode flag (`test`)
- `python.path`: Python executable for simulator startup
- `mqtt.topic`: subscribed topic (default `PREDICTION`)
- `mqtt_sim.enabled`: start simulator process on app startup
- `mqtt_sim.script`: simulator script path
- `unreal.enabled`: start Unreal-related processes
- `unreal.executable`: PowerShell script path for Unreal start
- `unreal.signalling_server.script`: signalling server batch path
Logger config: `src/main/resources/config/logger.properties`
- `logger.level`
- `logger.file.enabled`
- `logger.file`
- `logger.max.size.mb`
## Run
### 1) Start MQTT Broker
Start a broker on `localhost:1883` (for example Mosquitto).
### 2) Start the App
```powershell
mvn clean compile
mvn org.codehaus.mojo:exec-maven-plugin:3.5.0:java -Dexec.mainClass=vassistent.App
```
Or run `vassistent.App` directly from IntelliJ.
### 3) Send Test Data
Option A: enable simulator in `application.properties`:
- set `mqtt_sim.enabled=true`
- verify `python.path` and `mqtt_sim.script`
Option B: publish messages manually to topic `PREDICTION`.
## Tests
Run:
```powershell
mvn test
```
Current state in this repository on Java 17:
- `DataPersistenceServiceTest` and `StatisticsServiceTest` execute.
- Mockito-based tests fail because dependency is `mockito-all:2.0.2-beta` (legacy CGLIB + Java module access issue on modern JDKs).
## Project Structure
```text
src/main/java/vassistent
App.java
bootstrap/ # wiring, context, shutdown sequencing
controller/ # Swing controllers
model/ # state + data types
service/ # MQTT, DB, stats, evaluation, processes
ui/ # App window, dashboard, streaming, widgets
util/ # config + logger
src/main/resources
config/application.properties
config/logger.properties
scripts/mqtt_simulator.py
scripts/start_avatar.ps1
```
## Important Notes
- `AnimationFileService` currently writes to a hardcoded absolute path:
- `C:\Users\Student\Documents\Dannick\Prototyp1\Saved\animation.json`
- Unreal process handling also uses hardcoded PID/script paths.
- On app shutdown, `data/health.db` is deleted by `App.deleteDatabase()`.
- The signalling server process startup in `ProcessManagerService` is prepared but currently not launched (`pb.start()` commented).

View File

@ -10,8 +10,16 @@ import javax.swing.*;
import java.io.File;
/**
* Entry point for the virtual health assistant desktop application.
*/
public class App {
/**
* Starts the desktop application, initializes dependencies, and opens the main window.
*
* @param args command-line arguments (currently unused)
*/
public static void main(String[] args) {
ApplicationInitializer.initLookAndFeel();
@ -34,6 +42,9 @@ public class App {
});
}
/**
* Deletes the runtime SQLite database file if it exists.
*/
private static void deleteDatabase() {
try {
@ -55,4 +66,4 @@ public class App {
Logger.error("SYSTEM", "DB Löschung fehlgeschlagen", e);
}
}
}
}

View File

@ -3,6 +3,9 @@ package vassistent.bootstrap;
import vassistent.model.AppState;
import vassistent.service.*;
/**
* Lightweight container that stores shared application state and services.
*/
public class ApplicationContext {
private AppState appState;
@ -15,66 +18,146 @@ public class ApplicationContext {
private BinaryEventService binaryEventService;
private ProcessManagerService processManagerService;
/**
* Stores the shared in-memory application state instance.
*
* @param appState state container used by UI and services
*/
public void setAppState(AppState appState) {
this.appState = appState;
}
/**
* Stores the persistence service responsible for database access.
*
* @param persistenceService configured persistence service
*/
public void setPersistenceService(DataPersistenceService persistenceService) {
this.persistenceService = persistenceService;
}
/**
* Stores the statistics service used for ratio calculations.
*
* @param statisticsService configured statistics service
*/
public void setStatisticsService(StatisticsService statisticsService) {
this.statisticsService = statisticsService;
}
/**
* Stores the evaluation service used to derive problem levels.
*
* @param evaluationService configured evaluation service
*/
public void setEvaluationService(EvaluationService evaluationService) {
this.evaluationService = evaluationService;
}
/**
* Stores the animation integration service.
*
* @param unrealService configured animation file service
*/
public void setAnimationFileService(AnimationFileService unrealService) {
this.unrealService = unrealService;
}
/**
* Stores the MQTT client service.
*
* @param mqttService configured MQTT service
*/
public void setMqttService(MqttClientService mqttService) {
this.mqttService = mqttService;
}
/**
* Stores the binary event service handling incoming predictions.
*
* @param binaryEventService configured event service
*/
public void setBinaryEventService(BinaryEventService binaryEventService) {
this.binaryEventService = binaryEventService;
}
/**
* Returns the shared observable application state.
*
* @return application state
*/
public AppState getAppState() {
return appState;
}
/**
* Returns the persistence service instance.
*
* @return persistence service
*/
public DataPersistenceService getPersistenceService() {
return persistenceService;
}
/**
* Returns the statistics service instance.
*
* @return statistics service
*/
public StatisticsService getStatisticsService() {
return statisticsService;
}
/**
* Returns the evaluation service instance.
*
* @return evaluation service
*/
public EvaluationService getEvaluationService() {
return evaluationService;
}
/**
* Returns the animation file service instance.
*
* @return animation file service
*/
public AnimationFileService getAnimationFileService() {
return unrealService;
}
/**
* Returns the MQTT service instance.
*
* @return MQTT service
*/
public MqttClientService getMqttService() {
return mqttService;
}
/**
* Returns the binary event service instance.
*
* @return binary event service
*/
public BinaryEventService getBinaryEventService() {
return binaryEventService;
}
/**
* Returns the external process manager service.
*
* @return process manager service
*/
public ProcessManagerService getProcessManagerService() {
return processManagerService;
}
/**
* Stores the process manager service used for external helper processes.
*
* @param processManagerService configured process manager service
*/
public void setProcessManagerService(ProcessManagerService processManagerService) {
this.processManagerService = processManagerService;
}

View File

@ -8,6 +8,9 @@ import vassistent.util.ConfigLoader;
import javax.swing.*;
import java.util.Properties;
/**
* Bootstraps application services, subscriptions, and UI look-and-feel.
*/
public class ApplicationInitializer {
private static Properties config =
@ -15,6 +18,11 @@ public class ApplicationInitializer {
"config/application.properties"
);
/**
* Builds and wires the full application context including subscriptions and process startup.
*
* @return fully initialized application context
*/
public static ApplicationContext initialize() {
ApplicationContext context = new ApplicationContext();
@ -62,6 +70,9 @@ public class ApplicationInitializer {
return context;
}
/**
* Applies the FlatLaf look-and-feel and selected UI defaults.
*/
public static void initLookAndFeel() {
try {
UIManager.setLookAndFeel(new FlatDarkLaf());

View File

@ -4,10 +4,18 @@ import vassistent.service.MqttClientService;
import vassistent.service.ProcessManagerService;
import vassistent.util.Logger;
/**
* Coordinates graceful shutdown of MQTT and managed external processes.
*/
public class ApplicationShutdownManager {
private final ApplicationContext context;
/**
* Creates a shutdown manager and registers a JVM shutdown hook.
*
* @param context initialized application context
*/
public ApplicationShutdownManager(ApplicationContext context) {
this.context = context;
@ -16,6 +24,9 @@ public class ApplicationShutdownManager {
);
}
/**
* Executes shutdown steps for managed integrations.
*/
public void shutdown() {
Logger.info("SHUTDOWN", "Shutdown Sequencing gestartet");
@ -24,6 +35,9 @@ public class ApplicationShutdownManager {
stopExternalProcesses();
}
/**
* Disconnects from MQTT if a client service is available.
*/
private void disconnectMqtt() {
try {
@ -39,6 +53,9 @@ public class ApplicationShutdownManager {
}
}
/**
* Stops all external processes that were launched by the process manager.
*/
private void stopExternalProcesses() {
try {

View File

@ -13,6 +13,9 @@ import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
/**
* Controls the main window lifecycle and synchronizes UI elements with app state.
*/
public class AppWindowController {
private final ApplicationContext context;
@ -20,12 +23,20 @@ public class AppWindowController {
private AppWindow window;
/**
* Creates a controller for the main application window.
*
* @param context initialized application context with required services
*/
public AppWindowController(ApplicationContext context) {
this.context = context;
this.shutdownManager = new ApplicationShutdownManager(context);
subscribeAppState();
}
/**
* Builds the window, wires child controllers, and makes the UI visible.
*/
public void createAndShowWindow() {
window = new AppWindow();
@ -43,6 +54,11 @@ public class AppWindowController {
StreamingController previewController = new StreamingController(pixelStreamingView);
window.addWindowListener(new WindowAdapter() {
/**
* Triggers application shutdown when the window close button is pressed.
*
* @param e window event provided by Swing
*/
@Override
public void windowClosing(WindowEvent e) {
shutdown();
@ -52,6 +68,9 @@ public class AppWindowController {
window.setVisible(true);
}
/**
* Runs application shutdown sequence and exits the process.
*/
private void shutdown() {
shutdownManager.shutdown();
@ -62,6 +81,9 @@ public class AppWindowController {
System.exit(0);
}
/**
* Registers an observer to propagate {@link AppState} changes to the UI.
*/
private void subscribeAppState() {
Logger.info("Controller",
@ -75,6 +97,11 @@ public class AppWindowController {
"AppState Observer registriert");
}
/**
* Reacts to state updates and refreshes problem level and MQTT status labels.
*
* @param appState latest application state snapshot
*/
private void onStateChanged(AppState appState) {
if (window == null) {

View File

@ -9,11 +9,21 @@ import vassistent.util.Logger;
import javax.swing.*;
import java.util.List;
/**
* Updates the dashboard chart and level bar whenever application state changes.
*/
public class DashboardController {
private final StatisticsService statisticsService;
private final DashboardView dashboardView;
/**
* Creates a dashboard controller and subscribes to app state changes.
*
* @param statisticsService service used to compute chart data
* @param dashboardView dashboard UI component
* @param appState observable application state
*/
public DashboardController(
StatisticsService statisticsService,
DashboardView dashboardView,
@ -25,6 +35,11 @@ public class DashboardController {
appState.addListener(this::onStateChanged);
}
/**
* Recomputes dashboard content when state changes.
*
* @param state latest application state
*/
private void onStateChanged(AppState state) {
List<RatioPoint> points =

View File

@ -2,18 +2,34 @@ package vassistent.controller;
import vassistent.ui.PixelStreamingView;
/**
* Exposes basic stream view actions such as reload and focus handling.
*/
public class StreamingController {
private final PixelStreamingView view;
/**
* Creates a streaming controller for the given view.
*
* @param view pixel streaming view component
*/
public StreamingController(PixelStreamingView view) {
this.view = view;
}
/**
* Loads a new streaming URL in the embedded browser.
*
* @param url stream URL to load
*/
public void reloadStream(String url) {
view.loadStream(url);
}
/**
* Requests focus for the streaming component (used as a fullscreen shortcut hook).
*/
public void toggleFullscreen() {
view.requestFocus();
}

View File

@ -4,6 +4,9 @@ import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
/**
* Observable in-memory state shared between services and UI controllers.
*/
public class AppState {
private long dataVersion = 0;
@ -13,28 +16,56 @@ public class AppState {
private final List<Consumer<AppState>> listeners =
new ArrayList<>();
/**
* Registers a state listener that is notified on relevant updates.
*
* @param listener callback receiving the current state instance
*/
public void addListener(Consumer<AppState> listener) {
listeners.add(listener);
}
/**
* Notifies all currently registered listeners.
*/
private void notifyListeners() {
new ArrayList<>(listeners).forEach(l -> l.accept(this));
}
/**
* Returns the current computed problem level.
*
* @return current problem level
*/
public ProblemLevel getProblemLevel() {
return problemLevel;
}
/**
* Updates the problem level and notifies listeners.
*
* @param problemLevel new problem level
*/
public void setProblemLevel(ProblemLevel problemLevel) {
this.problemLevel = problemLevel;
dataVersion++;
notifyListeners();
}
/**
* Indicates whether MQTT is currently connected.
*
* @return {@code true} if connected, otherwise {@code false}
*/
public boolean isMqttConnected() {
return mqttConnected;
}
/**
* Updates MQTT connection status and notifies listeners only when changed.
*
* @param mqttConnected new MQTT connection flag
*/
public void setMqttConnected(boolean mqttConnected) {
if (this.mqttConnected != mqttConnected) {
this.mqttConnected = mqttConnected;

View File

@ -2,21 +2,40 @@ package vassistent.model;
import java.time.LocalDateTime;
/**
* Immutable timestamped value loaded from the persistence layer.
*/
public class DatabaseEntry {
private final LocalDateTime timestamp;
private final double value;
/**
* Creates an immutable database entry model.
*
* @param timestamp event timestamp
* @param value stored binary value as numeric type
*/
public DatabaseEntry(LocalDateTime timestamp, double value) {
this.timestamp = timestamp;
this.value = value;
}
/**
* Returns the event timestamp.
*
* @return timestamp of this entry
*/
public LocalDateTime getTimestamp() {
return timestamp;
}
/**
* Returns the stored value.
*
* @return numeric event value
*/
public double getValue() {
return value;
}

View File

@ -1,5 +1,8 @@
package vassistent.model;
/**
* Severity levels derived from the calculated prediction ratio.
*/
public enum ProblemLevel {
NONE,
WARNING,

View File

@ -2,21 +2,40 @@ package vassistent.model;
import java.time.LocalDateTime;
/**
* Immutable chart data point containing timestamp and ratio value.
*/
public class RatioPoint {
private final LocalDateTime timestamp;
private final double ratio;
/**
* Creates an immutable ratio point for chart rendering.
*
* @param timestamp timestamp associated with the ratio
* @param ratio ratio value in range {@code [0, 1]}
*/
public RatioPoint(LocalDateTime timestamp, double ratio) {
this.timestamp = timestamp;
this.ratio = ratio;
}
/**
* Returns the ratio timestamp.
*
* @return point timestamp
*/
public LocalDateTime getTimestamp() {
return timestamp;
}
/**
* Returns the ratio value.
*
* @return ratio for this point
*/
public double getRatio() {
return ratio;
}

View File

@ -10,9 +10,17 @@ import java.net.URI;
import java.util.concurrent.atomic.AtomicBoolean;
import jakarta.websocket.*;
/**
* Writes the current problem level as animation state JSON for Unreal integration.
*/
public class AnimationFileService {
private static final String PATH = "C:\\Users\\Student\\Documents\\Dannick\\Prototyp1\\Saved\\animation.json";
/**
* Writes the mapped animation state for the given level to the configured JSON file.
*
* @param level evaluated problem level
*/
public void wirteAnimationState(ProblemLevel level) {
String animation = mapLevelToAnimation(level);
@ -38,6 +46,12 @@ public class AnimationFileService {
}
}
/**
* Maps a problem level to the animation identifier expected by Unreal.
*
* @param level evaluated problem level
* @return animation key written into the JSON payload
*/
private String mapLevelToAnimation(ProblemLevel level) {
return switch (level) {

View File

@ -4,12 +4,21 @@ import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import vassistent.util.Logger;
/**
* Validates incoming JSON payloads and forwards accepted predictions to storage and evaluation.
*/
public class BinaryEventService {
private final DataPersistenceService persistenceService;
private final EvaluationService evaluationService;
private Integer lastId = null;
/**
* Creates a binary event service with required persistence and evaluation dependencies.
*
* @param persistenceService service used to persist accepted predictions
* @param evaluationService service used to recompute risk level after inserts
*/
public BinaryEventService(
DataPersistenceService persistenceService,
EvaluationService evaluationService
@ -18,6 +27,11 @@ public class BinaryEventService {
this.evaluationService = evaluationService;
}
/**
* Parses, validates, and processes an incoming MQTT payload.
*
* @param payload raw JSON message body
*/
public synchronized void handlePayload(String payload) {
try {

View File

@ -10,11 +10,17 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Handles SQLite initialization and read/write operations for binary event data.
*/
public class DataPersistenceService {
private static final String DB_FOLDER = "data";
private final String DB_URL;
/**
* Creates a persistence service using the default database location.
*/
public DataPersistenceService() {
DB_URL = "jdbc:sqlite:data/health.db";
createDatabaseFolder();
@ -22,12 +28,20 @@ public class DataPersistenceService {
}
// Für Tests
/**
* Creates a persistence service using a custom JDBC URL (primarily for tests).
*
* @param DB_URL JDBC URL to use for all database operations
*/
public DataPersistenceService(String DB_URL) {
this.DB_URL = DB_URL;
createDatabaseFolder();
init();
}
/**
* Ensures that the local database directory exists.
*/
public void createDatabaseFolder() {
File folder = new File(DB_FOLDER);
if (!folder.exists()) {
@ -40,6 +54,9 @@ public class DataPersistenceService {
}
}
/**
* Initializes the database schema if it does not exist.
*/
private void init() {
try (
Connection c = DriverManager.getConnection(DB_URL); Statement s = c.createStatement()) {
@ -59,6 +76,11 @@ public class DataPersistenceService {
}
}
/**
* Stores a binary prediction value with the current timestamp.
*
* @param value prediction value (expected {@code 0} or {@code 1})
*/
public void store(int value) {
try (
Connection c = DriverManager.getConnection(DB_URL); PreparedStatement ps = c.prepareStatement(
@ -73,6 +95,12 @@ public class DataPersistenceService {
}
}
/**
* Loads the newest entries and returns them in chronological order.
*
* @param count maximum number of entries to return
* @return list of entries ordered from oldest to newest within the selected window
*/
public List<DatabaseEntry> getLastEntries(int count) {
List<DatabaseEntry> result = new ArrayList<>();
@ -109,7 +137,12 @@ public class DataPersistenceService {
return result;
}
/**
* Returns the configured JDBC connection URL.
*
* @return JDBC URL used by this service
*/
public String getDbUrl() {
return DB_URL;
}
}
}

View File

@ -4,11 +4,21 @@ package vassistent.service;
import vassistent.model.AppState;
import vassistent.model.ProblemLevel;
/**
* Converts statistical ratios into problem levels and triggers animation updates.
*/
public class EvaluationService {
private final StatisticsService statisticsService;
private final AppState appState;
private final AnimationFileService animationFileService;
/**
* Creates an evaluation service.
*
* @param statisticsService service used to calculate ratios
* @param appState shared application state to update
* @param animationFileService service used to propagate animation changes
*/
public EvaluationService(
StatisticsService statisticsService,
AppState appState,
@ -20,6 +30,9 @@ public class EvaluationService {
this.animationFileService = animationFileService;
}
/**
* Recalculates the current ratio, updates state, and writes animation output.
*/
public void evaluate() {
double ratio = statisticsService.getRatio(20);
@ -29,6 +42,12 @@ public class EvaluationService {
animationFileService.wirteAnimationState(level);
}
/**
* Maps a ratio value to a problem level threshold.
*
* @param ratio computed ratio in the range {@code [0, 1]}
* @return derived problem level
*/
private ProblemLevel calculateLevel(Double ratio) {
if (ratio >= 0.9) {
return ProblemLevel.DISASTER;

View File

@ -10,6 +10,9 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
/**
* MQTT client wrapper that manages connection state, topic listeners, and callbacks.
*/
public class MqttClientService implements MqttCallback {
private AppState appState;
@ -21,6 +24,11 @@ public class MqttClientService implements MqttCallback {
private MqttClient client;
/**
* Creates and connects an MQTT client for the application.
*
* @param appState shared state updated with connection status
*/
public MqttClientService(AppState appState) {
this.appState = appState;
try {
@ -45,6 +53,9 @@ public class MqttClientService implements MqttCallback {
}
}
/**
* Disconnects the MQTT client when connected.
*/
public void disconnect() {
try {
if (client != null && client.isConnected()) {
@ -56,6 +67,12 @@ public class MqttClientService implements MqttCallback {
}
}
/**
* Subscribes to a topic and registers a payload listener.
*
* @param topic MQTT topic
* @param listener callback receiving payload strings
*/
public void subscribe(String topic, Consumer<String> listener) {
topicListeners.put(topic, listener);
try {
@ -66,6 +83,13 @@ public class MqttClientService implements MqttCallback {
}
}
/**
* Publishes a message to a topic.
*
* @param topic MQTT topic
* @param message payload body
* @param qos MQTT quality-of-service level
*/
public void publish(String topic, String message, int qos) {
try {
MqttMessage mqttMessage =
@ -83,6 +107,11 @@ public class MqttClientService implements MqttCallback {
}
}
/**
* MQTT callback invoked when the broker connection is lost.
*
* @param cause cause of the connection loss
*/
@Override
public void connectionLost(Throwable cause) {
Logger.warn(
@ -93,6 +122,13 @@ public class MqttClientService implements MqttCallback {
appState.setMqttConnected(false);
}
/**
* MQTT callback invoked for each incoming message.
*
* @param topic topic of the received message
* @param message received MQTT message
* @throws Exception propagated by callback handling if listener execution fails
*/
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
String payload =
@ -115,6 +151,11 @@ public class MqttClientService implements MqttCallback {
}
}
/**
* MQTT callback invoked when a published message is fully delivered.
*
* @param token delivery token associated with the completed publish
*/
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
Logger.debug(

View File

@ -8,6 +8,9 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;
/**
* Starts and stops optional external helper processes used by the application.
*/
public class ProcessManagerService {
private final Properties config;
@ -16,16 +19,27 @@ public class ProcessManagerService {
private Process unrealProcess;
private Process unrealSignallingProcess;
/**
* Creates a process manager using runtime configuration values.
*
* @param config loaded application properties
*/
public ProcessManagerService(Properties config) {
this.config = config;
}
/**
* Starts optional helper processes according to configuration flags.
*/
public void startProcesses() {
startPythonIfEnabled();
startUnrealIfEnabled();
}
/**
* Starts the Python MQTT simulator if enabled.
*/
private void startPythonIfEnabled() {
boolean enabled = Boolean.parseBoolean(
@ -49,6 +63,9 @@ public class ProcessManagerService {
}
}
/**
* Starts Unreal-related processes if enabled.
*/
private void startUnrealIfEnabled() {
boolean enabled = Boolean.parseBoolean(config.getProperty("unreal.enabled", "false"));
@ -65,6 +82,11 @@ public class ProcessManagerService {
}
}
/**
* Starts the Unreal signalling server script.
*
* @throws IOException if process startup fails
*/
private void startSignallingServer() throws IOException {
String script =
@ -86,6 +108,11 @@ public class ProcessManagerService {
"Unreal Signalling Server gestartet" + pb.command());
}
/**
* Starts the configured Unreal executable script via PowerShell.
*
* @throws IOException if process startup fails
*/
private void startUnrealEngine() throws IOException {
String unrealPsScript = config.getProperty("unreal.executable");
@ -106,6 +133,9 @@ public class ProcessManagerService {
"Unreal Engine gestartet" + pb.command());
}
/**
* Stops all managed processes and attempts cleanup based on known PID files.
*/
public void shutdown() {
Logger.info("PROCESS", "Shutdown externe Prozesse gestartet");
@ -125,6 +155,11 @@ public class ProcessManagerService {
Logger.info("PROCESS", "Externe Prozesse beendet");
}
/**
* Terminates a process tree for a tracked process handle.
*
* @param process process handle to terminate; ignored when {@code null}
*/
private void terminateProcess(Process process) {
if (process == null)
@ -153,6 +188,11 @@ public class ProcessManagerService {
}
}
/**
* Terminates a process tree using a PID read from a file.
*
* @param file absolute path to PID file
*/
private void killProcessFromPidFile(String file) {
try {

View File

@ -9,13 +9,27 @@ import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* Computes ratios and time-series aggregates from persisted binary events.
*/
public class StatisticsService {
private final DataPersistenceService persistenceService;
/**
* Creates a statistics service for persisted event data.
*
* @param persistenceService persistence service to query raw events
*/
public StatisticsService(DataPersistenceService persistenceService) {
this.persistenceService = persistenceService;
}
/**
* Computes the average of the newest {@code lastN} binary values.
*
* @param lastN number of newest entries to include
* @return ratio in range {@code [0, 1]} or {@code 0.0} when unavailable
*/
public double getRatio(int lastN) {
String query = """
SELECT AVG(value)
@ -41,6 +55,12 @@ public class StatisticsService {
return 0.0;
}
/**
* Computes rolling averages for chart rendering.
*
* @param windowSize number of previous entries considered for each point
* @return chronological list of ratio points
*/
public List<RatioPoint> getLastNAverages(int windowSize) {
List<DatabaseEntry> entries = persistenceService.getLastEntries(windowSize + 1);

View File

@ -7,6 +7,9 @@ import javax.swing.*;
import javax.swing.border.Border;
import java.awt.*;
/**
* Main Swing frame containing streaming and dashboard tabs plus status indicators.
*/
public class AppWindow extends JFrame {
private PixelStreamingView streamingView;
@ -16,6 +19,9 @@ public class AppWindow extends JFrame {
private JLabel mqttStatusLabel;
private JLabel problemLevelLabel;
/**
* Creates and initializes the main application window and its child views.
*/
public AppWindow() {
setTitle("Virtueller Gesundheitsassistent");
@ -42,8 +48,11 @@ public class AppWindow extends JFrame {
UIManager.put("TabbedPane.font", uiFont);
}
// ---------------- Tabs ----------------
/**
* Creates the tabbed pane that hosts streaming and dashboard views.
*
* @return configured tabbed pane
*/
private JTabbedPane createTabPane() {
JTabbedPane tabs = new JTabbedPane();
@ -58,8 +67,11 @@ public class AppWindow extends JFrame {
return tabs;
}
// ---------------- Status Bar ----------------
/**
* Creates the status bar with MQTT and problem-level indicators.
*
* @return configured status bar panel
*/
private JPanel createStatusBar() {
JPanel statusBar = new JPanel(new FlowLayout(FlowLayout.LEFT,15,5));
@ -79,23 +91,48 @@ public class AppWindow extends JFrame {
return statusBar;
}
/**
* Updates the MQTT connection status label.
*
* @param connected whether MQTT is currently connected
*/
public void updateMqttStatus(boolean connected) {
mqttStatusLabel.setText("MQTT: " +
(connected ? "Connected" : "Disconnected"));
}
/**
* Updates the currently displayed problem level label.
*
* @param level problem level name to display
*/
public void updateProblemLevel(String level) {
problemLevelLabel.setText("Problem: " + level);
}
/**
* Returns the dashboard view component.
*
* @return dashboard view
*/
public DashboardView getDashboardView() {
return dashboardView;
}
/**
* Returns the pixel streaming view component.
*
* @return streaming view
*/
public PixelStreamingView getStreamingView() {
return streamingView;
}
/**
* Returns the main tabbed pane.
*
* @return tab container
*/
public JTabbedPane getTabs() {
return tabs;
}

View File

@ -27,6 +27,9 @@ import java.util.Arrays;
import java.util.Date;
import java.util.List;
/**
* Dashboard panel that renders the ratio history chart and current level indicator.
*/
public class DashboardView extends JPanel {
private TimeSeries series;
@ -35,6 +38,9 @@ public class DashboardView extends JPanel {
private JButton reloadPixelStreamingViewButton;
private JButton openFullscreenButton;
/**
* Creates the dashboard panel with problem-level bar and time-series chart.
*/
public DashboardView() {
setLayout(new BorderLayout());
setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
@ -118,6 +124,11 @@ public class DashboardView extends JPanel {
add(card, BorderLayout.CENTER);
}
/**
* Replaces chart data with the given ratio points.
*
* @param points ratio points ordered by time
*/
public void updateChart(List<RatioPoint> points) {
if (points == null || points.isEmpty())
return;
@ -139,18 +150,40 @@ public class DashboardView extends JPanel {
}
}
/**
* Updates the visual problem level bar using the latest ratio.
*
* @param ratio latest computed ratio
*/
public void updateProblemLevel(double ratio) {
levelBar.setRatio(ratio);
}
/**
* Returns the button intended to reload the pixel streaming view.
*
* @return reload button (may be {@code null} until implemented)
*/
public JButton getReloadPixelStreamingViewButton() {
return reloadPixelStreamingViewButton;
}
/**
* Returns the button intended to open fullscreen mode.
*
* @return fullscreen button (may be {@code null} until implemented)
*/
public JButton getOpenFullscreenButton() {
return openFullscreenButton;
}
/**
* Adds a horizontal threshold marker with label to the chart.
*
* @param plot target plot
* @param value y-axis value for the threshold
* @param label label rendered near the marker
*/
private void addThresholdLine(XYPlot plot, double value, String label) {
ValueMarker marker = new ValueMarker(value);

View File

@ -11,6 +11,9 @@ import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
/**
* Embedded JCEF browser panel used to display the avatar pixel stream.
*/
public class PixelStreamingView extends JPanel {
private static final long serialVersionUID = -5570653778104813836L;
@ -19,10 +22,22 @@ public class PixelStreamingView extends JPanel {
private final CefBrowser browser;
private final Component browserUI_;
/**
* Creates an embedded Chromium browser panel.
*
* @param startURL initial URL to load
* @param useOSR whether off-screen rendering is enabled
* @param isTransparent whether transparent rendering is enabled
*/
public PixelStreamingView(String startURL, boolean useOSR, boolean isTransparent) {
super(new BorderLayout());
CefApp.addAppHandler(new CefAppHandlerAdapter(null) {
/**
* Handles JCEF application state transitions.
*
* @param state latest JCEF lifecycle state
*/
@Override
public void stateHasChanged(CefApp.CefAppState state) {
if (state == CefApp.CefAppState.TERMINATED){
@ -43,12 +58,20 @@ public class PixelStreamingView extends JPanel {
add(browserUI_, BorderLayout.CENTER);
}
/**
* Disposes browser resources held by JCEF objects.
*/
public void dispose() {
if (browser != null) browser.close(true);
if (client != null) client.dispose();
if (cefApp != null) cefApp.dispose();
}
/**
* Loads the given stream URL in the embedded browser.
*
* @param url stream URL
*/
public void loadStream(String url) {
browser.loadURL(url);
}

View File

@ -5,23 +5,40 @@ import vassistent.model.ProblemLevel;
import javax.swing.*;
import java.awt.*;
/**
* Custom progress-style component that visualizes ratio and mapped problem level.
*/
public class ProblemLevelBar extends JPanel {
private double ratio = 0.0;
private ProblemLevel level = ProblemLevel.NONE;
/**
* Creates the level bar component with preferred sizing and transparent background.
*/
public ProblemLevelBar() {
setPreferredSize(new Dimension(100, 50));
setOpaque(false);
setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
}
/**
* Updates the displayed ratio and derived problem level.
*
* @param ratio ratio value, clamped to {@code [0, 1]}
*/
public void setRatio(double ratio) {
this.ratio = Math.max(0.0, Math.min(1.0, ratio));
this.level = calculateLevel(this.ratio);
repaint();
}
/**
* Maps a ratio to a problem level threshold.
*
* @param ratio ratio value in {@code [0, 1]}
* @return mapped problem level
*/
private ProblemLevel calculateLevel(Double ratio) {
if (ratio >= 0.9) {
return ProblemLevel.DISASTER;
@ -34,6 +51,11 @@ public class ProblemLevelBar extends JPanel {
}
}
/**
* Paints the bar background, filled level area, and textual status.
*
* @param g graphics context provided by Swing
*/
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
@ -87,6 +109,12 @@ public class ProblemLevelBar extends JPanel {
);
}
/**
* Returns a base color for the given problem level.
*
* @param level problem level
* @return display color for the level
*/
private Color getColorForLevel(ProblemLevel level) {
switch (level) {
case DISASTER:

View File

@ -3,10 +3,22 @@ package vassistent.util;
import java.io.InputStream;
import java.util.Properties;
/**
* Utility for loading classpath-based properties files.
*/
public class ConfigLoader {
/**
* Creates a config loader instance.
*/
public ConfigLoader() {}
/**
* Loads a properties resource from the application classpath.
*
* @param path classpath-relative resource path
* @return loaded properties, or empty properties when resource is missing
*/
public static Properties loadProperties(String path) {
Properties props = new Properties();

View File

@ -7,8 +7,14 @@ import java.util.Properties;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* Asynchronous application logger with level filtering and file rotation support.
*/
public class Logger {
/**
* Log severity levels in ascending order.
*/
public enum Level {
DEBUG, INFO, WARN, ERROR
}
@ -30,10 +36,14 @@ public class Logger {
startLogWriterThread();
}
/**
* Utility class constructor.
*/
private Logger() {}
// ================= CONFIG =================
/**
* Loads logger configuration from {@code config/logger.properties}.
*/
private static void loadConfig() {
try (InputStream is = Logger.class.getClassLoader()
.getResourceAsStream("config/logger.properties")) {
@ -63,30 +73,65 @@ public class Logger {
}
}
// ================= API =================
/**
* Writes a debug-level log entry.
*
* @param source logical source component
* @param message log message
*/
public static void debug(String source, String message) {
log(Level.DEBUG, source, message, null);
}
/**
* Writes an info-level log entry.
*
* @param source logical source component
* @param message log message
*/
public static void info(String source, String message) {
log(Level.INFO, source, message, null);
}
/**
* Writes a warning-level log entry.
*
* @param source logical source component
* @param message log message
*/
public static void warn(String source, String message) {
log(Level.WARN, source, message, null);
}
/**
* Writes an error-level log entry without stack trace.
*
* @param source logical source component
* @param message log message
*/
public static void error(String source, String message) {
log(Level.ERROR, source, message, null);
}
/**
* Writes an error-level log entry with stack trace.
*
* @param source logical source component
* @param message log message
* @param t associated throwable
*/
public static void error(String source, String message, Throwable t) {
log(Level.ERROR, source, message, t);
}
// ================= CORE =================
/**
* Enqueues a log entry for asynchronous output if the level passes filtering.
*
* @param level severity level
* @param source logical source component
* @param message log message
* @param throwable optional throwable to append
*/
public static void log(Level level, String source, String message, Throwable throwable) {
if (level.ordinal() < currentLevel.ordinal()) return;
@ -108,8 +153,9 @@ public class Logger {
logQueue.offer(sb.toString());
}
// ================= BACKGROUND WRITER =================
/**
* Starts the daemon thread that drains the log queue and writes outputs.
*/
private static void startLogWriterThread() {
Thread writerThread = new Thread(() -> {
@ -145,8 +191,9 @@ public class Logger {
writerThread.start();
}
// ================= ROTATION =================
/**
* Rotates the active log file when size exceeds configured threshold.
*/
private static void rotateIfNeeded() {
try {
@ -178,6 +225,9 @@ public class Logger {
}
}
/**
* Performs a best-effort flush of queued messages during shutdown.
*/
public static void shutdown() {
System.out.println("Logger Shutdown gestartet");
@ -196,4 +246,4 @@ public class Logger {
System.err.println("Logger Shutdown Fehler: " + e.getMessage());
}
}
}
}

View File

@ -6,12 +6,18 @@ import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
/**
* Tests for payload validation and processing behavior in {@link BinaryEventService}.
*/
class BinaryEventServiceTest {
private DataPersistenceService persistenceService;
private EvaluationService evaluationService;
private BinaryEventService binaryEventService;
/**
* Creates mocked dependencies and the service under test.
*/
@BeforeEach
void setUp() {
persistenceService = mock(DataPersistenceService.class);
@ -19,6 +25,9 @@ class BinaryEventServiceTest {
binaryEventService = new BinaryEventService(persistenceService, evaluationService);
}
/**
* Clears references after each test run.
*/
@AfterEach
void tearDown() {
persistenceService = null;
@ -26,6 +35,9 @@ class BinaryEventServiceTest {
binaryEventService = null;
}
/**
* Verifies that a valid payload triggers persistence and evaluation.
*/
@Test
void handlePayloadValid() {
binaryEventService.handlePayload("1");
@ -33,6 +45,9 @@ class BinaryEventServiceTest {
verify(evaluationService).evaluate();
}
/**
* Verifies that an invalid numeric payload value is ignored.
*/
@Test
void handlePayloadInvalidValue() {
binaryEventService.handlePayload("5");
@ -40,10 +55,13 @@ class BinaryEventServiceTest {
verify(evaluationService, never()).evaluate();
}
/**
* Verifies that a non-numeric payload does not trigger downstream actions.
*/
@Test
void handlePayloadNonNumeric() {
binaryEventService.handlePayload("abc");
verify(persistenceService, never()).store(anyInt());
verify(evaluationService, never()).evaluate();
}
}
}

View File

@ -11,23 +11,35 @@ import java.sql.Statement;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests basic database initialization and persistence operations.
*/
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";
/**
* Initializes a persistence service backed by a test database.
*/
@BeforeEach
void setup() {
service = new DataPersistenceService(TEST_DB);
}
/**
* Removes test database artifacts after each test.
*/
@AfterEach
void cleanup() {
deleteTestDatabase();
}
/**
* Deletes the test database file when present.
*/
private void deleteTestDatabase() {
File db = new File(DB_FILE);
if (db.exists()) {
@ -35,6 +47,9 @@ class DataPersistenceServiceTest {
}
}
/**
* Verifies that the database folder exists after service initialization.
*/
@org.junit.jupiter.api.Test
void createDatabaseFolder() {
File folder = new File("data");
@ -42,6 +57,11 @@ class DataPersistenceServiceTest {
assertTrue(folder.exists(), "Datenbankordner sollte existieren");
}
/**
* Verifies that storing a value inserts at least one row.
*
* @throws Exception if direct JDBC validation fails
*/
@org.junit.jupiter.api.Test
void store() throws Exception {
service.store(1);
@ -59,8 +79,11 @@ class DataPersistenceServiceTest {
}
}
/**
* Verifies that the service returns its configured JDBC URL.
*/
@org.junit.jupiter.api.Test
void getDbUrl() {
assertEquals(TEST_DB, service.getDbUrl());
}
}
}

View File

@ -8,6 +8,9 @@ import vassistent.model.ProblemLevel;
import static org.mockito.Mockito.*;
/**
* Tests mapping of computed ratios to {@link ProblemLevel} values.
*/
class EvaluationServiceTest {
private StatisticsService statisticsService;
@ -15,6 +18,9 @@ class EvaluationServiceTest {
private AnimationFileService unrealService;
private EvaluationService evaluationService;
/**
* Creates mocks and initializes the evaluation service under test.
*/
@BeforeEach
void setUp() {
statisticsService = mock(StatisticsService.class);
@ -23,6 +29,9 @@ class EvaluationServiceTest {
evaluationService = new EvaluationService(statisticsService, appState, unrealService);
}
/**
* Clears test references.
*/
@org.junit.jupiter.api.Test
void tearDown() {
statisticsService = null;
@ -31,6 +40,9 @@ class EvaluationServiceTest {
evaluationService = null;
}
/**
* Verifies that high ratios map to {@link ProblemLevel#DISASTER}.
*/
@org.junit.jupiter.api.Test
void testEvaluateDisasterLevel() {
when(statisticsService.getRatio(anyInt())).thenReturn(0.95);
@ -39,6 +51,9 @@ class EvaluationServiceTest {
//verify(unrealService).speak("DISASTER");
}
/**
* Verifies that low ratios keep the state at {@link ProblemLevel#NONE}.
*/
@Test
void testEvaluateNoChange() {
appState.setProblemLevel(ProblemLevel.NONE);
@ -46,4 +61,4 @@ class EvaluationServiceTest {
evaluationService.evaluate();
//verify(unrealService, never()).speak(anyString());
}
}
}

View File

@ -9,6 +9,9 @@ import java.sql.SQLException;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests ratio calculations over persisted binary events.
*/
class StatisticsServiceTest {
private String DB_URL;
@ -16,6 +19,9 @@ class StatisticsServiceTest {
private StatisticsService statisticsService;
private File tempDbFile;
/**
* Creates an isolated temporary SQLite database for each test.
*/
@BeforeEach
void setUp() {
try {
@ -29,6 +35,9 @@ class StatisticsServiceTest {
statisticsService = new StatisticsService(persistenceService);
}
/**
* Deletes the temporary test database file.
*/
@AfterEach
void tearDown() {
if (tempDbFile != null && tempDbFile.exists()) {
@ -36,12 +45,20 @@ class StatisticsServiceTest {
}
}
/**
* Verifies ratio behavior when no events are available.
*/
@Test
void testGetRatioNoData() {
double ratio = statisticsService.getRatio(10);
assertEquals(0.0, ratio, "Ratio sollte 0 sein, wenn keine Daten vorhanden");
}
/**
* Verifies ratio behavior with exactly one stored value.
*
* @throws SQLException retained for compatibility with current method signature
*/
@Test
void testGetRatioSingleValue() throws SQLException {
persistenceService.store(1);
@ -50,6 +67,9 @@ class StatisticsServiceTest {
assertEquals(1.0, ratio, "Ratio sollte 1 sein, wenn nur 1 gespeichert wurde");
}
/**
* Verifies ratio behavior across multiple stored values.
*/
@Test
void testGetRatioMultipleValues() {
persistenceService.store(1);
@ -60,6 +80,9 @@ class StatisticsServiceTest {
assertEquals((1 + 0 + 1) / 3.0, ratio, 0.0001, "Ratio sollte Durchschnitt der letzten Werte sein");
}
/**
* Verifies that ratio calculation only considers the latest N values.
*/
@Test
void testGetRatioLimitLastN() {
persistenceService.store(1);
@ -71,4 +94,4 @@ class StatisticsServiceTest {
double ratio = statisticsService.getRatio(3);
assertEquals((1 + 0 + 0) / 3.0, ratio, 0.0001, "Ratio sollte Durchschnitt der letzten Werte sein");
}
}
}