""" analyse_jadx.py - Search jadx-decompiled Java source for KAIYU BLE protocol. After running decompile_kaiyu.ps1, this searches the Java output for: - UUID strings and integer constants - BluetoothGatt write calls with context - Byte arrays that look like LED commands (7E / 56 families) - Service/characteristic class definitions Usage: python sniffer/analyse_jadx.py --src D:\\Claude\\HH-BT-Controller\\decompile\\kaiyu_src python sniffer/analyse_jadx.py (auto-finds output dir) python sniffer/analyse_jadx.py --out results.json """ import os import re import sys import json import argparse from pathlib import Path UUID_RE = re.compile(r'["\']([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})["\']') FROM_STR_RE = re.compile(r'fromString\s*\(\s*["\']([0-9a-fA-F\-]{36})["\']') SHORT_UUID_RE = re.compile(r'0x([Ff]{2}[0-9a-fA-F]{2})|"(0000[0-9a-fA-F]{4})') BYTE_ARR_RE = re.compile(r'(?:new\s+byte\s*\[\s*\]\s*\{|=\s*\{)\s*((?:\(byte\)\s*)?(?:0x[0-9a-fA-F]{2}|-?\d+)(?:\s*,\s*(?:\(byte\)\s*)?(?:0x[0-9a-fA-F]{2}|-?\d+)){3,})\s*\}') WRITE_RE = re.compile(r'.{0,60}(?:writeCharacteristic|writeGattChar|setValue|write)\s*\(.{0,100}', re.IGNORECASE) BLE_CLASS_RE = re.compile(r'(setColor|setRGB|setBrightness|setEffect|turnOn|turnOff|sendCommand|writeCmd|sendData|controlLight|BluetoothGatt|BluetoothLeService|BleManager|GattCallback)', re.IGNORECASE) KNOWN_UUIDS = { "0000ffe0-0000-1000-8000-00805f9b34fb": "LEDBLE service", "0000ffe1-0000-1000-8000-00805f9b34fb": "LEDBLE WRITE CHARACTERISTIC", "0000fff0-0000-1000-8000-00805f9b34fb": "ELK-BLEDOM service", "0000fff3-0000-1000-8000-00805f9b34fb": "ELK-BLEDOM WRITE CHARACTERISTIC", "0000ffd5-0000-1000-8000-00805f9b34fb": "LED service", "0000ffd9-0000-1000-8000-00805f9b34fb": "LED WRITE CHARACTERISTIC", "0000ffb0-0000-1000-8000-00805f9b34fb": "LED service (alt)", "0000ffb2-0000-1000-8000-00805f9b34fb": "LED WRITE (alt)", } CMD_PREFIXES = { bytes.fromhex("7e0005"): "7E RGB colour", bytes.fromhex("7e0004"): "7E power", bytes.fromhex("7e0001"): "7E brightness", bytes.fromhex("cc2333"): "56 power ON", bytes.fromhex("cc2433"): "56 power OFF", bytes.fromhex("56ff00"): "56 RGB", } def decode_bytes(data: bytes) -> str: for prefix, label in CMD_PREFIXES.items(): if data[:len(prefix)] == prefix: return label if data and data[0] == 0x7e and len(data) > 1 and data[-1] == 0xef: return f"7E protocol cmd=0x{data[2]:02x}" return "" def scan_file(path: Path, base: Path): rel = str(path.relative_to(base)) try: text = path.read_text(encoding="utf-8", errors="ignore") except Exception: return [] findings = [] for i, line in enumerate(text.splitlines(), 1): s = line.strip() if not s or s.startswith("//"): continue for m in UUID_RE.finditer(line): findings.append(("uuid", rel, i, m.group(1).lower(), s[:120])) for m in FROM_STR_RE.finditer(line): findings.append(("uuid", rel, i, m.group(1).lower(), s[:120])) for m in SHORT_UUID_RE.finditer(line): val = (m.group(1) or m.group(2) or "").lower() if val: findings.append(("short_uuid", rel, i, val, s[:120])) for m in BYTE_ARR_RE.finditer(line): tokens = re.findall(r'0x[0-9a-fA-F]+|-?\d+', m.group(1)) bvals = [] for t in tokens: try: bvals.append(int(t, 16) if t.startswith("0x") else int(t)) except ValueError: pass if bvals: bdata = bytes(b & 0xff for b in bvals) decoded = decode_bytes(bdata) hex_str = bdata.hex() findings.append(("byte_array", rel, i, hex_str, decoded or s[:80])) if WRITE_RE.search(line): findings.append(("write_call", rel, i, "", s[:120])) m2 = BLE_CLASS_RE.search(line) if m2: findings.append(("ble_ref", rel, i, m2.group(1), s[:120])) return findings def main(): parser = argparse.ArgumentParser() parser.add_argument("--src", help="jadx output directory") parser.add_argument("--out", help="Save JSON results") args = parser.parse_args() candidates = [ Path(r"D:\Claude\HH-BT-Controller\decompile\kaiyu_src"), Path(r"D:\HereMyHope\hh-bt-controller\decompile\kaiyu_src"), Path("decompile/kaiyu_src"), ] src = Path(args.src) if args.src else next((c for c in candidates if c.exists()), None) if not src or not src.exists(): print("[ERROR] No decompiled source found.") print(" Run: powershell -ExecutionPolicy Bypass -File sniffer/decompile_kaiyu.ps1") sys.exit(1) java_files = list(src.rglob("*.java")) print(f"Source : {src}") print(f"Files : {len(java_files)} Java files") print() all_findings = [] for jf in java_files: all_findings.extend(scan_file(jf, src)) uuids = [f for f in all_findings if f[0] == "uuid"] short_uuids = [f for f in all_findings if f[0] == "short_uuid"] byte_arrays = [f for f in all_findings if f[0] == "byte_array"] write_calls = [f for f in all_findings if f[0] == "write_call"] ble_refs = [f for f in all_findings if f[0] == "ble_ref"] # UUIDs print("=" * 70) print(f" FULL UUIDs ({len(uuids)} hits)") print("=" * 70) seen_uuids: dict[str, list] = {} for f in uuids: seen_uuids.setdefault(f[3], []).append(f) if seen_uuids: for uuid, hits in sorted(seen_uuids.items(), key=lambda x: -len(x[1])): label = KNOWN_UUIDS.get(uuid, "") tag = " <-- *** " + label if label else "" print(f" {uuid} (x{len(hits)}){tag}") for h in hits[:2]: print(f" {h[1]}:{h[2]} {h[4][:80]}") else: print(" None found as string literals.") # Short UUIDs print(f"\n{'─'*70}") print(f" SHORT UUID constants ({len(short_uuids)} hits)") print(f"{'─'*70}") seen_short: dict[str, list] = {} for f in short_uuids: seen_short.setdefault(f[3], []).append(f) for val, hits in sorted(seen_short.items(), key=lambda x: -len(x[1])): full = f"0000{val}-0000-1000-8000-00805f9b34fb" label = KNOWN_UUIDS.get(full, "") tag = " <-- " + label if label else "" print(f" 0x{val} -> {full}{tag} (x{len(hits)})") for h in hits[:2]: print(f" {h[1]}:{h[2]} {h[4][:80]}") # Byte arrays print(f"\n{'─'*70}") print(f" BYTE ARRAYS ({len(byte_arrays)} hits — LED commands may be here)") print(f"{'─'*70}") for f in byte_arrays[:25]: tag = f" -> {f[4]}" if f[4] and not f[4].startswith("0") else "" print(f" {f[1]}:{f[2]} {f[3][:60]}{tag}") # Write calls print(f"\n{'─'*70}") print(f" GATT WRITE CALLS ({len(write_calls)} hits)") print(f"{'─'*70}") write_files: dict[str, list] = {} for f in write_calls: write_files.setdefault(f[1], []).append(f) for fname, hits in list(write_files.items())[:8]: print(f" {fname} ({len(hits)} calls)") for h in hits[:3]: print(f" line {h[2]}: {h[4][:90]}") # BLE class refs print(f"\n{'─'*70}") print(f" BLE CLASS/METHOD REFS ({len(ble_refs)} hits)") print(f"{'─'*70}") ref_files: dict[str, set] = {} for f in ble_refs: ref_files.setdefault(f[3], set()).add(f[1]) for method, files in sorted(ref_files.items()): print(f" {method} ({len(files)} files)") for fn in list(files)[:3]: print(f" {fn}") # Final recommendation print(f"\n{'='*70}") print(f" RESULT") print(f"{'='*70}") if seen_uuids: print("\n UUIDs found! Check the ones marked <-- above.") print(" Update WRITE_CHARACTERISTIC in controller/config.py.") elif seen_short: print("\n Short UUID constants found. Likely write characteristic candidates:") for val in seen_short: print(f" 0000{val}-0000-1000-8000-00805f9b34fb") print("\n Try each as WRITE_CHARACTERISTIC and run:") print(" python scanner/ble_scanner.py --probe") elif write_files: print(f"\n No UUIDs but found GATT write calls in {len(write_files)} file(s).") print(" Open these in VS Code and look for the characteristic being written to.") print(f" code \"{src}\"") else: print("\n Nothing found in Java source — UUIDs are in native .so libraries.") print(" Run: python sniffer/search_so_libs.py") print(" Or try direct probe: python scanner/ble_scanner.py --probe") if args.out: with open(args.out, "w") as f_out: json.dump({ "uuids": {u: [{"file": h[1], "line": h[2]} for h in hits] for u, hits in seen_uuids.items()}, "short_uuids": list(seen_short.keys()), "byte_arrays": [{"file": f[1], "line": f[2], "hex": f[3], "decoded": f[4]} for f in byte_arrays], "write_call_files": list(write_files.keys()), }, f_out, indent=2) print(f"\n Saved: {args.out}") if __name__ == "__main__": main()