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,109 @@
|
||||
"""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)
|
||||
Reference in new issue
Block a user