.Net Sockets vs. ESP8266 WiFiClient

Guten Morgen zusammen,

ich habe vor einiger Zeit herausgefunden, dass ich meinen HiFi Receiver per TCP steuern kann.
Dass es mit einer App geht, war mir schon klar. Jedoch bin ich von etwas mehr Sicherheit im Protokoll ausgegangen :grinning:

Nun habe ich eine .Net Anwendung in C#, welche alles empfängt, was der Receiver so tut. Lautstärke- oder Kanaländerungen, Anzeige auf dem Display, Album-Cover, Songtitel, usw.
Steuern ist ebenfalls möglich.

Ziel ist es, mit 2-3 Buttons feste Szenarien auszuführen: Per 433mhz den Subwoofer anschalten. Den Receiver aktivieren, einen bestimmten Eingang wählen und die Lautstärke festlegen. Je nach Uhrzeit sich vielleicht noch um die Beleuchtung kümmern.

Nun zurück zum Titel:
Die C# Anwendung verwendet Sockets

private static Socket _sock;
        private static void Connect()
        {
            try
            {
                if (_sock == null)
                    _sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) { ReceiveTimeout = 1000 };
                if (!_sock.Connected)
                    _sock.Connect(DeviceIp, DevicePort);
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine(ex.Message);
            }
        }

        private static void Send(string packet)
        {
            if (_sock != null && _sock.Connected)
            {
                var bytes = packet.GetBytes();
                _sock.Send(bytes, 0, bytes.Length, SocketFlags.None);
                var str = Encoding.UTF8.GetString(bytes);
                System.Diagnostics.Debug.WriteLine("Send:\t" + Helper.Converter.ByteToString(bytes));
            }
        }

Ein Nachrichtenpaket besteht aus einem Header, ein paar Platzhaltern und dann einer Nachricht mit abschließendem Footer (CarriageReturn).

Meine Umsetzung für den ESP sieht wie folgt aus:

WiFiClient client;
bool ClientConnected= false;

bool ConnectToDevice(){
  if (!client.connect(host, port)) {
    Serial.println("connection failed");
    ClientConnected = false;
  }
  else
    ClientConnected = true;
  return ClientConnected;
}

void SendDataToDevice(uint8_t *bts){
  if(!ClientConnected)
    if(!ConnectToDevice())
      return;
  for(int i = 0; i < sizeof(bts); i++){
      client.write(bts[i]);
    if(bts[i] == packetFooter[sizeof(packetFooter)-1]) //ToDo: Geht schöner...
      break;
  }
  ReadResponse();
}

void ReadResponse(){
  unsigned long timeout = millis();
  while (client.available() == 0) {
      if (millis() - timeout > 5000) {
        Serial.println(">>> Client Timeout !");
        client.stop();
        return;
      }
    }

    while(client.available()){
      String line = client.readStringUntil('\r');
      Serial.print(line);
    }    
}

void StopConnection(){
  client.stop();
}

Sie empfängt auch jede Statusänderung, analog zu meiner .Net Applikation. Nur das Versenden macht nix.

  • Der Receiver reagiert nicht.
  • Es kommt keine Antwort (Client Timeout)

Die Nachricht ist absolut identisch. Habe ich mir mehrfach ausgegeben lassen (Hyperterminal/Debugger) und verglichen:

C#
ISCP\0\0\0\u0010\0\0\0\b\u0001\0\0\0!1MVLUP\r
Arduino
ISCP\0\0\016\0\0\0\b1\0\0\0!1MVLUP\r

\u00XX steht für unicode 10hex == 16dec (Header-Länge)
\b ist BS 08hex, 08dec (Nachrichten-Länge [inkl. CR])

Daher tippe ich darauf, dass zwischen den .Net Sockets und dem WiFiClient noch irgendein Unterschied herrscht ::slight_smile:

Vielleicht hat jemand von Euch da eine Vermutung, bzw. eine Idee was ich noch testen könnte?!?

