

Was es so neues gibt, ist hier bei Heise beschrieben.
Dann mal ein Java Programm für die Demo von MCP geschrieben, um die Client – Serververbindung per MCP zu testen.
Die Anwendung verdeutlicht das zentrale Prinzip von MCP:
Eine KI-Anwendung kann über ein standardisiertes Protokoll auf externe Werkzeuge und Datenquellen zugreifen, ohne dass die jeweilige Funktionalität direkt in der KI-Anwendung implementiert sein muss.
Zum Testen der Client-Server-Kommunikation wurde ein kleines Java-Programm erstellt. Die Demo kommt ohne zusätzliche Bibliotheken aus und verwendet ausschließlich Standardklassen aus dem JDK. Voraussetzung ist Java 21, da im Programm virtuelle Threads verwendet werden.
Das Beispiel besteht aus zwei Komponenten:
-einem einfachen MCP-Server, der auf TCP-Port 8080 auf eingehende Verbindungen wartet,
-einer Swing-Oberfläche, die als MCP-Client beziehungsweise Host dient.
Die Kommunikation orientiert sich dabei an JSON-RPC 2.0. Der Client kann zunächst die verfügbaren Werkzeuge abfragen und anschließend ein Werkzeug mit einem Textargument aufrufen.
Was zeigt die Anwendung?
Die Demo bildet die wesentlichen Schritte einer MCP-Kommunikation vereinfacht nach:
1. Der Server wird gestartet und wartet auf TCP-Verbindungen.
2. Der Client verbindet sich mit localhost:8080 .
3. Mit tools/list fragt der Client die verfügbaren Werkzeuge ab.
4. Der Server meldet das Werkzeug string_transformer .
5. Mit tools/call kann der Client dieses Werkzeug aufrufen.
6. Der Server verarbeitet den übergebenen Text und gibt ihn in Großbuchstaben zurück.
Das registrierte Werkzeug ist bewusst einfach gehalten. Es konvertiert den übergebenen Text in Großbuchstaben. Dadurch lässt sich der Nachrichtenaustausch leicht nachvollziehen, ohne dass die eigentliche Funktionalität durch komplexe Geschäftslogik überlagert wird.
Um mal zu zeigen, wie die KI per MCP auf Server zugreift:
package de.wenzlaff.mcp;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.Font;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
/**
* Didaktische MCP-Demonstration (Zero-Dependencies).
*
*
* Zeigt JSON-RPC 2.0 Nachrichtenaustausch, Capability Discovery und Tool
* Execution.
*
* @author Thomas Wenzlaff
*/
public class SimpleMcpDemo {
private static final int PORT = 8080;
public static void main(String[] args) {
// 1. MCP Server via Java 21 Virtual Thread starten
startMcpServer();
// 2. Swing GUI auf dem Event Dispatch Thread (EDT) initialisieren
SwingUtilities.invokeLater(McpClientGui::new);
}
// =========================================================================
// MCP SERVER (Simuliert eine KI-Werkzeug-Schnittstelle)
// =========================================================================
private static void startMcpServer() {
Thread.ofVirtual().start(() -> {
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
System.out.println("[MCP-Server] Wartet auf eingehende Verbindungen auf Port " + PORT);
while (true) {
Socket clientSocket = serverSocket.accept();
// Pro Client-Verbindung ein leichtgewichtiger Virtual Thread
Thread.ofVirtual().start(() -> handleMcpClient(clientSocket));
}
} catch (IOException e) {
System.err.println("[MCP-Server Fehler] " + e.getMessage());
}
});
}
private static void handleMcpClient(Socket socket) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter writer = new PrintWriter(socket.getOutputStream(), true)) {
String rawJsonRpc;
while ((rawJsonRpc = reader.readLine()) != null) {
String response = processMcpProtocolRequest(rawJsonRpc);
writer.println(response);
}
} catch (IOException e) {
System.out.println("[MCP-Server] Client-Sitzung beendet.");
}
}
// Nachrichten-Dispatcher nach MCP / JSON-RPC 2.0 Spezifikation
private static String processMcpProtocolRequest(String jsonRequest) {
// Primitive Capability Discovery: Client fragt verfügbare Tools ab
if (jsonRequest.contains("\"method\":\"tools/list\"")) {
return """
{"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"string_transformer","description":"Konvertiert Eingabetext in Großbuchstaben"}]}}
"""
.strip();
}
// Primitive Tool Execution: Ausführen der registrierten Funktion
if (jsonRequest.contains("\"method\":\"tools/call\"")) {
String payload = jsonRequest.contains("message")
? jsonRequest.replaceAll(".*\"message\":\"([^\"]+)\".*", "$1")
: "Standard-Payload";
String resultText = payload.toUpperCase();
return """
{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"Server-Transformation: %s"}]}}
""".formatted(resultText).strip();
}
// Fallback: JSON-RPC Standardfehler "Method not found" (-32601)
return "{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32601,\"message\":\"Method not found\"}}";
}
// =========================================================================
// MCP CLIENT / HOST (Swing GUI)
// =========================================================================
static class McpClientGui extends JFrame {
private static final long serialVersionUID = 1L;
private final JTextArea consoleLog = new JTextArea(16, 200);
private final JTextField inputPayload = new JTextField("quick eclipse update check mit mcp", 100);
private PrintWriter outStream;
private BufferedReader inStream;
public McpClientGui() {
super("MCP Host - Swing Inspector");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new BorderLayout(5, 5));
consoleLog.setEditable(false);
consoleLog.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
consoleLog.setBackground(new Color(30, 30, 30));
consoleLog.setForeground(new Color(220, 220, 220));
JPanel controlPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
JButton btnListTools = new JButton("1. tools/list");
JButton btnCallTool = new JButton("2. tools/call");
controlPanel.add(new JLabel("Argument:"));
controlPanel.add(inputPayload);
controlPanel.add(btnListTools);
controlPanel.add(btnCallTool);
add(controlPanel, BorderLayout.NORTH);
add(new JScrollPane(consoleLog), BorderLayout.CENTER);
pack();
setLocationRelativeTo(null);
setVisible(true);
initNetworkConnection();
// Event-Handling: MCP Abfragen generieren
btnListTools.addActionListener(e -> {
String request = """
{"jsonrpc":"2.0","id":1,"method":"tools/list"}
""";
transmitMcpFrame(request);
});
btnCallTool.addActionListener(e -> {
String arg = inputPayload.getText();
String request = """
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"string_transformer","arguments":{"message":"%s"}}}
"""
.formatted(arg);
transmitMcpFrame(request);
});
}
private void initNetworkConnection() {
try {
Socket socket = new Socket("localhost", PORT);
outStream = new PrintWriter(socket.getOutputStream(), true);
inStream = new BufferedReader(new InputStreamReader(socket.getInputStream()));
appendLog("[SYSTEM] TCP-Verbindung zu MCP-Server etabliert (Port " + PORT + ")\n");
} catch (IOException e) {
appendLog("[FEHLER] Serververbindung fehlgeschlagen: " + e.getMessage());
}
}
private void transmitMcpFrame(String jsonFrame) {
String singleLineJson = jsonFrame.replaceAll("\\s+", " ").trim();
appendLog(">>> REQUEST (TX):\n" + singleLineJson);
outStream.println(singleLineJson);
try {
String response = inStream.readLine();
appendLog("<<< RESPONSE (RX):\n" + response + "\n");
} catch (IOException e) {
appendLog("[FEHLER] I/O-Fehler beim Empfang: " + e.getMessage());
}
}
private void appendLog(String message) {
consoleLog.append(message + "\n");
consoleLog.setCaretPosition(consoleLog.getDocument().getLength());
}
}
}
Nach dem start, erscheint das obige Programmfenster wo die Funktionen getestet werden können.
Das einfache Werkzeug zur Textumwandlung kann später problemlos durch realistischere Funktionen ersetzt werden, beispielsweise durch:
-das Auslesen eines Systemstatus,
-die Abfrage von Sensordaten,
-den Zugriff auf eine Datenbank,
-die Suche in Dokumenten,
-die Ausführung definierter Linux-Kommandos,
-oder den Zugriff auf eine eigene REST- beziehungsweise MQTT-Anwendung.
Damit bildet die Demo eine gute Grundlage für weitere Experimente mit Java, MCP, JSON-RPC, virtuellen Threads und selbst entwickelten Serverwerkzeugen.
Hier noch eine Visualisierung der Funktionsweise:

Für Programmierer …

Dann mal los …


]]>– Unterstützung für Java 26, präziseres Statement-Level-Stepping im Debugger und Auto-Fetch für Git-Repositories
– Zusätzlich wurden Java-Refactorings und Code-Hilfen erweitert, etwa für Convert Class to Record und das Umwandeln von Strings in Textblöcke
– Für die tägliche Arbeit praktisch sind auch die neuen globalen Such-Shortcuts sowie die überarbeitete History-Ansicht in Git, die Änderungen aus allen Zweigen standardmäßig einblendet
Das Update ist für Windows, macOS und Linux verfügbar und lässt sich über den Eclipse Installer oder per ZIP installieren; bestehende Installationen können über „Help“ → „Check for Updates“ aktualisiert werden.
Quicktest OK, mehr.

Wie hier beschrieben, hier noch eine Version mit externen Abhängigkeiten, dazu einfach den Filter entfernen.
Dieser Java 21-Code ist eine Stream-Pipeline, die Abhängigkeiten zwischen Java-Paketen analysiert und sie als gerichtete Graphen-Kanten (in der Form “PaketA –> PaketB“ ) in einer Set
void exportDependencyGraphAsPlantUML() throws Exception {
StringBuilder uml = new StringBuilder();
uml.append("@startuml\n");
uml.append("title Taxonomie Architektur (ArchUnit Export with extern Package)\n\n");
// =====================================================
// Knoten (Pakete)
// =====================================================
uml.append("package model {}\n");
uml.append("package service {}\n");
uml.append("package data {}\n");
uml.append("package arch {}\n\n");
// =====================================================
// Abhängigkeiten extrahieren
// =====================================================
Set<String> edges = classes.stream().flatMap(c -> c.getDirectDependenciesFromSelf().stream()).map(dep -> {
String from = dep.getOriginClass().getPackageName();
String to = dep.getTargetClass().getPackageName();
return from + " --> " + to;
}).collect(Collectors.toSet());
// =====================================================
// Kanten schreiben
// =====================================================
for (String edge : edges) {
String cleaned = edge.replace("de.wenzlaff.taxonomie.", "").replace("..", "");
uml.append(cleaned).append("\n");
}
uml.append("\n@enduml");
// =====================================================
// Datei schreiben
// =====================================================
Path output = Path.of("target/taxonomy-architecture.puml");
Files.createDirectories(output.getParent());
Files.writeString(output, uml.toString());
}]]>
Das reduziert die Gefahr veralteter oder inkonsistenter Architekturdiagramme erheblich, da jede Änderung im Code automatisch in der Struktur sichtbar wird.

