Skip to content

Commit affc097

Browse files
authored
fix: workaround kernel ABI inconsistency in Bluetooth mgmt socket send behavior (#303)
1 parent 75d1900 commit affc097

File tree

2 files changed

+152
-33
lines changed

2 files changed

+152
-33
lines changed

src/habluetooth/channels/bluez.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def __init__(
7272
scanners: dict[int, HaScanner],
7373
on_connection_lost: Callable[[], None],
7474
is_shutting_down: Callable[[], bool],
75+
sock: socket.socket,
7576
) -> None:
7677
"""Initialize the protocol."""
7778
self.transport: asyncio.Transport | None = None
@@ -83,12 +84,39 @@ def __init__(
8384
self._on_connection_lost = on_connection_lost
8485
self._is_shutting_down = is_shutting_down
8586
self._pending_commands: dict[int, asyncio.Future[tuple[int, bytes]]] = {}
87+
self._sock = sock
8688

8789
def connection_made(self, transport: asyncio.BaseTransport) -> None:
8890
"""Handle connection made."""
8991
_set_future_if_not_done(self.connection_made_future)
9092
self.transport = cast(asyncio.Transport, transport)
9193

94+
def _write_to_socket(self, data: bytes) -> None:
95+
"""
96+
Write data directly to the socket, bypassing asyncio transport.
97+
98+
This works around a kernel bug where sendto() on Bluetooth management
99+
sockets returns 0 instead of the number of bytes sent on some platforms
100+
(e.g., Odroid M1 with kernel 6.12.43). When asyncio sees 0, it thinks
101+
the send failed and retries forever.
102+
103+
Since mgmt sockets are SOCK_RAW, sends are atomic - either the entire
104+
packet is sent or nothing is sent.
105+
"""
106+
try:
107+
n = self._sock.send(data)
108+
# On buggy kernels, n might be 0 even though the data was sent
109+
# We treat 0 as success for mgmt sockets
110+
if n == 0 and len(data) > 0:
111+
# Kernel bug: returned 0 but data was actually sent
112+
_LOGGER.debug(
113+
"Bluetooth mgmt socket returned 0 for %d bytes (kernel bug fix)",
114+
len(data),
115+
)
116+
except Exception as exc:
117+
_LOGGER.error("Failed to write to mgmt socket: %s", exc)
118+
raise
119+
92120
@asynccontextmanager
93121
async def command_response(
94122
self, opcode: int
@@ -319,6 +347,7 @@ async def _establish_connection(self) -> None:
319347
self.scanners,
320348
self._on_connection_lost,
321349
lambda: self._shutting_down,
350+
self.sock,
322351
),
323352
None,
324353
None,
@@ -398,7 +427,7 @@ async def _do_mgmt_op_get_connections(self, header: bytes) -> bool:
398427
async with self.protocol.command_response(
399428
MGMT_OP_GET_CONNECTIONS
400429
) as response_future:
401-
self.protocol.transport.write(header)
430+
self.protocol._write_to_socket(header)
402431
# Wait for response with timeout
403432
async with asyncio_timeout(5.0):
404433
status, _ = await response_future
@@ -500,7 +529,7 @@ def load_conn_params(
500529
adapter_idx, # controller index
501530
len(cmd_data), # parameter length
502531
)
503-
self.protocol.transport.write(header + cmd_data)
532+
self.protocol._write_to_socket(header + cmd_data)
504533
_LOGGER.debug(
505534
"Loaded conn params for %s: interval=%d-%d, latency=%d, timeout=%d",
506535
address,

0 commit comments

Comments
 (0)