gm24g_lamp.tc¶
gm24g_lamp.tc — GM2.4G-Deckenlampe steuern (nRF24L01 als LT8900-Ersatz)
// ============================================================================
// gm24g_lamp.tc — GM2.4G-Deckenlampe steuern (nRF24L01 als LT8900-Ersatz)
// ============================================================================
//
// Sender-App: Bedienfeld auf der Tasmota-Hauptseite, der Fernbedienung
// nachempfunden, plus Konsolenbefehl LAMPTX. Alle 14 Tasten der Original-
// fernbedienung sind decodiert. Vorgeschichte: das Protokoll wurde am
// 2026-07-27/28 aus einem SPI-Mitschnitt der Fernbedienung, HackRF-Messungen
// und einem nRF24-Mithoerbetrieb erschlossen. Der Mithoerteil ist hier raus —
// er steckt in der git-Historie unter examples/lt8900_sniff.tc, falls das
// Protokoll je erneut vermessen werden muss.
//
// ── PROTOKOLL (gemessen, nicht geraten) ───────────────────────────────────
// Die Lampe hoert auf einen LT8900, GFSK, 1 Mbit/s. Kanaele: LT8900 4/39/74.
// LT8900 rechnet 2402+n, der nRF24 dagegen 2400+n — auf dem nRF24 sind das
// also die Kanaele 6/41/76. (2441.02 MHz per HackRF nachgemessen.)
//
// Auf der Luft: 55 55 55 55 | 8A 85 5A AA | Nutzlast bitgedreht | CRC
// Dekodiert: 07 | 5A 4B 07 b4 CC VV | SUM
// 07 = Laengenbyte
// 5A 4B 07 = Kennung des Fernbedienungspaars (siehe unten,
// es gibt hier ZWEI verschiedene)
// b4 = zuletzt ANGEWAEHLTE Zone, nicht die Zone des
// Befehls. Nur die EIN-Tasten setzen sie; AUS- und
// Ringtasten tragen sie unveraendert weiter.
// (Gemessen 2026-07-29: nach vier EIN-Tasten
// trugen alle vier AUS-Tasten b4=04, und Ringtasten
// trugen b4=03, weil davor Zone 3 gewaehlt war.)
// Die Zone des Befehls steckt allein im Nibble
// von CC. Wir senden b4 = Zone des Befehls; das
// entspricht dem Normalfall „erst EIN, dann AUS
// derselben Zone" und funktioniert nachweislich.
// CC = Tastencode; Bit 0x10 = Wiederholung
// VV = Zaehler, steigt je Tastendruck
// SUM = (Summe der sieben Bytes) & 0xFF
// CRC-16/CCITT, Poly 0x1021, Init 0x0000, MSB-first, ueber die LUFT-Bytes
// (also die bitgedrehten) inklusive Laengenbyte, angehaengt als hi,lo und
// NICHT gedreht.
//
// ── WARUM DAS MIT EINEM nRF24 GEHT ────────────────────────────────────────
// Beide Chips senden GFSK mit 1 Mbit/s. Man legt Praeambel + Syncword des
// LT8900 in das ADRESSFELD des nRF24, schaltet dessen CRC ab und dreht die
// Bitreihenfolge der Nutzlast um.
// Quelle: DIY-Multiprotocol-TX-Module/Multiprotocol/NRF24l01_SPI.ino
//
// ── DREI FALLEN, die je eine Runde gekostet haben ─────────────────────────
// 1. RF_SETUP 0x0E ist 2 Mbit/s, NICHT 1 Mbit/s (Bit 3 = RF_DR_HIGH).
// Richtig ist 0x06. Adresse und Kanal stimmten, die Symbolrate nicht.
// 2. CE muss >=10 us high bleiben. Zwei digitalWrite direkt hintereinander
// sind zu kurz — der Chip sendet dann GAR NICHT, ohne Fehlermeldung.
// Das delay(1) in tx_frame() ist Absicht und liefert nebenbei die
// richtige Kadenz (~1,1 ms je Rahmen; das Original braucht 1,135 ms).
// 3. Nicht pro Rahmen konfigurieren. Erst wurde der Chip fuer JEDEN Rahmen
// neu aufgesetzt (Power-Down, alle Register, Power-Up, 2 ms warten) —
// rund 3 ms statt 1,1 ms und sporadische Aussetzer ("manchmal muss man
// zweimal druecken"). Einmal pro Burst: tx_setup(), dann je Rahmen nur
// Kanal + Nutzlast + CE-Impuls.
//
// ── VERDRAHTUNG (.156 „Mini2", ESP32-S3, ESP32-Bridge) ────────────────────
// Vorgaenger .177 ist am 2026-07-29 bei einem Firmware-Update gestorben
// (nach dem OTA nicht mehr erreichbar, seriell nur noch Fehler). Die
// Funkchips wurden auf ein Ersatzgeraet umgesteckt. Die drei SPI-Pins sind
// im Tasmota-Template zu setzen, ein frisch geflashtes Geraet hat sie NICHT:
// GPIO9 = 704 (SPI MOSI) GPIO10 = 736 (SPI CLK) GPIO11 = 672 (SPI MISO)
// Tasmota-UI belegt damit: 9 = MOSI, 10 = SCLK, 11 = MISO
// Hier gesetzt: 7 = CSN des nRF24L01
// Hier gesetzt: 8 = CE des nRF24L01
// ⚠️ 10–100 µF Elko direkt am Modul zwischen VCC und GND, sonst bricht die
// Versorgung ein und das Modul verhaelt sich sporadisch.
// ⚠️ Der CC1101 (CS 13) haengt am SELBEN MISO. Sein CS muss aktiv auf HIGH
// gelegt werden, sonst treibt er die Leitung mit und der Bus liefert nur
// Muell. "Nicht anfassen" reicht NICHT — ein undefinierter Eingang ist
// nicht dasselbe wie abgewaehlt.
//
// ── BEFEHLE ───────────────────────────────────────────────────────────────
// Tasmota haengt das Unterkommando OHNE Leerzeichen an den Praefix an.
// LAMPTX <hex> -> Taste senden. alle=5/9 Z1=8/B Z2=D/3 Z3=7/A Z4=2/6
// heller=C dunkler=4 warm=E kalt=F
// LAMPRC <n> -> Fernbedienung waehlen (welche Lampengruppe zuhoert)
// LAMPLEARN -> neue Fernbedienung anlernen: danach 60 s lang eine
// beliebige Taste auf ihr druecken, NAH am Modul
// LAMPLIST -> bekannte Kennungen + Anlernstatus (+ RX_DR-Zaehler)
// LAMPFORGET n -> Kennung n loeschen
//
// Diagnose bei „alle Register FF" — in dieser Reihenfolge, jeder Schritt
// halbiert den Suchraum:
// LAMPCC -> CC1101 am SELBEN Bus lesen. VERSION 04/14 = CLK/MOSI/MISO
// und die SPI-Schicht sind bewiesen in Ordnung.
// LAMPPINS -> schalten CSN und CE ueberhaupt? (je 0 und 1 erwartet)
// LAMPHI -> CC1101 hart abwaehlen, dann lesen — schliesst aus, dass er
// den geteilten MISO festhaelt.
// LAMPSLOW -> dieselben Register mit 1 MHz statt 4. Unterscheidet
// Signalqualitaet von Unterbrechung.
// LAMPMAN -> ueber SPI-Slot 0 mit CS von Hand lesen, genau wie LAMPCC.
// Trennt einen Fehler der Slot-/CS-Verwaltung von Hardware.
// ⚠️ Bestehen ALLE diese Tests und es bleibt FF, waehrend Senden noch geht:
// Senden braucht MISO nicht. Dann ist der MISO-Ausgang des Moduls hin —
// bei billigen nRF24-Klonen die wahrscheinlichste Erklaerung. Folge:
// Steuern funktioniert weiter, ANLERNEN nicht.
//
// Diagnose, falls das Anlernen nichts findet — in dieser Reihenfolge:
// ⚠️ Anlernen findet nichts, obwohl die Register stimmen? Dann rastet das
// Modul auf die Adressbreite nicht ein. Das Anlernen faehrt 4/5/3 Byte
// durch; die getroffene Breite steht im Log und in LAMPLIST.
// LAMPCC -> ZUERST bei „alle Register FF": den CC1101 am SELBEN Bus
// auslesen. Antwortet er (VERSION 04/14), sind CLK/MOSI/MISO
// bewiesen in Ordnung und der Fehler sitzt beim nRF24.
// LAMPNOISE -> 2 s mit 3-Byte-Adresse lauschen. >0 Pakete = Antenne,
// MISO und Empfangszweig arbeiten; 0 = Empfang defekt.
// LAMPREG -> Register UND Empfangsadresse zurueckliegen. Waehrend des
// Anlernens erlaubt, weil es nichts umkonfiguriert.
// LAMPRAW -> RX_DR-Zaehler + erstes Rohpaket des letzten Versuchs.
// Trennt "es kommt nichts an" von "es kommt an, wird aber
// verworfen" — genau daran hing es beim Geraetewechsel.
// LAMPTEST -> nRF24-Register zuruecklesen (prueft Bus und Modul)
// LAMPONE -> EINEN Rahmen senden und TX_DS im Status pruefen.
// Der schnellste Beweis, dass wirklich gesendet wird —
// viel schneller als eine HackRF-Gegenmessung.
// ⚠️ Unterkommandos duerfen NICHT auf einer Ziffer enden: Tasmota liest
// eine abschliessende Ziffer als Geraeteindex (wie POWER1). Deshalb
// LAMPONE und nicht LAMPT1.
// ============================================================================
#define CSN_PIN 7
#define CE_PIN 8 // CE verloetet auf GPIO 8
#define CC_CS_PIN 13 // CS des CC1101 am SELBEN Bus — muss HIGH sein
#define SLOT 1
#define LED_PIN 21 // onboard WS2812 RGB status LED (board-specific: Lolin S3 Mini = GPIO21)
// nRF24L01 Befehle / Register
#define N_R_REG 0x00
#define N_W_REG 0x20
#define N_FLUSH_TX 0xE1
#define N_W_TX_PL 0xA0
#define N_CONFIG 0x00
#define N_EN_AA 0x01
#define N_AW 0x03
#define N_RF_CH 0x05
#define N_RF_SET 0x06
#define N_STATUS 0x07
#define N_FIFO 0x17
#define RF_1MBIT 0x06 // NICHT 0x0E — das waeren 2 Mbit/s
char spi[40];
char line[220];
int tx_total; // gesendete Rahmen insgesamt
int tx_seq; // Zaehler VV, steigt je Tastendruck
int ui_cmd; // von den Web-Knoepfen gesetzt: Befehlsnibble, 0 = nichts
int ui_rc; // Fernbedienungswahl aus dem Web: 1 oder 2, 0 = nichts
int tx_pend; // -1 = nichts zu senden, sonst Befehlsnibble
int last_sent; // zuletzt gesendetes Nibble, -1 = noch nichts
int last_ms; // millis() beim letzten Senden
// ── ZWEI FERNBEDIENUNGEN ──────────────────────────────────────────────────
// Jeder Rahmen fuehrt eine 3-Byte-Kennung des Fernbedienungspaars mit. Die
// Lampen sind je auf EINE Fernbedienung angelernt und ignorieren alles andere.
// Am 2026-07-29 gemessen (Empfaenger auf Laenge+Pruefsumme verankert statt auf
// eine fest verdrahtete Kennung, sonst sieht man die zweite gar nicht):
//
// Fernbedienung 1 5A 4B 07 Fernbedienung 2 5A 43 72
//
// Die Tastencodes sind bei BEIDEN identisch — Zone 1 EIN ist ueberall CC=08.
// Nur die Kennung unterscheidet sie. Das war die Ursache dafuer, dass Zone 2
// aus TinyC nichts bewirkte, mit der Fernbedienung aber einwandfrei ging:
// gesendet wurde die richtige Taste an die falsche Lampengruppe.
//
// NICHT beide Kennungen nacheinander senden — Zone 2 an Fernbedienung 1 und
// Zone 2 an Fernbedienung 2 sind VERSCHIEDENE Lampen; das schaltete beide.
// Die Kennungen sind NICHT mehr fest eingebaut — sie werden ANGELERNT.
// Wer dieses Skript auf eigener Hardware benutzt, hat andere Fernbedienungen
// und koennte eine fest verdrahtete Kennung nicht ermitteln, ohne sich den
// Empfaenger aus der git-Historie zu holen und eine HackRF-Messung
// nachzuvollziehen. Genau das nimmt LAMPLEARN bzw. das Untermenue
// „Fernbedienung anlernen" ab: Knopf druecken, an der neuen Fernbedienung eine
// beliebige Taste betaetigen, fertig.
//
// Ablage in `persist` statt in einer eigenen Datei: seit dem namensbasierten
// PVS3 wandern Eintraege beim Neuuebersetzen ueber ihren NAMEN mit, ein
// Hinzufuegen oder Umsortieren wirft die gelernten Kennungen also nicht mehr
// weg. persist-Initialisierer werden allerdings verworfen — alles startet auf
// 0, deshalb saet main() die Voreinstellung erst, wenn rc_cnt == 0 ist.
#define RC_MAX 4
#define LEARN_MS 60000 // Wartezeit auf einen Tastendruck.
// 20 s waren zu knapp: bis die Aufforderung
// gelesen und die Fernbedienung geholt war,
// lief das Fenster ab und die Meldung
// "nichts empfangen" sah wie ein Defekt aus.
// ⚠️ USE_PERSIST BRAUCHT TinyC >= 1.6.43. Auf .156 (1.6.44) VERIFIZIERT:
// Slot laedt, Auswahl ueberlebt den Slot-Neustart, .pvs wird angelegt.
// Auf 1.6.42 fuehrt SCHON DIE DEKLARATION einer persist-Variablen zum harten
// Neustart des ESP — nicht zu einem VM-Fehler, das Geraet bootet weg. Am
// 2026-07-29 auf .177 (Release 1.6.42, per /tc_api?cmd=status) mit einem
// Sechszeiler nachgestellt:
// persist int pcount;
// int main() { pcount = pcount + 1; addCommand("PVT"); return 0; }
// Es entsteht nicht einmal eine .pvs — es kracht beim LADEN, also beim
// Oeffnen der noch nicht vorhandenen Datei, nicht beim Speichern.
// Die persist-Ueberholung (namensbasiertes PV3, Absturzkorrekturen) kam mit
// 1.6.43. Auf einer aelteren Firmware also AUSKOMMENTIEREN, sonst bootet das
// Geraet weg; ohne die Zeile laufen die Kennungen nur bis zum naechsten
// Slot-Neustart (die beiden gemessenen kommen als Voreinstellung zurueck,
// angelernte nicht).
//
// ⚠️ ZUM EINSCHALTEN DIE NAECHSTE ZEILE EINKOMMENTIEREN, nicht auf 1 setzen:
// TinyCs `#if NAME` wertet den WERT NICHT AUS, es prueft nur, ob der Name
// definiert ist — `#define USE_PERSIST 0` nimmt trotzdem den Ja-Zweig (am
// 2026-07-29 am Praeprozessor nachgestellt). Nur `#ifdef`/`#ifndef` arbeiten
// hier zuverlaessig.
#define USE_PERSIST
#ifdef USE_PERSIST
persist int rc_cnt; // wie viele Fernbedienungen bekannt sind
persist int rc_sel; // 0..rc_cnt-1
persist int rc_i1[RC_MAX];
persist int rc_i2[RC_MAX];
persist int rc_i3[RC_MAX];
#else
int rc_cnt;
int rc_sel;
int rc_i1[RC_MAX];
int rc_i2[RC_MAX];
int rc_i3[RC_MAX];
#endif
// Einziger Ort, an dem gesichert wird — so kostet das Umschalten oben eine
// Zeile statt einer Suche durch die Datei.
void rc_save() {
#ifdef USE_PERSIST
saveVars();
#endif
}
int learn_state; // 0 = aus, 1 = lauscht
int learn_until; // millis(), bis wann
int learn_res; // 0 = nichts, 1 = neu gelernt, 2 = war schon bekannt,
// 3 = Abbruch/Zeit abgelaufen, 4 = kein Platz frei
int ui_learn; // aus dem Web: 1 = anlernen, 2 = abbrechen, 10+n = loeschen
// Diagnose: trennt „es kommt nichts an" von „es kommt an, wird aber verworfen".
int learn_rx; // wie oft RX_DR waehrend des Anlernens gesetzt war
int learn_raw[16]; // erstes Rohpaket, bitgedreht
int learn_have; // 1 = learn_raw gefuellt
// Bestaetigungszaehler: erst nach mehreren gleichen Kennungen uebernehmen.
int cand1;
int cand2;
int cand3;
int cand_n;
// Befehlsnibble -> Klartext. Bewusst if/else statt Ternaer: String-Ternaere
// scheiterten in TinyC lange still zur Laufzeit (im Codegen behoben am
// 2026-07-28, aber alte Firmware/IDE im Feld hat den Fix nicht).
// Vierzehn Tasten, vierzehn frei vergebene Codes — kein arithmetisches Muster,
// eine reine Tabelle. Byte 4 fuehrt die Zone redundant mit (00=alle, 01..04).
void btn_name(int nib, char out[]) {
if (nib == 0x4) { strcpy(out, "dunkler"); }
else if (nib == 0x5) { strcpy(out, "EIN alle"); }
else if (nib == 0x9) { strcpy(out, "AUS alle"); }
else if (nib == 0xC) { strcpy(out, "heller"); }
else if (nib == 0xE) { strcpy(out, "warm"); }
else if (nib == 0xF) { strcpy(out, "kalt"); }
else if (nib == 0x8) { strcpy(out, "EIN Zone 1"); }
else if (nib == 0xB) { strcpy(out, "AUS Zone 1"); }
else if (nib == 0xD) { strcpy(out, "EIN Zone 2"); }
else if (nib == 0x3) { strcpy(out, "AUS Zone 2"); }
else if (nib == 0x7) { strcpy(out, "EIN Zone 3"); }
else if (nib == 0xA) { strcpy(out, "AUS Zone 3"); }
else if (nib == 0x2) { strcpy(out, "EIN Zone 4"); }
else if (nib == 0x6) { strcpy(out, "AUS Zone 4"); }
else { strcpy(out, "unbekannt"); }
}
// Nibble -> zugehoeriges Byte 4 (Zone)
int nib_zone(int nib) {
if (nib == 0x8) { return 1; }
if (nib == 0xB) { return 1; }
if (nib == 0xD) { return 2; }
if (nib == 0x3) { return 2; }
if (nib == 0x7) { return 3; }
if (nib == 0xA) { return 3; }
if (nib == 0x2) { return 4; }
if (nib == 0x6) { return 4; }
return 0;
}
// ── nRF24-Grundfunktionen ─────────────────────────────────────────────────
void nrf_wr(int reg, int val) {
spi[0] = N_W_REG | (reg & 0x1F);
spi[1] = val & 0xFF;
spiTransfer(SLOT, spi, 2, 1);
}
int nrf_rd(int reg) {
spi[0] = N_R_REG | (reg & 0x1F);
spi[1] = 0;
spiTransfer(SLOT, spi, 2, 1);
return spi[1] & 0xFF;
}
void nrf_cmd(int c) {
spi[0] = c & 0xFF;
spiTransfer(SLOT, spi, 1, 1);
}
// Bits eines Bytes spiegeln — LT8900 sendet in umgekehrter Bitfolge zum nRF24
int bitrev(int b) {
int r = 0;
int i = 0;
while (i < 8) {
r = (r << 1) | ((b >> i) & 1);
i = i + 1;
}
return r & 0xFF;
}
// ── SENDEN ────────────────────────────────────────────────────────────────
int lt_crc(char d[], int n) {
int c = 0;
int i = 0;
while (i < n) {
c = c ^ ((d[i] & 0xFF) << 8);
int b = 0;
while (b < 8) {
if (c & 0x8000) { c = ((c << 1) ^ 0x1021) & 0xFFFF; }
else { c = (c << 1) & 0xFFFF; }
b = b + 1;
}
i = i + 1;
}
return c & 0xFFFF;
}
// Sender EINMAL pro Burst aufsetzen (siehe Falle 3 im Kopf).
void tx_setup() {
if (CE_PIN >= 0) { digitalWrite(CE_PIN, 0); }
nrf_wr(N_CONFIG, 0x00);
nrf_wr(N_EN_AA, 0x00);
nrf_wr(N_AW, 0x03);
nrf_wr(N_RF_SET, RF_1MBIT);
// TX_ADDR: Die Fernbedienung sendet VIER Praeambelbytes 0x55 vor dem
// Syncword. Der nRF24 stellt nur EINES voran; laege das ganze Syncword in
// der Adresse, kaemen nur zwei zusammen — der LT8900 in der Lampe rastet
// dann nicht zuverlaessig ein (per HackRF nachgewiesen). Deshalb die
// Grenze verschieben: Adresse = 55 55 55 8A 85, der Rest des Syncwords
// (5A AA) wandert an den Anfang der Nutzlast.
spi[0] = N_W_REG | 0x10;
spi[1] = 0x85; spi[2] = 0x8A; spi[3] = 0x55; spi[4] = 0x55; spi[5] = 0x55;
spiTransfer(SLOT, spi, 6, 1);
nrf_cmd(N_FLUSH_TX);
nrf_wr(N_STATUS, 0x70);
nrf_wr(N_CONFIG, 0x02); // PWR_UP, PRIM_RX=0, CRC AUS
delay(2); // einmalig einschwingen lassen
}
// einen Rahmen senden. Setzt nur Kanal, Nutzlast und den CE-Impuls.
void tx_frame(int nib, int vv, int b4, int chan, int flag) {
int cc = (nib & 0x0F);
if (flag) { cc = cc | 0x10; }
int i1 = rc_i1[rc_sel];
int i2 = rc_i2[rc_sel];
int i3 = rc_i3[rc_sel];
int sum = (0x07 + i1 + i2 + i3 + b4 + cc + vv) & 0xFF;
char air8[10];
air8[0] = bitrev(0x07); air8[1] = bitrev(i1); air8[2] = bitrev(i2);
air8[3] = bitrev(i3); air8[4] = bitrev(b4);
air8[5] = bitrev(cc); air8[6] = bitrev(vv); air8[7] = bitrev(sum);
int crc = lt_crc(air8, 8);
nrf_wr(N_RF_CH, chan & 0x7F);
nrf_cmd(N_FLUSH_TX);
nrf_wr(N_STATUS, 0x70);
// Nutzlast: Rest des Syncwords (5A AA) + 8 Luft-Bytes + CRC hi/lo
spi[0] = N_W_TX_PL;
spi[1] = 0x5A; spi[2] = 0xAA;
int k = 0;
while (k < 8) { spi[k+3] = air8[k]; k = k + 1; }
spi[11] = (crc >> 8) & 0xFF;
spi[12] = crc & 0xFF;
spiTransfer(SLOT, spi, 13, 1);
if (CE_PIN >= 0) {
digitalWrite(CE_PIN, 1);
delay(1); // CE MUSS >=10 us hoch bleiben; zwei
digitalWrite(CE_PIN, 0); // digitalWrite hintereinander sind zu kurz
} // und der Chip sendet dann GAR NICHT.
tx_total = tx_total + 1;
}
// ── ANLERNEN: EMPFANGEN ───────────────────────────────────────────────────
// Zum Anlernen muss der Chip kurz auf Empfang. Senden und Empfangen geht mit
// EINEM nRF24 nicht gleichzeitig, das Anlernen ist deshalb ein eigener
// Zustand, waehrend dessen nicht gesendet wird.
//
// Adresse = Praeambelrest + Syncword, in Registerreihenfolge (also umgekehrt
// zur Sendereihenfolge). Genau diese Werte sind am 2026-07-28 als einzige
// funktionierende Kombination herausgemessen worden.
// Adressbreite, die gerade probiert wird (5, 4 oder 3 Byte).
// Auf der Luft steht ... 55 8A 85 5A AA | Nutzlast. Der nRF24 vergleicht die
// Bytes UNMITTELBAR vor der Nutzlast; in Registerreihenfolge sind das
// AA, 5A, 85, 8A, 55 — eine kuerzere Adresse nimmt einfach die ersten w davon.
// Warum ueberhaupt kuerzer: manche Klone rasten auf die volle 5-Byte-Folge
// nicht ein, weil sie Praeambel und Adressbeginn strenger auswerten. Statt
// gemu fuer jede Variante einzeln druecken zu lassen, faehrt das Anlernen die
// Breiten im Wechsel durch.
// Vorgabe 4 — auf dem Modul vom 2026-07-29 rastet NUR 4 Byte ein, bei 5 blieb
// RX_DR auf 0, obwohl die Register nachweislich stimmten. Das alte Modul
// brauchte 5. Deshalb bleibt der Wechsel drin: beim naechsten Tausch kann es
// wieder anders sein, und ein Fenster probiert alle drei durch.
int rx_aw = 4;
void rx_setup() {
if (CE_PIN >= 0) { digitalWrite(CE_PIN, 0); }
nrf_wr(N_CONFIG, 0x00);
nrf_wr(N_EN_AA, 0x00);
nrf_wr(0x02, 0x01); // EN_RXADDR: nur Pipe 0
nrf_wr(N_AW, (rx_aw - 2) & 0x03); // 5, 4 oder 3 Byte
nrf_wr(N_RF_CH, 41); // Lampenkanal 39 + 2; dort landen je
nrf_wr(N_RF_SET, RF_1MBIT); // Tastendruck 30 Rahmen, das reicht
nrf_wr(0x11, 32); // RX_PW_P0: feste Nutzlastlaenge
spi[0] = N_W_REG | 0x0A; // RX_ADDR_P0
spi[1] = 0xAA; spi[2] = 0x5A; spi[3] = 0x85; spi[4] = 0x8A; spi[5] = 0x55;
spiTransfer(SLOT, spi, rx_aw + 1, 1);
nrf_cmd(0xE2); // FLUSH_RX
nrf_wr(N_STATUS, 0x70);
nrf_wr(N_CONFIG, 0x03); // PWR_UP | PRIM_RX, CRC im nRF24 AUS
delay(2);
if (CE_PIN >= 0) { digitalWrite(CE_PIN, 1); }
}
// Ein Paket abholen und auswerten. Rueckgabe 1, wenn ein gueltiger Rahmen
// erkannt wurde; die Kennung steht dann in got_i1..got_i3.
//
// ⚠️ NICHT auf eine bekannte Kennung matchen. Der urspruengliche Empfaenger
// suchte hart nach 5A 4B 07 und meldete sauber „0 Pakete", waehrend die
// zweite Fernbedienung munter sendete — beim Anlernen ist die gesuchte
// Kennung ja gerade unbekannt. Anker ist die STRUKTUR: Laengenbyte 0x07,
// danach sechs Bytes und eine Summenpruefsumme darueber.
int got_i1;
int got_i2;
int got_i3;
int rx_poll() {
int st = nrf_rd(N_STATUS);
if (!(st & 0x40)) { return 0; } // kein RX_DR
learn_rx = learn_rx + 1;
spi[0] = 0x61; // R_RX_PAYLOAD
int w = 1;
while (w <= 32) { spi[w] = 0; w = w + 1; }
spiTransfer(SLOT, spi, 33, 1);
nrf_wr(N_STATUS, 0x70);
nrf_cmd(0xE2); // FLUSH_RX
char rxb[34];
w = 0;
while (w < 32) { rxb[w] = bitrev(spi[w+1] & 0xFF); w = w + 1; }
if (!learn_have) {
int c = 0;
while (c < 16) { learn_raw[c] = rxb[c] & 0xFF; c = c + 1; }
learn_have = 1;
}
// ⚠️ DAS LAENGENBYTE NICHT AUS DER LUFT VERLANGEN.
// Auf .156 kommt das erste Nutzlastbyte als 0x00 an, obwohl der Sender
// 0x07 schickt (auf .177 kam es korrekt — offenbar eine Eigenheit des
// Adress-/Nutzlast-Uebergangs bei diesem Chip). Der REST des Rahmens ist
// fehlerfrei, die Pruefsumme des Senders rechnet mit 0x07. Also 0x07 als
// Protokollkonstante einsetzen statt es als Anker zu fordern — sonst wird
// JEDER gueltige Rahmen verworfen, und es sieht aus, als kaeme nichts an.
// Gemessen: 1531 Pakete empfangen, 0 erkannt.
int k = 0;
while (k < 24) {
int t = (0x07 + rxb[k+1] + rxb[k+2] + rxb[k+3]
+ rxb[k+4] + rxb[k+5] + rxb[k+6]) & 0xFF;
if (t == (rxb[k+7] & 0xFF)) {
int i1 = rxb[k+1] & 0xFF;
int i2 = rxb[k+2] & 0xFF;
int i3 = rxb[k+3] & 0xFF;
// Ohne das Laengenbyte als Anker traegt nur noch die Pruefsumme —
// das trifft rein zufaellig etwa jedes 256. Fenster. Bei 25 Paketen
// pro Sekunde waere das reichlich Rauschen. Deshalb erst nach DREI
// gleichen Kennungen uebernehmen; ein echter Tastendruck sendet 30
// Rahmen auf diesem Kanal, Rauschen wiederholt sich nicht.
if (i1 == cand1) {
if (i2 == cand2) {
if (i3 == cand3) { cand_n = cand_n + 1; }
else { cand1 = i1; cand2 = i2; cand3 = i3; cand_n = 1; }
} else { cand1 = i1; cand2 = i2; cand3 = i3; cand_n = 1; }
} else { cand1 = i1; cand2 = i2; cand3 = i3; cand_n = 1; }
if (cand_n >= 3) {
got_i1 = i1; got_i2 = i2; got_i3 = i3;
return 1;
}
return 0;
}
k = k + 1;
}
return 0;
}
// Kennung einsortieren. Rueckgabe: 1 = neu, 2 = war schon bekannt, 4 = voll.
int rc_store(int i1, int i2, int i3) {
int z = 0;
while (z < rc_cnt) {
if (rc_i1[z] == i1) {
if (rc_i2[z] == i2) {
if (rc_i3[z] == i3) { rc_sel = z; return 2; }
}
}
z = z + 1;
}
if (rc_cnt >= RC_MAX) { return 4; }
rc_i1[rc_cnt] = i1; rc_i2[rc_cnt] = i2; rc_i3[rc_cnt] = i3;
rc_sel = rc_cnt;
rc_cnt = rc_cnt + 1;
rc_save(); // sofort sichern, nicht erst bei TinyCStop —
return 1; // ein Stromausfall soll das Anlernen nicht fressen
}
void rc_forget(int idx) {
if (idx < 0) { return; }
if (idx >= rc_cnt) { return; }
int z = idx;
while (z < rc_cnt - 1) {
rc_i1[z] = rc_i1[z+1]; rc_i2[z] = rc_i2[z+1]; rc_i3[z] = rc_i3[z+1];
z = z + 1;
}
rc_cnt = rc_cnt - 1;
if (rc_sel >= rc_cnt) { rc_sel = rc_cnt - 1; }
if (rc_sel < 0) { rc_sel = 0; }
rc_save();
}
// Einen kompletten Tastendruck senden: 90 Rahmen ueber DREI Kanaele
// (LT8900 4/39/74 = nRF24 6/41/76), je 30 pro Kanal. Nur auf einem Kanal zu
// senden reichte fuer manche Befehle nicht.
void send_button(int nib) {
int tx_ch[3];
tx_ch[0] = 6; tx_ch[1] = 41; tx_ch[2] = 76;
tx_setup();
// Die Fernbedienung sendet BEIDE Varianten: erst einige Rahmen OHNE das
// 0x10-Bit, dann die Mehrzahl MIT. Gemessen bei "Zone 1 AUS": 20x 0x0B
// gegen 64x 0x1B, also grob ein Drittel ohne Flag. Offenbar wertet die
// Lampe die Fassung ohne Flag als eigentlichen Tastendruck und die mit
// Flag nur als Wiederholung — mit nur einer Variante schaltete sie nicht.
int r = 0;
while (r < 30) {
int fl = 1;
if (r < 10) { fl = 0; }
int c2 = 0;
while (c2 < 3) {
tx_frame(nib, tx_seq, nib_zone(nib), tx_ch[c2], fl);
c2 = c2 + 1;
}
r = r + 1;
}
tx_seq = (tx_seq + 1) & 0xFF;
last_sent = nib;
last_ms = millis();
}
// Senden laeuft in der eigenen Task, nicht im Tick: ein Burst dauert rund
// 100 ms und wuerde die Tasmota-Hauptschleife blockieren.
void TaskLoop() {
if (ui_learn != 0) {
int w = ui_learn;
ui_learn = 0;
if (w == 1) {
learn_state = 1;
learn_res = 0;
learn_rx = 0; learn_have = 0;
cand1 = -1; cand2 = -1; cand3 = -1; cand_n = 0;
learn_until = millis() + LEARN_MS;
rx_setup();
addLog("gm24g: Anlernen laeuft — Taste auf der neuen Fernbedienung druecken");
}
if (w == 2) { learn_state = 0; learn_res = 3; tx_setup(); }
if (w >= 10) { rc_forget(w - 10); }
}
// Anlernen hat Vorrang: solange der Chip auf Empfang steht, kann er nicht
// senden. Die Schleife arbeitet in kurzen Scheiben, damit „Abbrechen"
// und die Zeitgrenze zuegig greifen.
if (learn_state == 1) {
// Adressbreite alle ~3 s wechseln: 5 -> 4 -> 3 -> 5 ...
// Reihenfolge 4 -> 5 -> 3: die wahrscheinlichste zuerst.
if (rx_aw == 4) { rx_aw = 5; }
else if (rx_aw == 5) { rx_aw = 3; }
else { rx_aw = 4; }
rx_setup();
int q = 0;
while (q < 50) {
if (rx_poll()) {
learn_res = rc_store(got_i1, got_i2, got_i3);
learn_state = 0;
tx_setup(); // zurueck auf Senden
addLog("gm24g: Kennung %02X %02X %02X (%d) bei %d-Byte-Adresse",
got_i1, got_i2, got_i3, learn_res, rx_aw);
return;
}
delay(1);
q = q + 1;
}
if (millis() > learn_until) {
learn_state = 0;
learn_res = 3;
tx_setup();
addLog("gm24g: Anlernen abgelaufen — nichts empfangen");
}
return;
}
if (ui_rc != 0) {
int n = ui_rc - 1;
ui_rc = 0;
if (n >= 0) { if (n < rc_cnt) { rc_sel = n; rc_save(); } }
addLog("gm24g: Fernbedienung %d gewaehlt", rc_sel + 1);
}
if (ui_cmd != 0) {
tx_pend = ui_cmd & 0x0F;
ui_cmd = 0;
}
if (tx_pend >= 0) {
int nb = tx_pend;
tx_pend = -1;
send_button(nb);
char nm[16];
btn_name(nb, nm);
addLog("gm24g: %s gesendet (90 Rahmen, 3 Kanaele)", nm);
return;
}
delay(20);
}
// ── Anzeige auf der Tasmota-Hauptseite ─────────────────────────────────────
void WebCall() {
char nm[16];
// Der Anlernstatus MUSS hier stehen, nicht im Panel: WebPage() wird nur
// beim vollen Seitenaufbau gerendert, dieser Block dagegen bei jedem
// Ajax-Refresh — nur so sieht man den Fortschritt ohne Neuladen.
if (learn_state == 1) {
int rest = (learn_until - millis()) / 1000;
if (rest < 0) { rest = 0; }
sprintf(line, "{s}Anlernen{m}<b>warte auf Tastendruck</b> (%d s){e}", rest);
webSend(line);
} else {
if (learn_res == 1) { webSend("{s}Anlernen{m}neue Fernbedienung uebernommen{e}"); }
if (learn_res == 2) { webSend("{s}Anlernen{m}war bereits bekannt — ausgewaehlt{e}"); }
if (learn_res == 3) { webSend("{s}Anlernen{m}nichts empfangen (Abbruch/Zeit){e}"); }
if (learn_res == 4) { webSend("{s}Anlernen{m}kein Platz frei — erst eine loeschen{e}"); }
}
if (rc_cnt < 1) {
webSend("{s}Fernbedienung{m}<b>keine angelernt</b>{e}");
} else {
// Die beiden <span> sind KEIN Schmuck: das Bedienfeld liest sie aus, um
// zu merken, dass sich hinter seinem Ruecken etwas geaendert hat.
// Dieser Block wird beim Ajax-Refresh erneuert, WebPage() nicht.
sprintf(line, "{s}Fernbedienung{m}<b><span id='gms'>%d</span></b> von <span id='gmn'>%d</span> (Kennung %02X %02X %02X){e}",
rc_sel + 1, rc_cnt, rc_i1[rc_sel], rc_i2[rc_sel], rc_i3[rc_sel]);
webSend(line);
}
if (last_sent < 0) {
webSend("{s}Deckenlampe{m}bereit{e}");
} else {
btn_name(last_sent, nm);
int alt = (millis() - last_ms) / 1000;
sprintf(line, "{s}Zuletzt gesendet{m}<b>%s</b> (vor %d s){e}", nm, alt);
webSend(line);
sprintf(line, "{s}Rahmen{m}CC=0x%02X Zaehler=0x%02X{e}",
0x10 | last_sent, (tx_seq - 1) & 0xFF);
webSend(line);
}
sprintf(line, "{s}Gesendet gesamt{m}%d Rahmen{e}", tx_total);
webSend(line);
}
// ── Bedienfeld: der Fernbedienung nachempfunden ───────────────────────────
// Aufbau wie das Original: oben Ein/Aus fuer alle, in der Mitte der Ring mit
// vier Richtungen (oben/unten = Helligkeit, links/rechts = Farbtemperatur),
// unten die vier Zonen-Wippen mit je | oben und O unten.
//
// ⚠️ Prozentzeichen NUR in webSend-Literalen verwenden, NIE in einem
// sprintf-Format — dort wuerde % als Formatanweisung gelesen und das HTML
// zerstoeren. Deshalb ist der Aufbau in „sprintf fuer die Zahlen" und
// „webSend fuer das CSS" getrennt.
// Eigener Klick-Handler statt des eingebauten tcbtn(). Zwei Gruende:
//
// 1. tcbtn setzt `e.textContent = "✓"` und stellt danach den GESICHERTEN TEXT
// wieder her. Das zerstoert Kindelemente: der weisse <span>-Punkt auf dem
// Ring war nach dem ersten Klick fuer immer weg (gesichert wurde "", denn
// der span traegt keinen Text). Und `e.style.color = ''` loescht die
// Inline-Textfarbe endgueltig — gesichert wird nur der Hintergrund, nicht
// die Farbe — danach steht Tasmotas helle Standardfarbe auf hellem Grund
// und die Zonenzeichen sind unsichtbar. (In der Firmware behoben am
// 2026-07-28; alte Firmware im Feld hat den Fix nicht.)
// 2. tcbtn sperrt den Knopf 2,5 s (`dataset.b`). Bei einer Lampenfernbedienung
// falsch: „heller" will man mehrmals hintereinander druecken.
//
// gmb() fasst weder Inhalt noch Farben an, sondern staucht den Knopf kurz.
// `seva(v,i)` stellt die Firmware auf der Hauptseite bereit, sobald ein Slot
// ein WebCall() hat — das haben wir.
// Der Wahlschalter braucht einen EIGENEN Handler. gmb() staucht nur kurz und
// laesst das Aussehen sonst in Ruhe — richtig fuer Momentanknoepfe, falsch
// fuer eine Zustandsanzeige: WebPage() wird NUR beim vollen Seitenaufbau
// gerendert, der periodische Ajax-Refresh erneuert allein den Sensorblock aus
// WebCall(). Ein serverseitig umgeschaltetes rc_sel erreicht die Knopffarbe
// also nie, und FB 1 blieb dauerhaft schwarz. gmrc() faerbt darum im Browser
// selbst um; beim naechsten Neuladen liefert der Server denselben Zustand.
// ⚠️ TASMOTAS STYLESHEET MUSS FUER DAS PANEL ZURUECKGESETZT WERDEN.
// Am 2026-07-29 im Browser nachgemessen, nicht geraten:
// div { padding:5px; box-sizing:content-box }
// button { line-height:38.4px }
// Folgen, die beide wie Rechenfehler von uns aussahen:
// * Der Ring war auf dem Schirm 168 px breit statt der gesetzten 158 —
// und absolut positionierte Kinder rechnen ab der POLSTERkante. Die vier
// Richtungspunkte sassen dadurch 5 px nach links oben verschoben
// (gemessen: heller bei -59, dunkler bei +49 statt symmetrisch ±54).
// * In einem 22 px hohen Knopf erzeugt line-height 38.4 px eine Textzeile,
// die oben 12 px tief beginnt und unten 3,5 px HERAUSRAGT.
// Deshalb ein eigener Geltungsbereich #gmp mit gesetzten Grundwerten. Inline
// gesetzte Eigenschaften gewinnen weiterhin, die Sonderfaelle bleiben also.
void panel_css() {
webSend("<style>#gmp div{box-sizing:border-box;padding:0}");
webSend("#gmp button{box-sizing:border-box;line-height:1;margin:0;");
webSend("display:inline-flex;align-items:center;justify-content:center}");
webSend("</style>");
}
void panel_js() {
webSend("<script>function gmb(e,v,i){seva(v,i);");
webSend("e.style.transform='scale(.85)';");
webSend("setTimeout(function(){e.style.transform='';},260);}");
webSend("function gmrc(e,v,i){seva(v,i);");
webSend("var b=document.querySelectorAll('[data-rc]');");
webSend("for(var k=0;k<b.length;k++){");
webSend("var on=b[k].getAttribute('data-rc')==String(v);");
webSend("b[k].style.background=on?'#3a3a3a':'#fff';");
webSend("b[k].style.color=on?'#fff':'#666';}}");
// Loeschen und Anlernen aendern die STRUKTUR des Panels (eine Zeile faellt
// weg, ein Knopf kommt dazu). Umfaerben wie beim Wahlschalter reicht dafuer
// nicht, und WebPage() laeuft nur beim vollen Seitenaufbau — ohne das hier
// stand nach dem Loeschen weiter "2", und eine neu angelernte Fernbedienung
// wurde erst nach manuellem Neuladen sichtbar.
// Der WebCall-Block wird dagegen bei jedem Ajax-Refresh erneuert; er traegt
// Anzahl und Auswahl in #gmn/#gms. Weichen sie von dem ab, womit dieses
// Panel gerendert wurde, laedt die Seite EINMAL neu.
webSend("function gmwatch(n,s){setInterval(function(){");
webSend("var a=document.getElementById('gmn'),b=document.getElementById('gms');");
webSend("if(!a||!b)return;");
webSend("if(a.textContent!=String(n)||b.textContent!=String(s))location.reload();");
webSend("},1500);}</script>");
}
// Wahlschalter fuer die Fernbedienung. Die aktive ist dunkel hinterlegt —
// eine Zustandsanzeige, kein Momentanknopf, deshalb faerbt sich die Auswahl
// und springt nicht zurueck.
void rc_btn(int which) {
sprintf(line, "<button data-rc='%d' title='Kennung %02X %02X %02X' onclick='gmrc(this,%d,%d)' style='",
which, rc_i1[which-1], rc_i2[which-1], rc_i3[which-1],
which, varIdx(ui_rc));
webSend(line);
webSend("border:0;cursor:pointer;font-family:inherit;font-size:11px;");
webSend("border-radius:11px;width:56px;height:22px;margin:0 3px;padding:0;");
if ((which - 1) == rc_sel) { webSend("background:#3a3a3a;color:#fff;"); }
else { webSend("background:#fff;color:#666;"); }
sprintf(line, "'>FB %d</button>", which);
webSend(line);
}
// oeffnet ein <button ...> mit onclick; das Styling folgt per webSend
void btn_open(int nib) {
sprintf(line, "<button onclick='gmb(this,%d,%d)' style='", nib, varIdx(ui_cmd));
webSend(line);
}
// KEINE implizite String-Verkettung in TinyC ("a" "b" ist ein Parserfehler)
// -> gemeinsamer Stil als eigener webSend-Aufruf statt als #define-Praefix.
void btn_base() {
webSend("border:0;cursor:pointer;font-family:inherit;color:#111;");
}
// Pillen-Knopf oben (Ein bzw. Aus fuer alle Lampen)
void top_btn(int nib, char sym[], char extra[]) {
btn_open(nib);
btn_base();
// display:flex + auto-Raender: zwei <button> hintereinander stehen sonst
// NEBENeinander — auf der Fernbedienung sitzen sie untereinander.
// flex statt block, weil flex ebenfalls auf eigener Zeile steht UND den
// Inhalt senkrecht mittet; block ueberlaesst das der Schriftmetrik.
webSend("background:#fff;border-radius:24px;width:76px;height:44px;");
webSend("box-shadow:0 1px 3px rgba(0,0,0,.35);font-size:20px;");
webSend("display:flex;align-items:center;justify-content:center;margin:4px auto;");
webSend(extra);
sprintf(line, "'>%s</button>", sym);
webSend(line);
}
// Richtungsknopf auf dem Ring.
// ⚠️ Position als ZAHLEN, nicht als String-Parameter: `webSend(param)` gibt
// NICHTS aus, wenn param ein char[]-Parameter ist, der ein String-Literal
// bekommen hat — tc_stream_ref() in der Firmware stieg bei Konstanten-Pool-
// Referenzen stillschweigend aus (behoben 2026-07-28, aber alte Firmware im
// Feld hat den Fix nicht). Ergebnis waren vier uebereinanderliegende Knoepfe,
// nur ein sichtbarer Punkt. `sprintf %s` kann den Fall dagegen, deshalb ist
// der Tooltip weiterhin ein String-Parameter.
// Der Marker ist ein weisser Punkt als eigenes Element, kein Textzeichen —
// Tasmotas Stylesheet beansprucht die Button-Textfarbe fuer sich. Die echte
// Fernbedienung hat ohnehin Punkte, keine Pfeile.
void ring_btn(int nib, int x, int y, char tip[]) {
sprintf(line, "<button title='%s' onclick='gmb(this,%d,%d)' style='",
tip, nib, varIdx(ui_cmd));
webSend(line);
webSend("border:0;cursor:pointer;background:transparent;padding:0;");
webSend("position:absolute;width:44px;height:44px;border-radius:50%;");
webSend("display:flex;align-items:center;justify-content:center;");
sprintf(line, "left:%dpx;top:%dpx;'>", x, y);
webSend(line);
webSend("<span style='width:10px;height:10px;border-radius:50%;");
webSend("background:#f2f2f2;display:block'></span></button>");
}
// halbe Wippe einer Zone (oben = ein, unten = aus); rund = 1 oben, 2 unten
void rocker(int nib, char sym[], int rund) {
sprintf(line, "<button onclick='gmb(this,%d,%d)' style='", nib, varIdx(ui_cmd));
webSend(line);
webSend("border:0;cursor:pointer;font-family:inherit;color:#111;");
webSend("background:#fff;width:40px;height:36px;font-size:16px;padding:0;");
webSend("box-shadow:0 1px 2px rgba(0,0,0,.3);");
if (rund == 1) { webSend("border-radius:18px 18px 4px 4px;"); }
else { webSend("border-radius:4px 4px 18px 18px;"); }
sprintf(line, "'>%s</button>", sym);
webSend(line);
}
void zone_col(int on_nib, int off_nib, int nr, int shift) {
webSend("<div style='display:flex;flex-direction:column;align-items:center;");
sprintf(line, "margin-top:%dpx'>", shift);
webSend(line);
rocker(on_nib, "|", 1);
rocker(off_nib, "○", 2);
sprintf(line, "<div style='font-size:11px;color:#555;margin-top:3px'>%d</div></div>", nr);
webSend(line);
}
// ── Untermenue „Fernbedienung anlernen" ───────────────────────────────────
// Als <details> — ein natives, zugeklapptes Untermenue ohne eigenes
// JavaScript, das den Blick auf das Bedienfeld nicht stoert.
// Die Rueckmeldung steht bewusst NICHT hier, sondern in WebCall(): dieser
// Block wird nur beim vollen Seitenaufbau erzeugt und koennte einen Fortschritt
// gar nicht anzeigen.
void learn_panel() {
webSend("<details style='max-width:246px;margin:6px auto 0;font-size:12px'>");
webSend("<summary style='cursor:pointer;color:#666'>Fernbedienung anlernen</summary>");
webSend("<div style='padding:6px 2px 2px;color:#666;line-height:1.4'>");
webSend("Auf <b>Anlernen</b> klicken, dann innerhalb von 20 Sekunden eine ");
webSend("beliebige Taste auf der neuen Fernbedienung druecken. Der Fortschritt ");
webSend("steht oben in der Statuszeile.<br>");
webSend("Waehrend des Anlernens wird nicht gesendet — ein nRF24 kann nicht ");
webSend("gleichzeitig senden und empfangen.");
webSend("</div>");
webSend("<div style='margin:4px 0'>");
sprintf(line, "<button onclick='gmb(this,1,%d)' style='", varIdx(ui_learn));
webSend(line);
webSend("border:0;cursor:pointer;font-family:inherit;font-size:12px;");
webSend("background:#2b6;color:#fff;border-radius:4px;height:26px;");
webSend("padding:0 10px;margin-right:4px'>Anlernen</button>");
sprintf(line, "<button onclick='gmb(this,2,%d)' style='", varIdx(ui_learn));
webSend(line);
webSend("border:0;cursor:pointer;font-family:inherit;font-size:12px;");
webSend("background:#ddd;color:#333;border-radius:4px;height:26px;");
webSend("padding:0 10px'>Abbrechen</button>");
webSend("</div>");
// Bekannte Fernbedienungen mit Kennung und Loeschknopf
int z = 0;
while (z < rc_cnt) {
webSend("<div style='display:flex;align-items:center;gap:6px;margin:3px 0'>");
sprintf(line, "<span style='width:150px'>FB %d %02X %02X %02X</span>",
z + 1, rc_i1[z], rc_i2[z], rc_i3[z]);
webSend(line);
sprintf(line, "<button onclick='gmb(this,%d,%d)' style='", 10 + z, varIdx(ui_learn));
webSend(line);
webSend("border:0;cursor:pointer;font-family:inherit;font-size:11px;");
webSend("background:#e55;color:#fff;border-radius:4px;height:22px;");
webSend("padding:0 8px'>loeschen</button></div>");
z = z + 1;
}
if (rc_cnt >= RC_MAX) {
webSend("<div style='color:#a00'>Alle Plaetze belegt — erst eine loeschen.</div>");
}
webSend("</details>");
}
void WebPage() {
panel_css();
panel_js();
// Der frühere Ausgleich margin-left:-30px ist WEG — er kompensierte nur
// die 5-px-Polsterungen, die panel_css jetzt an der Wurzel abstellt.
webSend("<div id='gmp'>");
// Gehaeuse
webSend("<div style='max-width:246px;margin:10px auto;padding:16px 12px 14px;");
webSend("background:linear-gradient(#fdfdfd,#eeeeef);border-radius:118px 118px 96px 96px/150px 150px 110px 110px;");
webSend("box-shadow:0 2px 10px rgba(0,0,0,.25);text-align:center'>");
// Welche Fernbedienung wird nachgebildet? Steht ganz oben, weil davon
// abhaengt, WELCHE Lampen ueberhaupt zuhoeren.
webSend("<div style='margin:0 0 6px'>");
int z = 1;
while (z <= rc_cnt) { rc_btn(z); z = z + 1; }
if (rc_cnt < 1) {
webSend("<span style='font-size:11px;color:#a00'>keine Fernbedienung angelernt</span>");
}
webSend("</div>");
top_btn(0x5, "|", ""); // alle EIN
top_btn(0x9, "○", ""); // alle AUS
// Ring
webSend("<div style='position:relative;width:158px;height:158px;margin:12px auto 10px;");
webSend("border-radius:50%;background:radial-gradient(circle,#3a3a3a 0 34%,#1b1b1b 35% 100%);");
webSend("box-shadow:inset 0 1px 3px rgba(255,255,255,.15)'>");
// Mittelscheibe zuerst — sie ist nur Blende, kein Knopf, und darf die
// Richtungsknoepfe nicht ueberdecken.
webSend("<div style='position:absolute;left:47px;top:47px;width:64px;height:64px;");
webSend("border-radius:50%;background:linear-gradient(#fbfbfb,#e8e8e8);");
webSend("box-shadow:0 1px 3px rgba(0,0,0,.45)'></div>");
// Ring 158 px, Knopf 44 px -> Mitte bei (158-44)/2 = 57
ring_btn(0xC, 57, 3, "heller");
ring_btn(0x4, 57, 111, "dunkler");
ring_btn(0xE, 3, 57, "warm");
ring_btn(0xF, 111, 57, "kalt");
webSend("</div>");
webSend("<div style='font-size:10px;color:#666;margin:0 0 10px'>");
webSend("oben/unten heller & dunkler · links/rechts warm & kalt</div>");
// Zonen — leicht versetzt wie auf dem Original
webSend("<div style='display:flex;justify-content:center;gap:7px;margin-top:4px'>");
zone_col(0x8, 0xB, 1, 0);
zone_col(0xD, 0x3, 2, 16);
zone_col(0x7, 0xA, 3, 16);
zone_col(0x2, 0x6, 4, 0);
webSend("</div>");
webSend("<div style='font-size:10px;color:#888;margin-top:10px'>2.4G RF</div>");
webSend("</div>"); // Gehaeuse zu
learn_panel();
// Mit dem Stand starten, mit dem dieses Panel gebaut wurde.
sprintf(line, "<script>gmwatch(%d,%d);</script>", rc_cnt, rc_sel + 1);
webSend(line);
webSend("</div>"); // #gmp zu
}
// Hex-Ziffer -> Zahl. strToInt liest nur dezimal, und die Tastencodes sind
// nun einmal Nibbles.
int hex_nib(char s[]) {
strToUpper(s);
if (strcmp(s, "0") == 0) { return 0x0; }
if (strcmp(s, "1") == 0) { return 0x1; }
if (strcmp(s, "2") == 0) { return 0x2; }
if (strcmp(s, "3") == 0) { return 0x3; }
if (strcmp(s, "4") == 0) { return 0x4; }
if (strcmp(s, "5") == 0) { return 0x5; }
if (strcmp(s, "6") == 0) { return 0x6; }
if (strcmp(s, "7") == 0) { return 0x7; }
if (strcmp(s, "8") == 0) { return 0x8; }
if (strcmp(s, "9") == 0) { return 0x9; }
if (strcmp(s, "A") == 0) { return 0xA; }
if (strcmp(s, "B") == 0) { return 0xB; }
if (strcmp(s, "C") == 0) { return 0xC; }
if (strcmp(s, "D") == 0) { return 0xD; }
if (strcmp(s, "E") == 0) { return 0xE; }
if (strcmp(s, "F") == 0) { return 0xF; }
return -1;
}
void Command(char cmd[]) {
// Waehrend des Anlernens steht der Chip auf EMPFANG. Jeder Befehl, der
// tx_setup() oder tx_frame() anfasst, wuerde ihn umkonfigurieren und den
// Empfang stillschweigend zerstoeren — beim Testen genau so passiert:
// LAMPONE lief durch, der Anlernvorgang lief scheinbar weiter und haette
// nie etwas empfangen.
if (learn_state == 1) {
int blockiert = 0;
if (strStartsWith(cmd, "TX")) { blockiert = 1; }
if (strcmp(cmd, "ONE") == 0) { blockiert = 1; }
if (strcmp(cmd, "TEST") == 0) { blockiert = 1; }
if (blockiert) {
responseCmnd("Anlernen laeuft — erst abschliessen oder abbrechen (LAMPLEARN aus: sv-Knopf Abbrechen)");
return;
}
}
if (strStartsWith(cmd, "TX")) {
char a1[16];
strToken(a1, cmd, ' ', 2);
int nib = hex_nib(a1);
if (nib < 0) {
responseCmnd("LAMPTX <hex>: alle=5/9 Z1=8/B Z2=D/3 Z3=7/A Z4=2/6 heller=C dunkler=4 warm=E kalt=F");
return;
}
tx_pend = nib;
char nm[16];
btn_name(nib, nm);
sprintf(line, "sende %s (CC=0x%02X b4=%02X Zaehler 0x%02X) — 90 Rahmen",
nm, 0x10 | nib, nib_zone(nib), tx_seq);
responseCmnd(line);
return;
}
if (strcmp(cmd, "LEARN") == 0) {
if (rc_cnt >= RC_MAX) {
responseCmnd("kein Platz frei — erst LAMPFORGET n");
return;
}
ui_learn = 1;
responseCmnd("Anlernen: jetzt eine Taste auf der neuen Fernbedienung druecken (20 s). Status: LAMPLIST");
return;
}
if (strStartsWith(cmd, "FORGET")) {
char a1[16];
strToken(a1, cmd, ' ', 2);
int n = strToInt(a1);
if (n < 1) {
responseCmnd("LAMPFORGET <n> — n aus LAMPLIST");
return;
}
rc_forget(n - 1);
sprintf(line, "geloescht, jetzt %d Fernbedienung(en) bekannt", rc_cnt);
responseCmnd(line);
return;
}
if (strcmp(cmd, "LIST") == 0) {
char acc[160];
strcpy(acc, "");
int z = 0;
while (z < rc_cnt) {
char one[32];
sprintf(one, " [%d]%02X%02X%02X", z + 1, rc_i1[z], rc_i2[z], rc_i3[z]);
strcat(acc, one);
z = z + 1;
}
if (learn_state == 1) {
sprintf(line, "ANLERNEN laeuft, noch %d s, RX_DR=%d, Adresse %d Byte |%s",
(learn_until - millis()) / 1000, learn_rx, rx_aw, acc);
} else {
sprintf(line, "%d bekannt, aktiv %d |%s", rc_cnt, rc_sel + 1, acc);
}
responseCmnd(line);
return;
}
if (strStartsWith(cmd, "RC")) {
char a1[16];
strToken(a1, cmd, ' ', 2);
int n = strToInt(a1);
if (n >= 1) { if (n <= rc_cnt) { rc_sel = n - 1; rc_save(); } }
if (rc_cnt < 1) {
responseCmnd("keine Fernbedienung angelernt — LAMPLEARN");
return;
}
sprintf(line, "Fernbedienung %d von %d (Kennung %02X %02X %02X)",
rc_sel + 1, rc_cnt, rc_i1[rc_sel], rc_i2[rc_sel], rc_i3[rc_sel]);
responseCmnd(line);
return;
}
// Reiner Lesebefehl — konfiguriert NICHTS um und ist deshalb als einziger
// auch waehrend des Anlernens erlaubt. Genau dafuer gedacht: nachsehen, ob
// der Chip wirklich auf Empfang steht (CONFIG=03), statt es anzunehmen.
// Rohpaket des letzten Anlernversuchs. RX_DR>0 mit unbrauchbarem Rohpaket
// heisst: es kommt etwas an, aber die Rahmenerkennung passt nicht.
// RX_DR=0 heisst: es kommt schlicht nichts an.
if (strcmp(cmd, "RAW") == 0) {
if (!learn_have) {
sprintf(line, "kein Paket empfangen (RX_DR=%d)", learn_rx);
responseCmnd(line);
return;
}
sprintf(line, "RX_DR=%d roh: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X (gesucht: Laenge 07, dann 6 Byte + Summe)",
learn_rx, learn_raw[0], learn_raw[1], learn_raw[2], learn_raw[3],
learn_raw[4], learn_raw[5], learn_raw[6], learn_raw[7],
learn_raw[8], learn_raw[9], learn_raw[10], learn_raw[11]);
responseCmnd(line);
return;
}
// BUS GEGEN BEKANNT GUTE HARDWARE PRUEFEN. Der CC1101 haengt am selben
// SPI und antwortet auf 0xC0|0x30 (PARTNUM) und 0xC0|0x31 (VERSION -> 04
// oder 14). Kommt das sauber, sind CLK/MOSI/MISO und die SPI-Schicht
// bewiesen in Ordnung und der Fehler sitzt beim nRF24 (Versorgung,
// Verdrahtung, Modul). Antwortet auch er nur FF, ist der Bus selbst tot.
// Die nuetzlichste Diagnose des ganzen Projekts — spart enorm viel Raten.
if (strcmp(cmd, "CC") == 0) {
digitalWrite(CSN_PIN, 1); // nRF24 sicher abgewaehlt
pinMode(CC_CS_PIN, 3);
spi[0] = 0xF0; spi[1] = 0x00; // 0xC0 | 0x30 = PARTNUM
digitalWrite(CC_CS_PIN, 0);
spiTransfer(0, spi, 2, 1);
digitalWrite(CC_CS_PIN, 1);
int part = spi[1] & 0xFF;
spi[0] = 0xF1; spi[1] = 0x00; // 0xC0 | 0x31 = VERSION
digitalWrite(CC_CS_PIN, 0);
spiTransfer(0, spi, 2, 1);
digitalWrite(CC_CS_PIN, 1);
int ver = spi[1] & 0xFF;
digitalWrite(CC_CS_PIN, 1); // wieder abwaehlen, teilt MISO!
sprintf(line, "CC1101: PARTNUM=%02X VERSION=%02X — Version 04 oder 14 = SPI-Bus OK, dann liegt es am nRF24. Beides FF = Bus tot.",
part, ver);
responseCmnd(line);
return;
}
// CC1101 HART abwaehlen und sofort den nRF24 lesen. Beide teilen sich MISO;
// liegt der CS des CC1101 nicht sicher auf HIGH, treibt er die Leitung mit
// und der nRF24 kommt nicht durch. „Nicht anfassen" reicht NICHT.
if (strcmp(cmd, "HI") == 0) {
pinMode(CC_CS_PIN, 3);
digitalWrite(CC_CS_PIN, 1);
pinMode(CSN_PIN, 3);
digitalWrite(CSN_PIN, 1);
delay(5);
int rb = digitalRead(CC_CS_PIN);
sprintf(line, "CC-CS(GPIO%d) zurueckgelesen=%d | nRF24 danach: 00=%02X 01=%02X 02=%02X 03=%02X 05=%02X 06=%02X",
CC_CS_PIN, rb, nrf_rd(0), nrf_rd(1), nrf_rd(2), nrf_rd(3), nrf_rd(5), nrf_rd(6));
responseCmnd(line);
return;
}
// Gegenprobe mit 1 MHz statt 4. Eine grenzwertige Leitung (Laenge, fehlende
// Masse-Fuehrung, fehlender Stuetzkondensator) liest bei 4 MHz Muell und bei
// 1 MHz sauber — das unterscheidet Signalqualitaet von Unterbrechung.
if (strcmp(cmd, "SLOW") == 0) {
spiInit(-1, -1, -1, 1);
spiSetCS(SLOT, CSN_PIN);
delay(5);
int a = nrf_rd(0); int b = nrf_rd(1); int c = nrf_rd(2);
int d = nrf_rd(3); int e = nrf_rd(5); int g = nrf_rd(6);
spiInit(-1, -1, -1, 4); // zurueck auf 4 MHz
spiSetCS(SLOT, CSN_PIN);
sprintf(line, "1 MHz: 00=%02X 01=%02X 02=%02X 03=%02X 05=%02X 06=%02X (nach Reset erwartet 08/3F/03/03/02/0E)",
a, b, c, d, e, g);
responseCmnd(line);
return;
}
// Dieselben Register, aber ueber SPI-Slot 0 und mit CS VON HAND — genau so,
// wie LAMPCC den CC1101 erfolgreich liest. Trennt einen Fehler in der
// Slot-/CS-Verwaltung von einem echten Hardwareproblem: kommen hier
// plausible Werte, ist die Verdrahtung in Ordnung und der Fehler sitzt in
// der SPI-Slot-Nutzung des Skripts.
if (strcmp(cmd, "MAN") == 0) {
pinMode(CC_CS_PIN, 3);
digitalWrite(CC_CS_PIN, 1); // CC1101 sicher weg
pinMode(CSN_PIN, 3);
digitalWrite(CSN_PIN, 1);
delay(1);
int v[6];
int regs[6];
regs[0]=0; regs[1]=1; regs[2]=2; regs[3]=3; regs[4]=5; regs[5]=6;
int i = 0;
while (i < 6) {
spi[0] = regs[i]; // R_REGISTER | reg
spi[1] = 0xFF;
digitalWrite(CSN_PIN, 0);
spiTransfer(0, spi, 2, 1); // Slot 0, CS nicht vom Treiber
digitalWrite(CSN_PIN, 1);
v[i] = spi[1] & 0xFF;
i = i + 1;
}
sprintf(line, "manuell/Slot0: 00=%02X 01=%02X 02=%02X 03=%02X 05=%02X 06=%02X (nach Reset erwartet 08/3F/03/03/02/0E)",
v[0], v[1], v[2], v[3], v[4], v[5]);
responseCmnd(line);
return;
}
// Schalten CSN und CE ueberhaupt? Ausgang setzen und zuruecklesen. Wird ein
// Pin anderweitig belegt oder haengt er fest, ist der nRF24 nie ausgewaehlt
// — dann liest man FF UND es wird nicht gesendet.
if (strcmp(cmd, "PINS") == 0) {
pinMode(CSN_PIN, 3);
pinMode(CE_PIN, 3);
digitalWrite(CSN_PIN, 0); digitalWrite(CE_PIN, 0);
delay(2);
int c0 = digitalRead(CSN_PIN);
int e0 = digitalRead(CE_PIN);
digitalWrite(CSN_PIN, 1); digitalWrite(CE_PIN, 1);
delay(2);
int c1 = digitalRead(CSN_PIN);
int e1 = digitalRead(CE_PIN);
digitalWrite(CSN_PIN, 1);
digitalWrite(CE_PIN, 0);
sprintf(line, "GPIO%d (CSN): low=%d high=%d | GPIO%d (CE): low=%d high=%d (erwartet je 0 und 1)",
CSN_PIN, c0, c1, CE_PIN, e0, e1);
responseCmnd(line);
return;
}
// ENTSCHEIDET, OB GPIO13 WIRKLICH AM CS DES CC1101 HAENGT.
// LAMPCC beweist nur, dass der CC1101 antwortet, wenn wir GPIO13 toggeln —
// nicht, dass die Leitung ihn erreicht. Liegt sein CS lose oder auf Masse,
// ist er DAUERHAFT ausgewaehlt, treibt den geteilten MISO staendig mit, und
// der nRF24 kommt beim Lesen nicht durch. Senden ginge weiter, denn das
// braucht nur MOSI, SCK, CSN und CE — exakt unser Fehlerbild, und es
// erklaert auch, warum ZWEI Module gleich versagen.
// Test: einmal mit CS LOW lesen (soll antworten), einmal mit CS HIGH
// (darf NICHT antworten). Gleiches Ergebnis = CS-Leitung wirkungslos.
if (strcmp(cmd, "CCX") == 0) {
pinMode(CC_CS_PIN, 3);
digitalWrite(CSN_PIN, 1); // nRF24 sicher abgewaehlt
// a) korrekt ausgewaehlt
digitalWrite(CC_CS_PIN, 0);
spi[0] = 0xF1; spi[1] = 0x00;
spiTransfer(0, spi, 2, 1);
digitalWrite(CC_CS_PIN, 1);
int lo = spi[1] & 0xFF;
// b) ABGEWAEHLT — hier darf nichts Sinnvolles kommen
delay(2);
digitalWrite(CC_CS_PIN, 1);
spi[0] = 0xF1; spi[1] = 0x00;
spiTransfer(0, spi, 2, 1);
int hi = spi[1] & 0xFF;
sprintf(line, "CC1101 VERSION: CS=low -> %02X | CS=high -> %02X ==> gleich = CS-Leitung wirkungslos, Chip belegt MISO dauernd",
lo, hi);
responseCmnd(line);
return;
}
if (strcmp(cmd, "REG") == 0) {
// Erst die Einzelregister lesen, DANN die Adresse — und die Adresse
// sofort in Locals sichern.
// ⚠️ nrf_rd() benutzt denselben globalen spi[]-Puffer und ueberschreibt
// spi[0] und spi[1]. Wer die Adresse in spi[] stehen laesst und danach
// noch nrf_rd() im selben sprintf aufruft, liest als erstes Adressbyte
// den zuletzt gelesenen REGISTERWERT zurueck. Genau das sah hier nach
// einer falsch programmierten Adresse aus (0x20 statt 0xAA — das war
// RX_PW_P0=32) und haette uns auf eine falsche Faehrte geschickt.
int cfg = nrf_rd(N_CONFIG);
int aw = nrf_rd(N_AW);
int ch = nrf_rd(N_RF_CH);
int rf = nrf_rd(N_RF_SET);
int st = nrf_rd(N_STATUS);
int pw = nrf_rd(0x11);
spi[0] = N_R_REG | 0x0A;
int z = 1;
while (z <= 5) { spi[z] = 0; z = z + 1; }
spiTransfer(SLOT, spi, 6, 1);
int a1 = spi[1] & 0xFF; int a2 = spi[2] & 0xFF; int a3 = spi[3] & 0xFF;
int a4 = spi[4] & 0xFF; int a5 = spi[5] & 0xFF;
sprintf(line, "CONFIG=%02X AW=%02X RF_CH=%d RF_SETUP=%02X STATUS=%02X PW=%d ADDR=%02X %02X %02X %02X %02X (soll AA 5A 85 8A 55)",
cfg, aw, ch, rf, st, pw, a1, a2, a3, a4, a5);
responseCmnd(line);
return;
}
// Diagnose des EMPFANGSPFADS ohne fremde Hilfe. Mit einer 3-Byte-Adresse
// aus lauter 0xAA rastet der Chip staendig auf Rauschen ein — kommen hier
// Pakete, funktionieren Antenne, MISO und der Empfangszweig, und ein
// Fehlschlag beim Anlernen ist ein Signal-/Reichweitenproblem.
// Kommt NICHTS, ist der Empfang selbst kaputt. (Der alte Sniffer sah so
// rund 3 Pakete/s.)
if (strcmp(cmd, "NOISE") == 0) {
if (CE_PIN >= 0) { digitalWrite(CE_PIN, 0); }
nrf_wr(N_CONFIG, 0x00);
nrf_wr(N_EN_AA, 0x00);
nrf_wr(0x02, 0x01);
nrf_wr(N_AW, 0x01); // 3 Byte Adresse
nrf_wr(N_RF_CH, 41);
nrf_wr(N_RF_SET, RF_1MBIT);
nrf_wr(0x11, 32);
spi[0] = N_W_REG | 0x0A;
spi[1] = 0xAA; spi[2] = 0xAA; spi[3] = 0xAA;
spiTransfer(SLOT, spi, 4, 1);
nrf_cmd(0xE2);
nrf_wr(N_STATUS, 0x70);
nrf_wr(N_CONFIG, 0x03);
delay(2);
if (CE_PIN >= 0) { digitalWrite(CE_PIN, 1); }
int n = 0;
int q = 0;
while (q < 2000) {
if (nrf_rd(N_STATUS) & 0x40) {
n = n + 1;
nrf_wr(N_STATUS, 0x70);
nrf_cmd(0xE2);
}
delay(1);
q = q + 1;
}
tx_setup();
sprintf(line, "%d Pakete in 2 s bei 3-Byte-Adresse (0 = Empfangspfad defekt, >0 = Empfang lebt)", n);
responseCmnd(line);
return;
}
if (strcmp(cmd, "TEST") == 0) {
tx_setup();
sprintf(line, "CONFIG=%02X (soll 02) AW=%02X (soll 03) RF_SETUP=%02X (soll 06) STATUS=%02X (totes Modul: FF)",
nrf_rd(N_CONFIG), nrf_rd(N_AW), nrf_rd(N_RF_SET), nrf_rd(N_STATUS));
responseCmnd(line);
return;
}
if (strcmp(cmd, "ONE") == 0) {
// EINEN Rahmen senden und danach den Chip fragen, ob er ihn losgeworden
// ist. TX_DS (0x20) im STATUS = Rahmen raus. Bleibt es 0, sendet er nicht.
tx_setup();
int st0 = nrf_rd(N_STATUS);
int fi0 = nrf_rd(N_FIFO);
tx_frame(0x9, 0x77, 0, 41, 1);
delay(3);
sprintf(line, "vor: ST=%02X FIFO=%02X | nach: ST=%02X FIFO=%02X | CONFIG=%02X (TX_DS=0x20 erwartet)",
st0, fi0, nrf_rd(N_STATUS), nrf_rd(N_FIFO), nrf_rd(N_CONFIG));
responseCmnd(line);
return;
}
sprintf(line, "LAMPTX <hex> | LAMPTEST | LAMPONE (bisher %d Rahmen gesendet)", tx_total);
responseCmnd(line);
}
int main() {
tx_total = 0; tx_seq = 0x40; tx_pend = -1; ui_cmd = 0; ui_rc = 0;
last_sent = -1; last_ms = 0;
learn_state = 0; learn_res = 0; ui_learn = 0;
// persist-Initialisierer werden verworfen, alles kommt als 0 hoch. Ein
// rc_cnt von 0 heisst also „noch nie angelernt" — dann die beiden hier
// gemessenen Kennungen als Startbestand setzen. Wer die Datei auf eigener
// Hardware benutzt, loescht sie mit LAMPFORGET und lernt seine eigenen an.
if (rc_cnt < 1) {
rc_i1[0] = 0x5A; rc_i2[0] = 0x4B; rc_i3[0] = 0x07; // Fernbedienung 1
rc_i1[1] = 0x5A; rc_i2[1] = 0x43; rc_i3[1] = 0x72; // Fernbedienung 2
rc_cnt = 2;
rc_sel = 0;
rc_save();
}
if (rc_sel >= rc_cnt) { rc_sel = 0; }
// ZUERST den CC1101 abwaehlen — er teilt sich MISO mit dem nRF24.
pinMode(CC_CS_PIN, 3);
digitalWrite(CC_CS_PIN, 1);
spiInit(-1, -1, -1, 4);
spiSetCS(SLOT, CSN_PIN);
if (CE_PIN >= 0) {
pinMode(CE_PIN, 3); // OUTPUT (TinyC: 3, nicht 1)
digitalWrite(CE_PIN, 0); // im Sendebetrieb nur impulsweise high
}
delay(50);
tx_setup();
rgbLed(LED_PIN, 0x004000); // green
addCommand("LAMP");
addLog("gm24g_lamp: bereit, CONFIG=0x%02X (soll 0x02)", nrf_rd(N_CONFIG));
return 0;
}