Ein weiterer Vorteil liegt in der automatisierten Validierung der Architekturregeln durch ArchUnit.
Fehlende Schichttrennung, unerlaubte Abhängigkeiten oder zyklische Beziehungen werden frühzeitig erkannt und können bereits im Build-Prozess verhindert werden. Der PlantUML-Export ergänzt diese Regelprüfung um eine visuelle Ebene, die insbesondere bei komplexeren Systemen die Verständlichkeit der Architektur deutlich verbessert.
Zusätzlich entsteht eine hohe Transparenz über Paket- und Modulabhängigkeiten. Entwickler können auf einen Blick erkennen, wie sich Komponenten gegenseitig beeinflussen und wo potenzielle strukturelle Probleme liegen. Dies unterstützt sowohl die Wartbarkeit als auch die Weiterentwicklung des Systems, da Architekturentscheidungen nachvollziehbar und überprüfbar bleiben. Insgesamt führt dieser Ansatz zu einer stärker kontrollierten, konsistenten und langfristig stabilen Softwarearchitektur.
Automatisch generiert aus Code mal mit Archunit, erzeugt z.B. die obige UML Datei (puml mit Eclipse-Plugin für autom. Rendern) mit folgenden Vorteilen:
– echte Paketabhängigkeiten
– keine manuelle Pflege von UML
– immer aktuell mit jedem Build
Gute Architekturqualität (wichtig)
Dieses Setup erzwingt:
– Dynamische Architekturvisualisierung
– keine veralteten Diagramme mehr
– „Living Architecture“
– CI-fähig
Hier der Java Code im Testpackage am Beispiel der Mammutbäume:
package de.wenzlaff.taxonomie.arch;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
/**
* Erzeugen einer target/taxonomy-archirecture.puml Datei.
*
* @author Thomas Wenzlaff
*
*/
@AnalyzeClasses(packages = "de.wenzlaff.taxonomie")
public class ArchitecturePlantUmlExportTest {
private final JavaClasses classes = new ClassFileImporter().importPackages("de.wenzlaff.taxonomie");
// =========================================================
// 1. Architekturregeln (optional zusätzlich)
// =========================================================
@ArchTest
static final ArchRule model_is_isolated = noClasses().that().resideInAPackage("..model..").should()
.dependOnClassesThat().resideInAPackage("..service..");
// =========================================================
// 2. PlantUML Export Test
// =========================================================
@Test
void exportDependencyGraphAsPlantUML() throws Exception {
StringBuilder uml = new StringBuilder();
uml.append("@startuml\n");
uml.append("title Taxonomie Architektur (ArchUnit Export)\n\n");
// =====================================================
// Knoten (Pakete)
// =====================================================
uml.append("package model {}\n");
uml.append("package service {}\n");
uml.append("package data {}\n");
uml.append("package arch {}\n\n");
// =====================================================
// Abhängigkeiten extrahieren
// =====================================================
Set<String> edges = classes.stream().flatMap(c -> c.getDirectDependenciesFromSelf().stream())
.filter(dep -> dep.getTargetClass().getPackageName().startsWith("de.wenzlaff.taxonomie")).map(dep -> {
String from = dep.getOriginClass().getPackageName();
String to = dep.getTargetClass().getPackageName();
return from + " --> " + to;
}).collect(Collectors.toSet());
// =====================================================
// Kanten schreiben
// =====================================================
for (String edge : edges) {
String cleaned = edge.replace("de.wenzlaff.taxonomie.", "").replace("..", "");
uml.append(cleaned).append("\n");
}
uml.append("\n@enduml");
// =====================================================
// Datei schreiben
// =====================================================
Path output = Path.of("target/taxonomy-architecture.puml");
Files.createDirectories(output.getParent());
Files.writeString(output, uml.toString());
}
}]]>
DAG ist eine Struktur aus Knoten und Verbindungen, bei der jede Verbindung eine feste Richtung hat und keine geschlossenen Schleifen vorkommen.
In diesem Beispiel wird dieser Graph als Baum dargestellt, weil es genau einen Ausgangspunkt gibt und sich die Struktur strikt nach unten in immer feinere Einheiten verzweigt.
Wie läßt sich das in Java darstellen?
Hier die MammutbaumTaxonomie.java:
package de.wenzlaff.taxonomie;
/**
* MammutbaumTaxonomie
*
* @author Thomas Wenzlaff
*
*/
public class MammutbaumTaxonomie {
public static void main(String[] args) {
// ===== Wurzel =====
Taxon pflanzenreich = new Taxon("Pflanzen (Plantae)", TaxonRank.REICH);
Taxon gefaesspflanzen = new Taxon("Gefäßpflanzen (Tracheophyta)", TaxonRank.ABTEILUNG);
Taxon nadelbaeume = new Taxon("Nadelbäume (Pinopsida)", TaxonRank.KLASSE);
Taxon ordnung = new Taxon("Kiefernartige (Pinales)", TaxonRank.ORDNUNG);
Taxon familie = new Taxon("Zypressengewächse (Cupressaceae)", TaxonRank.FAMILIE);
Taxon unterfamilie = new Taxon("Sequoioideae (Mammutbaum-Unterfamilie)", TaxonRank.UNTERFAMILIE);
pflanzenreich.addKind(gefaesspflanzen);
gefaesspflanzen.addKind(nadelbaeume);
nadelbaeume.addKind(ordnung);
ordnung.addKind(familie);
familie.addKind(unterfamilie);
// ===== Gattung 1 =====
Taxon gattungSequoia = new Taxon("Küstenmammutbaum-Gattung (Sequoia)", TaxonRank.GATTUNG);
Taxon kuestenmammutbaum = new Taxon("Küstenmammutbaum (Sequoia sempervirens)", TaxonRank.ART);
gattungSequoia.addKind(kuestenmammutbaum);
// ===== Gattung 2 =====
Taxon gattungSequoiadendron = new Taxon("Riesenmammutbaum-Gattung (Sequoiadendron)", TaxonRank.GATTUNG);
Taxon riesenmammutbaum = new Taxon("Riesenmammutbaum (Sequoiadendron giganteum)", TaxonRank.ART);
gattungSequoiadendron.addKind(riesenmammutbaum);
// ===== Gattung 3 =====
Taxon gattungMetasequoia = new Taxon("Urweltmammutbaum-Gattung (Metasequoia)", TaxonRank.GATTUNG);
Taxon urweltmammutbaum = new Taxon("Urweltmammutbaum (Metasequoia glyptostroboides)", TaxonRank.ART);
gattungMetasequoia.addKind(urweltmammutbaum);
// ===== Einhängen in Unterfamilie =====
unterfamilie.addKind(gattungSequoia);
unterfamilie.addKind(gattungSequoiadendron);
unterfamilie.addKind(gattungMetasequoia);
// ===== Ausgabe =====
print(pflanzenreich, 0);
}
private static void print(Taxon taxon, int level) {
System.out.println(" ".repeat(level) + taxon);
for (Taxon kind : taxon.getKinder()) {
print(kind, level + 1);
}
}
}
Und die dazu gehörige TaxonRank und Taxon Klasse:
package de.wenzlaff.taxonomie;
import java.util.ArrayList;
import java.util.List;
enum TaxonRank {
REICH, // Kingdom
ABTEILUNG, // Division (bei Pflanzen üblich statt Stamm)
KLASSE, // Class
ORDNUNG, // Order
FAMILIE, // Family
UNTERFAMILIE, // Subfamily
TRIBUS, // Tribe (optional, feinere Untergliederung)
GATTUNG, // Genus
ART // Species
}
/**
* MammutbaumTaxonomie
*
* @author Thomas Wenzlaff
*
*/
class Taxon {
private final String name;
private final TaxonRank rank;
private final List<Taxon> kinder = new ArrayList<>();
public Taxon(String name, TaxonRank rank) {
this.name = name;
this.rank = rank;
}
public void addKind(Taxon taxon) {
kinder.add(taxon);
}
public String getName() {
return name;
}
public TaxonRank getRank() {
return rank;
}
public List<Taxon> getKinder() {
return kinder;
}
@Override
public String toString() {
return rank + ": " + name;
}
}
Liefert dann dieses Ergebnis:
REICH: Pflanzen (Plantae)
ABTEILUNG: Gefäßpflanzen (Tracheophyta)
KLASSE: Nadelbäume (Pinopsida)
ORDNUNG: Kiefernartige (Pinales)
FAMILIE: Zypressengewächse (Cupressaceae)
UNTERFAMILIE: Sequoioideae (Mammutbaum-Unterfamilie)
GATTUNG: Küstenmammutbaum-Gattung (Sequoia)
ART: Küstenmammutbaum (Sequoia sempervirens)
GATTUNG: Riesenmammutbaum-Gattung (Sequoiadendron)
ART: Riesenmammutbaum (Sequoiadendron giganteum)
GATTUNG: Urweltmammutbaum-Gattung (Metasequoia)
ART: Urweltmammutbaum (Metasequoia glyptostroboides)
Bilder von Mammutbäumen in Hannover und Langenhagen habe ich hier abgelegt. Das ganze Projekt auf Gitlab Branch v0.0.5.
Download taxo-mamutbaum.pdf
]]>Nun ist aus der Bucht ein ESP32 mit TFT und Touch angekommen, der noch eine Verwendung braucht.
Ok, hier schon mal vorab das Ergebnis, es werden alle Flugzeuge die aus dem osten kommen und landen wollen auf dem ESP32 ausgegeben, sogar in verschiedenen Farben, je nach Höhe:

Gestartet bin ich mit einem Bereich, von 20 km um Langenhagen, um das C++ Programm mit der Arduino IDE zu laufen zu bringen:

Als nächsten Schritt, habe ich mir die koordinaten für ein 2 km Rechteck von der Landebahn in Richtung Osten generiert:

Dann hat mich interessiert, in welchen Winkel die Flugzeuge landen, wenn ich sie sehe, sind sie so ca. 300 m hoch. Die KI sagt:
Spezifikation des Anflugvektors (Glide Path):
Bei einem Endanflug aus östlicher Richtung – was einer Ausrichtung auf die Pisten 27L oder 27R des Flughafens Hannover-Langenhagen (EDDV) entspricht -ist der vertikale Anflugwinkel für kommerzielle Passagierjets auf den Standardwert von 3° kalibriert. Das fand ich interessant. Dann habe ich mir noch eine Tabelle berechnen lassen, hier das Mathematisches Modell des Anflugprofils.
Zur Berechnung der vertikalen Position (Höhe h) des Luftfahrzeugs in Relation zur horizontalen Distanz (d) zum Aufsetzpunkt (Touchdown Zone) wird eine einfache trigonometrische Funktion appliziert. Da der Gleitpfad (Glide Slope) einen konstanten Winkel von 3,0° aufweist, lässt sich die Höhe über Grund (Above Ground Level, AGL) in Metern für jede diskrete Stützstelle berechnen.
Die zugehörige Formel lautet:

Ah, dann mal bis 30 km, bis nach Lehrte die Werte berechnen, und dies Bild mit KI generieren:

Dann beide Bilder zusammen morphen, cool:

Genau das wollte ich wissen.
Nun noch mit KI ein C++ Programm erstellen und in den ESP32 flashen. Hier die erste Version, mit 577 Zeilen. Da ist noch eine Einstellun nötig, die ich noch nie verwendet haben. Und zwar das Paritions Schema: „Huge APP …“ für die anderen ist das Programm zu groß geworden. Hier die Einstellung in der IDE:

Und nun der versprochene Code. Es muss da noch bei Touch, die Details angezeigt werden und einige andere Anpassungen. Aber wie immer, sind meine KI Token abgelaufen bzw. verbraucht 
/**
* FlugMonitor.ino
* ═══════════════════════════════════════════════════════════════════════════
* Zeigt Flugzeuge im ~20-km-Radius um Langenhagen, Niedersachsen an,
* die sich unterhalb von 2000 m Höhe befinden.
*
* Datenquelle : OpenSky Network REST API (authenticated)
* https://opensky-network.org/apidoc
* Display : 240×320 TFT via TFT_eSPI, LVGL 9.x
* Logische Auflösung nach Rotation 270°: 320×240
*
* Benötigte Arduino-Bibliotheken:
* - LVGL >= 9.x (lv_conf.h: LV_FONT_MONTSERRAT_12/14 = 1)
* - TFT_eSPI
* - ArduinoJson >= 6.x
*
* Dateien:
* - FlugMonitor.ino (diese Datei)
* - config.h (WiFi- und OpenSky-Zugangsdaten)
*
* Authentifizierung : OAuth2 Client Credentials (Bearer Token)
* @author Thomas Wenzlaff
*/
#include <lvgl.h>
#include <TFT_eSPI.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <map>
#include <vector>
#include "config.h"
// ────────────────────────────────────────────────────────────────────────────
// Display
// ────────────────────────────────────────────────────────────────────────────
#define SCREEN_WIDTH 240
#define SCREEN_HEIGHT 320
#define DRAW_BUF_SIZE (SCREEN_WIDTH * SCREEN_HEIGHT / 20 * (LV_COLOR_DEPTH / 8))
static uint32_t draw_buf[DRAW_BUF_SIZE / 4];
#define LOGICAL_W 320
#define LOGICAL_H 240
#define TOP_ZONE_H 46 // Header + Trennlinie + Statuszeile
// ────────────────────────────────────────────────────────────────────────────
// Datenstrukturen
// ────────────────────────────────────────────────────────────────────────────
struct FlightInfo {
String icao24;
String callsign;
String country;
float altitude;
float speedKmh;
float heading;
String departure;
String arrival;
};
// ────────────────────────────────────────────────────────────────────────────
// OAuth2-Token-Cache
// ────────────────────────────────────────────────────────────────────────────
static String g_accessToken = "";
static time_t g_tokenExpiry = 0;
// ────────────────────────────────────────────────────────────────────────────
// Flugdaten & Polling-Steuerung
// ────────────────────────────────────────────────────────────────────────────
static std::map<String, FlightInfo> g_cache;
static std::vector<FlightInfo> g_activeFlights;
static int g_lastStatesCode = 0;
static unsigned long g_lastStatesUpdate = 0;
const unsigned long INTERVAL_STATES = 15000UL; // 15 Sekunden
const unsigned long PENALTY_BACKOFF = 60000UL; // 60s Pause bei HTTP 429
static bool g_apiPenaltyActive = false;
static bool g_firstRun = true;
static unsigned long g_detailsPenaltyUntil = 0;
// ────────────────────────────────────────────────────────────────────────────
// LVGL-Objekte
// ────────────────────────────────────────────────────────────────────────────
static lv_obj_t* g_headerLabel = nullptr;
static lv_obj_t* g_statusLabel = nullptr;
static lv_obj_t* g_flightList = nullptr;
static lv_style_t g_styleHeader;
static lv_style_t g_styleStatus;
static lv_style_t g_styleItem;
static lv_style_t g_styleItemDim;
// ════════════════════════════════════════════════════════════════════════════
// Blockierendes Delay mit laufendem LVGL
// ════════════════════════════════════════════════════════════════════════════
static void delayLvgl(unsigned long ms) {
unsigned long start = millis();
while (millis() - start < ms) {
lv_timer_handler();
delay(5);
}
}
// ════════════════════════════════════════════════════════════════════════════
// Statuszeile
// ════════════════════════════════════════════════════════════════════════════
static void setStatus(const char* msg) {
if (g_statusLabel != nullptr) {
lv_label_set_text(g_statusLabel, msg);
lv_timer_handler();
}
Serial.printf("[Status] %s\n", msg);
}
// ════════════════════════════════════════════════════════════════════════════
// WiFi
// ════════════════════════════════════════════════════════════════════════════
static void connectWiFi() {
if (WiFi.status() == WL_CONNECTED) return;
setStatus("WiFi verbinden ...");
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
for (int i = 0; i < 40 && WiFi.status() != WL_CONNECTED; i++) {
delay(500);
lv_timer_handler();
Serial.print('.');
}
Serial.println();
if (WiFi.status() == WL_CONNECTED) {
Serial.printf("[WiFi] IP: %s\n", WiFi.localIP().toString().c_str());
setStatus("WiFi verbunden.");
} else {
setStatus("WiFi FEHLER!");
}
}
// ════════════════════════════════════════════════════════════════════════════
// OAuth2: Bearer-Token holen
// ════════════════════════════════════════════════════════════════════════════
static bool refreshOAuthToken() {
Serial.println("[OAuth2] Token wird erneuert ...");
setStatus("OAuth2-Token holen ...");
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
if (!http.begin(client, OPENSKY_TOKEN_URL)) return false;
http.addHeader("Content-Type", "application/x-www-form-urlencoded");
http.setTimeout(12000);
String body = "grant_type=client_credentials";
body += "&client_id="; body += String(OPENSKY_CLIENT_ID);
body += "&client_secret="; body += String(OPENSKY_CLIENT_SECRET);
int code = http.POST(body);
lv_timer_handler();
if (code != HTTP_CODE_OK) {
char msg[40];
snprintf(msg, sizeof(msg), "Token-Fehler: HTTP %d", code);
setStatus(msg);
http.end();
return false;
}
String json = http.getString();
http.end();
DynamicJsonDocument doc(2048);
if (deserializeJson(doc, json)) { setStatus("Token: JSON-Fehler"); return false; }
const char* token = doc["access_token"];
int expiresIn = doc["expires_in"] | 300;
if (!token || strlen(token) == 0) { setStatus("Token: leer"); return false; }
g_accessToken = String(token);
time_t now;
time(&now);
g_tokenExpiry = now + expiresIn - TOKEN_EXPIRY_BUFFER_S;
Serial.printf("[OAuth2] Token OK. Gültig für %d s.\n", expiresIn);
setStatus("OAuth2-Token OK.");
return true;
}
static bool ensureValidToken() {
time_t now;
time(&now);
if (g_accessToken.isEmpty() || now >= g_tokenExpiry) return refreshOAuthToken();
return true;
}
// ════════════════════════════════════════════════════════════════════════════
// HTTP-GET mit Bearer-Token
// ════════════════════════════════════════════════════════════════════════════
static int httpGet(const String& url, String& body) {
body = "";
if (WiFi.status() != WL_CONNECTED) return -1;
if (!ensureValidToken()) return -3;
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
if (!http.begin(client, url)) return -2;
http.addHeader("Authorization", "Bearer " + g_accessToken);
http.addHeader("Accept", "application/json");
http.setTimeout(12000);
int code = http.GET();
lv_timer_handler();
if (code == HTTP_CODE_OK) {
body = http.getString();
} else {
if (code != 404) {
Serial.printf("[HTTP] Statuscode %d\n", code);
}
if (code == 401) {
g_accessToken = "";
g_tokenExpiry = 0;
}
}
http.end();
return code;
}
// ════════════════════════════════════════════════════════════════════════════
// OpenSky: Zustandsvektoren (Bounding Box)
// ════════════════════════════════════════════════════════════════════════════
static std::vector<FlightInfo> fetchStates() {
std::vector<FlightInfo> result;
String url = String("https://opensky-network.org/api/states/all")
+ "?lamin=" + String(BBOX_LAT_MIN, 2)
+ "&lamax=" + String(BBOX_LAT_MAX, 2)
+ "&lomin=" + String(BBOX_LON_MIN, 2)
+ "&lomax=" + String(BBOX_LON_MAX, 2);
Serial.println("[States] Anfrage: " + url);
String json;
g_lastStatesCode = httpGet(url, json);
if (g_lastStatesCode == 429) { setStatus("Rate-Limit (429)"); return result; }
if (g_lastStatesCode == 401) { setStatus("Auth-Fehler (401)"); return result; }
if (g_lastStatesCode != HTTP_CODE_OK || json.isEmpty()) {
char msg[40];
snprintf(msg, sizeof(msg), "HTTP Fehler: %d", g_lastStatesCode);
setStatus(msg);
return result;
}
DynamicJsonDocument doc(32768);
if (deserializeJson(doc, json)) { setStatus("JSON-Fehler"); return result; }
JsonArray states = doc["states"].as<JsonArray>();
if (states.isNull()) { setStatus("Keine Daten."); return result; }
for (JsonArray s : states) {
if (s.size() < 14) continue;
if (s[8].as<bool>()) continue; // am Boden
float alt = -1.0f;
if (!s[7].isNull()) alt = s[7].as<float>();
else if (!s[13].isNull()) alt = s[13].as<float>();
if (alt < 0.0f || alt > MAX_ALTITUDE_M) continue;
FlightInfo fi;
fi.icao24 = s[0].as<String>(); fi.icao24.trim();
fi.callsign = s[1].isNull() ? "N/A" : s[1].as<String>(); fi.callsign.trim();
fi.country = s[2].isNull() ? "---" : s[2].as<String>();
fi.altitude = alt;
fi.speedKmh = s[9].isNull() ? 0.0f : s[9].as<float>() * 3.6f;
fi.heading = s[10].isNull() ? 0.0f : s[10].as<float>();
fi.departure = "---";
fi.arrival = "---";
result.push_back(fi);
}
Serial.printf("[States] %d Flugzeug(e) unter %.0f m.\n",
result.size(), MAX_ALTITUDE_M);
return result;
}
// ════════════════════════════════════════════════════════════════════════════
// OpenSky: Flugdetails für einzelnes Flugzeug
// ════════════════════════════════════════════════════════════════════════════
static int enrichWithFlightDetails(FlightInfo& fi) {
time_t now;
time(&now);
if (now < 100000L) return -1;
String url = String("https://opensky-network.org/api/flights/aircraft")
+ "?icao24=" + fi.icao24
+ "&begin=" + String((long)now - 43200L)
+ "&end=" + String((long)now);
String json;
int code = httpGet(url, json);
// Bei Fehler direkt den Code zurückgeben
if (code != HTTP_CODE_OK || json.isEmpty() || json == "[]") return code;
DynamicJsonDocument doc(4096);
if (deserializeJson(doc, json) || !doc.is<JsonArray>()) return code;
JsonArray arr = doc.as<JsonArray>();
if (arr.size() == 0) return code;
JsonObject flight = arr[arr.size() - 1].as<JsonObject>();
if (!flight["estDepartureAirport"].isNull())
fi.departure = flight["estDepartureAirport"].as<String>();
if (!flight["estArrivalAirport"].isNull())
fi.arrival = flight["estArrivalAirport"].as<String>();
Serial.printf("[Flights] %s: %s -> %s\n",
fi.icao24.c_str(), fi.departure.c_str(), fi.arrival.c_str());
return code; // Alles OK (200)
}
// ════════════════════════════════════════════════════════════════════════════
// LVGL: Anzeige aktualisieren
// ════════════════════════════════════════════════════════════════════════════
static void updateDisplay() {
// ── Header ────────────────────────────────────────────────────────────────
char header[52];
struct tm ti;
if (getLocalTime(&ti, 100)) {
snprintf(header, sizeof(header),
"* Langenhagen %02d:%02d:%02d",
ti.tm_hour, ti.tm_min, ti.tm_sec);
} else {
snprintf(header, sizeof(header), "* Langenhagen");
}
lv_label_set_text(g_headerLabel, header);
// ── Statuszeile ───────────────────────────────────────────────────────────
char status[52];
snprintf(status, sizeof(status), "%d Flugzeug(e) unter %.0f m",
(int)g_activeFlights.size(), MAX_ALTITUDE_M);
lv_label_set_text(g_statusLabel, status);
// ── Flug-Container ────────────────────────────────────────────────────────
lv_obj_clean(g_flightList);
if (g_activeFlights.empty()) {
lv_obj_t* lbl = lv_label_create(g_flightList);
lv_obj_add_style(lbl, &g_styleStatus, 0);
lv_label_set_text(lbl, "Keine Flugzeuge\nim Radius erkannt.");
lv_label_set_long_mode(lbl, LV_LABEL_LONG_WRAP);
lv_obj_set_width(lbl, LOGICAL_W - 16);
} else {
for (const FlightInfo& fi : g_activeFlights) {
lv_color_t cardColor;
if (fi.altitude < 500.0f) cardColor = lv_color_hex(0x3d1f1f);
else if (fi.altitude < 1000.0f) cardColor = lv_color_hex(0x3d2e18);
else cardColor = lv_color_hex(0x152535);
lv_obj_t* card = lv_obj_create(g_flightList);
lv_obj_set_width(card, LOGICAL_W - 18);
lv_obj_set_height(card, LV_SIZE_CONTENT);
lv_obj_set_style_bg_color(card, cardColor, 0);
lv_obj_set_style_bg_opa(card, LV_OPA_COVER, 0);
lv_obj_set_style_border_color(card, lv_color_hex(0x30363d), 0);
lv_obj_set_style_border_width(card, 1, 0);
lv_obj_set_style_radius(card, 4, 0);
lv_obj_set_style_pad_all(card, 4, 0);
lv_obj_set_style_pad_row(card, 2, 0);
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
lv_obj_remove_flag(card, LV_OBJ_FLAG_SCROLLABLE);
char line1[64];
snprintf(line1, sizeof(line1),
"[%s] %.0fm %.0fkm/h %.0f\xc2\xb0",
fi.callsign.c_str(), fi.altitude,
fi.speedKmh, fi.heading);
lv_obj_t* lbl1 = lv_label_create(card);
lv_obj_add_style(lbl1, &g_styleItem, 0);
lv_label_set_text(lbl1, line1);
lv_label_set_long_mode(lbl1, LV_LABEL_LONG_WRAP);
lv_obj_set_width(lbl1, LOGICAL_W - 30);
char line2[64];
snprintf(line2, sizeof(line2), "%s | %s -> %s",
fi.country.c_str(),
fi.departure.c_str(),
fi.arrival.c_str());
lv_obj_t* lbl2 = lv_label_create(card);
lv_obj_add_style(lbl2, &g_styleItemDim, 0);
lv_label_set_text(lbl2, line2);
lv_label_set_long_mode(lbl2, LV_LABEL_LONG_WRAP);
lv_obj_set_width(lbl2, LOGICAL_W - 30);
}
}
lv_timer_handler();
lv_refr_now(NULL);
}
// ════════════════════════════════════════════════════════════════════════════
// Haupt-Update
// ════════════════════════════════════════════════════════════════════════════
static void fetchAndUpdateStates() {
setStatus("Lade Flugdaten ...");
std::vector<FlightInfo> fresh = fetchStates();
delayLvgl(1000);
// Rate-Limit erkannt? Penalty aktivieren und abbrechen
if (g_lastStatesCode == 429) {
g_apiPenaltyActive = true;
g_lastStatesUpdate = millis(); // Timer resetten, damit Penalty greift
return;
}
g_apiPenaltyActive = false; // Wieder freigeben
if (g_lastStatesCode == HTTP_CODE_OK) {
std::map<String, FlightInfo> newCache;
bool detailsFetchedThisCycle = false; // Drosselung: max 1 Detail-Request pro Zyklus
for (FlightInfo& fi : fresh) {
auto it = g_cache.find(fi.icao24);
if (it != g_cache.end()) {
fi.departure = it->second.departure;
fi.arrival = it->second.arrival;
} else {
// Prüfen, ob wir überhaupt Detail-Abfragen machen dürfen (Circuit Breaker) TODO
if (!detailsFetchedThisCycle && millis() > g_detailsPenaltyUntil) {
setStatus(("Route prüfen: " + fi.icao24).c_str());
int detailCode = enrichWithFlightDetails(fi);
detailsFetchedThisCycle = true;
if (detailCode == 429) {
Serial.println("[Flights] Rate-Limit 429 erreicht. Sperre Routen-Abfrage für 5 Minuten.");
g_detailsPenaltyUntil = millis() + 300000UL; // 5 Minuten Backoff
}
delayLvgl(1000);
}
}
newCache[fi.icao24] = fi;
}
g_cache = std::move(newCache);
g_activeFlights = std::move(fresh);
} else {
Serial.printf("[Update] States HTTP %d – zeige letzte bekannte Daten.\n",
g_lastStatesCode);
}
updateDisplay();
}
// ════════════════════════════════════════════════════════════════════════════
// LVGL: UI aufbauen
// ════════════════════════════════════════════════════════════════════════════
static void initUI() {
lv_obj_t* scr = lv_screen_active();
lv_obj_set_style_bg_color(scr, lv_color_hex(0x0d1117), 0);
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
lv_style_init(&g_styleHeader);
lv_style_set_text_font(&g_styleHeader, &lv_font_montserrat_14);
lv_style_set_text_color(&g_styleHeader, lv_color_hex(0x58a6ff));
lv_style_init(&g_styleStatus);
lv_style_set_text_font(&g_styleStatus, &lv_font_montserrat_12);
lv_style_set_text_color(&g_styleStatus, lv_color_hex(0x8b949e));
lv_style_init(&g_styleItem);
lv_style_set_text_font(&g_styleItem, &lv_font_montserrat_12);
lv_style_set_text_color(&g_styleItem, lv_color_hex(0xe6edf3));
lv_style_init(&g_styleItemDim);
lv_style_set_text_font(&g_styleItemDim, &lv_font_montserrat_12);
lv_style_set_text_color(&g_styleItemDim, lv_color_hex(0x8b949e));
// Header
g_headerLabel = lv_label_create(scr);
lv_obj_add_style(g_headerLabel, &g_styleHeader, 0);
lv_label_set_text(g_headerLabel, "* Langenhagen");
lv_obj_align(g_headerLabel, LV_ALIGN_TOP_LEFT, 5, 4);
// Obere Trennlinie
lv_obj_t* sepTop = lv_obj_create(scr);
lv_obj_set_size(sepTop, LOGICAL_W - 2, 1);
lv_obj_align(sepTop, LV_ALIGN_TOP_MID, 0, 22);
lv_obj_set_style_bg_color(sepTop, lv_color_hex(0x30363d), 0);
lv_obj_set_style_border_width(sepTop, 0, 0);
lv_obj_set_style_pad_all(sepTop, 0, 0);
// Statuszeile
g_statusLabel = lv_label_create(scr);
lv_obj_add_style(g_statusLabel, &g_styleStatus, 0);
lv_label_set_text(g_statusLabel, "Starte ...");
lv_obj_align(g_statusLabel, LV_ALIGN_TOP_LEFT, 5, 26);
// Scrollbarer Flug-Container (Nimmt nun den restlichen Platz ein)
g_flightList = lv_obj_create(scr);
lv_obj_set_size(g_flightList, LOGICAL_W, LOGICAL_H - TOP_ZONE_H);
lv_obj_align(g_flightList, LV_ALIGN_TOP_MID, 0, TOP_ZONE_H);
lv_obj_set_style_bg_color(g_flightList, lv_color_hex(0x0d1117), 0);
lv_obj_set_style_bg_opa(g_flightList, LV_OPA_COVER, 0);
lv_obj_set_style_border_color(g_flightList, lv_color_hex(0x30363d), 0);
lv_obj_set_style_border_width(g_flightList, 1, 0);
lv_obj_set_style_pad_all(g_flightList, 4, 0);
lv_obj_set_style_pad_row(g_flightList, 5, 0);
lv_obj_set_scroll_dir(g_flightList, LV_DIR_VER);
lv_obj_set_scrollbar_mode(g_flightList, LV_SCROLLBAR_MODE_AUTO);
lv_obj_set_flex_flow(g_flightList, LV_FLEX_FLOW_COLUMN);
}
// ════════════════════════════════════════════════════════════════════════════
// setup()
// ════════════════════════════════════════════════════════════════════════════
void setup() {
Serial.begin(115200);
Serial.println("\n[Setup] FlugMonitor startet ...");
lv_init();
lv_display_t* disp = lv_tft_espi_create(
SCREEN_WIDTH, SCREEN_HEIGHT, draw_buf, sizeof(draw_buf));
lv_display_set_rotation(disp, LV_DISPLAY_ROTATION_270);
initUI();
lv_timer_handler();
lv_refr_now(NULL);
connectWiFi();
setStatus("NTP synchronisieren ...");
configTime(NTP_GMT_OFFSET_SEC, NTP_DAYLIGHT_OFFSET, NTP_SERVER);
struct tm ti;
if (getLocalTime(&ti, 8000)) {
Serial.printf("[NTP] OK: %02d:%02d:%02d\n",
ti.tm_hour, ti.tm_min, ti.tm_sec);
setStatus("NTP OK.");
} else {
setStatus("NTP fehlgeschlagen.");
}
if (!refreshOAuthToken()) {
setStatus("Auth fehlgeschlagen!");
delayLvgl(3000);
}
setStatus("Warte vor erster Abfrage ...");
delayLvgl(5000);
fetchAndUpdateStates();
g_lastStatesUpdate = millis();
g_firstRun = false;
Serial.println("[Setup] Bereit.");
}
// ════════════════════════════════════════════════════════════════════════════
// loop()
// ════════════════════════════════════════════════════════════════════════════
void loop() {
lv_timer_handler();
unsigned long now = millis();
if (WiFi.status() != WL_CONNECTED) connectWiFi();
// Ermitteln des aktuellen Intervalls (inkl. Backoff-Strafe bei 429)
unsigned long currentStatesInterval = g_apiPenaltyActive ? PENALTY_BACKOFF : INTERVAL_STATES;
// Flugzeuge aktualisieren
if (now - g_lastStatesUpdate >= currentStatesInterval) {
g_lastStatesUpdate = now;
fetchAndUpdateStates();
}
if (g_firstRun) g_firstRun = false;
delay(5);
}
Dann brauchen wir noch eine config.h für die WIFI und OpenSky Token im gleichen Verzeichnis:
#pragma once
// ════════════════════════════════════════════════════════════════════════════
// config.h – FlugMonitor Konfiguration
// ════════════════════════════════════════════════════════════════════════════
// ── WiFi ─────────────────────────────────────────────────────────────────────
#define WIFI_SSID "WIFI-SSID"
#define WIFI_PASSWORD "PASSWORT"
// ── OpenSky Network Zugangsdaten ─────────────────────────────────────────────
// Registrierung: https://opensky-network.org/index.php?option=com_users&view=registration
#define OPENSKY_CLIENT_ID "client id"
#define OPENSKY_CLIENT_SECRET "Secret"
// ── OAuth2 Token-Endpunkt ─────────────────────────────────────────────────────
#define OPENSKY_TOKEN_URL \
"https://auth.opensky-network.org/auth/realms/opensky-network" \
"/protocol/openid-connect/token"
// ── Bounding Box Langenhagen, Niedersachsen (~20 km Radius) ──────────────────
// Mittelpunkt: 52.4398° N, 9.7337° O
// Breite: ±0.18° (≈ 20 km)
// Länge: ±0.30° (≈ 20 km bei 52° Breite)
// #define BBOX_LAT_MIN 52.26
// #define BBOX_LAT_MAX 52.62
// #define BBOX_LON_MIN 9.43
// #define BBOX_LON_MAX 10.04
// Aktualisierte Bounding Box (Erweiterter Anflugsektor Osten)
// Definitionsbereich:
// Südliche Begrenzung (Latitude Min): 52.40°N
// Nördliche Begrenzung (Latitude Max): 52.48°N
// Westliche Begrenzung (Longitude Min): 9.71°E
// Östliche Begrenzung (Longitude Max): 10.60°E
#define BBOX_LAT_MIN 52.40
#define BBOX_LAT_MAX 52.48
#define BBOX_LON_MIN 9.71
#define BBOX_LON_MAX 10.60
// ── Filterkriterium ───────────────────────────────────────────────────────────
#define MAX_ALTITUDE_M 2000.0f // Meter (barometrisch)
// ── Aktualisierungsintervall ──────────────────────────────────────────────────
// OpenSky erlaubt authentifizierten Nutzern min. 5 s zwischen Anfragen.
#define REFRESH_INTERVAL_MS 120000UL
// ── Puffer vor Token-Ablauf (Sekunden) ───────────────────────────────────────
#define TOKEN_EXPIRY_BUFFER_S 60
// ── Wartezeit vor erstem Request nach Boot ────────────────────────────────────
// Verhindert 429 direkt nach dem Einschalten
#define INITIAL_DELAY_MS 15000UL // 15 Sekunden
// ── NTP-Zeitserver ────────────────────────────────────────────────────────────
#define NTP_SERVER "pool.ntp.org"
#define NTP_GMT_OFFSET_SEC 3600 // UTC+1 (MEZ)
#define NTP_DAYLIGHT_OFFSET 3600 // +1h Sommerzeit (MESZ)Code wird dann später nach Gitlab hochgeladen.
Auch wenn der Papst vor Gefahren von Künstlicher Intelligenz (KI) in seiner ersten Enzyklika „Magnifica Humanitas“ (Großartige Menschheit) warnt, kann man sagen, das dieses Projekt ohne KI für mich in herkömmlicher Weise einige Tage gebraucht hätte. So konnte alles in ein paar Stunden „just for fun“ erstellt werden.
Software-Programmierer werden in Zukunft anders arbeiten, den in diesem Bereich ist die KI seit Jahren nicht mehr wegzudenken. In diesem Sinne, noch einen schönen Tag und sonnige Grüße aus EDDV bei 26,5 Grad Celsius.
Haftungsausschluss (Disclaimer)
Die in diesem Dokument/generierten Inhalten bereitgestellten Daten, Simulationen und Informationen dienen ausschließlich zu Bildungs- und VerständZwecken. Sie wurden bewusst erzeugt, um theoretische Konzepte zu veranschaulichen und das Verständnis von Flugdynamik, Steuerungsprinzipien oder verwandten technischen Zusammenhängen zu fördern.
Die Daten sind ausdrücklich nicht für die Durchführung echter Flugmanöver, nicht für den operationellen Einsatz in der realen Luftfahrt und nicht für jegliche Art von praktischer Navigation oder Flugsteuerung geeignet.
Die Verwendung dieser Informationen für reale Flugoperationen erfolgt ausschließlich auf eigene Gefahr. Der Autor übernimmt keinerlei Haftung für Schäden, Verluste, Verletzungen oder sonstige Folgen, die direkt oder indirekt aus der Anwendung, Fehlinterpretation oder dem Missbrauch dieser Daten entstehen – sei es durch Personen, Organisationen oder Dritte.
Jegliche Ähnlichkeit mit tatsächlichen Flugmanövern oder operationalen Verfahren ist rein zufällig und nicht beabsichtigt. Für den echten Flugsport und die Luftfahrt sind ausschließlich offizielle, zertifizierte Quellen und qualifiziertes Fachpersonal heranzuziehen.
]]>
STOMP-Push an alle Browser-Clients ist ein Verfahren, bei dem ein Server über eine bestehende WebSocket-Verbindung Nachrichten an alle verbundenen Browser gleichzeitig sendet – ähnlich wie eine Durchsage in einem Flughafen, die alle hören.
In Java Spring Boot wird das typischerweise mit @MessageMapping und SimpMessagingTemplate umgesetzt, wobei convertAndSend(„/topic/global“, nachricht) alle Clients erreicht.
Das ist lesbar, einfach zu debuggen und funktioniert in allen modernen Browsern ohne Flash oder Plug-ins.
Wichtige Merkmale:
– /topic/ = Publish/Subscribe: Alle Empfänger bekommen dieselbe Nachricht (Broadcast)
– /queue/ = Punkt-zu-Punkt: Nur ein Empfänger bekommt die Nachricht (nicht für „an alle”)
– Die Verbindung bleibt offen → keine ständigen HTTP-Requests nötig (kein Polling)
– STOMP ist ein einfaches Text-Protokoll über WebSocket, das leicht zu implementieren ist
So funktioniert’s:
1. Starte die Spring Boot App → Server läuft auf http://localhost:8080
2. Öffne mehrere Browser-Tabs mit http://localhost:8080/index.html
3. Klicke auf “Nachricht senden” in einem Tab
4. Alle Tabs erhalten sofort die Nachricht “Nachricht an alle Browser!”
Das ist die minimalste funktionierende Demo für STOMP-Broadcast an alle Clients.
Spring Boot vs. Quarkus
Für die STOMP-Demo ist Spring Boot besser, weil:
– STOMP native Unterstützung hat (kein manueller Parser)
– Nur ca. 3 Zeilen Broadcast impl.
– Keine WebSocket-Boilerplate nötig ist
– Als Java/Spring-Entwickler man schon das Ökosystem kennt
Quarkus lohnt sich erst, wenn:
Du viele Microservices mit schnellem Scaling hast
– Serverless/Container-Umgebung mit Cold-Start-Constraints
– Speicher extrem limitiert ist
– Du keine STOMP-Bibliotheken brauchst (eigener Protokoll-Stack)
Beide sind valide – die Wahl hängt von deinen spezifischen Anforderungen, nicht von besser/schlechter ab.
Der Code, d.h. die zwei Klassen für die STOMP-Demo sind in meinem Git-Repo zu finden.
]]>
Markdown ist ein Klartextformat zum Schreiben strukturierter Dokumente, das auf Konventionen zur Textformatierung in E-Mails und Usenet-Beiträgen basiert. Es wurde im schon im Jahr 2004 von John Gruber (mit Unterstützung von Aaron Swartz) entwickelt, um die Lücke zwischen menschenlesbarem Klartext und maschinenlesbarem HTML-Code zu schließen.
Das wichtigste Ziel von Markdown ist die maximale Lesbarkeit. Ein in Markdown verfasstes Dokument soll im Idealfall direkt als reiner Text veröffentlicht werden können, ohne dass es so wirkt, als sei es mit technischen Befehlen oder Tags überladen. Hierfür verwendet die Sprache einfache Satzzeichen, die visuell andeuten, welche Formatierung sie bewirken – beispielsweise wirken Sternchen um ein Wort wie eine Hervorhebung und Bindestriche am Zeilenanfang wie eine Liste.
Markdown ist eine leichtgewichtige Auszeichnungssprache, die es sowohl technischen als auch nicht-technischen Nutzern ermöglicht, auf einfache Weise Dokumente zu erstellen, die sowohl im Rohzustand gut lesbar als auch reichhaltig gerendert werden können.
| Bedeutung | Markdown‑Code |
|---|---|
| Überschrift 1 | # Überschrift 1 |
| Überschrift 2 | ## Überschrift 2 |
| Fetter Text | **fett** |
| Kursiver Text | *kursiv* |
| Fett + kursiv | ***fett und kursiv*** |
| Durchgestrichen | ~~durchgestrichen~~ |
| Inline‑Code | |
| Codeblock (mehrzeilig) | |
| Bedeutung | Markdown‑Code |
|---|---|
| Ungeordnete Liste | - Punkt 1- Unterpunkt |
| Geordnete Liste | 1. Erstens2. Zweitens |
| Checkliste (Tasks) | - [ ] Offen- [x] Erledigt |
| Bedeutung | Markdown‑Code |
|---|---|
| Link | [Text](https://beispiel.de) |
| Bild |  |
| Zitat‑Block | > Das ist ein Zitat. |
| Horizontale Linie | --- |
| Tabelle | |
| Fußnote | Text mit Fußnote[^1]
|
| Metadaten‑Block | |
| Bedeutung | Markdown‑Code |
|---|---|
| Zeilenumbruch | Zeile 1Zeile 2 (Leerzeile dazwischen) |
| Escape‑Steuerzeichen | \*ohne Kursiv\* |
| HTML‑Link (ohne Text) | <https://beispiel.de> |
Hast du dich schon mal gefragt, was eigentlich über deinem Kopf passiert, wenn du in den Himmel schaust und ein Flugzeug siehst? In welcher Höhe fliegt es? Wie viele Maschinen sind gerade gleichzeitig in der Luft?
Genau das zeigt mein kleines Server Programm (spring-boot) – live, direkt im Browser. Als alternative zu dem Swing-Programm.
Die Idee dahinter
Es gibt ein offenes Netzwerk namens OpenSky, in dem tausende Hobbyisten und Flughäfen weltweit Funksignale von Flugzeugen empfangen und sammeln. Jedes Flugzeug sendet dabei ständig seine Position und Höhe aus – ähnlich wie ein Handy seinen Standort sendet. Diese Daten sind frei zugänglich.
Mein Programm fragt diese Daten alle 30 Sekunden ab und bereitet sie übersichtlich auf.
Was sieht man?
Sobald man die Seite im Browser öffnet, sieht man sofort:
– Wie viele Flugzeuge gerade in der Luft sind – zum Beispiel über Zentraleuropa
– In welcher Höhe sie fliegen, aufgeteilt in sinnvolle Bereiche (z. B. unter 1.000 m, Reiseflughöhe über 9.000 m usw.)
– Ein Balkendiagramm, das auf einen Blick zeigt, wo sich die meisten Maschinen tummeln
– Die Top 20 der höchstfliegenden Flugzeuge mit Rufzeichen, Herkunftsland und Geschwindigkeit
Und das alles aktualisiert sich automatisch – man muss die Seite nicht neu laden.
Wie funktioniert das technisch – ganz einfach erklärt
Stell dir das Programm wie einen fleißigen Assistenten vor, der alle 30 Sekunden zum OpenSky-Schalter geht, die neuesten Flugdaten abholt und sie dann sofort an alle Bildschirme weitergibt, die gerade geöffnet sind.
Technisch steckt dahinter ein kleiner Java-Server, der im Hintergrund läuft. Der Browser verbindet sich einmalig mit ihm – und bekommt dann automatisch neue Daten zugeschickt, sobald sie vorliegen. Das nennt man eine WebSocket-Verbindung: kein ständiges Neu-Laden, sondern eine offene Leitung zwischen Server und Browser.
Kann man auch weltweit schauen?
Ja! Per Knopfdruck lässt sich von der Europakarte auf weltweit umschalten. Dann sieht man alle Flugzeuge auf dem gesamten Globus – ein beeindruckender Blick darauf, wie dicht der Luftraum rund um die Uhr bevölkert ist.
Für wen ist das?
Für alle, die neugierig sind. Man braucht kein technisches Vorwissen, um die Seite zu nutzen. Für Entwicklerinnen und Entwickler ist der Quellcode frei verfügbar – als Ausgangspunkt für eigene Ideen rund um Echtzeit-Daten, Karten und Visualisierungen.
Anleitung für die Instalation auf dem Raspberry Pi kommt dann noch.
Hier noch mal der anblick von Europa:


Notfallcode 7700
Der Code 7700 signalisiert einen allgemeinen Luftnotfall, wie Triebwerksausfall, medizinische Probleme oder technische Defekte. ATC räumt dem Flugzeug sofort Priorität ein, bereitet Notdienste vor und koordiniert den Luftraum. Merkspruch: „Seven-seven – going to heaven“.
Notfallcode 7600
7600 wird bei Funkausfall (Kommunikationsverlust) aktiviert, ermöglicht aber weiterhin Positionsübertragung via Sekundärradar. ATC passt umliegende Verkehrsteilnehmer an und leitet das Flugzeug sicher zur Landung. Merkspruch: „Seven-six – hear nix“.
Notfallcode 7500
7500 steht für Flugzeugentführung oder unrechtmäßige Einflussnahme (Hijacking), wobei Piloten den Code diskret setzen, um Entführer nicht zu alarmieren. ATC initiiert Sicherheitsprotokolle mit Behörden, ohne direkte Bestätigung. Merkspruch: „Seven-five – man with a knife“.
Diese Codes sind global einheitlich und entscheidend für die aviative Sicherheit, da sie auf Radarschirmen sofort auffallen. Piloten trainieren ihre diskrete Anwendung regelmäßig.
Das kann nun mit meinem Flight Monitor 2.3.0 angezeigt und ermittelt werden, hier ein Beispiel:

Bei klick auf den Roten Button, z.B.

Genaue Anleitung in meinem Gitlab Repo. Oder auch auf Maven Central.
]]>
Der KML-Export ergänzt den FlugMonitor um eine leistungsfähige Archiv- und Analysefunktion. Aus den 17 Feldern eines einzelnen StateVectors lässt sich ein vollständiges Lagebild jedes Flugzeugs rekonstruieren: wo es ist, in welche Richtung es fliegt, wie schnell, wie hoch, wem es gehört und wie verlässlich die Daten sind. Multipliziert mit tausenden Flugzeugen gleichzeitig entsteht ein präziser Schnappschuss des globalen Luftverkehrs – zu jedem beliebigen Zeitpunkt.

