
Hier werde ich die Theorie und Praxis der Implementierung der TuyaMCU-Protokollunterstützung vorstellen. TuyaMCU ist ein UART-basiertes Protokoll, das zur Kommunikation des Wi-Fi-Moduls mit dem Hauptmikrocontroller des Tuya-Geräts verwendet wird. Dieses Protokoll wird in vielen IoT-Produkten verwendet, u. a. in Dimmern, Temperatur-/Feuchtigkeitssensoren usw. mit LCD-Display, in Heizungs- und Jalousiesteuerungen und sogar in Haushaltsgeräten (z. B. Fritteuse von Blitzwolf). Ich werde die TuyaMCU-Unterstützung hier auf der WiFi-Modulseite in Programmiersprache C programmieren. Ich werde sowohl das Lesen (Parsen) von Paketen als auch deren Senden realisieren. Codefragmente aus diesem Thema sind Teil meines OpenBK7231T-Projekts, das auf Github verfügbar ist.
TuyaMCU - Grundlagen
TuyaMCU ist eine Methode, ein Wi-Fi-Modul (es kann ein Modul mit ESP8266 sein, wie TYWE3S, oder ganz anders, wie WB3S mit BK7231T) mit einem Mikrocontroller (zum Beispiel MM32F003) zu kommunizieren. Diese Kommunikation erfolgt über den UART, d. h. über zwei Signale, RX zu TX und TX zu RX. Die Standard-Baudrate beträgt 9800.
Die TuyaMCU ermöglicht es, die Steuerungsaufgaben des IoT-Geräts auf zwei Chips aufzuteilen - das Wi-Fi-Modul, das für die Kommunikation mit der Außenwelt zuständig ist, und den Mikrocontroller, der sich im Allgemeinen um die anderen Dinge kümmert (z. B. LCD-Display, Tasten, Drehgeber, Dimmer, Triac-Steuerung usw.).
Nicht alle Tuya IoT-Geräte verwenden die TuyaMCU, da manchmal das WiFi-Modul selbst ausreicht und kein zusätzlicher Mikrocontroller benötigt wird. Es gibt zwei Betriebsmodi von TuyaMCU. In einem Fall hat das Wi-Fi-Modul immer noch einige Aufgaben:

Im zweiten Fall werden die meisten Dinge vom Mikrocontroller erledigt und das Wi-Fi-Modul sorgt nur für die Kommunikation:

