Files
lda Verified 038e152bbc
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
Add MX-T40 BLE desktop controller and protocol documentation
2026-10-25 04:43:47 +07:00

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())