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_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`

View File

@ -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).

View File

@ -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);
}
}
}

View File

@ -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

View File

@ -1,29 +1,82 @@
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)
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("logs/mqtt_simulator.log"),
logging.StreamHandler(sys.stdout)
]
logging.FileHandler(log_file),
logging.StreamHandler(sys.stdout),
],
force=True,
)
@ -31,55 +84,68 @@ 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()

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"
$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