Fix: scan-then-connect in one flow to avoid device going stale
This commit is contained in:
+89
-35
@@ -4,9 +4,13 @@ ble_scanner.py - Discover and dump BLE services/characteristics from nearby devi
|
|||||||
Run this first to find your KAIYU LED controller and identify its BLE services.
|
Run this first to find your KAIYU LED controller and identify its BLE services.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python scanner/ble_scanner.py
|
python scanner/ble_scanner.py # scan only
|
||||||
python scanner/ble_scanner.py --filter KAIYU
|
python scanner/ble_scanner.py --connect 7C:77:6C:6B:8E:C8 # scan then connect immediately
|
||||||
python scanner/ble_scanner.py --connect AA:BB:CC:DD:EE:FF
|
python scanner/ble_scanner.py --probe 7C:77:6C:6B:8E:C8 # scan then probe with test commands
|
||||||
|
python scanner/ble_scanner.py --filter KAIYU # scan with name filter
|
||||||
|
|
||||||
|
IMPORTANT: --connect and --probe now re-scan first so the device is still
|
||||||
|
advertising when we try to connect. Do not scan separately then connect.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -15,6 +19,28 @@ import json
|
|||||||
from bleak import BleakScanner, BleakClient
|
from bleak import BleakScanner, BleakClient
|
||||||
|
|
||||||
|
|
||||||
|
async def scan_and_get_device(address: str, timeout: float = 10.0):
|
||||||
|
"""
|
||||||
|
Scan for a specific address and return the BLEDevice object while it's
|
||||||
|
still in the scanner's cache (i.e. still advertising).
|
||||||
|
Returns None if not found.
|
||||||
|
"""
|
||||||
|
print(f"Scanning for {address} ({timeout}s)...")
|
||||||
|
target = None
|
||||||
|
|
||||||
|
def detection_callback(device, adv):
|
||||||
|
nonlocal target
|
||||||
|
if device.address.upper() == address.upper():
|
||||||
|
target = device
|
||||||
|
print(f"Found: {device.address} name={device.name or '(unknown)'} rssi={adv.rssi}")
|
||||||
|
|
||||||
|
scanner = BleakScanner(detection_callback=detection_callback)
|
||||||
|
await scanner.start()
|
||||||
|
await asyncio.sleep(timeout)
|
||||||
|
await scanner.stop()
|
||||||
|
return target
|
||||||
|
|
||||||
|
|
||||||
async def scan_devices(filter_name: str = None, timeout: float = 10.0):
|
async def scan_devices(filter_name: str = None, timeout: float = 10.0):
|
||||||
"""Scan for nearby BLE devices and print them."""
|
"""Scan for nearby BLE devices and print them."""
|
||||||
print(f"Scanning for BLE devices ({timeout}s)...\n")
|
print(f"Scanning for BLE devices ({timeout}s)...\n")
|
||||||
@@ -45,12 +71,19 @@ async def scan_devices(filter_name: str = None, timeout: float = 10.0):
|
|||||||
return found
|
return found
|
||||||
|
|
||||||
|
|
||||||
async def inspect_device(address: str):
|
async def inspect_device(address: str, timeout: float = 10.0):
|
||||||
"""Connect to a device and dump all its services and characteristics."""
|
"""Scan for device, then immediately connect and dump all services/characteristics."""
|
||||||
print(f"Connecting to {address}...")
|
|
||||||
|
|
||||||
async with BleakClient(address) as client:
|
device = await scan_and_get_device(address, timeout=timeout)
|
||||||
print(f"Connected: {client.is_connected}\n")
|
if device is None:
|
||||||
|
print(f"\nDevice {address} not found during scan.")
|
||||||
|
print("Make sure it is powered on and not connected to another device (e.g. your phone).")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"\nConnecting to {device.address}...")
|
||||||
|
|
||||||
|
async with BleakClient(device) as client:
|
||||||
|
print(f"Connected!\n")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print("SERVICES & CHARACTERISTICS")
|
print("SERVICES & CHARACTERISTICS")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
@@ -98,13 +131,12 @@ async def inspect_device(address: str):
|
|||||||
for descriptor in char.descriptors:
|
for descriptor in char.descriptors:
|
||||||
print(f" DESC: {descriptor.uuid} handle={descriptor.handle}")
|
print(f" DESC: {descriptor.uuid} handle={descriptor.handle}")
|
||||||
|
|
||||||
# Save dump to JSON for reference
|
# Save dump to JSON
|
||||||
dump_file = f"scanner/device_{address.replace(':', '-')}.json"
|
dump_file = f"scanner/device_{address.replace(':', '-')}.json"
|
||||||
with open(dump_file, "w") as f:
|
with open(dump_file, "w") as f:
|
||||||
json.dump(dump, f, indent=2)
|
json.dump(dump, f, indent=2)
|
||||||
print(f"\nDump saved to {dump_file}")
|
print(f"\nDump saved to {dump_file}")
|
||||||
|
|
||||||
# Identify likely write characteristic
|
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
print("LIKELY COMMAND CHARACTERISTICS (writable):")
|
print("LIKELY COMMAND CHARACTERISTICS (writable):")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
@@ -115,27 +147,38 @@ async def inspect_device(address: str):
|
|||||||
print(f" -> {char_uuid} [{', '.join(props)}]")
|
print(f" -> {char_uuid} [{', '.join(props)}]")
|
||||||
|
|
||||||
|
|
||||||
async def try_known_protocols(address: str):
|
async def try_known_protocols(address: str, timeout: float = 10.0):
|
||||||
"""Try sending known protocol commands to see what sticks."""
|
"""Scan for device then probe it with known protocol commands."""
|
||||||
|
|
||||||
|
device = await scan_and_get_device(address, timeout=timeout)
|
||||||
|
if device is None:
|
||||||
|
print(f"\nDevice {address} not found during scan.")
|
||||||
|
print("Make sure it is powered on and not connected to your phone.")
|
||||||
|
return
|
||||||
|
|
||||||
KNOWN_WRITE_UUIDS = [
|
KNOWN_WRITE_UUIDS = [
|
||||||
"0000ffe1-0000-1000-8000-00805f9b34fb", # LEDBLE / MagicLight
|
"0000ffe1-0000-1000-8000-00805f9b34fb", # LEDBLE / MagicLight
|
||||||
"0000fff3-0000-1000-8000-00805f9b34fb", # ELK-BLEDOM
|
"0000fff3-0000-1000-8000-00805f9b34fb", # ELK-BLEDOM
|
||||||
"0000ffd9-0000-1000-8000-00805f9b34fb", # Some controllers
|
"0000ffd9-0000-1000-8000-00805f9b34fb", # Some controllers
|
||||||
]
|
]
|
||||||
|
|
||||||
# Try the 0x7e command format (common in cheap controllers)
|
|
||||||
test_commands = {
|
test_commands = {
|
||||||
"Turn ON": bytes.fromhex("7e000400000000ff ef".replace(" ", "")),
|
"Turn ON (7e)": bytes.fromhex("7e000400000000ffef"),
|
||||||
"Turn OFF": bytes.fromhex("7e000400000000 00 ef".replace(" ", "")),
|
"Turn OFF (7e)": bytes.fromhex("7e000400000000 00ef".replace(" ", "")),
|
||||||
"Red": bytes.fromhex("7e0005 03 ff 00 00 00 ef".replace(" ", "")),
|
"Red (7e)": bytes.fromhex("7e000503ff000000ef"),
|
||||||
"Green": bytes.fromhex("7e0005 03 00 ff 00 00 ef".replace(" ", "")),
|
"Green (7e)": bytes.fromhex("7e00050300ff0000ef"),
|
||||||
"Blue": bytes.fromhex("7e0005 03 00 00 ff 00 ef".replace(" ", "")),
|
"Blue (7e)": bytes.fromhex("7e0005030000ff00ef"),
|
||||||
"White": bytes.fromhex("7e0005 03 ff ff ff 00 ef".replace(" ", "")),
|
"White (7e)": bytes.fromhex("7e000503ffffffff ef".replace(" ", "")),
|
||||||
"Warm Hellfire": bytes.fromhex("7e0005 03 ff 40 00 00 ef".replace(" ", "")),
|
"Turn ON (56/cc)": bytes.fromhex("cc2333"),
|
||||||
|
"Turn OFF (56/cc)": bytes.fromhex("cc2433"),
|
||||||
|
"Red (56)": bytes.fromhex("56ff000000f0aa"),
|
||||||
|
"Green (56)": bytes.fromhex("5600ff000000f0aa".replace(" ", "")),
|
||||||
|
"Blue (56)": bytes.fromhex("560000ff00f0aa"),
|
||||||
}
|
}
|
||||||
|
|
||||||
async with BleakClient(address) as client:
|
print(f"\nConnecting to {device.address}...")
|
||||||
print(f"Connected to {address}")
|
async with BleakClient(device) as client:
|
||||||
|
print(f"Connected!\n")
|
||||||
|
|
||||||
writable = None
|
writable = None
|
||||||
for svc in client.services:
|
for svc in client.services:
|
||||||
@@ -146,45 +189,56 @@ async def try_known_protocols(address: str):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if not writable:
|
if not writable:
|
||||||
# Try any writable characteristic
|
print("No known UUID found. Available writable characteristics:")
|
||||||
for svc in client.services:
|
for svc in client.services:
|
||||||
for char in svc.characteristics:
|
for char in svc.characteristics:
|
||||||
if "write" in char.properties or "write-without-response" in char.properties:
|
if "write" in char.properties or "write-without-response" in char.properties:
|
||||||
writable = char.uuid
|
print(f" {char.uuid} [{', '.join(char.properties)}]")
|
||||||
print(f"Using writable characteristic: {char.uuid}")
|
if not writable:
|
||||||
break
|
writable = char.uuid
|
||||||
|
print(f" ^ Using this one")
|
||||||
|
|
||||||
if not writable:
|
if not writable:
|
||||||
print("No writable characteristic found!")
|
print("No writable characteristic found at all!")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
print(f"\nSending test commands to: {writable}")
|
||||||
|
print("Watch the lights and press Enter for each one.\n")
|
||||||
|
|
||||||
for name, cmd in test_commands.items():
|
for name, cmd in test_commands.items():
|
||||||
input(f"\nPress Enter to send '{name}' ({cmd.hex()})...")
|
input(f" Press Enter to send '{name}' ({cmd.hex()})...")
|
||||||
try:
|
try:
|
||||||
await client.write_gatt_char(writable, cmd, response=False)
|
await client.write_gatt_char(writable, cmd, response=False)
|
||||||
print(f" Sent OK")
|
print(f" Sent OK")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" Error: {e}")
|
# Try with response=True as fallback
|
||||||
|
try:
|
||||||
|
await client.write_gatt_char(writable, cmd, response=True)
|
||||||
|
print(f" Sent OK (with response)")
|
||||||
|
except Exception as e2:
|
||||||
|
print(f" Error: {e2}")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="KAIYU BLE Scanner & Inspector")
|
parser = argparse.ArgumentParser(description="KAIYU BLE Scanner & Inspector")
|
||||||
parser.add_argument("--filter", "-f", help="Filter devices by name")
|
parser.add_argument("--filter", "-f", help="Filter devices by name")
|
||||||
parser.add_argument("--connect", "-c", help="Connect to device by MAC address and dump services")
|
parser.add_argument("--connect", "-c", help="Scan then connect to address and dump services")
|
||||||
parser.add_argument("--probe", "-p", help="Probe device with known commands")
|
parser.add_argument("--probe", "-p", help="Scan then probe address with known commands")
|
||||||
parser.add_argument("--timeout", "-t", type=float, default=10.0, help="Scan timeout (default 10s)")
|
parser.add_argument("--timeout", "-t", type=float, default=10.0, help="Scan timeout in seconds (default 10)")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.connect:
|
if args.connect:
|
||||||
asyncio.run(inspect_device(args.connect))
|
asyncio.run(inspect_device(args.connect, timeout=args.timeout))
|
||||||
elif args.probe:
|
elif args.probe:
|
||||||
asyncio.run(try_known_protocols(args.probe))
|
asyncio.run(try_known_protocols(args.probe, timeout=args.timeout))
|
||||||
else:
|
else:
|
||||||
async def scan_and_pick():
|
async def scan_and_pick():
|
||||||
found = await scan_devices(filter_name=args.filter, timeout=args.timeout)
|
found = await scan_devices(filter_name=args.filter, timeout=args.timeout)
|
||||||
if found:
|
if found:
|
||||||
print("Tip: run with --connect <ADDRESS> to inspect a device")
|
print("Tip: run with --connect <ADDRESS> to inspect a device")
|
||||||
print(" run with --probe <ADDRESS> to test known protocols")
|
print(" run with --probe <ADDRESS> to test known protocols")
|
||||||
|
print("\nIMPORTANT: run --connect immediately after the device appears")
|
||||||
|
print(" in a scan, or it may stop advertising.")
|
||||||
asyncio.run(scan_and_pick())
|
asyncio.run(scan_and_pick())
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user