AdSense

Samstag, 17. August 2013

"Home Automation" mit dem Arduino und 433 MHz - Der Rolladen, Teil 1

(Englisch version) Seit meinem letzten Beitrag können wir bereits erfolgreich mit der Funksteckdose kommunizieren. Das Ein- und wieder Ausschalten des Stroms für den Rolladen ist also schonmal kein Problem. Der spannende Teil ist die Kommunikation mit dem Rolladen selbst. Bekannt ist nur, dass sie (wie bei der Steckdose) auf dem 433MHz-Band abläuft. Zum verwendeten Protokoll waren aber keine Infos aufzutreiben. Als erstes habe ich einfach mal die "ReceiveDemo" der rc-switch-Library compiliert, auf den Arduino übertragen und damit versucht, die Signale der Fernbedienung des Rolladens zu lesen. Wie zu erwarten war, hatte ich hier natürlich kein Glück. Die Demo kann nur die üblichen Prokolle der Funksteckdosen auslesen und konnte daher mit der Fernbedienung des Rolladens nichts anfangen.

Reverse Engineering

Um überhaupt einen groben Überblick über das verwendete Protokoll zu erhalten, müssten die Signale der Fernbedienung ausgelesen werden. Dies wäre am einfachsten zu erledigen mit einem Logic Analyzer. Leider habe ich solches Geräte nicht daheim herumfahren - das gibt das Budget nicht her ;)

Also ein bisschen Google bemüht und eine Möglichkeit gefunden, wie man den Arduino als einfachen Logic Analyzer benutzen kann. Wie genau das geht werde ich in einem seperaten Beitrag beschreiben.

Also habe ich einfach den Daten-Pin des Empfangsmoduls mit dem Logic-Analyzer-Arduino verbunden und konnte damit direkt das Signal der Fernbedienung auslesen.

Ein Druck auf den "Hoch"-Knopf lieferte folgendes Signal:

Runter ergab folgendes Signal:

Und Stopp:

Die Übertragung wird immer mit einer Art Sync-Bit gestartet. Dazu sendet die Fernbedienung erst 4500µs ein HIGH-Signal und dann für 1500µs ein LOW-Signal. Danach folgt die Datenübertragung. Das Muster ist ähnlich wie das Tri-State-System bei den Steckdosen - aber eben nur ähnlich. Neben den dort bekannten Zuständen 0, 1 und F ist hier noch ein vierter Zustand zu erkennen:
 _ _ _       _
|         | _ |   | _
Es handelt sich also im Prinzip um ein "Quad-State-System". Dementsprechend habe ich den vierten Zustand "Q" genannt. Ein "kurzes" Signal hat die Dauer von 250µs, ein "langes" dauert dementsprechend 750µs.

Theoretisch könnte man das Signal auch rein binär auswerten und ein kurzes HIGH-Signal als "0" und ein langes HIGH-Signal als "1" deuten. Dies wäre jedoch (meiner Meinung nach) unübersichtlich, da man mit ewig langen Ketten aus 0 und 1 hantieren müsste.

Die Signale können nun also nach dem Quad-State-System ausgewertet werden. Es ergeben sich dabei folgende übertragene Code-Wörter:
Hoch =   0F0F0100QQ0F100F 0F0F
Runter = 0F0F0100QQ0F100F 0101
Stopp =  0F0F0100QQ0F100F FFFF

Es fällt sofort auf, dass die ersten 16 Bits aller Codewörter identisch sind, lediglich die letzten 4 Bits unterscheiden sich. Es liegt also die Vermutung nahe, dass es sich bei den ersten 16 Bits um die Adresse des Rolladens handelt und bei den restlichen Bits um den Befehl.

Fernbedienung auslesen

Um das Signal der Fernbedienung einfach auslesen zu können, habe ich zunächst ein Programm geschrieben, das auf Signale wartet und diese dann im Quad-State-Format ausgibt.

int logfile[40];
int i = 0;
float lastTime = 0;
boolean capturing = false;
boolean checking = false;
boolean dataIncoming = false;

void setup() {
  Serial.begin(9600);
  Serial.println("Bereit...");
  pinMode(2, INPUT);
  attachInterrupt(0, handleInterrupt, CHANGE);
}

void loop() {
}

Zuerst werden diverse Variablen deklariert - unter anderem ein "logfile", in dem später der ausgelesene Code gespeichert wird. Die setup()-Funktion ist sehr übersichtlich, hier wird nur die serielle Verbindung gestartet sowie der Interrupt auf Pin 2 aktiviert (dort hängt der Daten-Pin des Empfangsmoduls). Der Interrupt wird jedes Mal aktiviert, wenn sich das Signal am Pin ändert.
Noch übersichtlicher ist die loop()-Funktion - sie ist komplett leer ;)

Die eigentliche "Arbeit" erfolgt in der Interrupt-Funktion:

