Skip to content
Zurück zur Projektübersicht
In Arbeit 2026 — heute · 9 Min. Lesezeit

MSFS VFR Coach

Virtueller Fluglehrer für Microsoft Flight Simulator 2024 — objektives Manöver-Scoring nach EASA-Toleranzen, deterministisches Replay und Prüfungsmodus für deutsche PPL-Schüler

Trainings-Add-on für Microsoft Flight Simulator 2024, das deutschen PPL-Schülern einen virtuellen Fluglehrer an die Seite stellt: objektives Manöver-Scoring nach EASA-Skill-Test-Toleranzen, deutsches Sprach-Coaching, Debriefing mit Replay sowie Trainings- und Prüfungsmodus. Eine Out-of-Process-.NET-10-App liest 30-Hz-Telemetrie über SimConnect, erkennt Flugphasen und Manöver über Zustandsautomaten, bewertet sie gegen JSON-definierte Toleranzregeln und zeichnet jede Session in SQLite auf — deterministisch wiederabspielbar, sodass die gesamte Scoring-Pipeline ohne laufenden Simulator getestet werden kann. Ein dünnes In-Sim-Panel (TypeScript, WebSocket) zeigt nur, was der Server ihm erlaubt.

01 Problem

PPL-Flugstunden kosten 200–300 € pro Stunde, und das Üben zwischen den Stunden findet für viele Schüler im Heimsimulator statt — ohne Feedback. MSFS 2024 sagt einem nicht, ob die Steilkurve innerhalb der Prüfungstoleranzen lag (±150 ft Höhe, ±5° Querneigung, ±10 kt Fahrt), ob der Langsamflug sauber stabilisiert war oder warum die Landung hart wurde. Bestehende Tools sind englischsprachig, auf Airliner-IFR fokussiert oder reine Landungs-Bewerter ohne Methodik. Was fehlte: ein Werkzeug, das wie ein Fluglehrer nach EASA-Maßstäben bewertet, auf Deutsch coacht — und dessen Bewertung fair ist, also kommandierte Übergänge (Ein- und Ausleiten) nicht als Abweichung bestraft.

02 Lösung

Out-of-Process-Architektur nach Asobos offizieller Empfehlung: eine externe .NET-10-Desktop-App verbindet sich per SimConnect, führt Phasendetektor, Manöver-Evaluator und Scoring aus und hostet einen lokalen WebSocket-Server. Das In-Sim-Panel (Coherent GT, also Chrome 49) bleibt ein dünner Renderer. Manöver sind reine JSON-Daten — Entry-/Exit-Bedingungen, Toleranzregeln, deutsche Callouts — die Engine ist generisch. Jede Session wird mit 10 Hz in SQLite aufgezeichnet und treibt deterministisch dieselbe Pipeline wieder an: Regressionstests und Debriefing laufen ohne Simulator.

03 Ergebnis

Der M0/M1-Slice steht: Monorepo mit CI, Telemetrie-Pipeline, Phasendetektor, datengetriebene Scoring-Engine (Steilkurve, Langsamflug, Stall), SQLite-Recorder mit deterministischem Replay und das Panel-Protokoll inklusive serverseitigem Prüfungsmodus — 47 Tests grün, CI ohne MSFS-SDK-Abhängigkeit. Als Nächstes: Live-Verifikation gegen den Simulator, Sprachausgabe (Piper TTS), WPF-Debriefing-UI und der Platzrunden-/Lande-Evaluator.

Ein Fluglehrer als Pipeline

Der VFR Coach besteht aus einer Out-of-Process-Desktop-App (.NET 10) und einem dünnen In-Sim-Panel. Die App liest Telemetrie über SimConnect, schickt sie durch einen Phasendetektor (Cold → Taxi → Takeoff Roll → Climb → Airwork/Pattern → Approach → Flare → Touchdown → Rollout — jede Transition über eine SustainedCondition entprellt) und durch Manöver-Evaluatoren, die aus JSON-Definitionen instanziiert werden. Das Panel im Simulator bekommt fertige Frames über WebSocket und rendert sie — mehr nicht.

Was ein Manöver ist, steht nicht im Code, sondern in packages/content/maneuvers/:

