#!/usr/bin/env python3 """ decode_rcf.py - Dekodiert RCoid Remote Files (.rcf) der Android-App "RCoid". Eine .rcf-Datei ist ein Java-Objekt-Serialisierungsstrom (Magic AC ED 00 05). Darin liegen pro Taste: - eine Beschriftung (Textlabel oder ein Icon-Name "gfx_symbol_*") - der IR-Code als Raw-Timing-String im Format: ,,,,,,,... z. B. "38000,342,171,21,21,21,64,..." Zeiten in Einheiten von ~26.3 us (1/38kHz-naher Tick), Trägerfrequenz 38 kHz. Das Script extrahiert alle IR-Codes, dekodiert das NEC-Protokoll (Adresse, Kommando samt Prüfbytes) und ordnet jeder Taste ihre Beschriftung zu. Verwendung: python3 decode_rcf.py [weitere.rcf ...] python3 decode_rcf.py # dekodiert alle *.rcf im aktuellen Ordner Ausgabe: - Tabelle auf stdout - Je Eingabedatei eine Markdown-Datei ".decoded.md" daneben. """ import sys import glob import os # Icon-Dateinamen -> lesbarer Tastenname ICON_NAMES = { "gfx_symbol_play": "Play", "gfx_symbol_power": "Power", "gfx_symbol_sbw": "Rücklauf (<<)", "gfx_symbol_sfw": "Vorlauf (>>)", "gfx_symbol_pfeil1_hoch": "Pfeil hoch", "gfx_symbol_pfeil1_runter": "Pfeil runter", "gfx_symbol_pfeil1_rechts": "Pfeil rechts", "gfx_symbol_pfeil1_links": "Pfeil links", } # Reine Layout-/Hintergrund-Grafiken, die KEINE Taste beschriften LAYOUT_GFX = { "gfx_leer_solidcolor_white", "gfx_leer_solidcolor_circle", "gfx_folie_rund", } HEADER_STRINGS = {"RCoid Remote File", "Tastenfeld"} def extract_utf_strings(data): """Liefert (offset, text) aller Java-TC_STRING-Einträge (0x74 ).""" entries = [] pos = 0 n = len(data) while True: j = data.find(b"\x74", pos) if j < 0 or j + 3 > n: break ln = int.from_bytes(data[j + 1:j + 3], "big") s = data[j + 3:j + 3 + ln] if 0 < ln < 2000 and len(s) == ln and all(32 <= b < 127 or b == 9 for b in s): entries.append((j, s.decode("latin-1"))) pos = j + 1 return entries def classify(text): if text.startswith("38000,") or (text[:1].isdigit() and "," in text and text.count(",") > 10): return "CODE" if text in HEADER_STRINGS: return "HDR" if text in LAYOUT_GFX: return "GFX" if text in ICON_NAMES: return "ICON" if text.startswith("gfx_"): return "GFX" return "LABEL" def decode_nec(raw): """ Dekodiert einen Raw-Timing-String als NEC. Rückgabe: dict mit freq, bits, addr, naddr, cmd, ncmd, valid (oder None bei Fehler). """ nums = [int(x) for x in raw.split(",")] if len(nums) < 4: return None freq = nums[0] timings = nums[1:] # Leader = erstes Puls/Pause-Paar (ca. 342 / 171). Rest sind Datenbits. body = timings[2:] bits = [] i = 0 while i + 1 < len(body): space = body[i + 1] # kurze Pause (~21) = 0, lange Pause (~64) = 1 bits.append("1" if space >= 40 else "0") i += 2 bitstr = "".join(bits) if len(bitstr) < 32: return {"freq": freq, "bits": bitstr, "valid": False, "addr": None, "naddr": None, "cmd": None, "ncmd": None} def byte_lsb_first(b): v = 0 for k in range(8): if b[k] == "1": v |= (1 << k) return v addr = byte_lsb_first(bitstr[0:8]) naddr = byte_lsb_first(bitstr[8:16]) cmd = byte_lsb_first(bitstr[16:24]) ncmd = byte_lsb_first(bitstr[24:32]) valid = (addr ^ naddr) == 0xFF and (cmd ^ ncmd) == 0xFF return {"freq": freq, "bits": bitstr, "valid": valid, "addr": addr, "naddr": naddr, "cmd": cmd, "ncmd": ncmd} def parse_file(path): """Liefert Liste von dict: name, raw, decode-Ergebnis.""" data = open(path, "rb").read() if not data.startswith(b"\xac\xed"): print(f" WARNUNG: {path} ist kein Java-Serialisierungsstrom (Magic fehlt).", file=sys.stderr) entries = extract_utf_strings(data) # Jeder Code bekommt die zuletzt gesehene Beschriftung (LABEL oder ICON) # vor ihm zugeordnet; Layout-/Header-Grafiken zaehlen nicht als Beschriftung. results = [] last_name = None for _off, text in entries: kind = classify(text) if kind in ("LABEL", "ICON"): last_name = ICON_NAMES.get(text, text) elif kind == "CODE": results.append({"name": last_name, "raw": text, "nec": decode_nec(text)}) last_name = None # eine Beschriftung gehoert zu genau einem Code return results def render_markdown(path, results): base = os.path.basename(path) lines = [] lines.append(f"# IR-Codes: {os.path.splitext(base)[0]}") lines.append("") lines.append(f"Quelle: `{base}` ") lines.append("Format: RCoid Remote File (`.rcf`, Java-Serialisierung) ") lines.append("Protokoll: **NEC**, Trägerfrequenz **38 kHz** ") lines.append("Rohformat je Code: `freq,leader_mark,leader_space,mark,space,...` " "(Zeiten in ~26 µs-Ticks; Pause ~21 = 0-Bit, ~64 = 1-Bit)") lines.append("") lines.append("| Taste | Addr | Cmd (hex) | Cmd (dez) | NEC gültig |") lines.append("|---|---|---|---|---|") for r in results: nec = r["nec"] name = r["name"] if r["name"] is not None else "(ohne Beschriftung)" if nec and nec["addr"] is not None: lines.append( f"| {name} | 0x{nec['addr']:02X} | 0x{nec['cmd']:02X} | " f"{nec['cmd']} | {'ja' if nec['valid'] else 'NEIN'} |" ) else: lines.append(f"| {name} | – | – | – | nicht dekodierbar |") lines.append("") lines.append("## Hinweis zur Zuordnung") lines.append("") lines.append("Alle Codes sind als **gültiges NEC** dekodiert (Prüfbytes stimmen).") lines.append("Einträge mit `(ohne Beschriftung)` sind Icon-Tasten (z. B. Pfeile, " "Rück-/Vorlauf), deren Icon-Grafiken im Dateiformat als Block abgelegt " "sind; ihre Taste↔Code-Reihenfolge ist nicht zweifelsfrei bestimmbar. " "Die Protokoll-Dekodierung selbst ist davon unberührt.") lines.append("") lines.append("## Rohdaten") lines.append("") for r in results: name = r["name"] if r["name"] is not None else "(ohne Beschriftung)" lines.append(f"- **{name}**: `{r['raw']}`") lines.append("") out = os.path.join(os.path.dirname(os.path.abspath(path)), os.path.splitext(base)[0] + ".decoded.md") with open(out, "w", encoding="utf-8") as f: f.write("\n".join(lines)) return out def main(argv): paths = argv[1:] if not paths: paths = sorted(glob.glob("*.rcf")) if not paths: print("Keine .rcf-Dateien angegeben oder gefunden.", file=sys.stderr) return 1 for path in paths: print(f"\n=== {path} ===") results = parse_file(path) print(f"{'Taste':20} {'Addr':>5} {'Cmd':>5} {'Dez':>4} {'OK':>4}") print("-" * 42) for r in results: nec = r["nec"] name = r["name"] if r["name"] is not None else "(ohne)" if nec and nec["addr"] is not None: print(f"{name:20} 0x{nec['addr']:02X} 0x{nec['cmd']:02X} " f"{nec['cmd']:4d} {'ja' if nec['valid'] else 'NEIN':>4}") else: print(f"{name:20} – – – n/a") md = render_markdown(path, results) print(f"-> Markdown geschrieben: {md}") return 0 if __name__ == "__main__": sys.exit(main(sys.argv))