void handleInterrupt() {

  if (!capturing) {  //wenn keine Aufnahme läuft
    if (!checking) {  //wenn nicht gerade auf "Start-Signal" geprüft wird
      if (digitalRead(2) == HIGH) {  //wenn Wechsel von LOW nach (jetzt) HIGH
        lastTime = micros();
        checking = true;
      }
    }

    else {    //wenn gerade auf Start-Signal geprüft wird
      if ((micros() - lastTime > 4000) && (digitalRead(2) == LOW)) {    //wenn HIGH-Phase länger als 4ms war und wir jetzt LOW sind
        //das war das Start-Signal
        checking = false;
        capturing = true;
        lastTime = micros();
      }

      else {
        //das war nicht das Start-Signal
        checking = false;
      }
    }
  }

  else {  //es läuft eine Aufnahme
    if (!dataIncoming) {  //bisher noch keine Nutzdaten empfangen
      if ((micros() - lastTime > 1000) && digitalRead(2) == HIGH) {  //das war die lange LOW-Phase vor Beginn der Übertragung
        dataIncoming = true; //ab jetzt kommen Daten  
        lastTime = micros();
      }
    }

    else {  //jetzt wird es interessant, jetzt kommen die Daten
      //wenn steigene Flanke (also jetzt HIGH)
      if (digitalRead(2) == HIGH) {
        //Beginn der HIGH-Phase merken
        lastTime = micros();
      }  

      //wenn fallende Flanke (also jetzt LOW) 
      else if (digitalRead(2) == LOW) {
        //=> prüfe wie lange HIGH war
        if (micros() - lastTime > 500) {
          //long
          logfile[i] = 1;
        }

        else {
          //short
          logfile[i] = 0;
        }

        if (i < 39) {
          //solange noch nicht alle Bits empfangen wurden
          i++;
        }

        else {
          //wir sind fertig
          noInterrupts();  //Interrupts aus damit Ausgabe nicht gestört wird
          Serial.println("Empfangene Daten:");
          //Ausgabe als "quad-bit"
          for (i = 0; i <= 38; i = i + 2) {
            if ((logfile[i] == 0) && (logfile[i+1] == 0))
              Serial.print("0");

            else if ((logfile[i] == 0) && (logfile[i+1] == 1))
              Serial.print("F");

            else if ((logfile[i] == 1) && (logfile[i+1] == 0))
              Serial.print("Q");

            else if ((logfile[i] == 1) && (logfile[i+1] == 1))
              Serial.print("1");
          }
          Serial.println();
          i = 0;
          dataIncoming = false;
          capturing = false;
          interrupts();  //Interrupts wieder an
          return;  //und alles auf Anfang
        }
      }

    }
  }
}

Sie besteht aus mehreren if-else-Abfragen, die den Zustand der diversen Bool-Variablen überprüfen. Als erstes wird überprüft, ob gerade eine "Aufnahme" läuft (capturing). Falls dies nicht der Fall ist, wird untersucht, ob gerade auf das Start-Signal (Sync-Bit) geprüft wird (checking). Falls dies auch nicht der Fall ist (und gerade ein Wechsel von LOW nach HIGH statt gefunden hat) merkt sich der Arduino die aktuelle Zeit und setzt "checking" auf true. Beim nächsten Flankenwechsel läuft das Programm also in den else-Zweig (es wird auf Start-Signal geprüft). Ist der Wechsel auf HIGH länger als 4000µs her kann man davon ausgehen, dass man soeben die lange HIGH-Phase des Sync-Bits empfangen hat. Also merkt sich der Arduino die Zeit, setzt checking auf false und capturing auf true. Dauerte die HIGH-Phase nicht lange genug, wird checking wieder auf false gesetzt, sonst geschieht nichts.

Wurde checking auf true gesetzt, läuft das Programm beim nächsten Flankenwechsel in den entsprechenden else-Zweig. In diesem Zweig wird als erstes geprüft, ob bereits Nutzdaten empfangen werden oder ob das Ende des Sync-Bits erwartet wird (dataIncoming). Das Auslesen der Nutzdaten verläuft relativ simpel. Bei steigender Flanke wird die aktuelle Zeit gespeichert. Bei fallender Flanke wie geprüft, ob die HIGH-Phase länger als 500µs (und somit lang) oder kürzer (und somit kurz) war. Dementsprechend wird im logfile eine 1 oder eine 0 gespeichert - intern arbeitet das Programm als mit dem "doofen" unübersichtlichen Binär-System ;)
Das Ganze wird insgesamt 40mal durchgeführt, dann wurde das komplette Signal empfangen.  Nun wird das empfangene Signal über die serielle Schnittstelle im Quad-State-Format ausgegeben und alle Bedingungen werden wieder auf ihren Ursprungszustand gesetzt. Dann wird das Programm mit dem return-Befehl neu gestartet.