packages/content/maneuvers/steep_turn_45.json json
{
  "id": "steep_turn_45",
  "name": "Steilkurve 45°",
  "easaRef": "Skill test section 2 / Exercise 15 (Advanced Turning)",
  "entry":  { "bankAbsMinDeg": 30, "sustainSec": 2 },
  "target": { "bankDeg": 45, "turnDeg": 360 },
  "exit":   { "bankAbsMaxDeg": 10, "minTurnedDeg": 330, "timeoutSec": 240 },
  "rules": [
    { "metric": "altitude", "mode": "devFromEntry",   "tol": 150, "unit": "ft",  "weight": 0.35 },
    { "metric": "bankAbs",  "mode": "devFromTarget",  "tol": 5,   "unit": "deg", "weight": 0.25 },
    { "metric": "ias",      "mode": "devFromEntry",   "tol": 10,  "unit": "kt",  "weight": 0.20 },
    { "metric": "heading",  "mode": "rolloutVsEntry", "tol": 10,  "unit": "deg", "weight": 0.20 }
  ],
  "callouts": {
    "altitude.low":  "Du verlierst Höhe — etwas ziehen und Leistung nachführen.",
    "bankAbs.high":  "Querneigung über 50 Grad — etwas aufrichten."
  }
}

Die Toleranzen sind die der EASA-Praxisprüfung, die Bewertung folgt derselben Logik: Zeit innerhalb der Toleranz, Spitzenabweichung und RMS gehen gewichtet in den Score ein — aber eine anhaltende Exkursion (> 5 s außerhalb) oder eine grobe (> 2× Toleranz) bedeutet „nicht bestanden”, egal wie gut der Rest war. Genau wie beim Prüfer.

Tiefe Einblicke

Capture-Semantik: Übergänge sind keine Abweichungen

Das harte fachliche Problem im Scoring: Wer eine saubere Steilkurve fliegt, verletzt beim Eindrehen (0° → 45°) und Ausleiten (45° → 0°) zwangsläufig die ±5°-Bank-Toleranz. Naives Tracking würde jedes perfekte Manöver durchfallen lassen. Die Lösung: ziel-relative Regeln zählen erst ab erstem Erreichen des Zielbands („Capture”), Out-of-Band-Zeit landet danach in einem Pending-Puffer — committet wird sie nur, wenn der Wert ins Band zurückkehrt (echte Exkursion mitten im Manöver). Das abschließende Ausleiten kehrt nie zurück und wird verworfen: eine beabsichtigte Exit-Transition. Das ist die EASA-Philosophie in Code — bewertet wird das Halten des Ziels, nicht der Weg dorthin.

apps/desktop/src/VfrCoach.Scoring/DeviationTracker.cs csharp
public void TrackTarget(string metric, double deviation, double tol, double dt)
{
    var acc = GetOrAdd(metric);
    acc.LastDeviation = deviation;
    var inBand = Math.Abs(deviation) <= tol;

    if (!acc.Captured)
    {
        if (!inBand) return; // still rolling in / decelerating toward the target
        acc.Captured = true;
    }

    if (inBand)
    {
        if (acc.PendingSec > 0) CommitPending(acc);
        Commit(acc, deviation, tol, dt);
    }
    else
    {
        acc.PendingSec += dt;
        acc.PendingSumSqDt += deviation * deviation * dt;
        if (Math.Abs(deviation) > acc.PendingMaxAbs) acc.PendingMaxAbs = Math.Abs(deviation);
    }
}

Entry-Referenz per Backfill

Die Entry-Bedingung „Bank ≥ 30° für 2 Sekunden” greift erst, wenn das Flugzeug schon ~50° weitergedreht hat. Würde der Evaluator dort seine Referenz setzen, verglichen alle devFromEntry-Regeln (Höhe, Fahrt) und der Rollout-Kurs gegen einen Zustand mitten im Manöver — jede Bewertung wäre falsch. Stattdessen puffert der Armed-Zustand die letzten 45 Sekunden, und bei Aktivierung wandert der Evaluator rückwärts zum letzten Wings-Level-Sample vor dem Eindrehen. Von dort aus wird der gepufferte Übergang durch das Active-Tracking nachgespielt, sodass Drehwinkel-Integration und Abweichungen am aerodynamisch korrekten Startpunkt beginnen.

apps/desktop/src/VfrCoach.Scoring/ManeuverEvaluator.cs csharp
private void Activate()
{
    var refIndex = FindEntryReferenceIndex();
    _entrySample = _armedBuffer[refIndex];
    _turnedDeg = 0;
    _activeSince = _entrySample.SimTimeSec;
    Current = State.Active;
    Started?.Invoke(new ManeuverStarted(def.Id, _entrySample.SimTimeSec));

    // Replay the buffered transition (roll-in, deceleration) through the Active
    // tracking so deviations and turn integration start at the true entry.
    _backfilling = true;
    _prev = _entrySample;
    for (var i = refIndex + 1; i < _armedBuffer.Count && Current == State.Active; i++)
    {
        var sample = _armedBuffer[i];
        var dt = sample.SimTimeSec - _prev.SimTimeSec;
        if (dt <= 0 || dt > MaxPlausibleDtSec)
        {
            _prev = sample;
            continue;
        }
        StepActive(sample);
    }
    _backfilling = false;
    _armedBuffer.Clear();
}