Idee: mal mit Wireshark ansehen, was dein c# sendet bzw. was dein ESP wirklich sendet ...

aber wenn du den unterschied eh schon im Serial siehst ... dann ändere mal deine Ausgabe.

Die Whireshark-Idee hatte ich auch schon. Dann muss ich mir aber noch einen Server bauen, der die Anfragen vom ESP entgegen nimmt. Sonst kann ich ja nur ausgehende Anfragen mitschneiden.

Es gibt keine Unterschiede im Serial. Die Darstellung im Debugger ist nur anders, als die Ausgabe im HTerm. Es werden die Zeichen anders escaped, sie sind aber identisch:
\u0010 == 16
\u0001 == 1

Zu deinem Code: Was ist packetFooter?

Beim sizeof(bts) bin ich mir nicht sicher was der zurückgibt... vielleicht auch nur die Größe vom Pointer.
Besser der Funktion mitgeben wieviel Daten versendet werden müssen.

.
ISCP\0\0\0**\u0010**\0\0\0\b\u0001\0\0\0!1MVLUP\r
ISCP\0\0\016\0\0\0\b1\0\0\0!1MVLUP\r

Wenn \u0010 = 16
dann müsste 16 = 49 54 sein...

Kannst du mal einen kopmletten Code anhängen?
Und wenn du schon dabei bist, auch das Datenformat welches dein Verstärker erwartet.

Hab´s!
Wireshark hat geholfen.

Wie bereits gesagt, die Pakete sind identisch und nur vom Visual Studio Debugger anders als vom HTerm dargestellt.
Bei den \u0010 war ich vielleicht nicht exakt genug. Das entspricht 16 byte. Nicht int 16. Also exakt dem, was auch der Arduino verschickt hat.

In Wireshark habe ich immer zwei Pakete vom ESP8266 bekommen. Einmal "I" und einmal "SCP000..." mit dem Rest.
Meine Schleife sendet byte für byte und nicht alles in einem Rutsch. Macht auch Sinn, dass das nicht funktioniert, wenn man darüber nachdenkt :smiley:

client.write(bts, GetMessageLength());

versendet alles in einem Rutsch und es funktioniert unmittelbar!

Das Protokoll nennt sich ISCP und wurde von Onkyo eingeführt. Seit sie Pioneer gekauft haben (oder andersherum) wurde das Protokoll auch dort verwendet.
Es startete als RS323 Schnittstelle und wurde dann als eISCP für Ethernet übernommen.

GitHub C# Lösung (Auf der mein erster Versuch basiert)
Protokoll als XLS

Macht auch Sinn, dass das nicht funktioniert, wenn man darüber nachdenkt :smiley:

Bei TCP? Hört sich eher nach shitty parser auf Verstärkerseite an.

Rintin:
Bei TCP? Hört sich eher nach shitty parser auf Verstärkerseite an.

Ja, absolut! Denke das ist wie bei den meisten Umsetzungen in der IT: "Historisch gewachsen".

Sprich aus der RS323 Anbindung, die ohne zusätzliche TCP-Header auskommt.
In Wireshark konnte ich sehen, dass das erste Paket mit 1 byte rausgeht und auch vom Receiver Acknowledged wird. Wahrscheinlich interpretiert er das sofort, ohne die Pakete zusammen zu setzen.

noiasca:
stellst jetzt einen vollen compilierbaren ESP Sketch auch noch rein?

Das kann ich Dir nicht versprechen ob er bei Dir voll kompilierbar ist.
Kommt drauf an was Du mit Deiner IDE so angestellt hast oder wie gut Du mit Copy&Paste umgehen kannst :wink:

Es handelt sich um den ersten Versuch! Der Code ist daher weder optimiert, noch schön anzusehen.

eISCP_Test

#define DEBUG true
#include <ESP8266WiFi.h>
#include <WiFiClient.h>

