lt8900_lamp.tc¶
lt8900_lamp.tc — GaoMei GM2.4G Deckenlampe fernsteuern (LT8900/LT8910)
// ============================================================================
// lt8900_lamp.tc — GaoMei GM2.4G Deckenlampe fernsteuern (LT8900/LT8910)
// ============================================================================
//
// ⚠️ UNGETESTET auf der Sendeseite — es lag noch kein LT8900-Modul vor.
// Das PROTOKOLL ist dagegen vollständig verifiziert: erst aus dem SPI-Mit-
// schnitt der Fernbedienung (~/Desktop/gm24g.sr) abgeleitet, dann am
// 2026-07-28 per HackRF über die Luft bestätigt und die Tastentabelle
// ausgemessen. Erster Lauf mit Hardware = erster Sendetest.
//
// VORGESCHICHTE: Frühere Versuche mit einem A7105 (XL7105-SY-B) scheiterten —
// siehe a7105_gm24g.tc. Grund war nicht ein exotischer Klon mit proprietärer
// Präambel, sondern schlicht der falsche Chip: Die Fernbedienung fährt einen
// LT8900. Erkennungsmerkmal ohne jede Beschriftung: 21 Init-Register stimmen
// Byte für Byte mit der LT8900-Bibliothek (github.com/MINI-Qiang/LT8910)
// überein, und LT8900 hat 16-Bit-Register (Adresse + 2 Datenbytes), während
// der A7105 8-Bit-Register bis 0x32 hat.
//
// ── PROTOKOLL (aus dem Mitschnitt, 810/810 Rahmen bestätigt) ───────────────
//
// Paket im FIFO (Register 50 = 0x32), Längenbyte voran (PACK_LEN_ENABLE):
//
// 07 5A 4B 07 01 CC VV SUM
// │ └ Geräte-ID └ Gr. │ └ SUM = (0x07+0x5A+0x4B+0x07+0x01+CC+VV) & 0xFF
// └ Länge = 7 Folgebytes └ CC = Kommando, VV = Wert
//
// Der konstante Teil summiert sich zu 0xB4, daher SUM = (0xB4+CC+VV) & 0xFF.
//
// TASTENTABELLE (HackRF-Messung 2026-07-28, je 100-340 Pakete pro Taste):
// CC = 0x10-Flag | Befehlsnibble. Das Bit 0x10 unterscheidet nur erste
// Betätigung von Wiederholung; das untere Nibble ist der Befehl:
// 4 = dunkler 5 = EIN 9 = AUS
// C = heller E = warm F = kalt
// Die Fernbedienung hat genau diese sechs Tasten (EIN/AUS getrennt).
// VV ist ein Zähler, der pro Druck hochläuft und bei 0xFF überläuft.
// Byte 4 war bei der Luftmessung 0x00 (im SPI-Mitschnitt vom Mai 0x01);
// Bedeutung ungeklärt, für die Steuerung offenbar unkritisch.
//
// Sendeablauf je Rahmen, exakt wie die Fernbedienung:
// Reg 0x07 <- 0x0000 Leerlauf (TX aus)
// Reg 0x34 <- 0x8080 FIFO-Zeiger zurücksetzen
// Reg 0x32 <- 8 Byte Paket ins FIFO
// Reg 0x07 <- 0x0100 | kanal Bit 8 = TX-Enable -> senden
//
// FHSS: Kanäle 4 / 39 / 74 (2406 / 2441 / 2476 MHz) reihum, 1,135 ms
// Abstand, 90 Sendungen pro Tastendruck (30 je Kanal).
// Per HackRF bestätigt: Kanal 39 liegt auf 2441.02 MHz, die LT8900-Abbildung
// 2402+n stimmt also. Datenrate 1 Mbit/s, Burstdauer 182 us, Präambel 0x55,
// Syncword auf der Luft 8A 85 5A AA, Nutzlast bitgedreht.
//
// ── SYNCWORD, WICHTIG ──────────────────────────────────────────────────────
// Die Fernbedienung beschreibt NUR Reg 36 (0x151A) und Reg 39 (0x55AA);
// Reg 37 und 38 bleiben auf Reset-Vorgabe. Wir machen es GENAUSO und fassen
// 37/38 nicht an — dann stimmt das Syncword automatisch überein, ohne dass
// wir die Vorgabewerte kennen müssen.
//
// ── VERDRAHTUNG (LT8900-Modul, echtes 4-Draht-SPI) ─────────────────────────
// SCK -> Tasmota SPI CLK MISO -> SPI MISO
// MOSI -> SPI MOSI CS -> CS_PIN unten
// VCC -> 3V3 (NICHT 5V) GND -> GND
//
// ── BEFEHLE ────────────────────────────────────────────────────────────────
// Tasmota hängt Unterkommandos OHNE Leerzeichen an den Präfix an:
// LAMPTEST -> Register zurücklesen, prüft Verdrahtung
// LAMPEIN -> Lampe ein LAMPAUS -> Lampe aus
// LAMPHELL -> heller LAMPDUNKEL -> dunkler
// LAMPWARM -> wärmer LAMPKALT -> kälter
// LAMPRAW c -> Befehlsnibble c (0..15) direkt senden
// ============================================================================
#define CS_PIN 3
#define SLOT 2
#define R_CHANNEL 0x07
#define R_SYNC1 0x24
#define R_SYNC4 0x27
#define R_PKTCFG 0x29
#define R_FIFO 0x32
#define R_FIFOCTRL 0x34
#define TX_ENABLE 0x0100
#define REPEATS 30 // je Kanal -> 90 Sendungen wie das Original
// Befehlsnibbles, per HackRF ausgemessen
#define CMD_DUNKLER 0x4
#define CMD_EIN 0x5
#define CMD_AUS 0x9
#define CMD_HELLER 0xC
#define CMD_WARM 0xE // E und F waren zunaechst vertauscht notiert
#define CMD_KALT 0xF
#define CMD_FLAG 0x10 // erste Betätigung vs. Wiederholung
#define BYTE4 0x00 // bei der Luftmessung 0x00
char spi[12];
char rsp[160];
int ch_list[3];
int pend_cmd; // -1 = nichts zu senden
int seq; // Zähler VV, läuft pro Tastendruck hoch
int sent_total;
// ── SPI: LT8900 hat 16-Bit-Register ───────────────────────────────────────
void lt_write(int addr, int val) {
spi[0] = addr & 0x7F;
spi[1] = (val >> 8) & 0xFF;
spi[2] = val & 0xFF;
spiTransfer(SLOT, spi, 3, 1);
}
int lt_read(int addr) {
spi[0] = 0x80 | (addr & 0x7F);
spi[1] = 0;
spi[2] = 0;
spiTransfer(SLOT, spi, 3, 1);
return ((spi[1] & 0xFF) << 8) | (spi[2] & 0xFF);
}
// ── Init: exakt die Reihenfolge der Fernbedienung ─────────────────────────
// Reg 37/38 bewusst ausgelassen (siehe Kopf).
void lt_init() {
lt_write(0x00, 0x6FE0); lt_write(0x01, 0x5681);
lt_write(0x02, 0x6617); lt_write(0x04, 0x9CC9);
lt_write(0x05, 0x6637); lt_write(0x07, 0x004C);
lt_write(0x08, 0x6C90); lt_write(0x09, 0x4800);
lt_write(0x0A, 0x7FFD); lt_write(0x0B, 0x0008);
lt_write(0x0C, 0x0000); lt_write(0x0D, 0x48BD);
lt_write(0x16, 0x00FF); lt_write(0x17, 0x8005);
lt_write(0x18, 0x0067); lt_write(0x19, 0x1659);
lt_write(0x1A, 0x19E0); lt_write(0x1B, 0x1300);
lt_write(0x1C, 0x1800); lt_write(0x20, 0x4800);
lt_write(0x21, 0x3FC7); lt_write(0x22, 0x2000);
lt_write(0x23, 0x0300);
lt_write(R_SYNC1, 0x151A);
lt_write(R_SYNC4, 0x55AA);
lt_write(0x28, 0x4402);
lt_write(R_PKTCFG, 0xB000); // CRC an, SCRAMBLE AUS, Längenbyte an
lt_write(0x2A, 0xFDB0); lt_write(0x2B, 0x000F);
lt_write(R_CHANNEL, 0x0000);
lt_write(0x23, 0x4300); // die Fernbedienung ändert 0x23 nachträglich
}
// ── einen Rahmen auf einem Kanal senden ───────────────────────────────────
void send_one(int cmd, int val, int chan) {
// Prüfsumme = Summe der sieben Bytes: 07 + 5A + 4B + 07 + BYTE4 + cmd + val
int sum = (0x07 + 0x5A + 0x4B + 0x07 + BYTE4 + cmd + val) & 0xFF;
lt_write(R_CHANNEL, 0x0000); // Leerlauf
lt_write(R_FIFOCTRL, 0x8080); // FIFO-Zeiger zurücksetzen
spi[0] = R_FIFO;
spi[1] = 0x07; // Länge
spi[2] = 0x5A; spi[3] = 0x4B; // Geräte-ID
spi[4] = 0x07; spi[5] = BYTE4;
spi[6] = cmd & 0xFF;
spi[7] = val & 0xFF;
spi[8] = sum;
spiTransfer(SLOT, spi, 9, 1);
lt_write(R_CHANNEL, TX_ENABLE | (chan & 0x7F));
sent_total = sent_total + 1;
}
// ── TaskLoop sendet, damit delay() erlaubt ist ────────────────────────────
// 90 Rahmen a ~1,1 ms blockieren rund 100 ms — in einem Tick-Callback wäre
// das falsch, im eigenen Task ist es richtig.
void TaskLoop() {
if (pend_cmd < 0) {
delay(20);
return;
}
int c = pend_cmd;
int v = seq;
pend_cmd = -1;
seq = (seq + 1) & 0xFF; // Zähler wie im Original hochlaufen lassen
int i = 0;
while (i < REPEATS) {
int k = 0;
while (k < 3) {
send_one(c, v, ch_list[k]);
delay(1);
k = k + 1;
}
i = i + 1;
}
addLog("lt8900: CC=0x%02X VV=0x%02X gesendet (%d Rahmen)", c, v, REPEATS * 3);
}
void queue(int nibble, char name[]) {
// 0x10 setzen wie die Fernbedienung bei wiederholter Betaetigung
pend_cmd = CMD_FLAG | (nibble & 0x0F);
sprintf(rsp, "%s -> CC=0x%02X VV=0x%02X", name, pend_cmd, seq);
responseCmnd(rsp);
}
void Command(char cmd[]) {
if (strcmp(cmd, "TEST") == 0) {
int r0 = lt_read(0x00);
int r41 = lt_read(R_PKTCFG);
int rs1 = lt_read(R_SYNC1);
int rs4 = lt_read(R_SYNC4);
sprintf(rsp, "Reg0=%04X (soll 6FE0) PktCfg=%04X (soll B000) Sync1=%04X (soll 151A) Sync4=%04X (soll 55AA)", r0, r41, rs1, rs4);
responseCmnd(rsp);
return;
}
if (strcmp(cmd, "EIN") == 0) { queue(CMD_EIN, "ein"); return; }
if (strcmp(cmd, "AUS") == 0) { queue(CMD_AUS, "aus"); return; }
if (strcmp(cmd, "HELL") == 0) { queue(CMD_HELLER, "heller"); return; }
if (strcmp(cmd, "DUNKEL") == 0) { queue(CMD_DUNKLER, "dunkler"); return; }
if (strcmp(cmd, "WARM") == 0) { queue(CMD_WARM, "warm"); return; }
if (strcmp(cmd, "KALT") == 0) { queue(CMD_KALT, "kalt"); return; }
if (strStartsWith(cmd, "RAW")) {
char a1[16];
strToken(a1, cmd, ' ', 2);
queue(strToInt(a1), "raw");
return;
}
sprintf(rsp, "EIN AUS HELL DUNKEL WARM KALT | RAW <nibble> | TEST (gesendet: %d)",
sent_total);
responseCmnd(rsp);
}
int main() {
ch_list[0] = 4;
ch_list[1] = 39;
ch_list[2] = 74;
pend_cmd = -1;
seq = 0;
sent_total = 0;
spiInit(-1, -1, -1, 1);
spiSetCS(SLOT, CS_PIN);
delay(50);
lt_init();
addCommand("LAMP");
addLog("lt8900_lamp: bereit, Reg0=0x%04X (erwartet 0x6FE0)", lt_read(0x00));
return 0;
}