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

110 lines
3.7 KiB
Python

"""Byte-stream adapters for direct RFCOMM and optional serial COM ports."""
import asyncio
import re
import socket
class RfcommStream:
"""Adapt an RFCOMM socket to the worker's serial-like read/write API.
Idle reads return empty bytes, as pyserial does; a socket EOF instead
raises, so a lost speaker connection cannot look like endless idle time.
"""
def __init__(self, address, channel=1):
self.address = address
self.channel = channel
self.socket = None
def __enter__(self):
if not 1 <= self.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(
"Direct Bluetooth requires a Python build with RFCOMM support. Use Windows Python 3.14."
)
self.socket = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_STREAM, socket.BTPROTO_RFCOMM)
try:
self.socket.settimeout(10)
self.socket.connect((self.address, self.channel))
except TimeoutError as exc:
self.socket.close()
self.socket = None
raise TimeoutError(
f"Bluetooth connection timed out before the Samsung handshake "
f"({self.address}, channel {self.channel})."
) from exc
except BaseException:
self.socket.close()
self.socket = None
raise
return self
def __exit__(self, *args):
if self.socket is not None:
self.socket.close()
self.socket = None
def write(self, data):
self.socket.settimeout(2)
# sendall handles partial socket writes. Never retry a whole command
# after a send failure: remote key actions could otherwise repeat.
try:
self.socket.sendall(data)
except TimeoutError as exc:
raise TimeoutError(
"Bluetooth connected, but sending a Samsung packet timed out."
) from exc
return len(data)
def read(self, size):
self.socket.settimeout(0.05)
try:
data = self.socket.recv(size)
except TimeoutError:
return b""
if not data:
raise ConnectionError("The speaker closed the Bluetooth connection.")
return data
class PreparedRfcommStream(RfcommStream):
"""Keep the WinRT device alive across the socket session, as the probe did."""
def __init__(self, address, channel, report):
super().__init__(address, channel)
self.report = report
self.device = None
def __enter__(self):
from controller.windows_bluetooth import prepare_device
self.device = asyncio.run(prepare_device(self.address, self.report))
try:
self.report("Opening direct RFCOMM socket with Windows device handle retained…")
return super().__enter__()
except BaseException:
self.device.close()
self.device = None
raise
def __exit__(self, *args):
try:
super().__exit__(*args)
finally:
if self.device is not None:
self.device.close()
self.device = None
def open_stream(target, channel=1, prepare=False, report=lambda text: None):
"""A Bluetooth MAC selects direct RFCOMM; other targets select serial."""
if re.fullmatch(r"(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}", target):
if prepare:
return PreparedRfcommStream(target, channel, report)
return RfcommStream(target, channel)
import serial
return serial.Serial(target, 115200, timeout=0.05, write_timeout=2)