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

This commit is contained in:
lda committed 2026-10-25 04:43:47 +07:00 Verified
commit 038e152bbc
38 files changed
+2344

No files matched your search

+95
View File
@@ -0,0 +1,95 @@
"""Samsung app packet framing, independent of Bluetooth and the desktop UI."""
from dataclasses import dataclass
EQ = dict(
zip(("Party", "Standard", "Hip-hop", "EDM", "Rock", "Latin", "House", "Reggae"), range(1, 9))
)
LIGHTS = dict(
zip(("Off", "Ambient", "Party", "Dance", "Thunder", "Star", "Lover", "Color"), range(8))
)
@dataclass(frozen=True)
class Frame:
kind: int
sequence: int
payload: bytes
checksum_valid: bool = True
class Encoder:
"""Match CommandUtil, including its unusual signed-byte sequence rollover."""
def __init__(self):
self.sequence = 0
def encode(self, payload):
payload = bytes(payload)
if not 1 <= len(payload) <= 65535:
raise ValueError("Payload must contain 1–65535 bytes")
self.sequence = self.sequence % 127 + 1
body = bytes((1, self.sequence)) + len(payload).to_bytes(2, "big") + payload
return b"\x00\xbb" + body + bytes((sum(body) & 255,))
class Decoder:
"""Buffer split/combined reads and optionally reject unknown checksums.
Hardware captures show both checksum variants: connection/source replies
exclude the type byte, while an EQ reply includes it. Accept both observed
sums plus the observed fixed-value lighting formula. The exact firmware
paths selecting these variants are not yet known. Compatibility mode
retains mismatches but marks Frame.checksum_valid false.
A conservative 4096-byte cap prevents bogus lengths from indefinitely
hiding subsequent control frames.
"""
def __init__(self, strict_checksums=True):
self.buffer = bytearray()
self.strict_checksums = strict_checksums
def feed(self, chunk):
self.buffer.extend(chunk)
frames = []
while True:
start = self.buffer.find(b"\x00\xbb")
if start < 0:
self.buffer[:] = b"\x00" if self.buffer.endswith(b"\x00") else b""
break
del self.buffer[:start]
if len(self.buffer) < 7:
break
size = int.from_bytes(self.buffer[4:6], "big")
if self.buffer[2] not in (0, 1) or size > 4096:
del self.buffer[0]
continue
total = size + 7
if len(self.buffer) < total:
break
checksum = sum(self.buffer[3 : total - 1]) & 255
valid = self.buffer[total - 1] in (checksum, (checksum + self.buffer[2]) & 255)
# All captured 51/03 lighting reports (Ambient, Dance, Star, Lover)
# match a sum with the mode slot fixed at 1. This is an observed
# compatibility formula, not a claim about firmware implementation.
if (
self.buffer[2] == 1
and size == 3
and self.buffer[6:8] == b"\x51\x03"
and self.buffer[8] <= 7
):
lighting_checksum = (checksum - self.buffer[8] + 1) & 255
valid |= self.buffer[total - 1] == lighting_checksum
# Captured play/pause echoes sum as if the key slot were 1:
# sequence CD, payload 70/11 -> checksum 40; CE -> 41.
# Scope this rule to the observed payload bytes.
if self.buffer[2] == 1 and size == 2 and self.buffer[6:8] == b"\x70\x11":
valid |= self.buffer[total - 1] == (checksum - 0x11 + 1) & 255
if not valid and self.strict_checksums:
del self.buffer[0]
continue
frames.append(
Frame(self.buffer[2], self.buffer[3], bytes(self.buffer[6 : total - 1]), valid)
)
del self.buffer[:total]
return frames