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:
|
def from_json(cls, line: str) -> Response:
|
||||||
"""Parse a JSON line known to be a response."""
|
"""Parse a JSON line known to be a response."""
|
||||||
obj = json.loads(line)
|
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(
|
return cls(
|
||||||
type=MsgType(obj["type"]),
|
type=MsgType(obj["type"]),
|
||||||
id=obj["id"],
|
id=obj["id"],
|
||||||
status=Status(obj["status"]),
|
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
|
_client: SerialClient | None = None
|
||||||
|
|
||||||
|
|
||||||
async def get_client() -> SerialClient:
|
def get_client() -> SerialClient:
|
||||||
"""Get the singleton serial client.
|
"""Get the singleton serial client.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
|
|||||||
@ -35,7 +35,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Status dict from the ESP32.
|
Status dict from the ESP32.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = await get_client()
|
client = get_client()
|
||||||
response = await client.send_command(CMD_BLE_ENABLE)
|
response = await client.send_command(CMD_BLE_ENABLE)
|
||||||
return response.data
|
return response.data
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@ -52,7 +52,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Status dict from the ESP32.
|
Status dict from the ESP32.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = await get_client()
|
client = get_client()
|
||||||
response = await client.send_command(CMD_BLE_DISABLE)
|
response = await client.send_command(CMD_BLE_DISABLE)
|
||||||
return response.data
|
return response.data
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@ -76,7 +76,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Status dict from the ESP32.
|
Status dict from the ESP32.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = await get_client()
|
client = get_client()
|
||||||
params: dict[str, Any] = {
|
params: dict[str, Any] = {
|
||||||
"enable": enable,
|
"enable": enable,
|
||||||
"interval_ms": interval_ms,
|
"interval_ms": interval_ms,
|
||||||
@ -108,7 +108,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Status dict from the ESP32.
|
Status dict from the ESP32.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = await get_client()
|
client = get_client()
|
||||||
params: dict[str, Any] = {}
|
params: dict[str, Any] = {}
|
||||||
if name is not None:
|
if name is not None:
|
||||||
params["name"] = name
|
params["name"] = name
|
||||||
@ -141,7 +141,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Dict containing the assigned service handle.
|
Dict containing the assigned service handle.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = await get_client()
|
client = get_client()
|
||||||
params: dict[str, Any] = {
|
params: dict[str, Any] = {
|
||||||
"uuid": uuid,
|
"uuid": uuid,
|
||||||
"primary": primary,
|
"primary": primary,
|
||||||
@ -175,7 +175,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Dict containing the assigned characteristic handle.
|
Dict containing the assigned characteristic handle.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = await get_client()
|
client = get_client()
|
||||||
params: dict[str, Any] = {
|
params: dict[str, Any] = {
|
||||||
"service_handle": service_handle,
|
"service_handle": service_handle,
|
||||||
"uuid": uuid,
|
"uuid": uuid,
|
||||||
@ -207,7 +207,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Status dict from the ESP32.
|
Status dict from the ESP32.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = await get_client()
|
client = get_client()
|
||||||
params: dict[str, Any] = {
|
params: dict[str, Any] = {
|
||||||
"char_handle": char_handle,
|
"char_handle": char_handle,
|
||||||
"value": value,
|
"value": value,
|
||||||
@ -231,7 +231,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Status dict from the ESP32.
|
Status dict from the ESP32.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = await get_client()
|
client = get_client()
|
||||||
params: dict[str, Any] = {"char_handle": char_handle}
|
params: dict[str, Any] = {"char_handle": char_handle}
|
||||||
response = await client.send_command(CMD_GATT_NOTIFY, params)
|
response = await client.send_command(CMD_GATT_NOTIFY, params)
|
||||||
return response.data
|
return response.data
|
||||||
@ -250,7 +250,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Status dict from the ESP32.
|
Status dict from the ESP32.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = await get_client()
|
client = get_client()
|
||||||
response = await client.send_command(CMD_GATT_CLEAR)
|
response = await client.send_command(CMD_GATT_CLEAR)
|
||||||
return response.data
|
return response.data
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
@ -30,7 +30,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Response data from the ESP32 including current BT state.
|
Response data from the ESP32 including current BT state.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = await get_client()
|
client = get_client()
|
||||||
response = await client.send_command(CMD_CLASSIC_ENABLE)
|
response = await client.send_command(CMD_CLASSIC_ENABLE)
|
||||||
if response.status == Status.OK:
|
if response.status == Status.OK:
|
||||||
return {"status": "ok", **response.data}
|
return {"status": "ok", **response.data}
|
||||||
@ -49,7 +49,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Response data from the ESP32 confirming BT is disabled.
|
Response data from the ESP32 confirming BT is disabled.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = await get_client()
|
client = get_client()
|
||||||
response = await client.send_command(CMD_CLASSIC_DISABLE)
|
response = await client.send_command(CMD_CLASSIC_DISABLE)
|
||||||
if response.status == Status.OK:
|
if response.status == Status.OK:
|
||||||
return {"status": "ok", **response.data}
|
return {"status": "ok", **response.data}
|
||||||
@ -75,7 +75,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Response data confirming the new discoverable state.
|
Response data confirming the new discoverable state.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = await get_client()
|
client = get_client()
|
||||||
params = {"discoverable": discoverable, "timeout": timeout}
|
params = {"discoverable": discoverable, "timeout": timeout}
|
||||||
response = await client.send_command(CMD_CLASSIC_SET_DISCOVERABLE, params)
|
response = await client.send_command(CMD_CLASSIC_SET_DISCOVERABLE, params)
|
||||||
if response.status == Status.OK:
|
if response.status == Status.OK:
|
||||||
@ -110,7 +110,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Response data with the pairing result from the ESP32.
|
Response data with the pairing result from the ESP32.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = await get_client()
|
client = get_client()
|
||||||
params: dict[str, Any] = {"address": address, "accept": accept}
|
params: dict[str, Any] = {"address": address, "accept": accept}
|
||||||
if passkey is not None:
|
if passkey is not None:
|
||||||
params["passkey"] = passkey
|
params["passkey"] = passkey
|
||||||
|
|||||||
@ -32,9 +32,10 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Connection status including port, baudrate, and whether the
|
Connection status including port, baudrate, and whether the
|
||||||
device responded with a boot event.
|
device responded with a boot event.
|
||||||
"""
|
"""
|
||||||
try:
|
from ..serial_client import get_client_or_none
|
||||||
client = get_client()
|
|
||||||
except NotConnected:
|
client = get_client_or_none()
|
||||||
|
if client is None:
|
||||||
client = init_client(port=port, baudrate=baudrate)
|
client = init_client(port=port, baudrate=baudrate)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -45,7 +46,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
# Give the ESP32 a moment to send its boot event
|
# Give the ESP32 a moment to send its boot event
|
||||||
boot_received = False
|
boot_received = False
|
||||||
try:
|
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
|
boot_received = event is not None
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pass
|
pass
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user