Add MX-T40 BLE desktop controller and protocol documentation
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
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
This commit is contained in:
38 files changed
+2344
No files matched your search
@@ -0,0 +1,141 @@
|
||||
"""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())
|
||||
Reference in new issue
Block a user