const char* ssid = "SSID";
const char* password = "Password";

const char* host = "192.168.2.104"; //Can be obtained by UDP broadcast
const int   port = 60128; //Fix

void setup() {
  Serial.begin(115200);
  SetupWiFi();  
}

void loop() {
  if(Serial.available()){    
    String cmdString;
    while(Serial.available()){
      char c = Serial.read();
      if(c == 0x0D) //CarriageReturn
        SendCommand(cmdString);
      else
        cmdString += c;      
    }
  }
  //Constantly read status from receiver:
  ReadResponse();
}

void SetupWiFi(){
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.println("");

  // Wait for connection
  unsigned long timeout = millis();
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    if (millis() - timeout > 10000) {
        Serial.println("WiFi Timeout !");
        return;
      }
  }
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}

eISCP_Packet

static const uint8_t packetHeader[] = {0x49, 0x53, 0x43, 0x50, //ISCP
                                0x00, 0x00, 0x00, 0x10, //HeaderSize (Bigendian)
                                0x00, 0x00, 0x00, 0xFF, //DataSize (Bigendian) replace last with length
                                0x01, 0x00, 0x00, 0x00};//Version + Reserved

const uint8_t packetFooter[] = {0x0D};//{0x1A, 0x0D, 0x0A}; EOF, CR, LF
static const uint8_t lenghtBytePosHigh = 10;
static const uint8_t lenghtBytePosLow = 11;

char command[20];
void SetCommand(String str){
  memset(command, 0, sizeof(command));
  str.toCharArray(command, sizeof(command));
}

uint8_t GetCommandLength(){
  uint8_t i = 0;
  for(i = 0; i < sizeof(command); i++)
    if(command[i] == 0x00)
      break;
  return i;
}

uint8_t GetCommandLengthWithFooter(){
    return GetCommandLength() + sizeof(packetFooter);
}

uint8_t GetMessageLength(){
  return GetCommandLengthWithFooter() + sizeof(packetHeader);
}

void SendCommand(String str){  
  SetCommand(str);
  //Bytes to send:
  uint8_t bts[GetMessageLength()];
  
  //Set Header
  memcpy(bts, packetHeader, sizeof(packetHeader));
  uint8_t commandLenghWithFooter = GetCommandLengthWithFooter();
  
  //Overwrite length
  bts[lenghtBytePosHigh] = commandLenghWithFooter / 256;
  bts[lenghtBytePosLow] = commandLenghWithFooter % 256;
  
  //Add command
  memcpy(bts + sizeof(packetHeader), command, GetCommandLength());

  //Add Footer
  memcpy(bts + sizeof(packetHeader) + GetCommandLength(), packetFooter, sizeof(packetFooter));
  
#if DEBUG
  for(int i = 0; i < GetMessageLength(); i++)
      Serial.write(bts[i]);
#endif      
  SendDataToDevice(bts);  
}

void TestCommands(){  
  ConnectToDevice();
  ReadResponse();
  //Is Power On?
  SendCommand("!1PWRQSTN");
  //Set Power On
  //SendCommand("!1PWR01");  
  //Get current volume
  //SendCommand("!1MVLQSTN");
  //Raise volume
  //SendCommand("!1MVLUP");
  //Lower volume
  //SendCommand("!1MVLDOWN");
}

TCP_Socket

WiFiClient client;
bool clientConnected = false;

bool ConnectToDevice(){
  if (!client.connect(host, port)) {
    Serial.println("TCP connection failed");
    clientConnected = false;
  }
  else{
    Serial.println("TCP connection established");
    clientConnected = true;
  }
  return clientConnected;
}

void SendDataToDevice(uint8_t *bts){
  if(!clientConnected)
    if(!ConnectToDevice())
      return;
  //Send message as one package!!!
  //for(int i = 0; i < GetMessageLength(); i++){
  //    client.write(bts[i]);
    client.write(bts, GetMessageLength());
  ReadResponse();
}

