Wie schnell fährt der ICE gerade wirklich? Genau diese Frage kam mir während einer Zugfahrt. Passenderweise hatte ich das runde CrowPanel ESP32 Display mit 1,28 Zoll und 240 × 240 Pixeln dabei. Die Form des Displays bietet sich für einen Tachometer geradezu an – und statt einen simulierten Wert anzuzeigen, wollte ich die tatsächliche Geschwindigkeit des Zuges verwenden.
Die Geschwindigkeit stellt das ICE Portal im Bordnetz bereit. Was zunächst nach einem kleinen ESP32-Projekt aussah, wurde dadurch allerdings technisch interessanter als gedacht: Der ESP32 erreichte den Server, scheiterte aber beim TLS-Handshake. Die funktionierende Lösung besteht deshalb aus einer kleinen Java-Bridge auf dem Notebook und MicroPython auf dem CrowPanel. Das Ergebnis ist ein analoger Tacho mit einer Skala bis 300 km/h, roter Nadel und zusätzlicher digitaler Geschwindigkeitsanzeige.
Hardware und Aufbau des Projekts
Für den Prototyp bleibt der Aufbau bewusst einfach. Das CrowPanel ist per USB mit dem Notebook verbunden. Gleichzeitig befindet sich das Notebook im WLAN WIFIonICE. Weitere Sensoren oder ein GPS-Modul werden nicht benötigt.
| Komponente | Aufgabe |
|---|---|
| CrowPanel ESP32 Display 1.28″ | Rundes 240 × 240 Display mit ESP32-C3 |
| Notebook / PC | Ruft die ICE-Portal-API per HTTPS ab |
| USB-Kabel | Serielle Verbindung zwischen Java und ESP32 |
| WIFIonICE | Zugriff auf das lokale ICE Portal |
| MicroPython | Empfängt die Geschwindigkeit und zeichnet den Tacho |
Das Display ist das CrowPanel ESP32 Display 1.28inch. Auf meinem Board läuft MicroPython 1.29.0 auf einem ESP32-C3.



Die aktuelle ICE-Geschwindigkeit aus dem WIFIonICE
Im Bordnetz des ICE ist unter iceportal.de eine Status-API erreichbar. Für dieses Projekt ist der folgende Endpunkt interessant:
Hinweis zum ICE Portal: Der hier verwendete API-Endpunkt ist nur aus dem Bordnetz des Zuges erreichbar. Das Notebook muss dafür mit
WIFIonICEverbunden sein; außerhalb des Zuges lässt sich die Adresse in der Regel nicht aufrufen. Die Schnittstelle gehört zum ICE Portal und ist kein Bestandteil dieses Projekts. Aufbau, Endpunkte und verfügbare Felder können sich deshalb künftig ändern oder zeitweise nicht verfügbar sein.
https://iceportal.de/api1/rs/status
Während meiner Fahrt lieferte die API beispielsweise folgende Antwort:
{
"bapServiceStatus": "PAUSED",
"connection": true,
"serviceLevel": "AVAILABLE_SERVICE",
"gpsStatus": "VALID",
"internet": "HIGH",
"latitude": 50.8749156667,
"longitude": 9.5873923333,
"series": "401",
"speed": 220.6,
"trainType": "ICE",
"tzn": "ICE0157",
"vzn": 995,
"wagonClass": "FIRST"
}
Für den ersten Tacho interessiert mich nur das Feld speed. Der Wert 220.6 entspricht dabei 220,6 km/h. Die API liefert aber bereits deutlich mehr Informationen, die sich später für weitere Ansichten nutzen lassen.
Warum ich die API nicht direkt auf dem ESP32 abrufe
Mein erster Ansatz war naheliegend: ESP32 mit WIFIonICE verbinden, Status-API aufrufen und den Wert direkt darstellen. Das WLAN selbst funktionierte problemlos. Auch die Namensauflösung war erfolgreich; iceportal.de wurde im Zug auf die lokale Adresse 172.18.1.110 aufgelöst.
Ein Zugriff über HTTP auf Port 80 erreichte den Server ebenfalls, wurde aber konsequent auf HTTPS umgeleitet:
HTTP/1.1 301 Moved Permanently Location: https://iceportal.de/api1/rs/status
Beim HTTPS-Zugriff brach MicroPython den TLS-Handshake dagegen mit folgendem Fehler ab:
MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE
Ein Gegencheck mit dem Arduino-ESP32-Core endete an derselben Stelle. Der Fehler war damit nicht auf meinen MicroPython-Code beschränkt:
HTTP Code: -1 HTTP Fehler: connection refused TLS Error Code: -30592 TLS Error: SSL - A fatal alert message was received from our peer
Ein kleiner Test der beim Arduino-Build aktivierten mbedTLS-Optionen zeigte außerdem:
TLS 1.2: JA TLS 1.3: NEIN TLS 1.3 Ephemeral KEX: NEIN X25519: JA secp256r1 / P-256: JA
Auf meinem Windows-Notebook funktionierte derselbe API-Aufruf dagegen mit TLS 1.3 und lieferte HTTP 200. Für den Prototyp wollte ich deshalb nicht erst einen eigenen ESP-IDF- oder MicroPython-Build erstellen. Da das Notebook ohnehin per USB mit dem CrowPanel verbunden ist, übernimmt es einfach den HTTPS-Teil.
Architektur: Java als Bridge zwischen ICE Portal und ESP32
Damit ergibt sich für den Prototyp eine klare Aufgabenteilung:

Java ruft die API ab und extrahiert den Wert speed. Statt das komplette JSON an den ESP32 zu schicken, übertrage ich nur eine einzelne Textzeile wie 220.6. Das hält das serielle Protokoll klein und die MicroPython-Seite muss kein JSON mehr parsen.
Software-Voraussetzungen und Java-Projekt bauen
Bevor die Java-Bridge gestartet werden kann, müssen auf dem Notebook einige Werkzeuge vorhanden sein. Getestet habe ich das Projekt unter Windows mit OpenJDK / Eclipse Temurin 11 und Maven. Für die MicroPython-Seite verwende ich Thonny zum Kopieren der Dateien auf das CrowPanel.
| Software | Verwendung |
|---|---|
| OpenJDK / Eclipse Temurin 11 | Ausführen und Kompilieren der Java-Bridge |
| Apache Maven | Abhängigkeiten laden und das Java-Projekt bauen |
| jSerialComm 2.11.4 | Serielle Kommunikation mit dem ESP32-C3; wird über Maven eingebunden |
| Thonny | MicroPython-Dateien auf das CrowPanel übertragen und testen |
| MicroPython 1.29.0 | Getestete Firmware auf meinem ESP32-C3 |
Das Maven-Projekt ist auf Java 11 ausgelegt. Deshalb steht in meiner pom.xml auch das entsprechende Compiler-Release:
<properties>
<maven.compiler.release>11</maven.compiler.release>
</properties>
Im Verzeichnis des Java-Projekts wird anschließend das Projekt inklusive der benötigten Abhängigkeiten gebaut:
mvn clean package
Nach einem erfolgreichen Build liegen die kompilierten Klassen im Verzeichnis target/classes. Die Bibliothek jSerialComm wird dabei automatisch aus dem Maven-Repository geladen. Erst danach lässt sich die Bridge mit dem weiter unten gezeigten java -cp-Befehl starten.
Java-Projekt mit Maven und jSerialComm
Für die serielle Verbindung verwende ich jSerialComm. Im Maven-Projekt reicht dafür die folgende Dependency:
<dependency>
<groupId>com.fazecast</groupId>
<artifactId>jSerialComm</artifactId>
<version>2.11.4</version>
</dependency>
Die Java-Anwendung öffnet den seriellen Port mit 115200 Baud. Wichtig war bei meinem ESP32-C3, DTR und RTS zu deaktivieren. Beim ersten Versuch wurde das Board sonst beim Öffnen des Ports zurückgesetzt.
serialPort.setComPortParameters(
115200,
8,
SerialPort.ONE_STOP_BIT,
SerialPort.NO_PARITY
);
serialPort.setFlowControl(
SerialPort.FLOW_CONTROL_DISABLED
);
serialPort.clearDTR();
serialPort.clearRTS();
Nach dem Öffnen des Ports wartet die Anwendung einige Sekunden, damit main.py auf dem ESP32 sicher gestartet ist. Danach wird die ICE-API regelmäßig abgefragt.
Für dieses Projekt reicht es, das Feld speed aus der Antwort zu extrahieren und anschließend als eine Zeile zu senden:
private static final Pattern SPEED_PATTERN =
Pattern.compile(
"\"speed\"\\s*:\\s*([0-9]+(?:\\.[0-9]+)?)"
);
private static void sendSpeed(
OutputStream serialOut,
double speed
) throws Exception {
String message = String.format(
java.util.Locale.US,
"%.1f%n",
speed
);
serialOut.write(
message.getBytes(StandardCharsets.US_ASCII)
);
serialOut.flush();
}
In der Konsole sieht das anschließend beispielsweise so aus:
HTTP: 200 -> ESP32: 218.6 km/h HTTP: 200 -> ESP32: 220.4 km/h HTTP: 200 -> ESP32: 222.1 km/h
MicroPython und das Problem mit dem 115-KB-Framebuffer
Auf der Displayseite kam noch ein zweites Problem hinzu. Der originale GC9A01-Treiber legt einen kompletten RGB565-Framebuffer für alle 240 × 240 Pixel an. Dafür werden auf einen Schlag 115.200 Byte benötigt:
240 x 240 x 2 Byte = 115.200 Byte
Obwohl MicroPython noch deutlich mehr als 115 KB freien Heap meldete, konnte dieser große zusammenhängende Block auf meinem ESP32-C3 nicht reserviert werden:
MemoryError: memory allocation failed, allocating 115200 bytes
Die funktionierende Lösung ist ein Teil-Framebuffer. Statt das komplette Display im RAM zu halten, verwende ich nur 240 × 80 Pixel. Das reduziert den benötigten Speicher auf 38.400 Byte. Der Bildschirm wird in drei horizontale Bereiche aufgeteilt und nacheinander aktualisiert.
| Framebuffer | Speicherbedarf | Ergebnis |
|---|---|---|
| 240 × 240 Pixel | 115.200 Byte | MemoryError auf meinem ESP32-C3 |
| 240 × 80 Pixel | 38.400 Byte | läuft stabil |
Dafür erbt meine Klasse weiterhin von framebuf.FrameBuffer, verwendet aber nur die reduzierte Höhe. Die bewährten Methoden für Initialisierung und SPI-Zugriff kommen weiterhin aus dem vorhandenen Elecrow-Treiber.
class LCDPartial(framebuf.FrameBuffer):
WIDTH = 240
BAND_HEIGHT = 80
write_cmd = LCD_1inch28.write_cmd
write_data = LCD_1inch28.write_data
init_display = LCD_1inch28.init_display
setWindows = LCD_1inch28.setWindows
write_text = LCD_1inch28.write_text
def __init__(self):
self.width = self.WIDTH
self.height = self.BAND_HEIGHT
self.buffer = bytearray(
self.width * self.height * 2
)
super().__init__(
self.buffer,
self.width,
self.height,
framebuf.RGB565
)
Analoger ICE-Tacho mit Skala bis 300 km/h
Nachdem Kommunikation und Speicherproblem gelöst waren, konnte ich mich endlich der eigentlichen Anzeige widmen. Der Tacho besitzt eine weiße Skala von 0 bis 300 km/h, Markierungen im Abstand von 10 km/h und Beschriftungen bei 0, 50, 100, 150, 200, 250 und 300 km/h. Die aktuelle Geschwindigkeit wird über eine rote Tachonadel dargestellt.
Die Position auf der Skala ergibt sich direkt aus dem Verhältnis zwischen aktueller und maximaler Geschwindigkeit:
MAX_SPEED = 300 ARC_START = 150 ARC_SWEEP = 240 progress = speed / MAX_SPEED angle = ARC_START + ARC_SWEEP * progress
Aus dem Winkel berechne ich die Position der Nadelspitze mit Sinus und Kosinus:
def polar_point(angle_deg, radius):
rad = math.radians(angle_deg)
x = int(
CENTER_X +
math.cos(rad) * radius
)
y = int(
CENTER_Y +
math.sin(rad) * radius
)
return x, y
Zusätzlich befindet sich unten mittig zwischen den Enden der Skala eine kleine digitale Anzeige. So liefert die Nadel einen schnellen visuellen Eindruck, während der exakte Wert weiterhin direkt ablesbar bleibt.
Geschwindigkeit seriell in MicroPython empfangen
Da Java nur noch eine Zahl pro Zeile sendet, ist der Empfang auf dem ESP32 sehr kompakt. Eine Zeile wie 220.6 wird direkt in einen Float umgewandelt und anschließend neu gezeichnet:
while True:
try:
line = sys.stdin.readline()
if not line:
continue
line = line.strip()
if not line:
continue
speed = float(line)
render_speedometer(
lcd,
speed
)
except ValueError:
pass
Reihenfolge beim Starten des Prototyps
Beim Testen ist wichtig, dass Thonny und Java nicht gleichzeitig denselben COM-Port öffnen können. Deshalb starte ich das Projekt in dieser Reihenfolge:
main.pyunddisplay_partial.pyauf dem ESP32 speichern.- CrowPanel resetten und prüfen, ob die Tachoanzeige startet.
- Thonny vom seriellen Port trennen oder schließen.
- Java-Bridge auf demselben COM-Port starten.
- Java ruft die ICE-API ab und sendet die Geschwindigkeit an MicroPython.
In meinem Test war der MicroPython-Port COM10. Der Java-Aufruf sah entsprechend so aus:
java -cp "target\classes;$env:USERPROFILE\.m2\repository\com\fazecast\jSerialComm\2.11.4\jSerialComm-2.11.4.jar" de.draegerit.IceSerialBridge COM10
Mögliche Erweiterungen
Für die erste Version reicht mir die Geschwindigkeit. Die API liefert aber bereits weitere Daten wie Zugtyp, Zugnummer, Baureihe, GPS-Status, Internetqualität und Position. Daraus könnte später ein kleines ICE-Dashboard mit mehreren Ansichten entstehen.
- Zugnummer und Baureihe anzeigen
- GPS-Status und Internetqualität darstellen
- Position ausgeben
- nächsten Halt und Verspätung ergänzen
- Touchscreen zum Wechseln zwischen mehreren Ansichten nutzen
- Java-Bridge später durch einen eigenen TLS-1.3-fähigen ESP32-Build ersetzen
Komplettes Projekt herunterladen
Damit du das Projekt nicht aus den einzelnen Codeblöcken zusammensetzen musst, stelle ich dir die vollständigen Projektdateien als ZIP-Archiv zur Verfügung. Enthalten sind die Java-Bridge für den Zugriff auf das ICE Portal sowie die MicroPython-Dateien für das CrowPanel ESP32 Display inklusive des speichersparenden Teil-Framebuffers und der Tachoanzeige.
Nach dem Entpacken müssen gegebenenfalls noch der verwendete COM-Port und die lokale Umgebung angepasst werden. Auf dem CrowPanel werden die MicroPython-Dateien gespeichert, während die Java-Anwendung auf dem Notebook läuft. Wichtig ist außerdem, Thonny vor dem Start der Java-Bridge vom seriellen Port zu trennen, da nicht beide Programme gleichzeitig auf denselben COM-Port zugreifen können.
Fazit
Aus der einfachen Idee, die aktuelle ICE-Geschwindigkeit auf einem runden Display darzustellen, wurde ein Projekt mit mehreren interessanten technischen Problemen. Das WLAN und die lokale API waren schnell gefunden, der TLS-Handshake auf dem ESP32 machte den direkten Zugriff aber zunächst unbrauchbar. Mit der Java-Bridge lässt sich das sauber umgehen, ohne für den Prototyp gleich eine eigene ESP-IDF-Firmware bauen zu müssen.
Auch auf der Displayseite war der direkte Weg nicht der beste: Der vollständige 240 × 240 RGB565-Framebuffer benötigt 115.200 Byte am Stück und ließ sich unter meiner MicroPython-Konfiguration nicht zuverlässig reservieren. Der kleinere 240 × 80 Teil-Framebuffer läuft dagegen stabil und reicht aus, um den kompletten Tacho in drei Bereichen zu zeichnen.
Das Ergebnis ist genau das, was ich unterwegs haben wollte: Während der Fahrt zeigt mir das kleine runde CrowPanel die reale Geschwindigkeit des ICE auf einer analogen Skala bis 300 km/h an – inklusive roter Tachonadel und digitalem Wert.
Letzte Aktualisierung am: 01. November 2026



