135 lines
5.1 KiB
Python
135 lines
5.1 KiB
Python
"""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("<BQQ", MSG_START, sid & 0xFFFFFFFFFFFFFFFF,
|
|
seq & 0xFFFFFFFFFFFFFFFF)
|
|
frame = build_frame(key, tag, plain)
|
|
print(f"[dbg] plain hex: {hex16(plain)}")
|
|
print(f"[dbg] frame len={len(frame)} hex: {hex16(frame)}")
|
|
try:
|
|
await client.write_gatt_char(LIVE_KEYS_WRITE_UUID, frame,
|
|
response=True)
|
|
print(f"[dbg] START write completed")
|
|
except Exception as exc:
|
|
print(f"[dbg] START write FAILED: {exc!r}")
|
|
|
|
print(f"[dbg] waiting 15s more for response (ACK or anything)...")
|
|
for i in range(15):
|
|
await asyncio.sleep(1)
|
|
if i in (5, 10):
|
|
print(f"[dbg] ...{i}s, notify_count={notify_count}")
|
|
|
|
print(f"[dbg] done. total notifies received: {notify_count}")
|
|
try:
|
|
await client.stop_notify(LIVE_KEYS_NOTIFY_UUID)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
asyncio.run(main())
|
|
except KeyboardInterrupt:
|
|
sys.exit(1)
|