feat(config): externalize runtime paths and script settings for local setup

This commit is contained in:
Niklas Aumueller 2026-03-06 15:09:15 +01:00
parent 2a12f5aaa5
commit 16bcf7f06d
6 changed files with 289 additions and 54 deletions

View File

@ -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.topic` | MQTT topic to subscribe to | `PREDICTION` |
| `mqtt_sim.enabled` | Enables simulator startup | `false` | | `mqtt_sim.enabled` | Enables simulator startup | `false` |
| `mqtt_sim.script` | Simulator script path | `src/main/resources/scripts/mqtt_simulator.py` | | `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` | | `animation.output.path` | Generated animation state file path | `data/animation.json` |
| `unreal.enabled` | Enables Unreal startup flow | `true` | | `unreal.enabled` | Enables Unreal startup flow | `true` |
| `unreal.executable` | PowerShell script to start Unreal process | absolute path | | `unreal.executable` | PowerShell script to start Unreal process | absolute path |
| `unreal.signalling_server.script` | Signalling server BAT file path | 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.pid.file` | Unreal PID file for shutdown cleanup | absolute path |
| `unreal.signalling.pid.file` | Signalling 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: Notes:
- Paths can be provided with or without wrapping quotes; startup sanitizes surrounding quotes. - 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. - 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` ## `logger.properties`
@ -39,3 +56,27 @@ Notes:
- `src/main/resources/scripts/mqtt_simulator.py` - `src/main/resources/scripts/mqtt_simulator.py`
- `src/main/resources/scripts/start_avatar.ps1` - `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`

View File

@ -85,12 +85,14 @@ Main config: `src/main/resources/config/application.properties`
- `mqtt.topic`: subscribed topic (default `PREDICTION`) - `mqtt.topic`: subscribed topic (default `PREDICTION`)
- `mqtt_sim.enabled`: start simulator process on app startup - `mqtt_sim.enabled`: start simulator process on app startup
- `mqtt_sim.script`: simulator script path - `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 - `animation.output.path`: path for generated animation JSON file
- `unreal.enabled`: start Unreal-related processes - `unreal.enabled`: start Unreal-related processes
- `unreal.executable`: PowerShell script path for Unreal start - `unreal.executable`: PowerShell script path for Unreal start
- `unreal.signalling_server.script`: signalling server batch path - `unreal.signalling_server.script`: signalling server batch path
- `unreal.pid.file`: PID file used for Unreal shutdown cleanup - `unreal.pid.file`: PID file used for Unreal shutdown cleanup
- `unreal.signalling.pid.file`: PID file used for signalling 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` 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`). - Animation output path is now config-driven (`animation.output.path`).
- MQTT broker URL/client id are config-driven (`mqtt.broker.url`, `mqtt.client.id`). - 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`). - 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()`. - 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). - The signalling server process startup in `ProcessManagerService` is prepared but currently not launched (`pb.start()` commented).

View File

@ -104,6 +104,7 @@ public class ProcessManagerService {
ProcessBuilder pb = new ProcessBuilder(python, script); ProcessBuilder pb = new ProcessBuilder(python, script);
pb.redirectErrorStream(true); pb.redirectErrorStream(true);
configureMqttSimulatorEnvironment(pb);
pythonProcess = processLauncher.launch(pb); pythonProcess = processLauncher.launch(pb);
@ -176,6 +177,8 @@ public class ProcessManagerService {
unrealPsScript unrealPsScript
); );
configureUnrealScriptEnvironment(pb);
unrealProcess = processLauncher.launch(pb); unrealProcess = processLauncher.launch(pb);
//pb.directory(new File(exe).getParentFile()); //pb.directory(new File(exe).getParentFile());
@ -311,4 +314,69 @@ public class ProcessManagerService {
return trimmed; 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);
}
}
} }

View File