Deterministisches Replay: Tests ohne Simulator

Die Replay-Quelle liest aufgezeichnete Samples in Sim-Zeit-Reihenfolge aus SQLite und publiziert sie ohne Wall-Clock-Pacing auf denselben Telemetrie-Bus, den auch SimConnect bedient. Phasendetektor und Evaluator rechnen ihr dt aus SimTimeSec — sie können nicht unterscheiden, ob die Daten live oder aus der Datenbank kommen, nur dass das Replay so schnell läuft, wie SQLite Zeilen liefert. Damit ist der Simulator als Test-Abhängigkeit eliminiert: 47 Regressionstests fahren echte und synthetische Flüge in Millisekunden durch die identische Scoring-Pipeline, und die CI braucht das MSFS-SDK nicht einmal installiert.

apps/desktop/src/VfrCoach.Persistence/SqliteReplaySource.cs csharp
/// <summary>
/// Replays a recorded session through the telemetry bus — deterministically and as
/// fast as consumers accept (no wall-clock pacing; all downstream timing derives
/// from SimTimeSec). This is how scoring regression tests and the debriefing
/// replay work without the sim.
/// </summary>
public sealed class SqliteReplaySource(FlightDatabase db, long sessionId) : ITelemetrySource
{
    public Task RunAsync(TelemetryBus bus, CancellationToken ct)
    {
        foreach (var sample in ReadSamples())
        {
            ct.ThrowIfCancellationRequested();
            bus.Publish(sample);
        }
        bus.Complete();
        return Task.CompletedTask;
    }
}

Prüfungsmodus: Stripping im Server, nicht im Client

Das In-Sim-Panel ist JavaScript in Coherent GT — aus Sicht der Integrität ein nicht vertrauenswürdiger Client, den jeder Schüler lokal editieren kann. Deshalb wird der Prüfungsmodus nicht per CSS versteckt, sondern an genau einer Stelle im Server erzwungen: Der Frame-Builder schreibt Gauges, Toleranzen und Hinweise gar nicht erst ins Frame, und Manöver-Ergebnisse liefert er im Prüfungsmodus als null — Feedback gibt es erst im Debriefing, wie im echten Skill Test. Ein manipuliertes Panel kann nichts einblenden, was es nie empfangen hat.

apps/desktop/src/VfrCoach.Server/PanelFrameBuilder.cs csharp
/// <summary>
/// Builds the frames pushed to the panel. THE exam-mode enforcement point (4.2):
/// gauges, tolerances and hints are stripped here, server-side — a tampered panel
/// cannot show live values in exam mode because it never receives them.
/// </summary>
public static LiveFrame BuildLive(
    string? exerciseId,
    FlightPhase phase,
    IModePolicy mode,
    IReadOnlyList<LiveDeviation> liveDeviations,
    string? hint)
{
    var showLive = mode.ShowLiveGauges;
    return new LiveFrame
    {
        Exercise = exerciseId,
        Phase = phase.ToString().ToUpperInvariant(),
        Mode = mode.Mode.ToString().ToUpperInvariant(),
        AidsLevel = mode.AidsLevel.ToString().ToUpperInvariant(),
        Gauges = showLive && liveDeviations.Count > 0
            ? liveDeviations.ToDictionary(d => d.Metric, d => new GaugeDto(Round(d.Deviation), d.Tol, d.Unit))
            : null,
        Hint = showLive ? hint : null,
    };
}

Stand und nächste Schritte

Der M0/M1-Slice ist abgeschlossen: Monorepo mit CI, Telemetrie-Pipeline, Phasendetektor, Scoring-Engine mit drei Manövern (Steilkurve, Langsamflug, Stall), SQLite-Recorder mit deterministischem Replay und das WebSocket-Panel-Protokoll inklusive Prüfungsmodus — 47 Tests grün. Die demo- und replay-Kommandos des CLI-Hosts fahren synthetische und aufgezeichnete Flüge durch die komplette Pipeline, ganz ohne Simulator.

Als Nächstes stehen die Spikes gegen den echten Sim an (WebSocket-Verhalten in Coherent GT, Objekt-Spawning-Limits für die 3D-Hilfen), deutsche Sprachausgabe über Piper TTS, die WPF-Debriefing-UI als zweiter Head über dieselben Module — und der Platzrunden-/Lande-Evaluator, das Herzstück für die Zielgruppe.

05 Entscheidungen

Schlüssel-Entscheidungen

01

Out-of-Process statt In-Sim-WASM

Die gesamte Logik läuft in einer externen .NET-App, nicht als WASM-Modul im Simulator. Das ist Asobos offiziell empfohlenes Muster — und es entkoppelt die Pipeline von den Grenzen der In-Sim-Umgebung: volle .NET-Bibliotheken, SQLite, TTS, echtes Debugging. Das Panel im Sim bleibt ein dünner WebSocket-Client, der gerendert, aber nie gerechnet hat.

