Advertise with Googlier.com Wenzlaff.de – Rund um die Programmierung http://blog.wenzlaff.de mit Java, Raspberry Pi, SDR, Linux, Arduino, Sicherheit, Blender, KI, Statistik, Krypto und Blockchain Sat, 19 Sep 2026 09:16:18 +0000 de hourly 1 https://wordpress.org/?v=7.1.1 http://blog.wenzlaff.de/wp-content/uploads/2015/08/cropped-tw-logo-32x32.png Wenzlaff.de – Rund um die Programmierung http://blog.wenzlaff.de 32 32 Top 10 Entwickler-Nationen – Deutschland Platz 7 – Java +9,35 % http://blog.wenzlaff.de/?p=23326 Sat, 19 Sep 2026 09:13:38 +0000 http://blog.wenzlaff.de/?p=23326

Ähnliche Artikel:

  1. RaspberryPi Transponder Feed zu RadarBox jetzt auf Platz 19 in Deutschland und Platz 414 in der Welt
  2. Platz 4666 in Deutschland mit S920 Feed auf FR24
  3. Wie können kostenlos PDF Formulare schnell mit OpenOffice erstellt werden?
]]>


Siehe hier.

]]>
Quicktest Eclipse 2026-06 mit MCP Client Server Demo http://blog.wenzlaff.de/?p=23315 Fri, 11 Sep 2026 10:07:56 +0000 http://blog.wenzlaff.de/?p=23315 „Quicktest Eclipse 2026-06 mit MCP Client Server Demo“ weiterlesen

]]>
Das Update der vorhandenen Version ging leider nicht. Deshalb war eine neu Installation von Eclipse nötig.

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:

]]>
Drzewa mamutowe w Niemczech http://blog.wenzlaff.de/?p=23305 Sat, 25 Jul 2026 15:08:45 +0000 http://blog.wenzlaff.de/?p=23305

Ähnliche Artikel:

  1. PDF Dateien zusammenfügen und extrahieren auf dem Raspberry Pi 4 B mit pdfunite und pdfseparate
  2. Raspberry Pi: XML formatieren mit xmllint unter Linux
  3. Zweizeiler REST HTTPClient am Beispiel von AirLabs für Planespotter
]]>
Fotograficzna dokumentacja z Langenhagen, Hanoweru, Magdeburga, Bad Salzdetfurth i Bremy. Download: Drzewa-Mamutowe-w-Niemczech-PL.pdf (6MB).

]]>
Mammoth Trees in Germany http://blog.wenzlaff.de/?p=23301 Fri, 24 Jul 2026 05:13:31 +0000 http://blog.wenzlaff.de/?p=23301

Ähnliche Artikel:

  1. PDF Dateien zusammenfügen und extrahieren auf dem Raspberry Pi 4 B mit pdfunite und pdfseparate
  2. Raspberry Pi: XML formatieren mit xmllint unter Linux
  3. Zweizeiler REST HTTPClient am Beispiel von AirLabs für Planespotter
]]>
A photographic survey from Langenhagen, Hanover, Magdeburg, Bad Salzdetfurth and Bremen: Download Mammoth-Trees-in-Germany-EN.pdf.

]]>
Den Urweltmammutbaum selbst ziehen als Aktivitäts Diagramm (für Informatiker) http://blog.wenzlaff.de/?p=23292 Thu, 23 Jul 2026 16:45:14 +0000 http://blog.wenzlaff.de/?p=23292

Ähnliche Artikel:

  1. 24. Jahrestag wenzlaff.de: Dank per chatGPT
  2. Kryptowährung schürfen mit einem Raspberry Pi
  3. Welche Flughäfen und wieviele gibt es in DE, JP, US, RU …. ?
]]>

Für Programmierer …

Dann mal los …

]]>
23000 Mammutbäume in Deutschland (auch Bremen, Hannover und Langenhagen …) mit plantUML und Test http://blog.wenzlaff.de/?p=23283 Sat, 18 Jul 2026 15:38:49 +0000 http://blog.wenzlaff.de/?p=23283

Ähnliche Artikel:

  1. Raspberry Pi: XML formatieren mit xmllint unter Linux
  2. PDF Dateien zusammenfügen und extrahieren auf dem Raspberry Pi 4 B mit pdfunite und pdfseparate
  3. Software Bill of Materials (SBOM) mit CycloneDX Maven Plugin leicht erzeugen
]]>
Eine fotografische Bestandsaufnahme aus Langenhagen, Hannover, Magdeburg, Bad Salzdetfurth und Bremen zum kostenlosen Download als PDF (13 Seiten). Und hier die neue Version vom 21.7.2026 mit Quiz als PDF (6,5 MB).

]]>
Top Ten KI-Themen für Erwachsene (2026) http://blog.wenzlaff.de/?p=23273 Sat, 04 Jul 2026 14:05:45 +0000 http://blog.wenzlaff.de/?p=23273

