Zum Inhalt

lt8900_lamp.tc

lt8900_lamp.tc — GaoMei GM2.4G Deckenlampe fernsteuern (LT8900/LT8910)

Source on GitHub

// ============================================================================
// 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;
}