Was passiert beim Klick auf „KML Export“?
1. Es öffnet sich ein nativer Datei-Speichern-Dialog des Betriebssystems.
2. Als Dateiname ist automatisch ein Zeitstempel vorausgefüllt,
zum Beispiel: flugdaten_20260419_063345.kml
3. Nach Bestätigung holt die Anwendung im Hintergrund (SwingWorker)
die aktuellen Flugdaten frisch von der OpenSky-API ab:
OpenSkyStates os = OpenSkyApi.getOpenSkyApiInstance().getStates(0, null);
Collection<StateVector> states = os.getStates();
4. Die Rohdaten werden über die Funktion Kml.formatStateVectorToKml(states)
in gültiges KML-Format umgewandelt.
5. Die erzeugte KML-Datei wird UTF-8-kodiert auf dem gewählten Pfad
gespeichert. Eine Erfolgsmeldung mit dem vollständigen Dateipfad
bestätigt den Abschluss.
Welche Daten enthält ein StateVector – und was bedeuten sie?
Jedes einzelne Flugzeug liefert beim Abruf einen sogenannten StateVector –
einen Datensatz, der den aktuellen Zustand der Maschine zu einem bestimmten
Zeitpunkt vollständig beschreibt. Ein konkretes Beispiel:
StateVector{
geoAltitude = 8313.42,
longitude = 15.6949,
latitude = 41.3496,
velocity = 198.89,
heading = 306.69,
verticalRate = 8.78,
icao24 = ’39de4f‘,
callsign = ‚TVF38EG‘,
onGround = false,
lastContact = 1.776685361E9,
lastPositionUpdate = 1.776685361E9,
originCountry = ‚France‘,
squawk = ’null‘,
spi = false,
baroAltitude = 8161.02,
positionSource = ADS_B,
serials = null
}
Das sind die konkreten Felder im Einzelnen:
icao24 = ’39de4f‘
Der ICAO24-Transpondercode ist die weltweite, unveränderliche Kennung eines Luftfahrzeugs – vergleichbar mit einem Fahrzeugkennzeichen, nur dass er niemals wechselt. Er besteht aus sechs Hexadezimalzeichen (also Ziffern 0–9 und Buchstaben A–F). Anhand dieses Codes lässt sich jedes Flugzeug eindeutig identifizieren und seine gesamte Flughistorie in Datenbanken nachverfolgen.
Im Beispiel: 39de4f – ein französisch registriertes Flugzeug (Kennung beginnt mit 39, typisch für Frankreich).
callsign = ‚TVF38EG‘
Das Rufzeichen (englisch: Callsign) ist die Kennung, die der Pilot im Funkverkehr mit der Flugsicherung verwendet. Bei Linienflügen entspricht es meist dem IATA/ICAO-Airline-Code plus der Flugnummer. TVF steht für Transavia France, 38EG ist die interne Flugnummer.
Das Rufzeichen kann sich je nach Flug ändern – im Gegensatz zum ICAO24-Code.
latitude = 41.3496 / longitude = 15.6949
Die geografische Position des Flugzeugs in dezimalen Breitengraden (latitude, Nord-Süd) und Längengraden (longitude, Ost-West).
Im Beispiel: 41.35°N / 15.69°E – das entspricht dem Luftraum über Süditalien, ungefähr auf Höhe der Region Kampanien/Apulien.
Diese Koordinaten sind das Herzstück des KML-Exports: In Google Earth erscheint das Flugzeug genau an dieser Position auf der Weltkarte.
geoAltitude = 8313.42 (Meter)
Die geodätische Höhe – also die Höhe über dem Meeresspiegel – berechnet aus GPS-Satellitensignalen. Sie wird in Metern angegeben. 8313 Meter entsprechen in der Luftfahrtsprache ungefähr FL270 (Flugfläche 270 = 27.000 Fuß). Das ist ein typischer Steigflug auf dem Weg zur Reiseflughöhe. Der genaue Unterschied zur barometrischen Höhe (s. u.) entsteht durch Luftdruckschwankungen.
baroAltitude = 8161.02 (Meter)
Die barometrische Höhe wird vom Flugzeug selbst über einen Druckmesser (Barometer) berechnet und per Transponder gesendet. In der zivilen Luftfahrt ist diese Angabe die verbindliche Höhenreferenz für die Flugsicherung, da alle Flugzeuge denselben Referenzluftdruck (1013,25 hPa, sogenannter QNH-Standard) verwenden – so wird sichergestellt, dass alle Maschinen auf gleicher Basis staffeln.
Die Differenz von ca. 152 Metern zum GPS-Wert (geoAltitude) ist normal und entsteht durch aktuelle Luftdruckabweichungen.
velocity = 198.89 (Meter pro Sekunde)
Die Geschwindigkeit über Grund (Ground Speed) in m/s. 198,89 m/s entsprechen umgerechnet ca. 716 km/h – eine typische Geschwindigkeit im Steigflug. Im Reiseflug liegen Kurzstreckenflugzeuge meist bei 800–880 km/h.
Hinweis: Dies ist nicht die Eigengeschwindigkeit (True Airspeed), sondern die tatsächliche Bewegung über den Erdboden, beeinflusst durch Wind.
heading = 306.69 (Grad)
Der Kurs des Flugzeugs in Grad, gemessen im Uhrzeigersinn von Norden. 0° = Norden, 90° = Osten, 180° = Süden, 270° = Westen.
306,69° entspricht ungefähr Nordwest – das Flugzeug fliegt also von Süditalien in Richtung Nordwesteuropa (Frankreich / Iberische Halbinsel), was für einen Transavia-Rückflug nach Paris plausibel ist.
verticalRate = 8.78 (Meter pro Sekunde)
Die Steig- oder Sinkrate. Ein positiver Wert bedeutet Steigen, ein negativer Wert bedeutet Sinken.
8,78 m/s aufwärts entsprechen ca. 1.730 Fuß/Minute – eine moderate Steigrate, typisch für einen Mittelstreckenjet im oberen Steigflug.
onGround = false
Ein einfacher Ja/Nein-Wert: Ist das Flugzeug gerade am Boden? Wird true, wenn der Transponder meldet, dass das Fahrwerk belastet ist (Weight-on-Wheels-Signal). Im FlugMonitor dient dieses Flag dazu, Flugzeuge der Kategorie „Parken“ zuzuordnen.
originCountry = ‚France‘
Das Herkunftsland des Transpondercodes, abgeleitet aus dem ICAO24-Code. Es gibt Aufschluss darüber, wo das Flugzeug registriert ist – nicht notwendigerweise, woher der aktuelle Flug kommt.
squawk = ’null‘
Der Squawk-Code ist ein vierstelliger oktaler Code (0000–7777), den der Pilot auf Anweisung der Flugsicherung einstellt. Er dient der Identifikation auf dem Radar der Flugsicherung. Bestimmte Codes haben Sonderbedeutung: 7500 = Entführung, 7600 = Funkkontaktausfall, 7700 = Notsituation.
„null“ bedeutet, dass kein spezifischer Code gesetzt ist.
spi = false
SPI steht für „Special Position Identifier“. Der Pilot kann diesen Schalter drücken, um dem Radarbild einen kurzen Impuls zu geben – zum Beispiel auf Anforderung der Flugsicherung zur eindeutigen Identifikation. false bedeutet, der Schalter ist nicht aktiviert.
lastContact = 1.776685361E9 (Unix-Timestamp)
Der Zeitstempel des letzten empfangenen Signals, ausgedrückt als Unix-Zeitstempel (Sekunden seit dem 01.01.1970 00:00:00 UTC).
1.776685361E9 entspricht dem Zeitpunkt des Abrufs in diesem Beispiel.
OpenSky erfasst Flugzeuge auch dann noch, wenn der Funkkontakt kurz unterbrochen war – lastContact gibt an, wann zuletzt ein Signal ankam.
lastPositionUpdate = 1.776685361E9
Ähnlich wie lastContact, aber spezifisch für das letzte Update der Positionsdaten (Latitude/Longitude). In manchen Fällen können andere Daten (z. B. Geschwindigkeit) häufiger aktualisiert werden als die Positionsangabe.
positionSource = ADS_B
Woher stammt die Positionsinformation? Mögliche Werte:
ADS_B – direkt vom Flugzeug ausgestrahlt (modernste Methode, GPS-basiert, sehr präzise)
MLAT – Multilateration: die Position wird aus der Laufzeit-differenz mehrerer Bodenstationen berechnet
FLARM – Kurzstreckensystem für Kleinflugzeuge/Segelflugzeuge
ADS_B ist der Goldstandard: Das Flugzeug berechnet seine Position selbst per GPS und sendet sie aktiv aus – kein Radar nötig.
serials = null
Seriennummern von Empfangsstationen, die das Signal erfasst haben. null bedeutet, diese Information wird in der aktuellen API-Antwort nicht mitgeliefert oder ist nicht verfügbar.
Anwendungsmöglichkeiten der KML-Datei
Die exportierte KML-Datei kann in zahlreichen Programmen geöffnet
und weiterverarbeitet werden:
Google Earth: Alle Flugzeuge erscheinen als Punkte auf der 3D-Weltkarte.
Mit geoAltitude werden sie in der richtigen Flughöhe dargestellt – man sieht buchstäblich, wo in der Atmosphäre jede Maschine fliegt.
Google Maps / Maps-Import: KML-Dateien können als eigene Ebene importiert werden – für einfache 2D-Kartendarstellungen.
QGIS / ArcGIS: Professionelle GIS-Software kann KML importieren und Analysen durchführen – z. B. Verkehrsdichte über bestimmten Regionen, Höhenverteilungen auf Flugkorridoren, oder Vergleiche zu verschiedenen Tageszeiten.
Zeitvergleiche: Durch den Zeitstempel im Dateinamen lassen sich Snapshots aus verschiedenen Momenten vergleichen – morgens vs. abends, Werktag vs. Wochenende, Sommer vs. Winter.
Flugkorridor-Analyse: Mit den Heading- und Positionsdaten lassen sich
bevorzugte Flugrouten über Europa oder dem Atlantik sichtbar machen.
Was ist der FlugMonitor?
Der FlugMonitor ist eine Desktop-Anwendung, die ich mit Java 21 und Swing entwickelt habe. Swing ist das klassische GUI-Framework von Java – also das Werkzeugkasten, mit dem man Fenster, Tabellen und Buttons baut. Die Anwendung zeigt auf einen Blick, wie viele Flugzeuge sich gerade weltweit in welchem Höhenbereich befinden. Die Daten kommen dabei in Echtzeit direkt vom OpenSky Network, das Flugzeugpositionen aus aller Welt sammelt und kostenlos zur Verfügung stellt.
Das Besondere: Die Anwendung fragt bei jedem Klick auf „Aktualisieren“ live die OpenSky-API ab und wertet tausende Flugzeugpositionen in Millisekunden aus. Das Ergebnis landet sofort in einer übersichtlichen Tabelle und einem farbigen Balkendiagramm.

