Add support to draw images via MCP on the RLCD display
This commit is contained in:
@@ -24,6 +24,74 @@ def main():
|
|||||||
# Parse request to ensure it's valid JSON
|
# Parse request to ensure it's valid JSON
|
||||||
req_data = json.loads(line.strip())
|
req_data = json.loads(line.strip())
|
||||||
|
|
||||||
|
# Intercept draw_image tool and pre-process the image on the host PC
|
||||||
|
if req_data.get("method") == "tools/call" and req_data.get("params", {}).get("name") == "draw_image":
|
||||||
|
try:
|
||||||
|
args = req_data.get("params", {}).get("arguments", {})
|
||||||
|
image_base64 = args.get("image_base64")
|
||||||
|
|
||||||
|
if not image_base64:
|
||||||
|
raise ValueError("Missing image_base64 parameter")
|
||||||
|
|
||||||
|
# Strip data URI prefix if present
|
||||||
|
if image_base64.startswith("data:"):
|
||||||
|
if ";base64," in image_base64:
|
||||||
|
image_base64 = image_base64.split(";base64,")[1]
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import io
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
img_bytes = base64.b64decode(image_base64)
|
||||||
|
img = Image.open(io.BytesIO(img_bytes))
|
||||||
|
|
||||||
|
# Convert to grayscale
|
||||||
|
img = img.convert("L")
|
||||||
|
|
||||||
|
# Resize to fit screen dimensions (400x300 max)
|
||||||
|
max_w = int(args.get("maxWidth", 400))
|
||||||
|
max_h = int(args.get("maxHeight", 300))
|
||||||
|
|
||||||
|
# Prevent going over physical screen bounds
|
||||||
|
max_w = min(max_w, 400)
|
||||||
|
max_h = min(max_h, 300)
|
||||||
|
|
||||||
|
img.thumbnail((max_w, max_h))
|
||||||
|
|
||||||
|
# Convert to 1-bit monochrome (with optional Floyd-Steinberg dithering)
|
||||||
|
dither = args.get("dither", True)
|
||||||
|
img_1bit = img.convert("1", dither=Image.FLOYDSTEINBERG if dither else Image.NONE)
|
||||||
|
|
||||||
|
# Save as binary PBM (P4 format) using PPM format writer on mode 1
|
||||||
|
pbm_io = io.BytesIO()
|
||||||
|
img_1bit.save(pbm_io, format="PPM")
|
||||||
|
pbm_data = pbm_io.getvalue()
|
||||||
|
|
||||||
|
# Encode processed PBM to base64
|
||||||
|
pbm_b64 = base64.b64encode(pbm_data).decode("utf-8")
|
||||||
|
|
||||||
|
# Substitute arguments for ESP32
|
||||||
|
new_args = {
|
||||||
|
"pbm_base64": pbm_b64,
|
||||||
|
"x": args.get("x", 0),
|
||||||
|
"y": args.get("y", 0)
|
||||||
|
}
|
||||||
|
req_data["params"]["arguments"] = new_args
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Return error response directly to client without contacting ESP32
|
||||||
|
err_resp = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"error": {
|
||||||
|
"code": -32602,
|
||||||
|
"message": f"Bridge image preprocessing failed: {str(e)}"
|
||||||
|
},
|
||||||
|
"id": req_data.get("id")
|
||||||
|
}
|
||||||
|
sys.stdout.write(json.dumps(err_resp) + "\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
continue
|
||||||
|
|
||||||
# Forward the JSON-RPC request to the ESP32 board via HTTP POST
|
# Forward the JSON-RPC request to the ESP32 board via HTTP POST
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
url,
|
url,
|
||||||
|
|||||||
@@ -169,6 +169,20 @@ class MCPServer:
|
|||||||
"name": "get_screenshot",
|
"name": "get_screenshot",
|
||||||
"description": "Capture the current reflective LCD screen rendering as a PNG image.",
|
"description": "Capture the current reflective LCD screen rendering as a PNG image.",
|
||||||
"inputSchema": {"type": "object", "properties": {}}
|
"inputSchema": {"type": "object", "properties": {}}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "draw_image",
|
||||||
|
"description": "Draw an image (PNG, JPEG, GIF, BMP, etc.) on the screen. The image will be converted to 1-bit monochrome and fit to display boundaries.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"image_base64": {"type": "string", "description": "Base64 encoded string of the source image file"},
|
||||||
|
"x": {"type": "integer", "description": "X coordinate to place the image (default 0)", "default": 0},
|
||||||
|
"y": {"type": "integer", "description": "Y coordinate to place the image (default 0)", "default": 0},
|
||||||
|
"dither": {"type": "boolean", "description": "Whether to use Floyd-Steinberg dithering (default true)", "default": True}
|
||||||
|
},
|
||||||
|
"required": ["image_base64"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -281,5 +295,34 @@ class MCPServer:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError(f"Screenshot capture failed: {e}")
|
raise RuntimeError(f"Screenshot capture failed: {e}")
|
||||||
|
|
||||||
|
elif name == "draw_image":
|
||||||
|
self.override_active = True
|
||||||
|
self.override_timeout = time.ticks_ms() + 30000 # 30-sec override
|
||||||
|
|
||||||
|
pbm_b64 = args.get("pbm_base64")
|
||||||
|
x = int(args.get("x", 0))
|
||||||
|
y = int(args.get("y", 0))
|
||||||
|
|
||||||
|
if not pbm_b64:
|
||||||
|
raise ValueError("Missing pbm_base64 parameter (preprocessed by bridge)")
|
||||||
|
|
||||||
|
import binascii
|
||||||
|
pbm_bytes = binascii.a2b_base64(pbm_b64)
|
||||||
|
|
||||||
|
filename = 'temp_recv.pbm'
|
||||||
|
with open(filename, 'wb') as f:
|
||||||
|
f.write(pbm_bytes)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.display.draw_pbm(filename, x, y, scale=1)
|
||||||
|
self.display.show()
|
||||||
|
finally:
|
||||||
|
import os
|
||||||
|
try:
|
||||||
|
os.remove(filename)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return "Image displayed successfully."
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown tool: {name}")
|
raise ValueError(f"Unknown tool: {name}")
|
||||||
|
|||||||
Reference in New Issue
Block a user