void ReadResponse(){
  if(!clientConnected)
    return;
  unsigned long timeout = millis();
  while (client.available() == 0) {
      if (millis() - timeout > 5000) {
        Serial.println("TCP Client Timeout !");
        client.stop();
        return;
      }
    }
  //ToDo: read until EOF, CR & LF
    while(client.available()){
      String line = client.readStringUntil('\r');      
      InterpretResponse(line);
    }
}

void StopConnection(){
  client.stop();
}

void InterpretResponse(String response){
  //ToDo: Split Header, Command & Value
#if DEBUG  
  Serial.print(response);
#endif  
}

mir gings nur um den Umsetzungs-Unterschied zum Byte-weisen senden :wink:

Hallo zusammen,

habe nach einer kleinen Pause mit diesem Projekt weiter gemacht.
Eine Sache fehlt mir aber leider noch.

Meine .Net Anwendung kann sich per UDP Broadcast die IP Adresse des Receivers beziehen und mit ihm kommunizieren.
Mein ESP32 (Projekt ist umgezogen) kann aktuell nur letzteres mit einer fest hinterlegten IP.
Das möchte ich natürlich gerne ändern!

Allerdings habe ich noch nicht herausgefunden, wie ich das mit WiFiUDP realisieren kann.

Mein Port ist bekannt: 60128
Die IP sollte das Heimnetz sein: 192.168.1.255
Nun sende ich eine Nachricht und warte auf die Antwort:

bool udpSendMessage(IPAddress ipAddr, String udpMsg, int udpPort) {
  WiFiUDP udpClientServer;

  if (udpClientServer.begin(udpPort) == 0) {
    SetText("UDP could not get socket");
    return false;
  }
  udpClientServer.begin(udpPort);
  int beginOK = udpClientServer.beginPacket(ipAddr, udpPort);

  if (beginOK == 0) { // Problem occured!
    udpClientServer.stop();
    SetText("UDP connection failed");
    return false;
  }
  int bytesSent = udpClientServer.print(udpMsg);
  if (bytesSent == udpMsg.length()) {
    Serial.println("Sent " + String(bytesSent) + " bytes from " + udpMsg + " which had a length of " + String(udpMsg.length()) + " bytes");
    udpClientServer.endPacket();
    //Receive Response
    delay(200);
    int packetSize = udpClientServer.parsePacket();
    if (packetSize) {
      char packetBuffer[255];
      int len = udpClientServer.read(packetBuffer, 255);
      if (len > 0)
        packetBuffer[len] = 0;
      UnpackIpAdress(packetBuffer);
    }
    
    udpClientServer.stop();
    return true;
  } else {
    Serial.println("Failed to send " + udpMsg + ", sent " + String(bytesSent) + " of " + String(udpMsg.length()) + " bytes");
    udpClientServer.endPacket();
    udpClientServer.stop();
    return false;
  }
}

Die Antwort selbst beinhaltet leider nur Modell, Typ, Port und MAC-Adresse.
Leider gehen alle weiteren Nachrichten über eine 1:1 Verbindung und nicht mehr über UDP.

In .Net bekomme ich den Endpunkt des Receivers mit, wenn mein Client eine Antwort bekommt.
Worst case wäre nun, dass ich die MAC aus der Antwort extrahiere und darüber versuche an die IP zu gelangen.

Vielleicht kennst sich hier jemand etwas fundierter mit UDP aus und kann mir eine Tipp geben.

Dann ermittle doch mal mit Wireshark, wie Deine .net-Applikation über Broadcast die IP ermittelt und baue das dann nach.

Gruß Tommy

Die Anwendung, bzw. der UDP-Client kennt den Remotehost, da dieser sich ja zurückmeldet und eine Verbindung aufbaut. Die IP wird also irgendwo in den Kopfinformationen der Datenpakete des Absenders zu finden sein.