Ähnliche Artikel:

  1. nmon 16g für CPU, Speicher, Netzwerk, Disk, Top … es muss ja nicht immer top (htop) sein
  2. Neue Version: 2026-03 (4.39.0) der Eclipse IDE im Quicktest
  3. Quicktest: Eclipse 2026-06 (4.40.0)
]]>

]]>
Quicktest: Eclipse 2026-06 (4.40.0) http://blog.wenzlaff.de/?p=23266 Fri, 19 Jun 2026 06:19:35 +0000 http://blog.wenzlaff.de/?p=23266 „Quicktest: Eclipse 2026-06 (4.40.0)“ weiterlesen

]]>
Eclipse IDE 2026-06 bringt drei auffällige Verbesserungen:

– 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.

]]>
Automatisch UML Generierung (plantUML-Format) mit ArchUnit und externe Abhängigkeiten http://blog.wenzlaff.de/?p=23256 Sun, 07 Jun 2026 04:50:16 +0000 http://blog.wenzlaff.de/?p=23256 PaketB“ ) in einer  Set  sammelt. Er nutzt moderne Java-Features wie  Stream ,  flatMap ,  filter ,  map  und  Collectors . [crayon-6ab3e795b9b7e525726728/] ]]> externe UML

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  sammelt. Er nutzt moderne Java-Features wie  Stream ,  flatMap ,  filter ,  map  und  Collectors .

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

]]>
Automatisch UML Generierung (plantUML-Format) mit ArchUnit http://blog.wenzlaff.de/?p=23250 Sun, 07 Jun 2026 03:05:17 +0000 http://blog.wenzlaff.de/?p=23250 „Automatisch UML Generierung (plantUML-Format) mit ArchUnit“ weiterlesen

]]>
Die Kombination aus ArchUnit und einem PlantUML-Export für Dependency-Graphs bringt einen direkten Zusammenhang zwischen implementierter Softwarearchitektur und ihrer visuellen Darstellung. Dadurch entsteht eine sogenannte Living Architecture, bei der die Architektur nicht mehr als statisches Dokument existiert, sondern kontinuierlich aus dem tatsächlichen Codebestand abgeleitet wird.

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

]]>
Gerichteter azyklischer Graph (DAG) am Beispiel von Mammutbäumen http://blog.wenzlaff.de/?p=23243 Fri, 05 Jun 2026 07:04:32 +0000 http://blog.wenzlaff.de/?p=23243 „Gerichteter azyklischer Graph (DAG) am Beispiel von Mammutbäumen“ weiterlesen

]]>
azyklischer Graph

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

]]>
KI: Flugsensor mit ESP32 mit WIFI & Bluetooth Entwicklungsboard 2,8 Zoll LCD TFT Modul mit Touch http://blog.wenzlaff.de/?p=23230 Mon, 25 May 2026 15:41:01 +0000 http://blog.wenzlaff.de/?p=23230 „KI: Flugsensor mit ESP32 mit WIFI & Bluetooth Entwicklungsboard 2,8 Zoll LCD TFT Modul mit Touch“ weiterlesen

]]>
Wenn ich aus dem Küchenfenster schaue, sehe ich alle Flugzeuge die aus dem Osten kommen und in Langenhagen (EDDV, HAJ) landen wollen. Habe schon einige Projekte in diesem Zusammenhang gemacht, aber die sind noch alle per Hand, und nicht mit KI entstanden.

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

]]>
Spring Boot vs. Quarkus am Beispiel von STOMP http://blog.wenzlaff.de/?p=23209 Sun, 17 May 2026 03:52:44 +0000 http://blog.wenzlaff.de/?p=23209 „Spring Boot vs. Quarkus am Beispiel von STOMP“ weiterlesen

]]>
STOMP

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 Kurzreferenz http://blog.wenzlaff.de/?p=23215 Sat, 16 May 2026 18:37:40 +0000 http://blog.wenzlaff.de/?p=23215 „Markdown Kurzreferenz“ weiterlesen

]]>
markdown

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.

Markdown-Spickzettel (20 wichtigste Befehle)

Grundformatierung

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 Code
Codeblock (mehrzeilig)
Codezeilen

Listen

Bedeutung Markdown‑Code
Ungeordnete Liste - Punkt 1
  - Unterpunkt
Geordnete Liste 1. Erstens
  2. Zweitens
Checkliste (Tasks) - [ ] Offen
  - [x] Erledigt

Verweise und Inhalte

