0fb3c22f79
Main / Build (Brainfuck) (push) Has been cancelled
Main / Build (Breakout) (push) Has been cancelled
Main / Build (Calculator) (push) Has been cancelled
Main / Build (Diceware) (push) Has been cancelled
Main / Build (EpubReader) (push) Has been cancelled
Main / Build (GPIO) (push) Has been cancelled
Main / Build (GraphicsDemo) (push) Has been cancelled
Main / Build (HelloWorld) (push) Has been cancelled
Main / Build (M5UnitTest) (push) Has been cancelled
Main / Build (Magic8Ball) (push) Has been cancelled
Main / Build (MediaKeys) (push) Has been cancelled
Main / Build (MystifyDemo) (push) Has been cancelled
Main / Build (SerialConsole) (push) Has been cancelled
Main / Build (Snake) (push) Has been cancelled
Main / Build (TamaTac) (push) Has been cancelled
Main / Build (TodoList) (push) Has been cancelled
Main / Build (TwoEleven) (push) Has been cancelled
Main / Bundle (push) Has been cancelled
229 lines
6.4 KiB
Python
229 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Host smoke test for the Tactility MCP Screen Phase 2 API."""
|
|
|
|
import argparse
|
|
import base64
|
|
import json
|
|
import socket
|
|
import struct
|
|
import urllib.request
|
|
|
|
|
|
def discover(timeout: float = 2.0) -> str | None:
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
|
sock.settimeout(timeout)
|
|
try:
|
|
sock.sendto(b"DISCOVER_SCREEN", ("255.255.255.255", 5000))
|
|
data, address = sock.recvfrom(128)
|
|
if data == b"SCREEN_IP_80":
|
|
return address[0]
|
|
except TimeoutError:
|
|
return None
|
|
finally:
|
|
sock.close()
|
|
return None
|
|
|
|
|
|
def rpc(ip: str, request_id: int, method: str, params: dict | None = None) -> dict:
|
|
payload = {
|
|
"jsonrpc": "2.0",
|
|
"id": request_id,
|
|
"method": method,
|
|
}
|
|
if params is not None:
|
|
payload["params"] = params
|
|
|
|
request = urllib.request.Request(
|
|
f"http://{ip}/api/mcp",
|
|
data=json.dumps(payload).encode(),
|
|
headers={"Content-Type": "application/json"},
|
|
method="POST",
|
|
)
|
|
with urllib.request.urlopen(request, timeout=5) as response:
|
|
return json.loads(response.read())
|
|
|
|
|
|
def require_result(response: dict, label: str) -> None:
|
|
if "error" in response:
|
|
raise RuntimeError(f"{label} failed: {response['error']}")
|
|
if "result" not in response:
|
|
raise RuntimeError(f"{label} returned no result: {response}")
|
|
|
|
|
|
def make_bmp() -> bytes:
|
|
width = 4
|
|
height = 4
|
|
row_size = width * 3
|
|
pixels = bytearray()
|
|
colors = [
|
|
(0, 0, 255),
|
|
(0, 255, 0),
|
|
(255, 0, 0),
|
|
(255, 255, 255),
|
|
]
|
|
for y in range(height):
|
|
for x in range(width):
|
|
blue, green, red = colors[(x + y) % len(colors)]
|
|
pixels.extend((blue, green, red))
|
|
pixels.extend(b"\0" * ((4 - row_size % 4) % 4))
|
|
|
|
offset = 54
|
|
file_size = offset + len(pixels)
|
|
header = (
|
|
b"BM"
|
|
+ struct.pack("<IHHI", file_size, 0, 0, offset)
|
|
+ struct.pack("<IIIHHIIIIII", 40, width, height, 1, 24, 0, len(pixels), 0, 0, 0, 0)
|
|
)
|
|
return header + pixels
|
|
|
|
|
|
def raw_endpoint(ip: str) -> None:
|
|
pixels = bytes(
|
|
[
|
|
0xF8, 0x00,
|
|
0x07, 0xE0,
|
|
0x00, 0x1F,
|
|
0xFF, 0xFF,
|
|
]
|
|
)
|
|
request = urllib.request.Request(
|
|
f"http://{ip}/api/screen/raw?x=2&y=2&w=2&h=2",
|
|
data=pixels,
|
|
headers={"Content-Type": "application/octet-stream"},
|
|
method="POST",
|
|
)
|
|
with urllib.request.urlopen(request, timeout=5) as response:
|
|
if response.read() != b"OK":
|
|
raise RuntimeError("Raw endpoint returned an unexpected response")
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--ip", help="Device IP; omit to use UDP discovery")
|
|
parser.add_argument(
|
|
"--draw",
|
|
action="store_true",
|
|
help="Also clear the app canvas and draw visible test text",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
ip = args.ip or discover()
|
|
if not ip:
|
|
raise SystemExit("MCP Screen was not discovered. Pass --ip DEVICE_IP to test directly.")
|
|
|
|
print(f"Testing MCP Screen at {ip}")
|
|
|
|
tools = rpc(ip, 1, "tools/list")
|
|
require_result(tools, "tools/list")
|
|
names = [tool["name"] for tool in tools["result"]["tools"]]
|
|
expected = {"get_capabilities", "clear_screen", "draw_text"}
|
|
if not expected.issubset(names):
|
|
raise RuntimeError(f"Missing tools: {sorted(expected - set(names))}")
|
|
print(f"tools/list: {', '.join(names)}")
|
|
|
|
capabilities = rpc(
|
|
ip,
|
|
2,
|
|
"tools/call",
|
|
{"name": "get_capabilities", "arguments": {}},
|
|
)
|
|
require_result(capabilities, "get_capabilities")
|
|
print("get_capabilities:", capabilities["result"]["content"][0]["text"])
|
|
|
|
if args.draw:
|
|
cleared = rpc(
|
|
ip,
|
|
3,
|
|
"tools/call",
|
|
{"name": "clear_screen", "arguments": {"color": 1}},
|
|
)
|
|
require_result(cleared, "clear_screen")
|
|
|
|
drawn = rpc(
|
|
ip,
|
|
4,
|
|
"tools/call",
|
|
{
|
|
"name": "draw_text",
|
|
"arguments": {
|
|
"text": "Tactility MCP is alive",
|
|
"x": 20,
|
|
"y": 30,
|
|
"size": 2,
|
|
},
|
|
},
|
|
)
|
|
require_result(drawn, "draw_text")
|
|
|
|
raw_rpc = rpc(
|
|
ip,
|
|
5,
|
|
"tools/call",
|
|
{
|
|
"name": "draw_raw_rgb565",
|
|
"arguments": {
|
|
"rgb565_base64": base64.b64encode(
|
|
bytes([0xFF, 0xE0, 0xF8, 0x1F, 0x07, 0xFF, 0x00, 0x00])
|
|
).decode(),
|
|
"x": 20,
|
|
"y": 70,
|
|
"w": 4,
|
|
"h": 1,
|
|
},
|
|
},
|
|
)
|
|
require_result(raw_rpc, "draw_raw_rgb565")
|
|
|
|
bmp = rpc(
|
|
ip,
|
|
6,
|
|
"tools/call",
|
|
{
|
|
"name": "draw_color_bmp",
|
|
"arguments": {
|
|
"bmp_base64": base64.b64encode(make_bmp()).decode(),
|
|
"x": 30,
|
|
"y": 90,
|
|
},
|
|
},
|
|
)
|
|
require_result(bmp, "draw_color_bmp")
|
|
|
|
pbm_bytes = b"P4\n8 2\n" + bytes([0b10101010, 0b01010101])
|
|
pbm = rpc(
|
|
ip,
|
|
7,
|
|
"tools/call",
|
|
{
|
|
"name": "draw_image",
|
|
"arguments": {
|
|
"pbm_base64": base64.b64encode(pbm_bytes).decode(),
|
|
"x": 50,
|
|
"y": 110,
|
|
},
|
|
},
|
|
)
|
|
require_result(pbm, "draw_image")
|
|
|
|
raw_endpoint(ip)
|
|
|
|
screenshot = rpc(
|
|
ip,
|
|
8,
|
|
"tools/call",
|
|
{"name": "get_screenshot", "arguments": {}},
|
|
)
|
|
require_result(screenshot, "get_screenshot")
|
|
screenshot_text = screenshot["result"]["content"][0]["text"]
|
|
if not screenshot_text.startswith("__PBM_BASE64__:"):
|
|
raise RuntimeError("Screenshot did not return PBM data")
|
|
base64.b64decode(screenshot_text.split(":", 1)[1], validate=True)
|
|
print("Visible Phase 2 draw and screenshot tests sent.")
|
|
|
|
print("Phase 2 smoke test passed.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|