Die Schaltpläne enthalten eine UART-Signalpegelanpassung, die jedoch im Allgemeinen nicht erforderlich ist, da sowohl die MCU als auch das Wi-Fi-Modul mit 3,3 V arbeiten.
Die TuyaMCU ist ein binäres Protokoll, d. h. nach dem Vorschauen einer UART-Kommunikation, werden wir "Zeichen" sehen, es ist kein "pure ASCII", das wir mit bloßem Auge lesen können.
Nachrichten, die in beide Richtungen gesendet werden, sind in Pakete unterteilt.
Die Struktur des Pakets ist in beiden Richtungen identisch:
Feld | Länge (Bytes) | Beschreibung | Header | 2 | Wert immer 0x55AA | Version | 1 | Die Version des Protokolls kann sich je nach Kommunikationsrichtung ändern | Befehl | 1 | Art des Pakets (Inhalt) | Datenlänge | 2 | Die Größe des Dateninhalts des Pakets in Bytes | Daten | x | Paketdaten, Anzahl der im vorherigen Feld angegebenen Bytes | Prüfsumme | 1 | Prüfsumme des Pakets als Rest der Summe aller vorherigen Felder bis 256 (ich werde den Algorithmus später vorstellen) |
Die Befehle umfassen u. a:
Befehl | Beschreibung | 0x00 | Sog. "heartbeat", d. h. die regelmäßige Aufrechterhaltung der Kommunikation - Meldung, dass das Gerät immer noch funktioniert | 0x01 | Anfrage über Information zum Gerät | 0x02 | Anfrage über den Betriebsstatus des Wi-Fi-Moduls | 0x03 | Informationen zur Wi-Fi-Netzwerkverbindung | 0x04 | Paket, das das Wi-Fi-Modul auf einen bestimmten Konfigurationsmodus zurücksetzt | 0x05 | Paket, das das Wi-Fi-Modul auf einen bestimmten Konfigurationsmodus zurücksetzt | 0x06 | Einstellen des Gerätezustands (z. B. Einschalten des Relais, Einstellen der Helligkeit des Dimmers) | 0x07 | Ermitteln des Gerätestatus (wie oben, auch zum Ermitteln von z. B. Temperatur oder Luftfeuchtigkeit) | 0x1C | Ermitteln der aktuellen Uhrzeit für den Kalender |
Diese Tabelle ist nicht vollständig. In der Regel werden nur einige wenige Befehle verwendet.
Einer der wichtigeren Befehle ist 0x06/0x07, er enthält auch eine spezielle Datenunterstruktur, die ich unten zeige:
Feld | Länge (Bytes) | Bedeutung | dpID | 1 | Funktionsbezeichner (abhängig vom Gerätetyp) | Datentyp | 1 | Gibt den Datentyp des Pakets an, gemäß der etwas weiter dargestellten Aufzählung (integer, enum, string, usw.) | Länge | 2 | Datenlänge | Daten | x | Datenfeld mit der zuvor angegebenen Länge |
Der dpID, oder Funktions-/Variablenindex, hat je nach Gerät unterschiedliche Bedeutungen. Wenn wir ein bestimmtes Gerät (z. B. TH06 Thermometer/Hygrometer) unterstützen wollen, muss er erraten, dass z. B. dpID = 1 die Temperatur ist, dpID = 2 die Luftfeuchtigkeit, usw. usw.
Dasselbe gilt für Tasmota - auch hier müssen wir dies bei der Konfiguration des Geräts manuell angeben.
Versprochene Datentypen:
Name | Code | Beschreibung | DP_TYPE_RAW | 0x00 | Keine festgelegte Bedeutung | DP_TYPE_BOOL | 0x01 | Byte, das einen booleschen Ausdruck darstellt (wahr oder falsch) | DP_TYPE_VALUE | 0x02 | Vier-Byte-Ganzzahlwert | DP_TYPE_STRING | 0x03 | Beschriftung (Zeichenfolge) ASCII | DP_TYPE_ENUM | 0x04 | Wert der Aufzählung | DP_TYPE_BITMAP | 0x05 | Bitmap (Bild) |
Theoretisch erfordert das TuyaMCU-Protokoll eine Initialisierung durch Meldung der Version und des Betriebsmodus des Geräts, aber in der Praxis scheint mir dies nicht notwendig zu sein – weder Tasmota noch meine Firmware führen derzeit eine vollständige Aushandlung mit dem Mikrocontroller durch, im Grunde werden die Daten auf einmal nur gesendet und empfangen.
Nachfolgend ein Diagramm der TuyaMCU-Zustände laut Tuya:

Etwas später werde ich die erfassten TuyaMCU-Pakete (kurz nach dem Start des Geräts) präsentieren, deren Reihenfolge mit dem Diagramm übereinstimmt.
TuyaMCU - Beispiel für einen Dimmer auf TYWE3S
Schauen wir uns das erste Beispielprodukt an, aufgeteilt in ein Wi-Fi-Modul und einen Mikrocontroller. Es wird ein Wi-Fi gesteuerter Dimmer sein, WF-DS01:




Schaltplan:


Im Inneren befinden sich das TYWE3S (Wi-Fi-Modul mit ESP) und der Mikrocontroller MM32F003. Wir haben hier auch einen dritten Chip, der die Touch-Tasten unterstützt.
TuyaMCU - Beispiel für einen Dimmer auf WB3S
Ein etwas anderer Dimmer - EDM-01-AA-EU KER_V1.6. In früheren Versionen wurde er mit RTL8710BN realisiert, jetzt ist er mit WB3S. Fotos von innen:

Schaltplan, von mir skizziert:

Im Inneren befindet sich der SC95F861X:

Hier sehen wir, dass der SC95F861 praktisch die gesamte Beleuchtungssteuerung übernimmt - die RESET-Taste ist mit ihm verbunden, die LEDs 1-12 zur Anzeige des Beleuchtungsniveaus sind angeschlossen, sowie das Nullerkennungssignal für den Dimmer und die Triac-Steuerung selbst (im Schaltplan als PWM bezeichnet).
TuyaMCU – Beispiel für Uhr/Thermometer – TH06
Das dritte Beispiel für ein Gerät, das TuyaMCU verwendet, es könnte die Uhr/das Thermometer/Hygrometer/Kalender TH06 sein:

