Checks / tests (ubuntu-latest, 3.11) (push) Canceled after 0s
Checks / tests (ubuntu-latest, 3.14) (push) Canceled after 0s
Checks / tests (windows-latest, 3.11) (push) Canceled after 0s
Checks / tests (windows-latest, 3.14) (push) Canceled after 0s
Checks / package (push) Canceled after 0s
142 lines
6.0 KiB
Python
142 lines
6.0 KiB
Python
"""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())
|