Deshalb wollte ich jetzt ungern die gesamte WiFiUDP-Library analysieren und umschreiben, wenn es dort ggf. bereits eine Funktion gibt...

Offenbar gibt es ein Property remoteIp, wie ich gerade sehe. Allerdings nicht wann und wo dieses gesetzt wird.
Werde ich einfach heute Abend mal ausprobieren ob dort die korrekte IP oder nur die Broadcast-IP (*255) enthalten ist.
Geht sicherlich schneller als die ganze Library zu verstehen :o

RemoteIp ist mit der Absender-IP gefüllt, nachdem ein Datenpacket eingegangen ist.
Ich hatte Dich so verstanden, dass der Empfänger auf eine Broadcastmessage antwortet und Du diese Möglichkeit suchst.

Gruß Tommy

Ähhm, jein? :stuck_out_tongue_closed_eyes:
Vermutlich bringe ich gerade mit Client/Server & Sender/Empfänger alles durcheinander. Es gibt ja auch keine Unterscheidung in der WiFiUDP.

Der ESP32 sendet eine Broadcast-Nachricht raus und wartet, dass jemand darauf antwortet. Also bin erstmal ich der Absender.
Die Anlage fühlt sich angesprochen und schickt Modell, Typ und MAC-Adresse als Antwort zurück.
Das empfange ich mit parsePacket() und brauche nun die IP der Anlage zur weiteren Kommunikation.

udpClientServer.beginPacket(ipAddr, udpPort);
int bytesSent = udpClientServer.print(udpMsg);
udpClientServer.endPacket();
delay(200);  //Placeholder for testing
int packetSize = udpClientServer.parsePacket();
if (packetSize) {
      IPAddress remoteIp = udpClientServer.remoteIP();
      remoteIp.toString().toCharArray(host, sizeof(host));      
    }

So stelle ich mir das zumindest vor.

Die IP des ESP32 habe ich ja bereits nach Aufbau der WiFi-Verbindung und nutze sie um die IP für die Broadcast Nachricht zu erstellen:

IPAddress localIp = WiFi.localIP();
  //change to broadcast adress
  localIp[3] = 255;  
  localIp.toString().toCharArray(udpHost, sizeof(udpHost));

Leider kann man so eine HiFi-Anlage samt lokalem Netzwerk nicht mal eben mitnehmen. Sonst hätte ich schon längst ausprobiert was in der remoteIP steht :-\

Warum willst Du die remoteIp erst umständlich in einen String umwandeln? Das ist unnötig.
Zum Rücksenden brauchst Du die IP und die hast Du doch jetzt.

Gruß Tommy

Das ist relativ einfach zu erklären:
Die Kommunikation mit dem Receiver passiert mit dem WiFiClient, der sich mit Zeichenketten als IP-Adresse zufrieden gibt => Ei
Die UDP Broadcast Geschichte ist einzig zum beziehen der IP-Adresse und kam erst später dazu => Huhn

Nun konkurrieren die zwei ein wenig.
Und in der Regel belasse ich den funktionierenden Teil des Codes so lange unangetastet, bis der neu hinzugekommene Teil bewiesen hat, dass er funktioniert.

Wenn der Pioneer heute Abend zeigt, dass es klappt, dann wird der Sketch aufgeräumt!
Und irgendwann auch auf GitHub gestellt.

Ein kleines Update:

Der Sketch funktioniert grundsätzlich und ich habe mein Ziel, den Receiver zu steuern erreicht.

Leider liefert mir die UDP-Geschichte keine Antwort, habe aber gerade beim Schreiben meinen Fehler gefunden :roll_eyes:
Auch diese Nachricht muss den ISCP Header und Footer beinhalten. Hatte einfach nur den Befehl abgesendet...
Teste ich heute Abend nochmal!

