"""Read Windows' RFCOMM service discovery results without sending app commands. Run separately from Tk so WinRT async operations use a clean event loop. This diagnoses the real Windows Bluetooth stack, which WSL cannot reproduce. """ import argparse import asyncio import socket import sys import warnings SPP_UUID = "00001101-0000-1000-8000-00805f9b34fb" def check_channel(address, channel): """Test the app's channel-1 fallback, without writing protocol bytes. A successful socket connection is evidence of an RFCOMM listener, not evidence that the Samsung control handshake or commands are accepted. """ if not 1 <= channel <= 30: raise ValueError("RFCOMM channel must be between 1 and 30") if not hasattr(socket, "AF_BLUETOOTH") or not hasattr(socket, "BTPROTO_RFCOMM"): raise RuntimeError( "This Python build lacks Bluetooth sockets. Use Windows CPython 3.14 or newer." ) with socket.socket(socket.AF_BLUETOOTH, socket.SOCK_STREAM, socket.BTPROTO_RFCOMM) as stream: stream.settimeout(10) stream.connect((address, channel)) async def probe(name_filter, channel=None): from winrt.windows.devices.bluetooth import BluetoothCacheMode, BluetoothDevice from winrt.windows.devices.enumeration import DeviceInformation selector = BluetoothDevice.get_device_selector_from_pairing_state(True) # Python/WinRT exposes the AQS string overload under a separate name. # find_all_async(selector) can instead try converting the string to a # DeviceClass integer, unlike the similarly named C# API. devices = await asyncio.wait_for(DeviceInformation.find_all_async_aqs_filter(selector), 20) matches = [device for device in devices if name_filter.casefold() in device.name.casefold()] if not matches: print(f"No paired Bluetooth Classic device matching {name_filter!r}.") print("Paired device names:", ", ".join(device.name for device in devices) or "(none)") return 1 found_spp = False channel_open = False failures = False for info in matches: print(f"\nDevice: {info.name}") device = None try: device = await asyncio.wait_for(BluetoothDevice.from_id_async(info.id), 20) if device is None: print("Windows could not open this device. Check Bluetooth access permissions.") failures = True continue address_hex = f"{device.bluetooth_address:012X}" address = ":".join(address_hex[i : i + 2] for i in range(0, 12, 2)) print("Bluetooth address:", address) # Uncached discovery asks the speaker instead of relying on the # cached service list used by some Windows settings screens. # Keep the explicit overload for compatibility with older WinRT # projections; recent versions retain it as a deprecated alias. with warnings.catch_warnings(): warnings.filterwarnings( "ignore", message=r"BluetoothDevice\.get_rfcomm_services_with_cache_mode_async\(\).*", category=DeprecationWarning, ) result = await asyncio.wait_for( device.get_rfcomm_services_with_cache_mode_async(BluetoothCacheMode.UNCACHED), 30, ) print("Discovery status:", result.error) if int(result.error) != 0: failures = True if not result.services: print("No RFCOMM services returned. This is not proof the firmware lacks SPP.") for service in result.services: try: service_uuid = str(service.service_id.uuid).lower() is_spp = service_uuid == SPP_UUID found_spp |= is_spp print("Service:", service_uuid, "(SPP)" if is_spp else "") print( "Endpoint:", service.connection_host_name.raw_name, service.connection_service_name, ) finally: service.close() if channel is not None: print(f"Testing direct RFCOMM channel {channel} (no application data)…", flush=True) try: await asyncio.to_thread(check_channel, address, channel) channel_open = True print("Direct channel connection: SUCCESS") except Exception as exc: failures = True print(f"Direct channel connection: FAILED: {type(exc).__name__}: {exc}") except Exception as exc: print(f"Discovery failed: {type(exc).__name__}: {exc}") failures = True finally: if device is not None: device.close() print("\nSPP service found:", found_spp) print("No application commands were sent.") return 0 if (found_spp or channel_open) and not failures else 1 def main(): if sys.platform != "win32": print("Run this with Windows uv.exe in PowerShell, not Linux Python in WSL.") return 2 try: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("name", nargs="?", default="MX-T40") parser.add_argument( "--channel", type=int, choices=range(1, 31), help="Test a direct RFCOMM channel without sending app commands", ) args = parser.parse_args() return asyncio.run(probe(args.name, args.channel)) except ImportError as exc: print("Missing Windows diagnostic dependency:", exc) print( "Use: uv.exe run --with-requirements requirements-diagnostics.txt -m controller.probe" ) return 2 except Exception as exc: print(f"Windows Bluetooth query failed: {type(exc).__name__}: {exc}") return 1 if __name__ == "__main__": raise SystemExit(main())