Files
m5stack-automation-tool/debug_connect.py
T
2026-07-17 15:29:53 -04:00

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)