Motivation

UrsWiFiSerial kommuniziert per TCP.  TCP stellt sicher, dass der Client die Daten korrekt erhalten hat, und dass sie in der richtigen Reihenfolge ankommen. Durch die hierbei durchzuführenden Handshakes dauert die Übertragung eines Datenpakets relativ lange. Wenn dann auch noch die Datenpakete relativ klein sind, z.B. wie bei der zeilenweisen Übertragung bei UrsWifiSerial, steht nur eine geringe Bandbreite zur Verfügung. Des Weiteren ist nur ein Unicast-Modus möglich, d.h. Sender und Empfänger müssen sich (d.h. IPs und Ports) "kennen".

Der Datentransfer per UDP ist wesentlich schlanker und damit der Transport eines einzelnen Pakets weniger aufwändig. Pakete können in schnellerer Folge versandt werden. Außerdem bietet UDP den Multicast-Modus. Jeder der einer Multicast-Gruppe beitritt, erhält sämtlich Nachrichten, die an die Gruppe versandt werden. Damit ist es möglich, mit anonymen IP-Endpunkten (IP-Adresse und Port) zu arbeiten. Der ESP, der Daten per WLAN senden oder empfangen will, tut dies über eine für ihn spezifische Multicast-Gruppen-Endpunkt. Jedes andere Gerät, z.B. ein PC der Logging-Daten vom ESP per WLAN empfangen will, meldet sich an der Gruppe an bzw. sendet Daten an die Gruppe. Weder muss der ESP die IP des PCs kennen noch muss der PC die Adresse des ESP kennen. Beide Geräte sind damit einfach austauschbar.

Der große Nachteil von UDP ist, das nicht sichergestellt ist, dass alle Pakete beim Empfänger ankommen und das dies in der richtigen Reihenfolge geschieht. Außerdem ist möglich, dass Pakete doppelt ankommen, wenn sie über verschieden Wege laufen. In einem lokalen WLAN werden diese Probleme äußerst selten bis gar nicht auftreten. Die Transportstrecke ist eindeutig und das ein Paket ein anderes überholt ist nahezu ausgeschlossen.

2019-02-11: Eine abgeleitete Klasse eignet sich gut zum Debugging.

In­halts­ver­zeich­nis

Bibliothek UrsUdpSerial

Verwendung

Funktionsübersicht

Beispiel

Implementierung

UrsUdpDebug

Download


Version Anpassungen
1.0 (2017-05-26) Basis-Version
1.1 (2017-07-08)

Nach jedem Senden (endpacket()) ein delay(1) eingefügt. Es gab Probleme, wenn direkt hintereinander viele Zeilen ausgegeben wurden.