02

Manöver als Daten, nicht als Code

Eine Steilkurve ist eine JSON-Datei: Entry-Bedingung (Bank ≥ 30° für 2 s), Ziel (45°, 360° Drehung), Exit, gewichtete Toleranzregeln und deutsche Callouts. Der Evaluator ist ein generischer Zustandsautomat (Idle → Armed → Active → Done). Neue Übungen — Langsamflug, Stall, später Platzrunde — sind Content, kein Engine-Umbau.

03

Deterministisches Replay als Test-Fundament

Jede Session wird mit 10 Hz in SQLite aufgezeichnet. Die Replay-Quelle publiziert die Samples ohne Wall-Clock-Pacing auf denselben Telemetrie-Bus wie SimConnect — alles Downstream-Timing hängt an der Sim-Zeit. Aufgezeichnete und synthetische Flüge laufen so in Millisekunden durch die echte Scoring-Pipeline: 47 Tests, kein Simulator, kein SDK in der CI.

04

Prüfungsmodus serverseitig erzwungen

Im Prüfungsmodus werden Live-Gauges, Toleranzen und Hinweise nicht im Panel ausgeblendet, sondern im Server gar nicht erst ins Frame geschrieben. Ein manipuliertes Panel kann nichts anzeigen, was es nie empfangen hat. Ergebnisse gibt es erst im Debriefing — wie bei der echten Prüfung.

05

Capture-Semantik und Entry-Backfill für faires Scoring

Naives Abweichungs-Tracking würde jede saubere Steilkurve durchfallen lassen: Das Eindrehen von 0° auf 45° verletzt die ±5°-Bank-Toleranz sekundenlang. Ziel-relative Regeln zählen deshalb erst ab erstem Erreichen des Zielbands, das Ausleiten wird als beabsichtigte Exit-Transition verworfen — und die Entry-Referenz wird per Backfill auf den letzten Wings-Level-Zustand vor dem Eindrehen gesetzt, sonst misst der Rollout-Vergleich gegen einen um ~50° verdrehten Kurs.

06

Plain ADO statt EF Core im Recorder-Hot-Path

Der Recorder puffert 64 Samples und schreibt sie als parametrisierte Bulk-Inserts in einer Transaktion direkt über Microsoft.Data.Sqlite. EF Core mit Change-Tracking hat in einem 10-Hz-Hot-Path nichts verloren — für App-Queries kann es später dazukommen, das Schema bleibt unverändert.

07 Was hängenbleibt

  • 01 Sim-Zeit statt Wall-Clock als einzige Zeitbasis zahlt sich dreifach aus: Pause, Zeitbeschleunigung und Replay-Geschwindigkeit sind automatisch korrekt — und jeder Konsument validiert dt gegen ein Plausibilitätsfenster, weil Sim-Reloads und Slew sonst absurde Deltas liefern.
  • 02 MSFS 2024 ist eine instabile Laufzeitumgebung, und man plant besser damit: SIMCONNECT_RECV_ID_QUIT kommt inkonsistent an, SimConnect-Crashes wurden erst in Punkt-Releases gefixt, und Objekt-Spawning verursacht ab ~45–60 Injektionen reproduzierbar CTDs. Defensive dt-Checks und empirisch dokumentierte Limits sind kein Übereifer, sondern Notwendigkeit.
  • 03 Das In-Sim-Panel rendert in Coherent GT — effektiv Chrome 49 von 2016, ohne WebGL, mit bekannten WebSocket-Leaks. Die richtige Konsequenz ist architektonisch: so wenig Logik wie möglich dort hineinlegen und das Panel als dünnen, reconnect-fähigen Client bauen.
  • 04 Fairness im Scoring ist das eigentliche Fachproblem, nicht die Toleranzen selbst. EASA bewertet das Halten des Ziels, nicht den Übergang dorthin — das sauber abzubilden (Capture-Semantik, Entry-Backfill, Pending-Puffer für Exkursionen) hat mehr Iterationen gekostet als die gesamte restliche Pipeline.
  • 05 Bei Code-Übernahme aus fremden Repos lohnt eine harte Lizenz-Linie von Tag 1: MIT/Apache-Projekte (SkyDolly, fs2ff, FsConnect) dürfen als Code-Quelle dienen, GPL/LGPL-Projekte ausschließlich als Lern-Referenz. Das nachträglich zu entwirren wäre teurer als jede Eigenentwicklung.
Kontakt

Klingt vertraut?

Schreiben Sie mir kurz, worum es geht. Ich melde mich innerhalb von 24 Stunden mit einer ehrlichen Einschätzung — auch wenn ich nicht der richtige Partner bin.