JavaDoc comments added
This commit is contained in:
parent
31b81cde1b
commit
3cf2cf9288
148
readme.md
Normal file
148
readme.md
Normal 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).
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
package vassistent.model;
|
||||
|
||||
/**
|
||||
* Severity levels derived from the calculated prediction ratio.
|
||||
*/
|
||||
public enum ProblemLevel {
|
||||
NONE,
|
||||
WARNING,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,6 +137,11 @@ public class DataPersistenceService {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configured JDBC connection URL.
|
||||
*
|
||||
* @return JDBC URL used by this service
|
||||
*/
|
||||
public String getDbUrl() {
|
||||
return DB_URL;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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,6 +55,9 @@ class BinaryEventServiceTest {
|
||||
verify(evaluationService, never()).evaluate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a non-numeric payload does not trigger downstream actions.
|
||||
*/
|
||||
@Test
|
||||
void handlePayloadNonNumeric() {
|
||||
binaryEventService.handlePayload("abc");
|
||||
|
||||
@ -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,6 +79,9 @@ class DataPersistenceServiceTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the service returns its configured JDBC URL.
|
||||
*/
|
||||
@org.junit.jupiter.api.Test
|
||||
void getDbUrl() {
|
||||
assertEquals(TEST_DB, service.getDbUrl());
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user