1.2 (2017-11-06) Viele Schreibfehler ("UPD" statt "UDP") korrigiert.
1.3 (2018-01-11) Wenn das UDP-Paket nicht versandt werden kann, blieb der Pufferinhalt erhalten. Dieser wurde dann beim nächsten Versand mit versendet. Ggf. konnte auch ein Pufferüberlauf statt finden.
Ab dieser Version wird der Puffer bei jedem Sendeversuch gelöscht.
1.4 (2018-01-20) - Methode remoteIP() -> getRemoteIP()
- Methode remotePort() -> getRemotePort()
- Neue Option: Betrieb ohne Empfangsfunktion, z.B. für Logging
1.5 (2018-07-08) Return-Wert bei "begin" wurde nicht weitergeleitet.
1.6 (2018-07-08) flush() überträgt zuerst noch nicht gesendete Daten.
1.7 (2018-02-11 - Destruktor als virtual deklariert.
- Klasse UrsUdpDebug hinzugefügt.

Bibliothek UrsUdpSerial

UrsUdpSerial ist von Stream abgeleitet, liefert also den gleichen Funktionsumfang, puffert aber die Übertragungsdaten zwischen und steigert somit die Performance auf Grund der Reduzierung der zu übertragenden UDP-Datagramme. Ein interne Instanz von WiFiUDP übernimmt die Datenübertragung. Die Datenübertragung erfolgt, wenn

Verwendung

Der Constructor von UrsUdpSerial legt die Größe des Sende-Puffers in Byte fest.

UrsUdpSerial MySerial(64);

legt eine Instanz von UrsUdpSerial mit dem Namen MySerial  und einer Puffergröße von 64 Byte an. Ohne die explizite Angaben (UrsUdpSerial MySerial;) einer Puffergröße wird der Standardwert 32 genommen.

MySerial.begin(remoteIP, remotePort, subscriberPort);

startet den Datenempfang über subscriberPort und legt fest, dass die Ausgabe an den IP-Endpoint (remoteIP, remotePort) erfolgen soll. Wenn nur eine Sendefunktion gewünscht wird, kann subscriberPort fortgelassen oder auf 0 (Voreinstellung) gesetzt werden. Wenn nur eine Empfangsfunktion gewünscht wird, kann remotePort auf 0  gesetzt werden. Ein erneutes Aufrufen von begin() stoppt einen aktiven Server und versucht dann einen neuen zu starten.

MySerial.println("Hello World");

stellt die angegebene Zeichenfolge in den Zeichenpuffer und überträgt sie auf Grund des Zeilenende-Zeichen wegen println().

MySerial.xmitOnLf = false;

stellt den Zeilenende-Mechanismus ab. true schalten ihn wieder an.

MySerial.xmit();

sogt für die Übertragung der Zeichen im Puffer.

MySerial.available();

und

MySerial.read();

dienen dem Datenempfang.

Multicast

Wenn die übergebene Remote-IP-Adresse eine IP-Adresse im Bereich 224.0.0.0 und 239.255.255.255 ist, wird davon ausgegangen, dass Multicast-Betrieb gewünscht ist. UrsUdpSerial benutzt dann die entsprechenden Multicast-Funktion von WiFiUDP. Für den Datenempfang meldet sich WiFiUDP an der angegebenen Gruppe an und empfängt Daten, die an die Gruppe gesandt werden, ausgehende Daten werden an die Gruppe gesendet.

Funktionsübersicht

Funktion Beschreibung Anmerkung
UrsUdpSerial (
        uint16_t bufferSize = 32)
Legt eine neue Instanz von UrsUdpSerial mit einem Zeichen-Puffer der angegebenen Größe unter dem Namen MySerial an.

Die optimale Größe des Puffers ist abhängig von der Struktur der zu übertragenden Daten. 32 Byte ist ein guter Kompromiss.

uint8_t begin (
             IPAddress remoteIP,
             uint16_t    remotePort,
             uint16_t    subscriberPort,
             uint16_t    ttl = 1)

Startet den UDP-Client.

  • remoteIP: IP, an die die Datagramme gesendet werden sollen.
  • remotePort: zugehöriger Port.
  • subscriberPort: Lokaler Port, auf dem empfangen werden soll.
  • ttl: Time To Live = Anzahl erlaubter Hops (nur bei Multicast relevant, der Standardwert ist 1).

Liefert 1, wenn erfolgreich, 0 bei Fehler.

Bei Adressen (remoteIP) zwischen 224.0.0.0 und 239.255.255.255 wird Multicast angenommen. remoteIP ist dann die Gruppen-IP. Es wird an die Gruppe gesendet und von ihr empfangen.

Wenn nur eine Sendefunktion gewünscht wird, kann subscriberPort fortgelassen oder auf 0 (Voreinstellung) gesetzt werden.

Wenn nur Daten empfangen werden sollen, können remoteIP und
remotePort auf 0 gesetzt werden.

Ein erneutes Aufrufen von begin() stoppt einen aktiven Server und versucht dann einen neuen zu starten.

 uint8_t begin (
             const char* remoteHost,
             uint16_t remotePort,
             uint16_t localPort,
             uint16_t ttl = 1)

Startet den UDP-Client.

  • remoteHost: Name des Host, an die die Datagramme gesendet werden sollen.
  • remotePort: zugehöriger Port.
  • subscriberPort: Lokaler Port, auf dem empfangen werden soll.
  • ttl: Time To Live = Anzahl erlaubter Hops (nur bei Multicast relevant, der Standardwert ist 1).

Liefert 1, wenn erfolgreich, 0 bei Fehler.

 
 uint8_t begin (
             const String remoteHost,
             uint16_t remotePort,
             uint16_t localPort,
             uint16_t ttl = 1)
wie oben  
void stop() Trennt die Verbindung zum Server. Gibt sämtliche Ressourcen frei, die während der UDP-Sitzung verwendet wurden. Erneuter Start durch Methode begin(...).
XmitOnLf Die boolesche Variable XmitOnLf legt fest, ob eine automatische Übertragung nach dem Senden eines Zeilenendezeichens '\n' erfolgen soll. Der Standardwert ist true.
 bool isMulticast() Gibt an, ob Multicast-Betrieb vorliegt.  
IPAddress getRemoteIP() Ruft die IP-Adresse der Remote-Verbindung ab. Wird an UDPClient.remoteIP() weiter geleitet.
uint16_t getRemotePort() Ruft den Port der Remote-Verbindung ab. Wird an UDPClient.remotePort() weiter geleitet.
size_t xmit (
         const uint8_t * = NULL,
         size_t size = 0)
Sendet noch im Zeichenpuffer vorhandene Zeichen und überträgt
anschließend den übergebenen Datenblock.
Der Datenblock wird nicht gepuffert, sondern direkt übertragen.
Die Angabe des Puffers ist optional. Wird er nicht angegeben, wird nur der Inhalt des Zeichenpuffers übertragen.
size_t NumberOfBytesToSend() Liefert die Anzahl Zeichen, die im Zeichenpuffer aktuell auf die Übertragung warten.  
 size_t write(uint8_t)
Schreibt einzelnes Byte in den Zeichenpuffer. Diese Methode wird von Print zur Ausgabe eines Zeichens genutzt. Überlädt Print::write(..)
size_t write(const uint8_t *, size_t) Schreibt einen Block in den Zeichenpuffer. Diese Methode wird von Print zur Ausgabe eines Zeichenblocks genutzt. Überlädt Print::write(..)
int available() Liefert die Anzahl der (im aktuellen Paket) verbleibenden Bytes. Überlädt Stream::available()
Pakete werden automatisch nachgeladen.
int read() Liest ein einzelnes Byte. Überlädt Stream::read()
Pakete werden automatisch nachgeladen.
int peek() Liest ein Byte aus der Datei, ohne zum nächsten zu wechseln. D.h., aufeinanderfolgende Aufrufe von peek () werden den gleichen Wert zurückgeben, wie der nächste Aufruf von read(). Überlädt Stream::peek()
Pakete werden automatisch nachgeladen.
int flush() Sendet zunächst noch nicht übertragene Daten. Anschließend wird die Bearbeitung des aktuellen UDP-Pakets beendet. Überlädt Stream::flush()
siehe auch WiFiUPD::flush()

Alle anderen Funktionen sind identisch mit denen der Stream-Klasse. read(), peek() und available() beziehen sich auf das aktuell eingelesene Datenpaket. Gibt es ein solches nicht oder ist das vorhandene komplett ausgelesen, wird automatisch nachgeladen.

Links:

Beispiel

Das folgende Beispiel – UDP-Serial-Bridge – zeigt die Verwendung.

// UrsUdpSerial example
// UDP-Serial-Bridge
//
// Autor: http://UllisRoboterSeite.de
// Doku:  http://ullisroboterseite.de/esp8266-udpserial.html

#include <ESP8266WiFi.h>
#include <UrsUdpSerial.h>

// Anmeldedaten für das Netzwerk
const char* ssid = "....";             // !!! Ersetzen !!!
const char* password = "....";         // !!! Ersetzen !!!

// Multicast-Gruppen-Adresse, an die Daten gesandt werden sollen
const IPAddress GroupIP = IPAddress(230, 230, 230, 230);                         
const unsigned int GroupPort = 2001;  // zug. Ports

// Multicast-Gruppen-Addresse, über die Daten empfangen werden sollen
// IP wie oben
const unsigned int subscriberPort = 2001; // zug. Port 

UrsUdpSerial UdpSerial(64);


void setup() { 
  Serial.begin(115200);
  delay(1);
  Serial.println("\n\nESP8266 UDP-Serial-Bridge");

  Serial.print("\n\nVerbinden mit "); Serial.println(ssid);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED)
  { Serial.print(".");
    delay(500);
  }
  Serial.println();
  Serial.println("WiFi verbunden");

  if (UdpSerial.begin(GroupIP, GroupPort, subscriberPort))
    Serial.println("UdpSerial gestartet");
  else
    Serial.println("UdpSerial Fehler");

  Serial.print("isMulticast: ");
  Serial.println(UdpSerial.isMulticast());
}


