From 16bcf7f06dc87a0f166ec8f5111ed477189b6a5b Mon Sep 17 00:00:00 2001 From: naumueller Date: Fri, 6 Mar 2026 15:09:15 +0100 Subject: [PATCH] feat(config): externalize runtime paths and script settings for local setup --- docs/CONFIGURATION.md | 41 +++++ readme.md | 3 + .../service/ProcessManagerService.java | 68 ++++++++ .../resources/config/application.properties | 15 ++ src/main/resources/scripts/mqtt_simulator.py | 146 +++++++++++++----- src/main/resources/scripts/start_avatar.ps1 | 70 +++++++-- 6 files changed, 289 insertions(+), 54 deletions(-) diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 5511c56..e9e5f02 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -14,17 +14,34 @@ Application settings are loaded from classpath properties files in `src/main/res | `mqtt.topic` | MQTT topic to subscribe to | `PREDICTION` | | `mqtt_sim.enabled` | Enables simulator startup | `false` | | `mqtt_sim.script` | Simulator script path | `src/main/resources/scripts/mqtt_simulator.py` | +| `mqtt_sim.broker` | Simulator broker override (env passthrough) | `localhost` | +| `mqtt_sim.port` | Simulator broker port override | `1883` | +| `mqtt_sim.topic` | Simulator publish topic override | `PREDICTION` | +| `mqtt_sim.qos` | Simulator QoS override | `0` | +| `mqtt_sim.interval_seconds` | Simulator publish interval override | `5` | +| `mqtt_sim.username` | Optional simulator auth username | empty | +| `mqtt_sim.password` | Optional simulator auth password | empty | +| `mqtt_sim.start_id` | Optional simulator initial payload id | `1` | +| `mqtt_sim.client_id` | Optional simulator MQTT client id | `mqtt-simulator` | +| `mqtt_sim.log_file` | Optional simulator log file path | `logs/mqtt_simulator.log` | | `animation.output.path` | Generated animation state file path | `data/animation.json` | | `unreal.enabled` | Enables Unreal startup flow | `true` | | `unreal.executable` | PowerShell script to start Unreal process | absolute path | | `unreal.signalling_server.script` | Signalling server BAT file path | absolute path | | `unreal.pid.file` | Unreal PID file for shutdown cleanup | absolute path | | `unreal.signalling.pid.file` | Signalling PID file for shutdown cleanup | absolute path | +| `unreal.pid.dir` | Script-side PID directory override | absolute path | +| `unreal.target.executable` | Script-side Unreal executable override | absolute path | +| `unreal.target.args` | Script-side Unreal argument string override | command string | +| `unreal.target.working_dir` | Script-side Unreal working dir override | absolute path | +| `unreal.startup.delay.seconds` | Script-side signalling warmup delay override | `5` | Notes: - Paths can be provided with or without wrapping quotes; startup sanitizes surrounding quotes. - Unreal-related defaults are environment-specific and should be replaced per machine. +- `mqtt_sim.*` overrides are forwarded as environment variables to `mqtt_simulator.py`. +- `unreal.target.*` and `unreal.pid.dir` are forwarded as environment variables to `start_avatar.ps1`. ## `logger.properties` @@ -39,3 +56,27 @@ Notes: - `src/main/resources/scripts/mqtt_simulator.py` - `src/main/resources/scripts/start_avatar.ps1` + +## Script Runtime Inputs + +`mqtt_simulator.py` supports CLI options and equivalent env vars: + +- `--broker` / `MQTT_SIM_BROKER` +- `--port` / `MQTT_SIM_PORT` +- `--topic` / `MQTT_SIM_TOPIC` +- `--username` / `MQTT_SIM_USERNAME` +- `--password` / `MQTT_SIM_PASSWORD` +- `--qos` / `MQTT_SIM_QOS` +- `--interval-seconds` / `MQTT_SIM_INTERVAL_SECONDS` +- `--start-id` / `MQTT_SIM_START_ID` +- `--client-id` / `MQTT_SIM_CLIENT_ID` +- `--log-file` / `MQTT_SIM_LOG_FILE` + +`start_avatar.ps1` supports parameters and equivalent env vars: + +- `-PidDir` / `VGA_PID_DIR` +- `-UeExe` / `VGA_UE_EXE` +- `-UeArgs` / `VGA_UE_ARGS` +- `-SignallingBat` / `VGA_SIGNALLING_BAT` +- `-UeWorkingDir` / `VGA_UE_WORKDIR` +- `-SignallingStartupDelaySeconds` / `VGA_SIGNALLING_DELAY` diff --git a/readme.md b/readme.md index b62e127..471e077 100644 --- a/readme.md +++ b/readme.md @@ -85,12 +85,14 @@ Main config: `src/main/resources/config/application.properties` - `mqtt.topic`: subscribed topic (default `PREDICTION`) - `mqtt_sim.enabled`: start simulator process on app startup - `mqtt_sim.script`: simulator script path +- `mqtt_sim.broker` / `mqtt_sim.port` / `mqtt_sim.topic`: simulator runtime overrides - `animation.output.path`: path for generated animation JSON file - `unreal.enabled`: start Unreal-related processes - `unreal.executable`: PowerShell script path for Unreal start - `unreal.signalling_server.script`: signalling server batch path - `unreal.pid.file`: PID file used for Unreal shutdown cleanup - `unreal.signalling.pid.file`: PID file used for signalling shutdown cleanup +- `unreal.target.executable` / `unreal.target.args` / `unreal.target.working_dir`: optional overrides passed into `start_avatar.ps1` Logger config: `src/main/resources/config/logger.properties` @@ -161,5 +163,6 @@ src/main/resources - Animation output path is now config-driven (`animation.output.path`). - MQTT broker URL/client id are config-driven (`mqtt.broker.url`, `mqtt.client.id`). - Unreal PID cleanup paths are config-driven (`unreal.pid.file`, `unreal.signalling.pid.file`). +- Script runtime values can be passed from config into `mqtt_simulator.py` and `start_avatar.ps1`. - 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). diff --git a/src/main/java/vassistent/service/ProcessManagerService.java b/src/main/java/vassistent/service/ProcessManagerService.java index 9eeb336..48aeb94 100644 --- a/src/main/java/vassistent/service/ProcessManagerService.java +++ b/src/main/java/vassistent/service/ProcessManagerService.java @@ -104,6 +104,7 @@ public class ProcessManagerService { ProcessBuilder pb = new ProcessBuilder(python, script); pb.redirectErrorStream(true); + configureMqttSimulatorEnvironment(pb); pythonProcess = processLauncher.launch(pb); @@ -176,6 +177,8 @@ public class ProcessManagerService { unrealPsScript ); + configureUnrealScriptEnvironment(pb); + unrealProcess = processLauncher.launch(pb); //pb.directory(new File(exe).getParentFile()); @@ -311,4 +314,69 @@ public class ProcessManagerService { return trimmed; } + + /** + * Passes optional simulator settings from app config to the Python process environment. + * + * @param processBuilder simulator process builder + */ + private void configureMqttSimulatorEnvironment(ProcessBuilder processBuilder) { + putEnvFromConfig(processBuilder, "mqtt_sim.broker", "MQTT_SIM_BROKER"); + putEnvFromConfig(processBuilder, "mqtt_sim.port", "MQTT_SIM_PORT"); + putEnvFromConfig(processBuilder, "mqtt_sim.topic", "MQTT_SIM_TOPIC"); + putEnvFromConfig(processBuilder, "mqtt_sim.username", "MQTT_SIM_USERNAME"); + putEnvFromConfig(processBuilder, "mqtt_sim.password", "MQTT_SIM_PASSWORD"); + putEnvFromConfig(processBuilder, "mqtt_sim.qos", "MQTT_SIM_QOS"); + putEnvFromConfig(processBuilder, "mqtt_sim.interval_seconds", "MQTT_SIM_INTERVAL_SECONDS"); + putEnvFromConfig(processBuilder, "mqtt_sim.start_id", "MQTT_SIM_START_ID"); + putEnvFromConfig(processBuilder, "mqtt_sim.client_id", "MQTT_SIM_CLIENT_ID"); + putEnvFromConfig(processBuilder, "mqtt_sim.log_file", "MQTT_SIM_LOG_FILE"); + } + + /** + * Passes optional Unreal launcher settings from app config to the PowerShell process environment. + * + * @param processBuilder Unreal process builder + */ + private void configureUnrealScriptEnvironment(ProcessBuilder processBuilder) { + putEnvFromConfig(processBuilder, "unreal.target.executable", "VGA_UE_EXE"); + putEnvFromConfig(processBuilder, "unreal.target.args", "VGA_UE_ARGS"); + putEnvFromConfig(processBuilder, "unreal.target.working_dir", "VGA_UE_WORKDIR"); + putEnvFromConfig(processBuilder, "unreal.signalling_server.script", "VGA_SIGNALLING_BAT"); + putEnvFromConfig(processBuilder, "unreal.startup.delay.seconds", "VGA_SIGNALLING_DELAY"); + + String pidDir = cleanConfigValue(config.getProperty("unreal.pid.dir")); + if (pidDir == null || pidDir.isBlank()) { + try { + Path parent = Path.of(unrealPidFile).getParent(); + if (parent != null) { + pidDir = parent.toString(); + } + } catch (Exception ignored) { + pidDir = null; + } + } + + if (pidDir != null && !pidDir.isBlank()) { + processBuilder.environment().put("VGA_PID_DIR", pidDir); + } + } + + /** + * Adds a config value to process environment when present. + * + * @param processBuilder target process builder + * @param configKey application properties key + * @param envName environment variable name + */ + private void putEnvFromConfig( + ProcessBuilder processBuilder, + String configKey, + String envName + ) { + String value = cleanConfigValue(config.getProperty(configKey)); + if (value != null && !value.isBlank()) { + processBuilder.environment().put(envName, value); + } + } } diff --git a/src/main/resources/config/application.properties b/src/main/resources/config/application.properties index b05bef3..4e3996d 100644 --- a/src/main/resources/config/application.properties +++ b/src/main/resources/config/application.properties @@ -15,6 +15,16 @@ mqtt.topic=PREDICTION # ===== MQTT SIMULATOR ===== mqtt_sim.enabled=false mqtt_sim.script=src/main/resources/scripts/mqtt_simulator.py +mqtt_sim.broker=localhost +mqtt_sim.port=1883 +mqtt_sim.topic=PREDICTION +mqtt_sim.qos=0 +mqtt_sim.interval_seconds=5 +# mqtt_sim.username= +# mqtt_sim.password= +# mqtt_sim.start_id=1 +# mqtt_sim.client_id=mqtt-simulator +# mqtt_sim.log_file=logs/mqtt_simulator.log # ===== ANIMATION ===== animation.output.path=data/animation.json @@ -25,3 +35,8 @@ unreal.executable=C:\\Users\\Student\\Documents\\Dannick\\avatar\\start_avatar.p unreal.signalling_server.script=C:\\Users\\Student\\Documents\\Dannick\\avatar\\Windows\\Prototyp1\\Samples\\PixelStreaming\\WebServers\\SignallingWebServer\\platform_scripts\\cmd\\start_with_stun.bat unreal.pid.file=C:\\Users\\Student\\Documents\\Dannick\\avatar\\unreal.pid unreal.signalling.pid.file=C:\\Users\\Student\\Documents\\Dannick\\avatar\\signalling.pid +# unreal.pid.dir=C:\\Users\\Student\\Documents\\Dannick\\avatar +# unreal.target.executable=C:\\Users\\Student\\Documents\\Dannick\\avatar\\Windows\\Prototyp1.exe +# unreal.target.args=-PixelStreamingURL=ws://127.0.0.1 -RenderOffscreen -PixelStreamingWebRTCDisableTransmitAudio +# unreal.target.working_dir=C:\\Users\\Student\\Documents\\Dannick\\avatar\\Windows +# unreal.startup.delay.seconds=5 diff --git a/src/main/resources/scripts/mqtt_simulator.py b/src/main/resources/scripts/mqtt_simulator.py index 99c39c0..2ff80b3 100644 --- a/src/main/resources/scripts/mqtt_simulator.py +++ b/src/main/resources/scripts/mqtt_simulator.py @@ -1,85 +1,151 @@ +import argparse import json +import logging +import os +import random +import sys +import time import paho.mqtt.client as mqtt -import sys -import random -import time -import logging -# ===== KONFIGURATION ===== -BROKER = "141.75.223.13" -PORT = 1883 -TOPIC = "PREDICTION" -USERNAME = None -PASSWORD = None -QOS = 0 -INTERVAL_SECONDS = 5 -# ========================== -# Logging konfigurieren (Console + Datei) -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(levelname)s - %(message)s", - handlers=[ - logging.FileHandler("logs/mqtt_simulator.log"), - logging.StreamHandler(sys.stdout) - ] -) +DEFAULT_BROKER = "141.75.223.13" +DEFAULT_PORT = 1883 +DEFAULT_TOPIC = "PREDICTION" +DEFAULT_QOS = 0 +DEFAULT_INTERVAL_SECONDS = 5.0 +DEFAULT_START_ID = 1 +DEFAULT_CLIENT_ID = "mqtt-simulator" +DEFAULT_LOG_FILE = "logs/mqtt_simulator.log" + + +def env_int(name, default): + value = os.getenv(name) + if value is None or value.strip() == "": + return default + + try: + return int(value) + except ValueError: + return default + + +def env_float(name, default): + value = os.getenv(name) + if value is None or value.strip() == "": + return default + + try: + return float(value) + except ValueError: + return default + + +def build_args(): + parser = argparse.ArgumentParser( + description="MQTT simulator for binary prediction events." + ) + + parser.add_argument("--broker", default=os.getenv("MQTT_SIM_BROKER", DEFAULT_BROKER)) + parser.add_argument("--port", type=int, default=env_int("MQTT_SIM_PORT", DEFAULT_PORT)) + parser.add_argument("--topic", default=os.getenv("MQTT_SIM_TOPIC", DEFAULT_TOPIC)) + parser.add_argument("--username", default=os.getenv("MQTT_SIM_USERNAME")) + parser.add_argument("--password", default=os.getenv("MQTT_SIM_PASSWORD")) + parser.add_argument("--qos", type=int, choices=[0, 1, 2], default=env_int("MQTT_SIM_QOS", DEFAULT_QOS)) + parser.add_argument( + "--interval-seconds", + type=float, + default=env_float("MQTT_SIM_INTERVAL_SECONDS", DEFAULT_INTERVAL_SECONDS), + ) + parser.add_argument("--start-id", type=int, default=env_int("MQTT_SIM_START_ID", DEFAULT_START_ID)) + parser.add_argument("--client-id", default=os.getenv("MQTT_SIM_CLIENT_ID", DEFAULT_CLIENT_ID)) + parser.add_argument("--log-file", default=os.getenv("MQTT_SIM_LOG_FILE", DEFAULT_LOG_FILE)) + + return parser.parse_args() + + +def configure_logging(log_file): + log_dir = os.path.dirname(log_file) + if log_dir: + os.makedirs(log_dir, exist_ok=True) + + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s", + handlers=[ + logging.FileHandler(log_file), + logging.StreamHandler(sys.stdout), + ], + force=True, + ) def on_connect(client, userdata, flags, rc): if rc == 0: logging.info("Erfolgreich mit Broker verbunden") else: - logging.error(f"Verbindung fehlgeschlagen mit Code {rc}") + logging.error("Verbindung fehlgeschlagen mit Code %s", rc) def main(): + args = build_args() + configure_logging(args.log_file) + logging.info("Python script gestartet") + logging.info( + "Konfiguration: broker=%s port=%s topic=%s qos=%s interval=%s", + args.broker, + args.port, + args.topic, + args.qos, + args.interval_seconds, + ) - client = mqtt.Client() + client = mqtt.Client(client_id=args.client_id) - if USERNAME and PASSWORD: - client.username_pw_set(USERNAME, PASSWORD) + if args.username: + client.username_pw_set(args.username, args.password) client.on_connect = on_connect try: - logging.info(f"Verbinde mit Broker {BROKER}:{PORT}") - client.connect(BROKER, PORT, 60) + logging.info("Verbinde mit Broker %s:%s", args.broker, args.port) + client.connect(args.broker, args.port, 60) client.loop_start() logging.info("Starte kontinuierliches Senden...") - current_id = 1 + current_id = args.start_id while True: payload = { "valid": True, "_id": current_id, - "prediction": random.randint(0, 1) + "prediction": random.randint(0, 1), } json_payload = json.dumps(payload) - - client.publish(TOPIC, json_payload, qos=QOS) - logging.info(f"Gesendet an '{TOPIC}': {json_payload}") + client.publish(args.topic, json_payload, qos=args.qos) + logging.info("Gesendet an '%s': %s", args.topic, json_payload) current_id += 1 - - time.sleep(INTERVAL_SECONDS) + time.sleep(args.interval_seconds) except KeyboardInterrupt: logging.info("Publisher manuell beendet") - - client.loop_stop() - client.disconnect() sys.exit(0) - except Exception as e: + except Exception: logging.exception("Unerwarteter Fehler") sys.exit(1) + finally: + try: + client.loop_stop() + client.disconnect() + except Exception: + pass + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/src/main/resources/scripts/start_avatar.ps1 b/src/main/resources/scripts/start_avatar.ps1 index 383b71b..7f8e0ac 100644 --- a/src/main/resources/scripts/start_avatar.ps1 +++ b/src/main/resources/scripts/start_avatar.ps1 @@ -1,22 +1,64 @@ -$pidDir = "C:\Users\Student\Documents\Dannick\avatar" +param( + [string]$PidDir = $env:VGA_PID_DIR, + [string]$UeExe = $env:VGA_UE_EXE, + [string]$UeArgs = $env:VGA_UE_ARGS, + [string]$SignallingBat = $env:VGA_SIGNALLING_BAT, + [string]$UeWorkingDir = $env:VGA_UE_WORKDIR, + [int]$SignallingStartupDelaySeconds = $(if ($env:VGA_SIGNALLING_DELAY) { [int]$env:VGA_SIGNALLING_DELAY } else { 5 }), + [switch]$ShowSignallingWindow +) -$ueExe = "C:\Users\Student\Documents\Dannick\avatar\Windows\Prototyp1.exe" -$ueArgs = "-PixelStreamingURL=ws://127.0.0.1 -RenderOffscreen -PixelStreamingWebRTCDisableTransmitAudio" +if (-not $PidDir) { + $PidDir = "C:\Users\Student\Documents\Dannick\avatar" +} -$signallingBat = "C:\Users\Student\Documents\Dannick\avatar\Windows\Prototyp1\Samples\PixelStreaming\WebServers\SignallingWebServer\platform_scripts\cmd\start_with_stun.bat" +if (-not $UeExe) { + $UeExe = "C:\Users\Student\Documents\Dannick\avatar\Windows\Prototyp1.exe" +} -$ueWorkingDir = "C:\Users\Student\Documents\Dannick\avatar\Windows" +if (-not $UeArgs) { + $UeArgs = "-PixelStreamingURL=ws://127.0.0.1 -RenderOffscreen -PixelStreamingWebRTCDisableTransmitAudio" +} -$signalling = Start-Process -FilePath $signallingBat -PassThru -WindowStyle Hidden +if (-not $SignallingBat) { + $SignallingBat = "C:\Users\Student\Documents\Dannick\avatar\Windows\Prototyp1\Samples\PixelStreaming\WebServers\SignallingWebServer\platform_scripts\cmd\start_with_stun.bat" +} -Start-Sleep -Seconds 5 +if (-not $UeWorkingDir) { + $UeWorkingDir = Split-Path -Path $UeExe -Parent +} -$ue = Start-Process ` - -FilePath $ueExe ` - -ArgumentList $ueArgs ` - -WorkingDirectory $ueWorkingDir ` - -PassThru +if (-not (Test-Path -LiteralPath $SignallingBat)) { + throw "Signalling script not found: $SignallingBat" +} -$ue.Id | Out-File "$pidDir\unreal.pid" -Encoding ascii -$signalling.Id | Out-File "$pidDir\signalling.pid" -Encoding ascii +if (-not (Test-Path -LiteralPath $UeExe)) { + throw "Unreal executable not found: $UeExe" +} + +if (-not (Test-Path -LiteralPath $UeWorkingDir)) { + throw "Unreal working directory not found: $UeWorkingDir" +} + +New-Item -ItemType Directory -Path $PidDir -Force | Out-Null + +$signallingWindowStyle = if ($ShowSignallingWindow.IsPresent) { "Normal" } else { "Hidden" } +$signalling = Start-Process -FilePath $SignallingBat -PassThru -WindowStyle $signallingWindowStyle + +Start-Sleep -Seconds $SignallingStartupDelaySeconds + +$ueStartParams = @{ + FilePath = $UeExe + WorkingDirectory = $UeWorkingDir + PassThru = $true +} + +if ($UeArgs) { + $ueStartParams["ArgumentList"] = $UeArgs +} + +$ue = Start-Process @ueStartParams + +$ue.Id | Out-File (Join-Path $PidDir "unreal.pid") -Encoding ascii +$signalling.Id | Out-File (Join-Path $PidDir "signalling.pid") -Encoding ascii