Der Mikrocontroller im Inneren ist nicht beschriftet und das Wi-Fi-Modul ist WB3S. Der große Chip in der Mitte ist der Display-Controller TM1621B.

Dennoch werde ich an dieser Stelle anhalten. Ich werde TH06 als praktische Demonstration verwenden.
Man muss mit dem Erfassen von Paketen beginnen.
Wir erfassen Pakete, indem wir einfach die RX-Leitung unseres UART-Konverters zuerst mit TX und dann mit RX von der Platine verbinden (beide Leitungen funktionieren).
TuyaMCU verwendet Baud 9800.
Auf der Computerseite verwende ich RealTerm und die Option Capture:

Die gesammelten Daten selbst können in einem freien Hex-Viewer, z. B. xvi32, analysiert werden, oder die RealTerm-Optionen können so eingestellt werden, dass die Bytes als ASCII (für Menschen lesbar) gespeichert werden, und dann reicht ein Notizbuch:

Bei einer so großen Anzahl von Bytes kann man sich leicht verirren. Der Einfachheit halber habe ich die Ergebnisse in Word übertragen, wo ich den Hintergrund der einzelnen Datenabschnitte farblich markieren konnte, d. h. Kopfzeile, Typ, Länge, Prüfsumme usw. hervorheben konnte:

Auf diese Weise habe ich die gesamte "Unterhaltung" des WB3S mit dem Mikrocontroller erfasst, Daten in beide Richtungen. Ich werde sie im nächsten Abschnitt analysieren.
TH06 – Paketinhalte in der Praxis
Ich habe die unten gezeigten Pakete gleich nach der Inbetriebnahme des Geräts zusammengestellt, so dass sie auch den anfänglichen Verbindungsaufbau und das Paket mit dem Mikrocontroller-Deskriptor enthalten.
HINWEIS: Einige weniger bekannte (weniger wichtige) Pakettypen habe ich hier weggelassen. Vollständige Kommunikation im .doc-Anhang.
Zunächst sendet das Modul mit Wi-Fi einen "heartbeat" und die MCU antwortet darauf:


Dann sendet das Wi-Fi-Modul „Query product information“ und die MCU antwortet darauf:


Die Antwort der MCU enthält ASCII-Text, genauer gesagt im JSON-Format:
{"p":"7akwzwfwhukkdsib","v":"1.0.0","m":0}
Dann sendet das Wi-Fi-Modul „Query WiFi information“:


Die Antwort darauf beinhaltet auch die Rolle der Pins (wo ist RESET usw.).
Dann fragt das Wi-Fi-Modul den Status des Geräts ab, genauer gesagt die Temperatur- und Luftfeuchtigkeitswerte (diese werden von der MCU ausgeführt):

Daraufhin können mehrere Pakete kommen. Bei diesem Gerät sind es drei, zwei davon zeige ich hier:

Die so gewonnenen Temperaturinformationen gehen dann an die Tuya-Cloud, so dass man auch Diagramme erstellen, diese aus der Ferne ablesen kann usw. In die andere Richtung ist noch Kommunikation übrig - die Uhrzeit wird vom Wi-Fi-Modul von den NTP-Servern abgerufen und mit dem folgenden Paket an die MCU gesendet:

UART - Ringpuffer für Datenempfang
In diesem Schritt fangen wir bereits an, das Programmieren zu üben. Wir werden ein Programm für das Wi-Fi-Modul schreiben, das auf dem WB3S oder TYWE3S (ESP8266) läuft. Aber grundsätzlich ist ein UART-Ringpuffer auf beiden Seiten notwendig.
Der Puffer ist am Prozess des Datenempfangs vom UART beteiligt. Der UART-Interrupt gibt Zeichen für Zeichen an den Puffer weiter, und wir leeren den Puffer über eine externe Funktion, wenn sich das gesamte Paket darin angesammelt hat. Die Daten im Ringpuffer werden in einer Schleife gespeichert (daher der Name), und seine Länge sollte viel größer sein als die Länge des gesamten Pakets, das wir erwarten zu empfangen.
Für die Dauer des Lesens aus dem Ringpuffer sollten wir z. B. UART-Interrupts deaktivieren (oder einen Mutex verwenden).
Was braucht man, um einen solchen Puffer zu beschreiben?
Es werden vier Variablen benötigt:
- Puffer selbst (Byte-Array)
- die Größe dieses Arrays
- der Zeichenindex "in" des Eingangs, d. h. wo wir das nächste vom UART empfangene Zeichen schreiben
- der Zeichenindex des Ausgangs, "out", d. h. wo der Cursor Daten ausliest
Code: C / C++
HINWEIS: Abhängig von der Sprache, in der wir schreiben (C++ oder C), lohnt es sich natürlich zu erwägen, es in eine Struktur oder Klasse zu packen. Die Initialisierung des Puffers ist einfach - es wird Speicher für den Puffer zugewiesen und die Cursor werden gesetzt:
Code: C / C++
Ich habe das Hinzufügen zum Puffer in zwei Funktionen aufgeteilt, es ist das, was den UART-Interrupt auslöst, wenn wir ein Zeichen empfangen:
Code: C / C++
Die Bedingung im verschachtelten if durchläuft in einer Schleife den Index, wenn das Ende des Puffers erreicht ist. Das erste if prüft, ob noch Platz im Puffer ist (bei Bedarf wird er freigegeben, dann „verliert es Zeichen“, die Bedingung >= ist etwas übertrieben, da es unmöglich ist, dass der Puffer mehr Zeichen enthalten kann als seine volle Größe). test_ty_read_uart_data_to_buffer wird vom UART-Interrupt aufgerufen.
Und Funktionen, die das Lesen aus dem Puffer ermöglichen:
Code: C / C++
Hier haben wir drei separate Funktionen:
- die Funktion zum Abrufen der aktuellen Puffergröße (die Differenz zwischen dem Schreib- und dem Leseindex, in einer Schleife),
- eine Funktion zum 'Erspähen' eines bestimmten Bytes im Puffer, ausgehend vom Leseindex
- und eine Funktion, die die N Bytes, die wir bereits geladen haben, „verbraucht“ (überspringt).
Wir werden diese Funktionen im nächsten Abschnitt verwenden.
Grundlegendes Parsing des Pakets und Zurückweisung fehlerhafter Daten
Wir haben bereits einen Ringpuffer, wissen aber noch nicht, wie wir das gesamte Paket erkennen können. Wir müssen wissen, wo ein Datenabschnitt beginnt und wo er endet.
Zu diesem Zweck ist es nützlich, die Struktur des Pakets und seine Kennung (0x55AA) zu kennen.
So sieht die Funktion aus, die vom Hauptprogramm regelmäßig in einer Schleife ausgeführt wird:
Code: C / C++
Hier sehen wir die folgenden Vorgänge:
- die Funktion überspringt den "Müll", der vor dem Header des nächsten Pakets steht, d. h. 0x55AA (zwei Bytes)
- Die Funktion prüft, ob das gesamte Paket bereits im Puffer verfügbar ist (konstruiert seine Länge aus den Len-Feldern - zwei Bytes - seiner Daten)
- wenn bereits ein komplettes Paket vorhanden ist, kopiert die Funktion dessen Daten 'nach außen' und verbraucht Bytes aus dem Ringpuffer
Nutzung der Funktion:
Code: C / C++
Die Funktion TuyaMCU_ProcessIncoming ist im Folgenden zu implementieren, ihr Argument ist im Grunde das gesamte bereits getrennte TuyaMCU-Paket.
Code: C / C++
Die obige Funktion besteht in erster Linie aus Sicherheitsmaßnahmen, darunter:
- Überprüfung des Headers von Datenpaket
- Prüfung, ob die in den Daten angegebene Länge mit der tatsächlichen Länge übereinstimmt
- Berechnen und Prüfen der Prüfsumme (ob es keine Störungen, kein "Bitflip" gab)
Darüber hinaus bestimmt sie dann den Pakettyp und wählt mithilfe des Switch-Blocks geeignete Funktionen zur Paketverarbeitung (z. B. Gerätestatus) aus.
Senden der Informationen ans Gerät
Alle Pakete haben die gleiche Grundstruktur, so dass wir sie mit einer Funktion versenden können, die unter anderem automatisch die Prüfsumme der Daten berechnet und die Werte in die entsprechenden Felder (z. B. Pakettyp) einträgt:
Code: C / C++
"payload_len" wird besonders behandelt, weil es ein "Wort"-Typ ist, ein Zwei-Byte-Wort, und wir es in Bytes aufteilen müssen. Die Prüfsumme wird sehr einfach berechnet und nutzt den Byte-Überlauf.
Senden der Informationen ans Gerät (Beispiel für das Senden von Datum/Uhrzeit an ein Display)
Im Grunde habe ich ein solches Paket bereits in dem Abschnitt über die Analyse der erfassten Kommunikation mit TH06 gezeigt.
So sieht die Funktion aus, die es erstellt:
Code: C / C++
Das Anwendungsbeispiel:
Code: C / C++
Das Ergebnis:

