Skip to content

Commit bb79c10

Browse files
authored
Fix BLE RPC communication with devices that have frame length corruption bug (#991)
1 parent b0d88f4 commit bb79c10

File tree

2 files changed

+54
-0
lines changed

2 files changed

+54
-0
lines changed

aioshelly/rpc_device/blerpc.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,11 @@ async def _receive_response(self) -> bytes:
319319
raise DeviceConnectionError(msg)
320320

321321
frame_length = _UNPACK_UINT32_BE(length_data[:UINT32_BYTES])[0]
322+
_LOGGER.debug(
323+
"RX control raw bytes: %s, frame_length=%d",
324+
length_data[:UINT32_BYTES].hex(),
325+
frame_length,
326+
)
322327
if frame_length == 0:
323328
# Device hasn't prepared response yet, wait and retry
324329
_LOGGER.debug("Frame length 0, polling again in %ss", RX_POLL_INTERVAL)
@@ -360,6 +365,22 @@ async def _receive_response(self) -> bytes:
360365
data_bytes.extend(chunk)
361366

362367
if len(data_bytes) < frame_length:
368+
# Workaround for firmware bug: some devices return corrupted frame length
369+
# in RX control but provide complete valid JSON response in data
370+
# characteristic.
371+
# If we got valid complete JSON, use it despite the incorrect frame length.
372+
try:
373+
json_loads(data_bytes)
374+
_LOGGER.debug(
375+
"Frame length mismatch (expected %d, got %d) but data is valid",
376+
frame_length,
377+
len(data_bytes),
378+
)
379+
return bytes(data_bytes)
380+
except ValueError:
381+
# Not valid JSON or incomplete - this is a real error
382+
pass
383+
363384
msg = (
364385
f"Incomplete data received: expected {frame_length} bytes, "
365386
f"got {len(data_bytes)}"

tests/rpc_device/test_blerpc.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import asyncio
6+
import json
67
from typing import Any
78
from unittest.mock import AsyncMock, MagicMock, Mock, patch
89

@@ -323,6 +324,38 @@ async def test_blerpc_call_incomplete_data(
323324
await ble_rpc.call("Shelly.GetDeviceInfo")
324325

325326

327+
@pytest.mark.asyncio
328+
@pytest.mark.usefixtures("mock_establish_connection")
329+
async def test_blerpc_call_corrupted_frame_length_valid_json(
330+
ble_device: BLEDevice, mock_ble_client: MagicMock
331+
) -> None:
332+
"""Test BLE RPC call with corrupted frame length but valid complete JSON.
333+
334+
Workaround for firmware bug where RX control returns wrong frame length
335+
but the actual data is complete valid JSON.
336+
"""
337+
ble_rpc = BleRPC(ble_device)
338+
339+
mock_ble_client.write_gatt_char = AsyncMock()
340+
mock_ble_client.read_gatt_char = AsyncMock()
341+
342+
response = {"id": 1, "src": "test", "result": {"name": "Test Device"}}
343+
response_bytes = json.dumps(response).encode()
344+
345+
# Mock RX control says 840106079 bytes (corrupted), but we get complete JSON
346+
mock_ble_client.read_gatt_char.side_effect = [
347+
(840106079).to_bytes(4, "big"), # Corrupted frame length
348+
response_bytes, # Complete valid JSON response
349+
b"", # Empty chunk (no more data)
350+
]
351+
352+
await ble_rpc.connect()
353+
354+
# Should succeed despite corrupted frame length because JSON is valid
355+
result = await ble_rpc.call("Shelly.GetDeviceInfo")
356+
assert result == {"name": "Test Device"}
357+
358+
326359
@pytest.mark.asyncio
327360
@pytest.mark.usefixtures("mock_establish_connection")
328361
async def test_blerpc_call_invalid_json(

0 commit comments

Comments
 (0)