Bedeutung Markdown‑Code
Link [Text](https://beispiel.de)
Bild ![Alt-Text](bild.png)
Zitat‑Block > Das ist ein Zitat.
Horizontale Linie ---
Tabelle
| Header 1 | Header 2 |
|---------|---------|
| A | 1 |
Fußnote Text mit Fußnote[^1]

[^1]: Erklärung der Fußnote.

Metadaten‑Block
---
title: Titel
author: Name
---

Sonstiges

Bedeutung Markdown‑Code
Zeilenumbruch Zeile 1
Zeile 2 (Leerzeile dazwischen)
Escape‑Steuerzeichen \*ohne Kursiv\*
HTML‑Link (ohne Text) <https://beispiel.de>
]]>
Wie hoch fliegen die Flugzeuge gerade über uns? – Echtzeit im Browser http://blog.wenzlaff.de/?p=23199 Sun, 10 May 2026 08:20:41 +0000 http://blog.wenzlaff.de/?p=23199 „Wie hoch fliegen die Flugzeuge gerade über uns? – Echtzeit im Browser“ weiterlesen

]]>

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:

]]>
Squawk-Codes, auch Transpondercodes weltweit anzeigen: „Seven-seven – going to heaven“ – „Seven-six – hear nix“ – „Seven-five – man with a knife“ http://blog.wenzlaff.de/?p=23187 Wed, 22 Apr 2026 17:24:00 +0000 http://blog.wenzlaff.de/?p=23187 „Squawk-Codes, auch Transpondercodes weltweit anzeigen: „Seven-seven – going to heaven“ – „Seven-six – hear nix“ – „Seven-five – man with a knife““ weiterlesen

]]>
Squawk-Codes, auch Transpondercodes genannt, sind standardisierte vierstellige Oktalzahlen (Ziffern 0–7), die Piloten in den Bordtransponder eingeben, um Luftfahrzeuge für die Flugsicherung (ATC) eindeutig identifizierbar zu machen. Im Notfall dienen spezifische Codes der ICAO als universelle Signale, die unabhängig von Sprachbarrieren oder Funkausfällen priorisierte Maßnahmen auslösen.

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.

]]>
KML-Export: Globale Flugzeugdaten speichern und auswerten http://blog.wenzlaff.de/?p=23177 Tue, 21 Apr 2026 02:36:14 +0000 http://blog.wenzlaff.de/?p=23177 „KML-Export: Globale Flugzeugdaten speichern und auswerten“ weiterlesen

]]>
Mit dem neuen Button „KML Export“ in der FlugMonitor-Anwendung lassen sich die Echtzeit-Daten aller aktuell von OpenSky erfassten Flugzeuge weltweit in eine KML-Datei exportieren. KML steht für „Keyhole Markup Language“ – ein XML-basiertes Dateiformat, das von Google Earth, Google Maps, QGIS und vielen anderen Kartenprogrammen direkt geöffnet werden kann. Hier mal eine Ansicht in Google Earth:

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.

Flugzeugdaten

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.

]]>
FlugMonitor – Echtzeit-Flugverkehr mit Java und OpenSky visualisieren http://blog.wenzlaff.de/?p=23164 Sun, 19 Apr 2026 05:37:25 +0000 http://blog.wenzlaff.de/?p=23164 „FlugMonitor – Echtzeit-Flugverkehr mit Java und OpenSky visualisieren“ weiterlesen

]]>
Wer kennt das nicht: Man schaut in den Himmel, sieht ein Flugzeug und fragt sich, wo es gerade herkommt und in welcher Höhe es fliegt. Mit dem FlugMonitor lässt sich genau diese Frage beantworten – und zwar für alle Flugzeuge auf der Welt gleichzeitig. In diesem Artikel zeige ich, wie ich eine Java-Swing-Anwendung gebaut habe, die Echtzeit-Daten des OpenSky Network abruft, auswertet und übersichtlich darstellt.

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!

]]>
Fünfter Streiktag in Folge bei der Lufthansa: In welcher Höhe fliegen 10100 Flugzeuge? http://blog.wenzlaff.de/?p=23160 Fri, 17 Apr 2026 13:09:18 +0000 http://blog.wenzlaff.de/?p=23160 „Fünfter Streiktag in Folge bei der Lufthansa: In welcher Höhe fliegen 10100 Flugzeuge?“ weiterlesen

]]>
In welcher Höhe fliegen Flugzeuge?

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

]]>
Backup? Hab ich vergessen – aber ich hab noch Hoffnung auf „Ewiges Glück“ http://blog.wenzlaff.de/?p=23150 Tue, 31 Mar 2026 06:26:16 +0000 http://blog.wenzlaff.de/?p=23150 „Backup? Hab ich vergessen – aber ich hab noch Hoffnung auf „Ewiges Glück““ weiterlesen

]]>
Heute, am 31. März, ist der World Backup Day – ein Aktionstag, der daran erinnert, dass zuverlässige Backups heute zum IT‑Standard gehören und nicht nur für Unternehmen, sondern auch für Privatpersonen lebenswichtig sind. Für Entwickler, Systemadmins und IT‑Nutzer ist das der ideale Zeitpunkt, um seine Backup‑Strategie zu hinterfragen und gegebenenfalls zu erneuern.

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:

n8n 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.

]]>