Was ist das OpenSky Network?
Das OpenSky Network (opensky-network.org) ist ein internationales Forschungsprojekt, das Flugzeugdaten per ADS-B empfängt. ADS-B steht für „Automatic Dependent Surveillance–Broadcast“ – ein System, bei dem moderne Flugzeuge automatisch ihre Position, Geschwindigkeit und Höhe per Funksignal ausstrahlen. Tausende freiwillige Helfer auf der ganzen Welt haben ADS-B-Empfänger aufgestellt und senden ihre Empfangsdaten kontinuierlich an das OpenSky-Netzwerk.
Für Entwickler stellt OpenSky eine kostenlose REST-API bereit. Eine Java-Bibliothek macht den Zugriff besonders einfach: Mit wenigen Zeilen Code holt man sich den aktuellen Zustand aller sichtbaren Flugzeuge als Liste von „StateVector“-Objekten. Jedes StateVector enthält unter anderem:
– Rufzeichen des Flugzeugs (ICAO24-Transpondercode)
– Geografische Position (Längen- und Breitengrad)
– Geodätische Höhe in Metern
– Geschwindigkeit und Kursrichtung
– Ob das Flugzeug am Boden ist (onGround-Flag)
Die Benutzeroberfläche im Überblick
Beim Start der Anwendung öffnet sich ein Fenster mit drei Bereichen:

1. Zwei Schaltflächen oben:
– „Aktualisieren“ (blau): Holt die neuesten Daten vom OpenSky-Server.
– „Kopieren“ (grün): Überträgt die gesamte Tabelle als formatierten Text
in die Zwischenablage, zum Beispiel für Berichte oder E-Mails.
2. Eine Tabelle in der Mitte mit zwei Spalten:
– „Flugbereich in Meter“: Die Höhenkategorie, z. B. „11.000 – 11.500“
– „Flughöhe in Meter“: Die Anzahl der Flugzeuge in diesem Bereich
Die Tabelle hat 18 Zeilen für verschiedene Höhenbänder, eine
hervorgehobene Summenzeile am Ende sowie Zebrastreifen zur besseren
Lesbarkeit.
3. Ein Balkendiagramm unten:
Das Diagramm zeigt dieselben Daten visuell. Jeder Balken entspricht einer
Höhenkategorie. Die Farben verlaufen von Graugrün (am Boden / „Parken“)
über Türkis und Blau (mittlere Flughöhen) bis zu Violett (Reiseflughöhen
um 11.000 Meter) und zurück zu Grau (Stratosphäre über 13.000 Meter).
So erkennt man auf den ersten Blick, welche Höhen am stärksten genutzt
werden.
Unten in der Statusleiste steht nach jedem Abruf der genaue Zeitstempel,
z. B. „Zeitpunkt: So. 19.04.2026 06:33:30“.
Die Höhenbereiche – was bedeuten sie?
Die 18 Kategorien in der Tabelle folgen einer fachlichen Logik:
Parken (0 m): Flugzeuge, die am Boden stehen. Das erkennt die Anwendung
am „onGround“-Flag im StateVector oder daran, dass keine Höhenangabe
vorliegt. An einem normalen Tag sind das mehrere Hundert Maschinen weltweit,
die von OpenSky trotzdem erfasst werden (z. B. auf belebten Rollfeldern).
1 bis 500 m: Start- und Landephase. Flugzeuge steigen kurz nach dem Abheben
sehr steil, daher ist diese Kategorie weniger stark besetzt als man erwarten
würde.
500 bis 5.000 m: Steig- und Sinkflug. Hier befinden sich Maschinen auf dem
Weg zu ihrer Reiseflughöhe sowie Propellermaschinen und Hubschrauber auf
kürzeren Strecken.
5.000 bis 9.000 m: Unterer Reiseflugbereich. Kleinere Verkehrsflugzeuge und
Regionalflüge nutzen diesen Bereich.
9.000 bis 13.000 m: Oberer Reiseflugbereich. Das ist die Welt der großen
Verkehrsflugzeuge wie Airbus A320, Boeing 737 oder A380. Der deutliche
Gipfel bei 11.000 bis 11.500 Metern entspricht der typischen Reiseflughöhe
(FL350 bis FL380 in der Luftfahrt-Sprache). Hier fliegen die meisten
Langstreckenmaschinen.
Über 13.000 m: Wenige spezialisierte Flugzeuge, z. B. bestimmte
Businessjets oder Militärflugzeuge.
Das Diagramm auf dem Screenshot zeigt schön den charakteristischen Einbruch
bei 5.000 bis 7.000 Metern (kaum Flugzeuge in diesem Zwischenbereich) und
den deutlichen Gipfel bei 11.000 bis 11.500 Metern – der klassischen
Reiseflughöhe moderner Verkehrsflugzeuge.
Technischer Aufbau – Schritt für Schritt erklärt
Die Projektstruktur
Das Projekt besteht für die Gui aus einer einzigen Java-Datei: FlugMonitorSwing.java.
Maven verwaltet die Abhängigkeiten.
Die Hauptklasse heißt FlugMonitorSwing und erbt von JFrame – dem Standard-Fenster in Swing. Innerhalb der Klasse gibt es zwei wichtige innere Klassen: DiagramPanel für das Balkendiagramm und ZebraRenderer für die Tabellendarstellung.
Datenabruf mit SwingWorker
In Java gibt es eine wichtige Regel: Alles, was die Benutzeroberfläche betrifft, muss im sogenannten Event Dispatch Thread (EDT) laufen. Netzwerk- anfragen blockieren aber den Thread, solange sie laufen – das würde das Fenster einfrieren.
Die Lösung heißt SwingWorker. Das ist eine Klasse, die einen Hintergrundthread startet, die Arbeit erledigt (hier: Netzwerkabruf) und danach sauber in den EDT zurückwechselt, um die Oberfläche zu aktualisieren:
new SwingWorker<int[], Void>() {
@Override
protected int[] doInBackground() throws IOException {
// Läuft im Hintergrund – Netzwerkabruf
OpenSkyStates states = OpenSkyApi
.getOpenSkyApiInstance()
.getStates(0, null);
return berechneVerteilung(states.getStates());
}
@Override
protected void done() {
// Läuft im EDT – Oberfläche aktualisieren
int[] z = get();
aktualisiereTabelle(z);
diagramPanel.setDaten(z);
}
}.execute();
Während der Abruf läuft, sind die Buttons deaktiviert (setEnabled(false)), damit man nicht versehentlich einen zweiten Abruf startet. Nach dem Abruf werden sie wieder aktiviert.
Verteilung berechnen
Die Methode berechneVerteilung() geht alle StateVector-Objekte durch und entscheidet für jedes Flugzeug, in welche Höhenkategorie es gehört:
private int[] berechneVerteilung(Collection<StateVector> vektoren) {
int[] z = new int[BEREICHE.size()];
for (StateVector sv : vektoren) z[bestimmeIndex(sv)]++;
return z;
}
private int bestimmeIndex(StateVector sv) {
if (Boolean.TRUE.equals(sv.isOnGround())
|| sv.getGeoAltitude() == null) return 0;
double h = sv.getGeoAltitude();
for (int i = 1; i < BEREICHE.size(); i++) {
Bereich b = BEREICHE.get(i);
if (h >= b.lower && h < b.upper) return i;
}
return BEREICHE.size() - 1;
}
Die Bereiche sind als unveränderliche Java-Records definiert:
private record Bereich(String label, int lower, int upper) {}
Records sind ein modernes Java-Feature (ab Java 16), das kompakte Datenklassen ohne Boilerplate ermöglicht.
Das Balkendiagramm mit Graphics2D
Für das Diagramm wurde bewusst keine externe Bibliothek (wie JFreeChart) eingesetzt. Stattdessen zeichnet die innere Klasse DiagramPanel alles selbst mit Java2D, dem eingebauten Grafiksystem von Java.
Jede Swing-Komponente kann die Methode paintComponent(Graphics g) überschreiben und dann beliebige Grafiken zeichnen. Der Schlüssel ist das Casting auf Graphics2D, das erweiterte Möglichkeiten bietet:
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
// ... Diagramm zeichnen ...
g2.dispose();
}
Antialiasing sorgt dafür, dass Kanten und Schrift weich und nicht pixelig erscheinen. Wichtig: g.create() erzeugt eine Kopie des Graphics-Objekts, die am Ende mit dispose() sauber freigegeben wird.
Die Balken werden als RoundRectangle2D gezeichnet – ein Rechteck mit abgerundeten Ecken:
g2.fill(new RoundRectangle2D.Float(mL, y, effLen, barH, 4, 4));
Darüber kommt ein halbtransparenter weißer Streifen (Alpha-Wert 35 von 255), der einen Glanzeffekt imitiert:
g2.setColor(new Color(255, 255, 255, 35));
g2.fillRect(mL, y, effLen, Math.max(barH / 3, 1));
Farben nach Höhe
Die 18 Farben für die Balken wurden manuell als Hex-Werte festgelegt. Der Verlauf folgt einer inhaltlichen Logik: Graugrün für geparkte Flugzeuge am Boden, über lebendiges Grün (Niedrigflug), Türkis und Blau (mittlere Höhen), bis zu sattem Violett für den Reiseflugbereich, und zurück zu gedämpftem Blaugrau für die Stratosphäre:
private static final Color[] FARBEN = {
new Color(120, 140, 100), // Parken – graugrün
new Color( 72, 168, 72), // 1-500 m – kräftiges Grün
// ... weitere Stufen ...
new Color( 98, 48, 188), // 10.500-11.000 – Violett (Gipfel)
// ...
new Color( 90, 85, 120), // über 13.000 – Grauviolett
};
Tabelle mit Zebrastreifen
Die Tabelle verwendet einen eigenen ZebraRenderer, der von DefaultTableCellRenderer erbt. Je nach Zeilennummer wird die Hintergrundfarbe gewechselt. Die Summenzeile erhält eine Sonderbehandlung:
@Override
public Component getTableCellRendererComponent(...) {
super.getTableCellRendererComponent(...);
if (!isSelected) {
boolean istSumme = row == table.getRowCount() - 1;
setFont(getFont().deriveFont(
istSumme ? Font.BOLD : Font.PLAIN));
setBackground(istSumme ? SUMME :
(row % 2 == 0 ? GERADE : UNGERADE));
}
return this;
}
// Farben:
GERADE = new Color(240, 246, 252) // helles Blaugrau
UNGERADE = Color.WHITE
SUMME = new Color(210, 232, 205) // helles Grün
Zwischenablage
Der „Kopieren“-Button baut einen formatierten String aus dem TableModel:
for (int i = 0; i < tabellenModell.getRowCount(); i++)
sb.append(String.format("%-24s ; %s%n",
tabellenModell.getValueAt(i, 0),
tabellenModell.getValueAt(i, 1)));
Dieser String wird dann per Toolkit in die System-Zwischenablage gelegt:
Toolkit.getDefaultToolkit().getSystemClipboard()
.setContents(new StringSelection(sb.toString()), null);
Damit kann der Inhalt direkt in Excel, eine E-Mail oder einen Texteditor eingefügt werden.
Build und Start
Das Projekt nutzt Maven als Build-System. Die pom.xml definiert Java 21 als Zielversion und bindet das Maven Shade Plugin ein, das alle
Abhängigkeiten in eine einzige ausführbare JAR-Datei bündelt (Fat-JAR):
mvn package
java -jar target/flug-monitor-swing-2.0.0.jar
Alternativ kann die Anwendung direkt aus der IDE (IntelliJ IDEA, Eclipse) gestartet werden, indem man FlugMonitorSwing.main() ausführt.
Voraussetzung ist eine Java-21-Installation sowie eine Internetverbindung zum OpenSky-Server. Die OpenSky-API ist ohne Registrierung nutzbar, unterliegt aber einem Rate-Limit (ca. 10 Anfragen pro Minute für anonyme Nutzer). Wer mehr Anfragen benötigt, kann sich kostenlos registrieren und erhält dann ein erhöhtes Kontingent.
Was zeigt der aktuelle Screenshot?
Der Screenshot entstand am Sonntag, dem 19. April 2026 um 06:33:30 Uhr.
Zu diesem frühen Sonntagmorgen-Zeitpunkt waren 4.821 Flugzeuge weltweit
von OpenSky erfasst. Die Verteilung zeigt das typische Muster:
– 662 Flugzeuge stehen am Boden („Parken“) – wartende Maschinen auf
Flughäfen in Asien, Europa und Amerika.
– Der Höhenbereich 11.000 – 11.500 m ist mit 498 Flugzeugen am
stärksten besetzt. Das sind überwiegend Langstreckenmaschinen auf
Interkontinentalrouten (z. B. Nordatlantik oder Transpazifik).
– Zwischen 5.000 und 7.000 Metern gibt es einen deutlichen Einbruch
(136 und 134 Flugzeuge). Diese „Lücke“ ist charakteristisch: Flugzeuge
durchqueren diesen Bereich nur kurz beim Steigen und Sinken, niemand
fliegt dort dauerhaft.
– Der zweite Gipfel bei 10.500 – 11.000 m (380 Flugzeuge) und
11.500 – 12.000 m (435 Flugzeuge) bestätigt, dass der gesamte Bereich
zwischen FL320 und FL390 der Hauptverkehrsweg der zivilen Luftfahrt ist.
Mögliche Erweiterungen
Der FlugMonitor lässt sich auf viele Weisen ausbauen:
Filterung nach Region: Die OpenSky-API erlaubt es, einen geografischen Ausschnitt (Bounding Box) zu übergeben. So könnte man nur Flugzeuge über Deutschland oder Europa anzeigen.
Automatische Aktualisierung: Ein javax.swing.Timer könnte alle 60 Sekunden automatisch aktualisieren, ohne dass der Nutzer auf den Button klicken muss.
Historische Daten: OpenSky bietet auch eine History-API. Damit ließe sich der Tagesverlauf darstellen – morgens weniger, tagsüber mehr Flugzeuge.
Karte: Mit einem Kartenbibliothek (z. B. JXMapViewer oder einer eingebetteten WebView) könnten die Flugzeuge auf einer Weltkarte als Punkte dargestellt werden.
CSV-Export: Neben dem Kopieren-Button könnte ein Export-Button eine CSV-Datei schreiben, die sich direkt in Excel öffnen lässt.
Dark Mode: Swing unterstützt mit modernen Look-and-Feels wie FlatLaf einen echten Dark Mode, der die dunkle DiagramPanel-Optik nahtlos in die gesamte Oberfläche integrieren würde.
Fazit
Der FlugMonitor zeigt, wie viel man mit purem Java und ohne externe Grafikbibliotheken erreichen kann. Die Kombination aus SwingWorker (für reaktive UI), Graphics2D (für das Diagramm) und der OpenSky-API (für Echtzeit-Daten) ergibt eine vollständige Desktop-Anwendung, die sich in wenigen Minuten bauen und starten lässt.
Der vollständige Quellcode und die Maven-Konfiguration stehen auf GitLab zum Download bereit.
Wer Fragen oder Anmerkungen hat, kann diese gerne in den Kommentaren hinterlassen – ich freue mich über jede Rückmeldung!

