@@ -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