Die anschließende Kommunikation klappt verlässlich und überraschend schnell.
Das Display des Receivers und das OLED des WEMOS 32 stellen nahezu zeitgleich die Informationen bereit.
Das gilt es noch aufzuhübschen und zu optimieren. Was landet in welcher Zeile (Titel, Laufende Zeit,...), was kann/muss neu gezeichnet werden, usw.

Die Steuerung ist ebenfalls noch nicht ganz optimal, da der Receiver dauerhaft viele, viele, Informationen sendet. Z.B. die URL zum Album-Cover, gewählter Eingang und alles was auf dem Display zu sehen ist.
Deshalb muss ich warten, bis die von mir erwartete Nachricht reinkommt, um zu entscheiden wie es weitergeht.
Klappt zwar, ist aber noch etwas delay-verseucht.

Beispiel Setze Kanal auf "NET":

 //Print to OLED:
  SetText("Send", "Input: Net");
  SendCommand("!1SLI2B");
  WaitForResponse("SLI");
  if(MessageInt != 0x2B)
    SetText("SLI", "Wrong Input!");

WaitForResponse liest alle eingehenden Nachrichten aus und springt ab, sobald der Typ "SLI" eingeht (Oder Timeout). Vorher wird in MessageInt der Rückgabewert gespeichert. Da es auch Text & Bool gibt, habe ich das erstmal über eine globale Variable gelöst. Geht sicherlich auch eleganter!


Mein Handy belichtet leider zu lange, daher sieht der durchlaufende Text etwas verschwommen aus.

Hoffe ich kann den Sketch zum Wochenende soweit finalisieren und auf GitHub stellen. Da ich mehrere Tabs verwende, ist der immer schlecht hier im Forum anzuhängen...

Kommt gut, und so motiviert wie ich, ins Wochenende :slight_smile:

Noch nicht perfekt, aber es tut zumindest schon mal was es soll!

Ab und an läuft der Sketch noch in einen Timeout, weil die richtigen Steuerbefehle vom Receiver auf sich warten lassen. Aber das verlangsamt das Ganze momentan nur, ohne die Funktion einzubüßen.
Ist der Receiver aus, so wird er gestartet und auf den richtigen Sender & Lautstärke eingestellt.
Läuft er bereits, so zeigt er nur an welche Nachrichten gerade so eingehen. Meist der Bildschirmtext.


YouTube

Und hier nun auch endlich der versprochenen Link zum Code:
GitHub\eISCP-Interface

Sicherlich nicht der sauberste oder gar effizienteste Code, aber für meinen kleinen Anwendungsfall reicht es aus.

Kleines Update:

Die Firmware des AV-Receivers wurde aktualisiert.
Zwar hat die Steuerung grundsätzlich funktioniert, jedoch kam ständig ein "Buffer exeeded". Also innerhalb von 100 Zeichen kam kein Linefeed oder Carriage Return.

Deshalb musste ich nochmal ran! Herausgefunden habe ich, dass auf einmal riesige Nachrichten eingehen, die 200-300 Zeichen umfassen.
Und was noch schlimmer war: Keinen ISCP-Header hatten, wie alle anderen Nachrichten!

Es gibt eine neue Nachricht (bzw. eine die vorher nicht gesendet wurde). Diese beinhaltet das Album-Cover. Entweder als Bmp, Jpg oder URL. Die erste Nachricht (noch mit Header) gibt an, welcher Typ gesendet wird und dass es sich um die erste Nachricht handelt. Alle weiteren haben keinen Header mehr und besitzen den Typ "Next", bis dann bei der letzten Nachricht ein "End" kommt.

Deshalb habe ich den Code aktualisiert und den Buffer vergrößert, die Nachrichten ignoriert, die keinen Header besitzen (weil sie mich nicht interessieren) und zu guter letzt einen Befehl abgesendet, der diese Funktionalität deaktiviert.
Bis sie wieder aktiviert wird, wenn ich die offizielle Pioneer Remote-App zur Steuerung verwende.

Nun läuft es wieder :slight_smile: