Metadata
Ich habe im Zivi Leo kennengelernt, welcher noch einen alten Kassenbildschirm aus seiner Schule hatte. Wir hatten uns das Ziel gesetzt, ihn anzusteuern und daraus ein Wiener-Linien-Anzeigedisplay zu bauen.
Er hat dazu hier seinen eigenen Blog post verfasst.
Der Code zur Steuerung befindet sich hier.
Vieles ist handgeschrieben, allerdings wurde alles, was mit dem Webinterface interagiert, mithilfe von Deepseek verfasst, wobei sich die Kosten dafuer auf 50c belaufen haben.
Features und Funktion
Setup
Wenn das System zum ersten Mal angesteckt wird, zeigt es dir eine IP-Adresse und den Namen des Hotspots an, welchen der ESP hostet. Verbinde dich mit dem WLAN, gehe auf die Adresse mit dem Port 80 und gib die SSID und das Passwort des WLANs ein, mit welchem der Bildschirm arbeiten soll. Daraufhin wird er neu starten und versuchen, sich zu verbinden.
Dies wird dadurch gezeigt, dass ueber den Bildschirm Punkte ausgegeben werden.
Aufsetzen der Linien
Der Bildschirm sollte nun eine IP-Adresse anzeigen; besuche diese, um Linien hinzuzufuegen.
Bitte geduldig sein, der ESP ist in den weiteren Schritten nicht so schnell, da die Daten von einer externen Website gescraped werden muessen.
Im Dropdown musst du eine Linie auswaehlen, welche du hinzufuegen willst. Dann ein wenig warten, und alle Stationen sowie die Richtung werden alphabetisch angezeigt. Daraufhin musst du eine Station auswaehlen, ein wenig warten, und dann sollte sie in der Liste darueber hinzugefuegt sein. Dies muss fuer jede Station, die beobachtet werden soll, wiederholt werden.
Wenn der Beeper genutzt werden soll, muss neben den Stationen auf die Glocke gedrueckt und ein Zeitrahmen sowie Wochentage festgelegt werden, an denen gebeept werden soll und in welchem Abstand sich ein Fahrzeug befinden soll. Hierbei muss bedacht werden, dass der Beeper so lange beept, wie sich ein Fahrzeug in dieser Entfernung befindet; folglich kann es oefter beepen (nach jedem Refresh wird neu gecheckt).
Damit der Beeper aktiviert ist, muss diese Einstellung auch manuell mit diesem Button gespeichert werden.
Lesen der Anzeige
Die Anzeige hat zwei Modi, wobei der Debug-Modus automatisch eingestellt ist, wenn keine Eintraege vorhanden sind oder nach einem Neustart.
Im Debug-Modus werden die IP-Adresse des Displays und die Stationen, fuer die keine Infos ausgelesen werden koennen, angezeigt. Dies kann aus einigen Gruenden passieren:
- Die Station wurde aufgelassen.
- Es ist Nacht und dort haelt nichts mehr.
- Ein Bug in der Software.
Mit dem gruenen Button kann der Modus in den Anzeigemodus gewechselt werden.
Die Anzeige rotiert automatisch durch alle Eintraege; dies kann mit dem roten Button beschleunigt werden.
An erster Stelle steht die Linie, an zweiter die Endstation, welche angefahren wird; darauf kommen die Minuten fuer das naechste Fahrzeug und hinter dem Strich das darauffolgende Fahrzeug.
Falls das zweite Fahrzeug laenger als 99 Minuten brauchen wuerde, wird dieses nicht mehr angezeigt; falls das erste auch mehr als 99 Minuten dauern wuerde, wird die gesamte Linie ausgeblendet.
Wenn sich das Fahrzeug in der Station befindet und eine U-Bahn ist, beginnt ein * zu huepfen. Falls es ein Bus oder eine Strassenbahn ist, erscheint ein Block.
DISPLAY
Zum Ansteuern des Displays lag ein kleines Handbuch vor, in welchem das Protokoll beschrieben wurde und wie das Alphabet aufgebaut ist.
Dazu habe ich ein Headerfile entworfen, welches die Funktionen des Protokolls widerspiegelt.
class DSP800 {
private:
HardwareSerial &SerialPort;
public:
enum DisplayMode {
HORIZONTAL,
VERTICAL,
};
enum Language {
USA,
GERMANY,
UK,
};
DSP800(HardwareSerial &SerialPort) : SerialPort(SerialPort) {}
int setLanguage(Language l){
byte buf[] = {0x04, 0x01, 0x49, 0x00, 0x17};
if(l == USA){
buf[3] = 0x30;
} else if(l == GERMANY) {
buf[3] = 0x32;
} else if(l == UK){
buf[3] = 0x33;
}
SerialPort.write(buf, 5);
return 0;
}
int clear(int start = 0, int end = 39) {
if(end < start || start < 0 || end > 39) return -1;
byte buf[] = {0x04, 0x01,0x43,0x31 + start,0x31+end,0x17};
SerialPort.write(buf,6);
return 0;
}
int save(int layer = 0) {
if(layer > 2 || layer < 0) return -1;
byte buf[] = {0x04, 0x01,0x53,0x31 + (layer),0x17};
SerialPort.write(buf,5);
return 0;
}
int cursor_position(int position = 0) {
if(position >= 39 || position < 0) return -1;
byte buf[] = {0x04, 0x01,0x50,0x31 + (position),0x17};
SerialPort.write(buf,5);
return 0;
}
int display(std::array<bool, 3> layers, DisplayMode dpmode) {
byte buf[] = {0x04, 0x01,0x44,0x31, (dpmode == HORIZONTAL)? 0x31 : 0x32 ,0x17};
if(layers[0] && !layers[1] && !layers[2]){
} else if(!layers[0] && layers[1] && !layers[2]) {
buf[3] = 0x32;
} else if(layers[0] && layers[1] && !layers[2]) {
buf[3] = 0x33;
}else if(!layers[0] && !layers[1] && layers[2]) {
buf[3] = 0x34;
}else if(layers[0] && !layers[1] && layers[2]) {
buf[3] = 0x35;
}else if(!layers[0] && layers[1] && layers[2]) {
buf[3] = 0x36;
} else {
buf[3] = 0x37;
}
SerialPort.write(buf,6);
return 0;
}
void print(auto str = "") {
SerialPort.print(str);
}
};
Infolgedessen habe ich, um das Flickern zu verringern, einen Buffer eingebaut, um eine reine Update-Funktion (ueberschreibt nur die sich veraendernden Zeichen) zu implementieren.
Dies fuehrt immer noch zu lokalem Flickern beim Umschreiben, also bei den ~2 Zeichen vor und hinter der Aenderung, ist allerdings um Weiten besser als ein konstantes Screen-Flickern.
Des Weiteren werden die Aenderungen jetzt einzeln geschrieben, was bedeutet, dass man der Update-Funktion mit dem bloßen Auge dabei zusehen kann, wie sie sich von links oben nach rechts unten arbeitet. Doch glaube ich nicht, dass mir viel uebrig bleibt, als dies zu akzeptieren, da ich den Cursor einzeln setzen muss und der VFD nur einen Cursor besitzt, der geaendert werden kann.
Zudem habe ich mit dieser Funktion:
template <typename Container>
static char to_character_table(const Container& str, int &index){
if ((unsigned char)str[index] == 0xC3 && index + 1 < getLength(str)) {
index++;
switch ((unsigned char)str[index]) {
case 0xBC: return (char)0x7D; // ü
case 0xB6: return (char)0x7C; // ö
case 0xA4: return (char)0x7B; // ä
case 0x9F: return (char)0x7E; // ß
default:
index--;
return (char)0xC3;
}
}
return str[index];
}
die oesterreichischen Umlaute in das Characterset des VFDs uebersetzt, welches in der Anleitung abgebildet war.
Allgemein koennte man vielleicht noch mit den virtuellen Bildschirmen des VFDs selbst arbeiten, um bei grossen Schreibarbeiten das Verfahren zu beschleunigen, aber ich bin mit der derzeitigen Geschwindigkeit und Aesthetik sehr zufrieden.
Implementation unter DSP800.h
EEPROM
Or lack thereof
Der ESP hat keinen EEPROM, kann allerdings einen emulieren. Dies hat dann jedoch zu verschiedenen Synchronitaetsproblemen gefuehrt, weshalb ich am Ende bei der Arduino-eigenen Library (Preferences) gelandet bin, welche auf einem etwas hoeheren Level arbeitet. Fuer das Speichern der Stationsnamen habe ich dann allerdings die Hilfe von Deepseek in Anspruch genommen, um dafuer ein eigenes Dateiformat zu erstellen.
Abgesehen davon ist “Preferences” relativ leicht zu bedienen und funktioniert ohne Probleme.
Implementation unter EEPROM.h
SOUND
Der eingebaute Lautsprecher nimmt einem sehr viel Arbeit ab, und man muss sich nicht, wie ich urspruenglich gedacht hatte, selbst mit der Frequenz herumspielen, sondern muss nur den verbundenen GPIO-Pin auf HIGH setzen. Damit ist ein simpler Beep-Sound sehr einfach zu erzeugen:
class Buzzer {
int port;
public:
Buzzer(int port) : port(port) {};
void sound(int length) {
digitalWrite(port, 1);
delay(length);
digitalWrite(port, 0);
}
void blink() {
sound(50);
delay(20);
sound(50);
}
};
Vielleicht sollte ich dies, wenn moeglich, allerdings noch ueberarbeiten, da das Geraeusch sehr penetrant ist.
Implementation unter Sound.h
Wienerliniendisplay
Hier wird die Wiener-Linien-API genutzt, um die Abfahrtszeiten von bestimmten Stationen abzufragen. Dazu werden zuerst die Daten aus dem API-Request gefiltert, da dieser fuer meinen Nutzen nicht direkt ausgelegt ist. Nach Tests wissen wir, dass die Anzeigen in den Stationen nicht exakt dieselbe API nutzen koennen, aber die API ist trotzdem so aufgebaut, als waere sie dafuer ausgelegt. Es ist naemlich nicht moeglich, nach einzelnen Linien zu querien, sondern nur nach Bahnsteigen; dort werden dann einfach alle ankommenden Fahrzeuge wiedergegeben. Da mich allerdings nicht automatisch alle Fahrzeuge am selben Steig interessieren, sondern ich nach einzelnen querien koennen will, muss ich nach deren Bahnsteigen querien und dann nach den richtigen Fahrzeugen filtern.
Das System nimmt sich dann die ersten zwei Zuege und rendert diese in einen Vektor zur Anzeige auf dem Bildschirm.
Falls ein Zug derzeit in der Station ist, ersetze ich die Zahl entweder durch einen Stern oder einen Wuerfel und lasse diese hin und her springen.
Wobei die Wuerfel doppelt so schnell sind wie die Sterne; ich frage mich wirklich, wie es dazu gekommen ist, dass die zwei Systeme so unterschiedlich sind. Vielleicht lag es an den Anzeigen?
Implementation unter Wienerlinien.cpp
Webserver
Hierbei hatte ich viel Hilfe von Deepseek
Website
Der ESP hostet seine eigene Konfigurationsseite, auf welcher man die Fahrzeuge und die Stationen auswaehlen kann, die man beobachten will.
Wenn diese dann ausgewaehlt sind, koennen sie gespeichert werden, um danach den Beeper einzurichten, wann dieser einen Sound machen soll.
Da ich keinen einfachen Weg gefunden hatte, eine simple index.html-Datei zu hosten, habe ich mich dazu entschieden, den HTML-Teil von Deepseek schreiben zu lassen und ihn einfach direkt in die .cpp-Datei einzubetten. (Dieser braucht kein Syntax-Highlighting.)
Webscraping
Eine weitere Aufgabe der Datei ist das Webscraping. Um die aktuellen Linien sowie deren Stationen und IDs abzufragen, nutze ich die Website https://till.mabe.at/rbl/, da ich weder herausfinden konnte, wo man diese Infos von den Wiener Linien bekommt, noch, wie diese Website sie erhaelt. Somit blieb mir nichts anderes uebrig, als sie als Middleman zu nutzen.
Main und Rendering
Da das Display immer wieder eingefroren ist, musste ich das Fetchen der Daten von der API auf den anderen Core verlegen.
Das System laesst sich dann immer wieder die Anzeige aus der Wienerlinien.h rendern, damit die Sterne im richtigen Takt blinken und die Anzeige kontinuierlich weitergescrollt wird.
Weiters wird hier das WLAN-Setup fuer den Login gemanagt (danke Deepseek).