void loop() { 
  while (UdpSerial.available()) {
    int c = UdpSerial.read();
    Serial.print((char)c);
  }

  // !!!! Übertragen wird erst dann, wenn ein Zeilenende '\n' empfangen wird !!!!
  while (Serial.available()) {
    int c = Serial.read();
    UdpSerial.print((char)c);
  }
}

Implementierung

Stream implementiert bereits alle notwendigen Funktionen für die Ein- und Ausgabe diverser Datentypen. Deshalb ist es nur notwendig die Methoden zu überschreiben, die letztendlich für die Ausgabe per UDP zuständig sind. Die Klasse Print leitet letztendlich alle Ausgaben an zwei virtuelle Methoden weiter: virtual size_t Print::write(uint8_t) und virtual size_t write(const uint8_t *buffer, size_t size). Beide führen in der durch WiFiUDP implementierten Version direkt zur Übertragung eines Datenpakets. UrsUdpSerial überschreibt beide Methoden und zwar so, dass die übergebenen Zeichen zunächst zwischengepuffert werden.

Die weiteren zu Stream gehörenden Methoden werden nahezu direkt an die interne UDPClient-Instanz weitergeleitet. Die Besonderheit ist, dass sich die UDPClient-Methoden read(), peek() und available() auf das aktuell eingelesene Datenpaket beziehen. Ist dieses "leer" muss deshalb geprüft werden, ob weitere Datenpakete (UDPClient::parsePacket()) eingelesen werden können.