Vor 9 Jahren hat mich die Frage interessiert, in welcher Höhe eigentlich die meisten Flugzeuge unterwegs sind.
Statt mich nur auf vorhandene Tabellen zu verlassen, habe ich ein kleines Java‑Programm geschrieben, das in Echtzeit die Daten von The OpenSky Network abfragt. Es berücksichtigt also alle Flugzeuge mit Transponder, die von diesem Netzwerk erfasst werden – also eine repräsentative, wenn auch nicht vollständige Stichprobe des zivilen Luftverkehrs.

Die hier gezeigten Ergebnisse basieren auf einem Snapshot von 10100 Flugzeugen. Die Daten sagen nichts über Militärflugzeuge oder sehr spezielle Sonderflüge aus, geben aber einen guten Einblick in den typischen zivilen Reiseflughöhenbereich.
Flugbereich in Meter ; Flughöhe in Meter Parken 0 ; 1075 1-500 ; 824 500-1000 ; 820 1000-2000 ; 902 2000-3000 ; 594 3000-4000 ; 318 4000-5000 ; 281 5000-6000 ; 266 6000-7000 ; 260 7000-8000 ; 344 8000-9000 ; 343 9000-10000 ; 596 10000-10500 ; 452 10500-11000 ; 821 11000-11500 ; 958 11500-12000 ; 725 12000-13000 ; 429 über 13000 ; 92 Summme ; 10100 Zeitpunkt ; Fri Apr 17 14:47:08 CEST 2026 Quelle ; The OpenSky Network, http://www.opensky-network.org
Typische Flughöhen im zivilen Luftverkehr
Fachliche Übersichten und Betriebsdaten zeigen, dass kommerzielle Verkehrsflugzeuge sich im Reiseflug meist zwischen 9.000 und 12.000 Metern über dem Meeresspiegel bewegen. Das entspricht etwa 30.000 bis 40.000 Fuß, ein Bereich, in dem sich Treibstoffeffizienz, Luftwiderstand, Motoreffizienz und Sicherheitsanforderungen gut ausbalancieren.
Langstreckenjets wie A350 oder Boeing 787 nutzen oft höhere Reiseflüge (bei voller Ladung gerne bis knapp 13.000 m), während Kurzstreckenjets wie A320 oder B737 eher im Bereich von 31.000–38.000 Fuß operieren. Businessjets und ähnliche Spezialflugzeuge können noch höher steigen, bis in die Region von 15.000–16.000 Metern, was aber nur einen kleinen Teil der Gesamtflugzeuge im Luftraum ausmacht.
Technische Umsetzung: Java + OpenSky API
Da die API vor ein paar Wochen auf API-Keys umgestellt wurde, wird jetzt eine credentials.json Datei eingelesen. Die muss zuvor auf der Webseite im Userbereich erzeugt werden. Dann wird sie wie folgt eingelesen:
private void getData() throws IOException {
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(Path.of("credentials.json").toFile());
String clientId = root.get("clientId").asText();
String clientSecret = root.get("clientSecret").asText();
OpenSkyApi api = new OpenSkyApi(clientId, clientSecret);
OpenSkyStates states = api.getStates(0, null);
Collection<StateVector> stateVector = states.getStates();
FlugAltitude.printFlugHoehen(stateVector);
}
Das von mir eingesetzte Java‑Programm nutzt die OpenSky Network API, um aktuelle Flugdaten in Echtzeit zu beziehen.
Der Kern ist ein einfacher REST‑Client, der periodisch die Flugzeugpositionen, Höhen und Flugnummern abfragt und die Höhen in Meter bzw. Fuß aggregiert.
Da der Java-Client nicht mehr weiterentwickelt wird, musste ich die OpenSkyApi Methode getResponse wie folgt anpassen:
/**
* Make the actual HTTP Request and return the parsed response
*
* @param baseUri base uri to request
* @param nvps name value pairs to be sent as query parameters
* @return parsed states
* @throws IOException if there was an HTTP error
*/
private OpenSkyStates getResponse(String baseUri, Collection<AbstractMap.Entry<String, String>> nvps)
throws IOException {
HttpUrl parsedUrl = HttpUrl.parse(baseUri);
if (parsedUrl == null) {
throw new MalformedURLException("Could not parse uri " + baseUri);
}
HttpUrl.Builder urlBuilder = parsedUrl.newBuilder();
for (AbstractMap.Entry<String, String> nvp : nvps) {
urlBuilder.addQueryParameter(nvp.getKey(), nvp.getValue());
}
Request req = new Request.Builder().url(urlBuilder.build()).build();
Response response = okHttpClient.newCall(req).execute();
if (!response.isSuccessful()) {
throw new IOException("Could not get OpenSky Vectors, response " + response);
}
// Default: UTF-8, falls kein charset angegeben ist
Charset charset = StandardCharsets.UTF_8;
String contentType = response.header("Content-Type");
if (contentType != null) {
MediaType mediaType = MediaType.parse(contentType);
if (mediaType != null) {
Charset detected = mediaType.charset();
if (detected != null) {
charset = detected;
}
}
}
return mapper.readValue(new InputStreamReader(response.body().byteStream(), charset), OpenSkyStates.class);
}]]>