@ -15,6 +15,16 @@ mqtt.topic=PREDICTION
# ===== MQTT SIMULATOR ===== # ===== MQTT SIMULATOR =====
mqtt_sim.enabled=false mqtt_sim.enabled=false
mqtt_sim.script=src/main/resources/scripts/mqtt_simulator.py 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 =====
animation.output.path=data/animation.json 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.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.pid.file=C:\\Users\\Student\\Documents\\Dannick\\avatar\\unreal.pid
unreal.signalling.pid.file=C:\\Users\\Student\\Documents\\Dannick\\avatar\\signalling.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

View File

@ -1,29 +1,82 @@
import argparse
import json import json
import logging
import os
import random
import sys
import time
import paho.mqtt.client as mqtt 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) 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( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s", format="%(asctime)s - %(levelname)s - %(message)s",
handlers=[ handlers=[
logging.FileHandler("logs/mqtt_simulator.log"), logging.FileHandler(log_file),
logging.StreamHandler(sys.stdout) logging.StreamHandler(sys.stdout),
] ],
force=True,
) )
@ -31,55 +84,68 @@ def on_connect(client, userdata, flags, rc):
if rc == 0: if rc == 0:
logging.info("Erfolgreich mit Broker verbunden") logging.info("Erfolgreich mit Broker verbunden")
else: else:
logging.error(f"Verbindung fehlgeschlagen mit Code {rc}") logging.error("Verbindung fehlgeschlagen mit Code %s", rc)
def main(): def main():
args = build_args()
configure_logging(args.log_file)
logging.info("Python script gestartet") 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: if args.username:
client.username_pw_set(USERNAME, PASSWORD) client.username_pw_set(args.username, args.password)
client.on_connect = on_connect client.on_connect = on_connect
try: try:
logging.info(f"Verbinde mit Broker {BROKER}:{PORT}") logging.info("Verbinde mit Broker %s:%s", args.broker, args.port)
client.connect(BROKER, PORT, 60) client.connect(args.broker, args.port, 60)
client.loop_start() client.loop_start()
logging.info("Starte kontinuierliches Senden...") logging.info("Starte kontinuierliches Senden...")
current_id = 1 current_id = args.start_id
while True: while True:
payload = { payload = {
"valid": True, "valid": True,
"_id": current_id, "_id": current_id,
"prediction": random.randint(0, 1) "prediction": random.randint(0, 1),
} }
json_payload = json.dumps(payload) json_payload = json.dumps(payload)
client.publish(args.topic, json_payload, qos=args.qos)
client.publish(TOPIC, json_payload, qos=QOS) logging.info("Gesendet an '%s': %s", args.topic, json_payload)
logging.info(f"Gesendet an '{TOPIC}': {json_payload}")
current_id += 1 current_id += 1
time.sleep(args.interval_seconds)
time.sleep(INTERVAL_SECONDS)
except KeyboardInterrupt: except KeyboardInterrupt:
logging.info("Publisher manuell beendet") logging.info("Publisher manuell beendet")
client.loop_stop()
client.disconnect()
sys.exit(0) sys.exit(0)
except Exception as e: except Exception:
logging.exception("Unerwarteter Fehler") logging.exception("Unerwarteter Fehler")
sys.exit(1) sys.exit(1)
finally:
try:
client.loop_stop()
client.disconnect()
except Exception:
pass
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -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" if (-not $PidDir) {
$ueArgs = "-PixelStreamingURL=ws://127.0.0.1 -RenderOffscreen -PixelStreamingWebRTCDisableTransmitAudio" $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 ` if (-not (Test-Path -LiteralPath $SignallingBat)) {
-FilePath $ueExe ` throw "Signalling script not found: $SignallingBat"
-ArgumentList $ueArgs ` }
-WorkingDirectory $ueWorkingDir `
-PassThru
$ue.Id | Out-File "$pidDir\unreal.pid" -Encoding ascii if (-not (Test-Path -LiteralPath $UeExe)) {
$signalling.Id | Out-File "$pidDir\signalling.pid" -Encoding ascii 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