Hinzu kommt die Methode begin(). begin() prüft, ob Multicast-Betrieb gewünscht ist, und startet den UDP-Client.

UrsUdpDebug

UrsUdpSerial eignet hervorragend zum Debuggen von Systemen, bei denen das serielle Interface nicht zur Verfügung steht. Herbert hat mich auf die Idee gebracht, das Programm-Logging durch zusätzliche Methoden zu erleichtern. Natürlich ist es möglich, dies durch einfache Makros zu realisieren. Dabei kann man dann aber das Debugging nicht zur Laufzeit konfigurieren und die Typsicherheit geht z.T. verloren.

Die Klasse UrsUdpDebug wurde von UrsUdpSerial abgeleitet. Sämtliche "print..."-Methoden stehen in den Varianten "debug...", "warn..." und "error..." zur Verfügung, also z.B. debugf, debug_P, debug, debugln. Diese Methoden können genauso, wie die entsprechende print-Methode verwandt werden. Ob aber eine Ausgabe über den UDP-Kanal erfolgt, hängt von der Variablen loggingLevel ab. loggingLevel kann per Programm jederzeit mit neuen Werten belegt werden, so dass das Ausgabeverhalten bei Bedarf zur Laufzeit verändert werden kann. loggingLevel ist vom Enumerationstyp loggingLevel mit folgenden Werten:

Wert Wirkung Anmerkung
Debug Alle Methoden erzeugen Ausgaben. debug...: debug dient der Analyse von Sachverhalten, zumeist Fehlern/Bugs, und daher protokolliert man prinzipiell alle Parameter, die der Nachvollziehbarkeit von Ergebnissen dienen (Vorsicht bei sensiblen Daten wie z.B. Passwörtern). Übrigens: Parameter können an dieser Stelle ganz wörtlich Funktions-/Methodenparameter oder Zwischenstände in Funktion/Methoden sein. Wichtig ist, dass sie aufschlussreich sind.
Warn warn... und error... erzeugen Ausgaben. warn...:Alles, was zu Fehlern im weiteren Programmfluss führen kann, aber nicht muss, kann unter warn eingestuft werden. Ist z.B. eine Konfiguration unvollständig oder falsch bzw. fehlerhaft und man fällt auf ein Standardverhalten zurück, könnte man dies unter warn melden. Sachverhalte, die die Ausführungsgeschwindigkeit reduzieren oder den Speicherverbrauch unnötig erhöhen, oder Komponenten, die nicht für den Produktiveinsatz gedacht sind, kann man über warn mitteilen.
Error Nur error... erzeugt Ausgaben error...: In diesem Level sollten Fehler protokolliert werden – egal welcher Art. In der Meldungen sollten alle Informationen enthalten sein, die Aufschluss über die Ursache geben könnten. Quellen sind ein guter Anfang (URLs, URIs, Dateipfade, Primär- oder Alternativschlüssel, etc.). Also z.B.:
LOG.errorf("Fehler beim Ermitteln der notwendigen Daten zur Berechnung der Konditionen. Code: %i\n", code);
None Keine Ausgabe. Dies ist die Standard-Einstellung.

Angelehnt an: Logging im richtigen Level von Enno Thieleke.

Beispiel:

#include <UrsUdpDebug.h>
// ...

UrsUdpDebug dbg(64);

void setup() { 
 // ...

  dbg.begin(DebugIP, DebugPort, 0);
  dbg.loggingLevel = LoggingLevel::Warn; // Logging-Level setzen
}


void loop() { 
  dbg.debug("Debug: Aktuelle millis: 0x"); dbg.debugln(millis(), HEX); // Wird unterdrückt
  dbg.warnf(F("Warn: Aktuelle millis: 0x%u\n"), millis()); // Wird ausgegeben
  dbg.errorln("Error: Endlosschleife!"); // Wird ausgegeben
  while (true) {
    delay(10);
  }
}

Download

Das ZIP-Archiv für Bibliothek UrsUdpSerial zum Download. Die entpackten Dateien ins Verzeichnis <user>\Documents\Arduino\libraries kopieren (siehe Installing Additional Arduino Libraries).

Das Archiv enthält die Bibliotheksdateien