Fix async/await bugs found by headless E2E test
- Make get_client() sync (was async but did no async work). Callers that omitted await silently got a coroutine object instead of the SerialClient, causing "'coroutine' object has no attribute 'connect'" errors on every tool call. - Fix esp32_connect: use get_client_or_none() for init check and client.event_queue.wait_for() for boot event (wait_event() didn't exist on SerialClient). - Normalise Response.data to dict at parse time — firmware returns bare strings on some error paths, which broke .get() calls in tool error handlers. - Remove stale await from ble.py (9 calls) and classic.py (4 calls). Tested with dual-MCP headless claude session: 26/27 PASS.
This commit is contained in:
parent
0e7b8c2ef5
commit
ea22f2f9db
@ -138,11 +138,15 @@ class Response:
|
||||
def from_json(cls, line: str) -> Response:
|
||||
"""Parse a JSON line known to be a response."""
|
||||
obj = json.loads(line)
|
||||
raw_data = obj.get("data", {})
|
||||
# Firmware may return a bare string on some error paths — normalise to dict
|
||||
if isinstance(raw_data, str):
|
||||
raw_data = {"error": raw_data}
|
||||
return cls(
|
||||
type=MsgType(obj["type"]),
|
||||
id=obj["id"],
|
||||
status=Status(obj["status"]),
|
||||
data=obj.get("data", {}),
|
||||
data=raw_data if isinstance(raw_data, dict) else {},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -263,7 +263,7 @@ class SerialClient:
|
||||
_client: SerialClient | None = None
|
||||
|
||||
|
||||
async def get_client() -> SerialClient:
|
||||
def get_client() -> SerialClient:
|
||||
"""Get the singleton serial client.
|
||||
|
||||
Raises:
|
||||
|
||||
@ -35,7 +35,7 @@ def register_tools(mcp: FastMCP) -> None:
|
||||
Status dict from the ESP32.
|
||||
"""
|
||||
try:
|
||||
client = await get_client()
|
||||
client = get_client()
|
||||
response = await client.send_command(CMD_BLE_ENABLE)
|
||||
return response.data
|
||||
except Exception as exc:
|
||||
@ -52,7 +52,7 @@ def register_tools(mcp: FastMCP) -> None:
|
||||
Status dict from the ESP32.
|
||||
"""
|
||||
try:
|
||||
client = await get_client()
|
||||
client = get_client()
|
||||
response = await client.send_command(CMD_BLE_DISABLE)
|
||||
return response.data
|
||||
except Exception as exc:
|
||||
@ -76,7 +76,7 @@ def register_tools(mcp: FastMCP) -> None:
|
||||
Status dict from the ESP32.
|
||||
"""
|
||||
try:
|
||||
client = await get_client()
|
||||
client = get_client()
|
||||
params: dict[str, Any] = {
|
||||
"enable": enable,
|
||||
"interval_ms": interval_ms,
|
||||
@ -108,7 +108,7 @@ def register_tools(mcp: FastMCP) -> None:
|
||||
Status dict from the ESP32.
|
||||
"""
|
||||
try:
|
||||
client = await get_client()
|
||||
client = get_client()
|
||||
params: dict[str, Any] = {}
|
||||
if name is not None:
|
||||
params["name"] = name
|
||||
@ -141,7 +141,7 @@ def register_tools(mcp: FastMCP) -> None:
|
||||
Dict containing the assigned service handle.
|
||||
"""
|
||||
try:
|
||||
client = await get_client()
|
||||
client = get_client()
|
||||
params: dict[str, Any] = {
|
||||
"uuid": uuid,
|
||||
"primary": primary,
|
||||
@ -175,7 +175,7 @@ def register_tools(mcp: FastMCP) -> None:
|
||||
Dict containing the assigned characteristic handle.
|
||||
"""
|
||||
try:
|
||||
client = await get_client()
|
||||
client = get_client()
|
||||
params: dict[str, Any] = {
|
||||
"service_handle": service_handle,
|
||||
"uuid": uuid,
|
||||
@ -207,7 +207,7 @@ def register_tools(mcp: FastMCP) -> None:
|
||||
Status dict from the ESP32.
|
||||
"""
|
||||
try:
|
||||
client = await get_client()
|
||||
client = get_client()
|
||||
params: dict[str, Any] = {
|
||||
"char_handle": char_handle,
|
||||
"value": value,
|
||||
@ -231,7 +231,7 @@ def register_tools(mcp: FastMCP) -> None:
|
||||
Status dict from the ESP32.
|
||||
"""
|
||||
try:
|
||||
client = await get_client()
|
||||
client = get_client()
|
||||
params: dict[str, Any] = {"char_handle": char_handle}
|
||||
response = await client.send_command(CMD_GATT_NOTIFY, params)
|
||||
return response.data
|
||||
@ -250,7 +250,7 @@ def register_tools(mcp: FastMCP) -> None:
|
||||
Status dict from the ESP32.
|
||||
"""
|
||||
try:
|
||||
client = await get_client()
|
||||
client = get_client()
|
||||
response = await client.send_command(CMD_GATT_CLEAR)
|
||||
return response.data
|
||||
except Exception as exc:
|
||||
|
||||
@ -30,7 +30,7 @@ def register_tools(mcp: FastMCP) -> None:
|
||||
Response data from the ESP32 including current BT state.
|
||||
"""
|
||||
try:
|
||||
client = await get_client()
|
||||
client = get_client()
|
||||
response = await client.send_command(CMD_CLASSIC_ENABLE)
|
||||
if response.status == Status.OK:
|
||||
return {"status": "ok", **response.data}
|
||||
@ -49,7 +49,7 @@ def register_tools(mcp: FastMCP) -> None:
|
||||
Response data from the ESP32 confirming BT is disabled.
|
||||
"""
|
||||
try:
|
||||
client = await get_client()
|
||||
client = get_client()
|
||||
response = await client.send_command(CMD_CLASSIC_DISABLE)
|
||||
if response.status == Status.OK:
|
||||
return {"status": "ok", **response.data}
|
||||
@ -75,7 +75,7 @@ def register_tools(mcp: FastMCP) -> None:
|
||||
Response data confirming the new discoverable state.
|
||||
"""
|
||||
try:
|
||||
client = await get_client()
|
||||
client = get_client()
|
||||
params = {"discoverable": discoverable, "timeout": timeout}
|
||||
response = await client.send_command(CMD_CLASSIC_SET_DISCOVERABLE, params)
|
||||
if response.status == Status.OK:
|
||||
@ -110,7 +110,7 @@ def register_tools(mcp: FastMCP) -> None:
|
||||
Response data with the pairing result from the ESP32.
|
||||
"""
|
||||
try:
|
||||
client = await get_client()
|
||||
client = get_client()
|
||||
params: dict[str, Any] = {"address": address, "accept": accept}
|
||||
if passkey is not None:
|
||||
params["passkey"] = passkey
|
||||
|
||||
@ -32,9 +32,10 @@ def register_tools(mcp: FastMCP) -> None:
|
||||
Connection status including port, baudrate, and whether the
|
||||
device responded with a boot event.
|
||||
"""
|
||||
try:
|
||||
client = get_client()
|
||||
except NotConnected:
|
||||
from ..serial_client import get_client_or_none
|
||||
|
||||
client = get_client_or_none()
|
||||
if client is None:
|
||||
client = init_client(port=port, baudrate=baudrate)
|
||||
|
||||
try:
|
||||
@ -45,7 +46,7 @@ def register_tools(mcp: FastMCP) -> None:
|
||||
# Give the ESP32 a moment to send its boot event
|
||||
boot_received = False
|
||||
try:
|
||||
event = await asyncio.wait_for(client.wait_event("boot"), timeout=2.0)
|
||||
event = await client.event_queue.wait_for(event_name="boot", timeout=2.0)
|
||||
boot_received = event is not None
|
||||
except TimeoutError:
|
||||
pass
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user