Was man heute konkret tun sollte
– Prüfe, ob deine wichtigsten Systeme (Laptop, Server, NAS, Smartphone, Raspberry Pi, …) überhaupt ein aktuelles Backup haben und seit wann das letzte Backup liefert.
– Führe mindestens ein vollständiges, konsistentes Backup deiner kritischen Daten aus: Quellcode‑Repositories, Konfigurationsdateien, Docker‑Volumes, Datenbank‑Dumps, Home‑Ordner, wichtige Dokumente.
– Verifiziere die Wiederherstellbarkeit: Versuche, eine Datei oder ein Verzeichnis aus dem Backup zu restoren, statt nur auf „Backup‑OK“ zu vertrauen.
Technische Best Practices für den Tag
– Nutze die 3‑2‑1‑Regel: mindestens drei Kopien, auf zwei Medientypen, mit einer Kopie außerhalb des lokalen Standorts (z. B. Cloud, externes Rechenzentrum).
– Automatisiere Backups soweit möglich (Skripte, cron, CI/CD‑Jobs, Backup‑Server‑Software) und dokumentiere den Ablauf, inkl. Restore‑Pfad.
– Sicher verschlüsselte Backups und achte auf Zugriffskontrollen, vor allem bei Cloud‑Backends.
Für Entwickler und Architekten
– Sichere regelmäßig nicht nur Dateien, sondern auch:
– Docker‑Images und Container‑Konfigurationen (Compose, Helm).
– Datenbank‑Backups mit definiertem Recovery‑Point.
– Führe mindestens einmal pro Quartal eine „Disaster‑Recovery‑Übung“ durch, bei der ein Entwickler oder ein Teil‑Team ohne den Originalsystemen produktiv werden muss.
Kurz: Was du heute tun kannst
– Heute ein vollständiges Backup deiner wichtigsten Systeme erstellen.
– Backup‑Kette und Restore‑Pfad testen.
– Notfalls eine einfache, automatisierte Backup‑Strategie für deine Entwickler‑ und Privat‑Systeme aufsetzen (z. B. rsync + externe Platte + Cloud‑Kopie).
Bei mir läuft auf dem Pi übrigens ein automatischer n8n Flow für das Backup:

Der World Backup Day ist technisch gesehen ein „Pflichttermin“ wie ein Patch‑Tag oder ein Security‑Audit – wer heute ein Backup macht, spart morgen vermutlich Zeit, Geld und Nerven.
]]>