Wurde alles passend verkabelt und das Programm auf den Arduino gespielt, sollte der Serielle Monitor nun bei jedem Druck auf eine Taste der Fernbedienung den passenden Code ausspucken.
Das klappt sogar auf Anhieb.

Dabei sind mir zwei Dinge aufgefallen, die bei der reinen Analyse mit dem Logic Analyzer nicht offensichtlich waren.
1. Die Fernbedieung sendet alle Signale jeweils 4mal - vermutlich um Übertragungsfehler auszuschließen
2. Der Signal zum hoch und runter fahren besteht jeweils aus zwei Befehlen: "hoch" sendet 4mal den schon bekannten Befehl "0F0F0100QQ0F100F 0F0F" gefolgt von 4mal "0F0F0100QQ0F100F 0F1Q" - "runter" ist entsprechend "0F0F0100QQ0F100F 0101" und "0F0F0100QQ0F100F 0110". Warum genau das so ist habe ich leider nicht heraus gefunden.

Im nächsten Beitrag werde ich beschreiben, wie man mit den so gewonnenen Erkenntnissen endlich den Rolladen steuern kann.

8 Kommentare:

  1. Hallo Udo,

    wenn ich deinen sketch starten fängt er direkt an codes im Serial Monitor anzuzeigen ohne dass ich einen Knopf der Fernbedienung gedrückt habe (siehe folgenden). Wenn ich dann einen Knopf drücke habe ich den Eindruck die Ausgabe hält kurz an und läuft dann aber weiter.

    Habe Sie eine Idee woran dass liegen kann? Vielen Dank vorab für Ihre Rückmeldung.

    Bereit...
    Empfangene Daten:
    000001101Q0100FFQ101
    Empfangene Daten:
    QFF0QF0F11F11F0Q0Q1Q
    Empfangene Daten:
    10110FQ0110011Q111FF
    Empfangene Daten:
    FQF110Q1F11FQFF1QQF1
    Empfangene Daten:
    Q1QF01011Q00101F10F0
    Empfangene Daten:
    011QFQ1Q000FQ1QQQ011

    AntwortenLöschen
    Antworten
    1. Das ist ein ganz normales Verhalten. Die angezeigten Daten sind einfach Störungen, die das Programm für "sinnvolle" Nutzdaten hält. Die wirklich relevanten Daten (die von der Fernbedienung gesendet werden) erkennt man nachher nur an den zahlreichen Wiederholungen (immer wieder das gleiche "Muster" so lange man den Knopf gedrückt hält). Der obige Sketch ist genau auf den Aufbau und das Timing der Signale dieser einen spezifischen Fernbedinung angepasst. Es ist somit äußerst wahrscheinlich, dass du damit deine Fernbedinung nicht einfach so ohne weiteres auslesen kannst, da diese vielleicht mit einem ganz anderen Timing, also einer anderen Bit-Länge arbeitet. Dafür müsste der Sketch entsprechend angepasst werden.

      Löschen
  2. Hi, vielen Dank für den Beitrag. Das funktioniert übrigens (natürlich mit anderen Addressen) auch mit Portos Rolladen.

    AntwortenLöschen
    Antworten
    1. Hi, tatsächlich? Ich habe auch Portos Rolläden, kann aber bislang leider kein Muster aus den Daten erkennen. Ich erkenne definitiv eine Häufung von empfangenen Codes, wenn ich auf meiner Portos Fernbedienung Tasten drücke, jedoch scheint hier immer ein anderer Code gesendet zu werden - keine gleichlautenden Ketten aus Adresse / Befehl erkennbar...

      Löschen
  3. Hallo Udo,

    in welcher Programmiersprache ist dein Code geschrieben?

    Danke und Grüße,
    Paul

    AntwortenLöschen
  4. hello, many thanks for your page that help me a lot!
    I am not a german man, but thanks for google translate !
    I think, i am able to read my remote control....
    Do you have any code able to send the logfile to the emetter ?
    I want to be able to send a code from MQTT to the emetter..
    Best regards
    Thierry Vorms vormsty@gmail.com

    AntwortenLöschen
  5. Hallo Udo,
    den Logic Analyser kannst du dir garantiert leisten:
    https://www.az-delivery.de/products/saleae-logic-analyzer?_pos=2&_sid=5cc8dd184&_ss=r

    schlappe 10 Euro
    Dazu gibt es eine Software sigrok pulseView die kann dann gleich noch eine Menge Protokolle wie Seriell, I2C, onewire usw. interpretieren
    viele Grüße

    Stefan Ludwig

    AntwortenLöschen
  6. Toller Beitrag. Ganz herzlichen Dank. Funktioniert. Wertvolle Grundlage und Voraussetzung damit ich die Markise in mein SmartHome (Openhab) integrieren kann. Falls jemand dazu schon etwas gemacht hat (zB Ansteuerung via MQTT) bin ich für Hinweise sehr dankbar.
    Thomas Gfeller

    AntwortenLöschen