Datenpaket zum Gerätestatus
Das Gerätestatuspaket erfordert ein wenig mehr Verarbeitung als ein gewöhnliches Paket. Der Gerätestatus umfasst z. B. Temperaturwerte, Luftfeuchtigkeit, Status von Relais, Tasten usw. Die Statusvariablen haben einmalige Indexe (dpID), die ich bereits zu Beginn erwähnt habe, außerdem haben sie Typen (DP_TYPE_RAW, DP_TYPE_STRING, usw.) und Datengrößen.
Theoretisch kann ein Statuspaket mehrere Variablen enthalten, aber in TH06 wird es in drei Pakete (je eine Variable) aufgeteilt.
Hier ist die Funktion, die ein solches Paket parst:
Code: C / C++
Die Funktion zeigt an dieser Stelle nur die erfassten Informationen an und verarbeitet sie nicht weiter. Dennoch ist sie in der Lage, Temperatur und Luftfeuchtigkeit korrekt aus dem TH06 zu "extrahieren", was ich in meiner Firmware für den BK7231T (OpenBK7231T) getestet habe.
Zunächst muss ich jedoch manuell eine Abfrage des Gerätestatus senden (der Screenshot zeigt das Webpanel des Wi-Fi-Moduls):

Dann erhalte ich 3 Pakete, Temperatur- und Luftfeuchtigkeitswerte im Screenshot markiert:

230, also 23,0 Grad und 24 (% Luftfeuchtigkeit). Dies stimmt mit den Werten auf dem Display überein (naja, die Temperatur hatte sich bereits um den Bruchteil eines Grads geändert, bevor ich das Foto gemacht habe):

Auf diese Weise können wir über TuyaMCU sowohl Daten senden als auch empfangen. Wir sind in der Lage, den Status des Geräts zu lesen und zu setzen.
Zusammenfassung
Die Kenntnis des TuyaMCU-Protokolls kann in vielen Situationen nützlich sein - sowohl wenn wir unser Gerät so konfigurieren wollen, dass es mit Tasmota (wenn es auf dem ESP8266 realisiert ist) oder mit meiner Firmware (wenn es auf dem BK7231T/BK7231N/XR809 realisiert ist) arbeitet, als auch wenn wir unsere eigene Firmware für solche Produkte schreiben oder bestehende überarbeiten wollen (TuyaMCU ist einfach auf dem ESP ohne zusätzliche Besonderheiten zu betreiben, es gibt sogar solche Projekte auf github, z. B. WThermostatBeca). Dies kann uns helfen, das gekaufte Gerät unabhängig von den Servern des Herstellers zu machen, ihm mehr Funktionalität hinzuzufügen und es mit Geräten aus einem anderen Ökosystem zu verbinden, mit denen es normalerweise nicht kompatibel war.
Ich werde meine TuyaMCU-Implementierung weiterentwickeln, es ist nicht die endgültige Version, und außerdem habe ich mehrere Probleme im Artikel ausgelassen (z. B. das Deaktivieren von Interrupts/Semaphoren, die im Tuya SDK enthalten sind – siehe tuya_common/src/driver/tuya_uart.c von). tuya-iotos-embeded-sdk-wifi-ble-bk7231t).
Zusätzliche Materialien:
- Repository meiner Firmware für BK7231T und ähnliche (dafür habe ich TuyaMCU implementiert)
- Bibliothek TuyaMCU für Arduino
- Tasmota-Dokumentation zu TuyaMCU
- "Getting started guide" von Tuya
- Quellcode für TuyaMCU-Unterstützung von Tasmota
Ich füge meine .doc-Datei mit den erfassten TH06-Paketen bei (einige entschlüsselt, einige nicht).
Cool? DIY-Rangliste Hilfreicher Beitrag? Kauf mir einen Kaffee.