"""Direct-connect troubleshooter for the live-mode BLE channel. Scans for the device advertising LIVE_SERVICE_UUID, connects, subscribes, and prints EVERY notify received (whether or not it decrypts). Also sends a START frame after a brief delay to exercise the device's write callback path — if that triggers an ACK notify, we know the write-callback plumbing is wired but the unsolicited-hello path is the only thing broken. """ import asyncio import os import struct import sys from bleak import BleakClient, BleakScanner import ble_keystore import ble_replay from ble_frame import build_frame, parse_frame, DEVICE_TAG_PREFIX from ble_server import LIVE_SERVICE_UUID, LIVE_KEYS_NOTIFY_UUID, LIVE_KEYS_WRITE_UUID from ble_live import MSG_START, ERR_LABELS def hex16(b): return " ".join(f"{x:02x}" for x in b[:16]) + (" ..." if len(b) > 16 else "") async def main(): key = ble_keystore.load_key() print(f"[dbg] key loaded: {key is not None} len={len(key) if key else 0}") print("[dbg] scanning for LIVE_SERVICE_UUID...") device = await BleakScanner.find_device_by_filter( lambda d, adv: LIVE_SERVICE_UUID in (adv.service_uuids or []), timeout=10.0, ) if not device: print("[dbg] NO DEVICE FOUND advertising LIVE_SERVICE_UUID") # Show what we DID see print("[dbg] doing a broader scan to see what's around...") devs = await BleakScanner.discover(timeout=5.0, return_adv=True) for addr, (d, adv) in devs.items(): name = d.name or adv.local_name if name and "macro" in name.lower(): print(f"[dbg] MATCH? {addr} name={name} uuids={adv.service_uuids}") return print(f"[dbg] found: {device.address} (name={device.name})") notify_count = 0 def handle_notify(char, data: bytearray): nonlocal notify_count notify_count += 1 raw = bytes(data) print(f"[dbg] NOTIFY #{notify_count} len={len(raw)} bytes={hex16(raw)}") if key is None: print("[dbg] (no key — can't decrypt)") return parsed = parse_frame(key, raw) if parsed is None: print("[dbg] parse_frame returned None — auth failed or malformed") # Try to show tag prefix if len(raw) > 1: tag_len = raw[0] if 0 < tag_len < len(raw): tag = raw[1:1+tag_len] try: print(f"[dbg] tag bytes -> {tag.decode('ascii', errors='replace')}") except Exception: pass return tag, plain = parsed print(f"[dbg] tag={tag} plain_len={len(plain)}") print(f"[dbg] plain[0:17]={hex16(plain[:17])}") if len(plain) >= 1: msg_type = plain[0] print(f"[dbg] msg_type=0x{msg_type:02x}") async with BleakClient(device, timeout=15.0) as client: print(f"[dbg] connected. discovering services...") for svc in client.services: print(f"[dbg] svc {svc.uuid}") for ch in svc.characteristics: props = ",".join(ch.properties) print(f"[dbg] ch {ch.uuid} ({props})") print(f"[dbg] subscribing to LIVE_KEYS_NOTIFY...") await client.start_notify(LIVE_KEYS_NOTIFY_UUID, handle_notify) print(f"[dbg] subscribed. waiting 5s for any unsolicited hello...") for i in range(5): await asyncio.sleep(1) print(f"[dbg] after 5s: notify_count={notify_count}") # Find the device tag by reading any frame we already got (if any), # otherwise reconstruct from the MAC. mac_clean = device.address.upper() tag = DEVICE_TAG_PREFIX + mac_clean print(f"[dbg] sending START frame tag={tag}") # Use the host's replay-state — this writes to disk, which is # fine for diagnostics. replay = ble_replay.ReplayState() seq = replay.next_send_seq() sid = replay.host_session_id() plain = struct.pack("