Fix keyword argument decoding issue on MicroPython and finalize raw image endpoint and clock sync changes

This commit is contained in:
Adolfo Reyna
2026-06-19 13:30:26 -04:00
parent c944d8c48f
commit c8853cc5df
7 changed files with 711 additions and 24 deletions
+222 -22
View File
@@ -96,11 +96,21 @@ class MCPServer:
try:
# Handle incoming connection
client.settimeout(2.0)
req = client.recv(2048).decode('utf-8')
# Simple HTTP parser
lines = req.split('\r\n')
if len(lines) == 0:
req_bytes = client.recv(2048)
if not req_bytes:
client.close()
return
header_end = req_bytes.find(b'\r\n\r\n')
if header_end == -1:
header_end = len(req_bytes)
body_start_idx = len(req_bytes)
else:
body_start_idx = header_end + 4
header_text = req_bytes[:header_end].decode('utf-8', 'ignore')
lines = header_text.split('\r\n')
if len(lines) == 0 or not lines[0]:
client.close()
return
@@ -120,17 +130,15 @@ class MCPServer:
content_length = int(line.split(':')[1].strip())
break
# Locate start of JSON body
body = ""
if '\r\n\r\n' in req:
body = req.split('\r\n\r\n', 1)[1]
body_bytes = bytearray(req_bytes[body_start_idx:])
while len(body_bytes) < content_length:
chunk = client.recv(min(1024, content_length - len(body_bytes)))
if not chunk:
break
body_bytes.extend(chunk)
# Read remaining body if not fully received
while len(body) < content_length:
body += client.recv(1024).decode('utf-8')
# Parse JSON-RPC 2.0 Request
rpc_req = json.loads(body)
body_text = body_bytes.decode('utf-8', 'ignore')
rpc_req = json.loads(body_text)
rpc_resp = self._handle_rpc(rpc_req)
resp_body = json.dumps(rpc_resp)
@@ -140,6 +148,56 @@ class MCPServer:
resp += "Connection: close\r\n\r\n"
resp += resp_body
data_to_send = resp.encode('utf-8')
total_sent = 0
while total_sent < len(data_to_send):
sent = client.write(data_to_send[total_sent:])
if sent is None or sent == 0:
time.sleep_ms(10)
continue
total_sent += sent
elif method == 'POST' and path.startswith('/api/screen/raw'):
# Read content length
content_length = 0
for line in lines:
if line.lower().startswith('content-length:'):
content_length = int(line.split(':')[1].strip())
break
body_bytes = bytearray(req_bytes[body_start_idx:])
while len(body_bytes) < content_length:
chunk = client.recv(min(1024, content_length - len(body_bytes)))
if not chunk:
break
body_bytes.extend(chunk)
# Parse query parameters from path (e.g. /api/screen/raw?x=0&y=0&w=320&h=240)
width = getattr(self.display, 'width', 320)
height = getattr(self.display, 'height', 240)
x, y, w, h = 0, 0, width, height
if '?' in path:
q_str = path.split('?', 1)[1]
for param in q_str.split('&'):
if '=' in param:
k, v = param.split('=', 1)
if k == 'x': x = int(v)
elif k == 'y': y = int(v)
elif k == 'w': w = int(v)
elif k == 'h': h = int(v)
# Draw directly using display raw RGB565 method
self.override_active = True
self.display.draw_rgb565(x, y, w, h, body_bytes)
resp_body = "OK"
resp = "HTTP/1.1 200 OK\r\n"
resp += "Content-Type: text/plain\r\n"
resp += f"Content-Length: {len(resp_body)}\r\n"
resp += "Connection: close\r\n\r\n"
resp += resp_body
data_to_send = resp.encode('utf-8')
total_sent = 0
while total_sent < len(data_to_send):
@@ -151,7 +209,7 @@ class MCPServer:
else:
# Return 404
resp = "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
client.send(resp.encode('utf-8'))
client.write(resp.encode('utf-8'))
except Exception as e:
print("Error handling client:", e)
finally:
@@ -162,6 +220,10 @@ class MCPServer:
method = req.get('method')
params = req.get('params', {})
# Get active display dimensions dynamically to report correct screen resolution
width = getattr(self.display, 'width', 320)
height = getattr(self.display, 'height', 240)
if method == 'tools/list':
return {
"jsonrpc": "2.0",
@@ -169,7 +231,7 @@ class MCPServer:
"tools": [
{
"name": "clear_screen",
"description": "Clear the 480x320 screen to white (0) or black (1).",
"description": f"Clear the {width}x{height} screen to white (0) or black (1).",
"inputSchema": {
"type": "object",
"properties": {
@@ -180,13 +242,13 @@ class MCPServer:
},
{
"name": "draw_text",
"description": "Draw text on the screen at specified (x,y) coordinates.",
"description": f"Draw text on the screen at specified (x,y) coordinates. Screen resolution is {width}x{height}.",
"inputSchema": {
"type": "object",
"properties": {
"text": {"type": "string", "description": "The message to display"},
"x": {"type": "integer", "description": "X coordinate (0-470)"},
"y": {"type": "integer", "description": "Y coordinate (0-310)"},
"x": {"type": "integer", "description": f"X coordinate (0-{width-1})"},
"y": {"type": "integer", "description": f"Y coordinate (0-{height-1})"},
"size": {"type": "integer", "enum": [1, 2], "description": "Text scale (1=normal, 2=large)"}
},
"required": ["text", "x", "y"]
@@ -233,12 +295,12 @@ class MCPServer:
},
{
"name": "get_screenshot",
"description": "Capture the current reflective LCD screen rendering as a PNG image.",
"description": "Capture the current screen rendering as a PNG image.",
"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.",
"description": f"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 (max {width}x{height}).",
"inputSchema": {
"type": "object",
"properties": {
@@ -250,6 +312,44 @@ class MCPServer:
"required": ["image_base64"]
}
},
{
"name": "draw_color_bmp",
"description": f"Draw a color BMP image on the {width}x{height} color screen at specified (x,y) coordinates.",
"inputSchema": {
"type": "object",
"properties": {
"bmp_base64": {"type": "string", "description": "Base64 encoded BMP image file (uncompressed 24-bit or 32-bit format)"},
"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}
},
"required": ["bmp_base64"]
}
},
{
"name": "get_capabilities",
"description": "Get screen capabilities (resolution, color support, and supported formats).",
"inputSchema": {"type": "object", "properties": {}}
},
{
"name": "sync_time",
"description": "Synchronize the hardware and system clock with an internet NTP server.",
"inputSchema": {"type": "object", "properties": {}}
},
{
"name": "draw_raw_rgb565",
"description": f"Draw raw RGB565 pixel data on the {width}x{height} screen at specified (x,y) coordinates with width (w) and height (h).",
"inputSchema": {
"type": "object",
"properties": {
"rgb565_base64": {"type": "string", "description": "Base64 encoded raw RGB565 pixel data (Big-Endian, 2 bytes per pixel)"},
"x": {"type": "integer", "description": "X coordinate to place the image"},
"y": {"type": "integer", "description": "Y coordinate to place the image"},
"w": {"type": "integer", "description": "Width of the raw pixel block"},
"h": {"type": "integer", "description": "Height of the raw pixel block"}
},
"required": ["rgb565_base64", "x", "y", "w", "h"]
}
},
{
"name": "write_file",
"description": "Write a file (e.g., python script or config) to the board's flash storage.",
@@ -507,6 +607,106 @@ class MCPServer:
pass
return "Image displayed successfully."
elif name == "draw_color_bmp":
self.override_active = True
bmp_b64 = args.get("bmp_base64")
x = int(args.get("x", 0))
y = int(args.get("y", 0))
if not bmp_b64:
raise ValueError("Missing bmp_base64 parameter")
import binascii
try:
bmp_bytes = binascii.a2b_base64(bmp_b64)
except Exception as e:
raise ValueError(f"Failed to decode base64 BMP: {e}")
filename = 'temp_recv.bmp'
with open(filename, 'wb') as f:
f.write(bmp_bytes)
try:
success = self.display.draw_bmp(filename, x, y)
if not success:
raise RuntimeError("Failed to parse or draw BMP image on screen.")
finally:
import os
try:
os.remove(filename)
except:
pass
return "Color BMP image displayed successfully."
elif name == "get_capabilities":
is_color = getattr(self.display, '__class__', None) is not None and self.display.__class__.__name__ != 'RLCD'
formats = ["rgb565_base64", "bmp_base64", "pbm_base64"] if is_color else ["pbm_base64", "bmp_base64", "rgb565_base64"]
return json.dumps({
"color": is_color,
"width": width,
"height": height,
"formats": formats
})
elif name == "sync_time":
import ntptime
import wifi_config
tz_offset = getattr(wifi_config, 'TZ_OFFSET', 0)
success = False
t = None
for attempt in range(3):
try:
utc_sec = ntptime.time()
local_sec = utc_sec + int(tz_offset * 3600)
t = time.localtime(local_sec)
dt = (t[0], t[1], t[2], t[6], t[3], t[4], t[5])
if self.rtc:
self.rtc.set_datetime(dt)
self.rtc.sync_to_system()
success = True
break
except Exception as e:
time.sleep_ms(200)
if success and t is not None:
t_str = f"{t[0]:04d}-{t[1]:02d}-{t[2]:02d} {t[3]:02d}:{t[4]:02d}:{t[5]:02d}"
return f"Successfully synchronized board clock with NTP server. Local time: {t_str}"
else:
raise RuntimeError("Failed to sync clock with NTP server.")
elif name == "draw_raw_rgb565":
self.override_active = True
rgb_b64 = args.get("rgb565_base64")
x = int(args.get("x", 0))
y = int(args.get("y", 0))
w = int(args.get("w", 0))
h = int(args.get("h", 0))
if not rgb_b64:
raise ValueError("Missing rgb565_base64 parameter")
import binascii
try:
rgb_bytes = binascii.a2b_base64(rgb_b64)
except Exception as e:
raise ValueError(f"Failed to decode base64 RGB565: {e}")
if len(rgb_bytes) < w * h * 2:
raise ValueError(f"RGB565 data size too small (expected {w * h * 2} bytes, got {len(rgb_bytes)} bytes)")
try:
self.display.draw_rgb565(x, y, w, h, rgb_bytes)
except Exception as e:
raise RuntimeError(f"Failed to draw RGB565: {e}")
return "Raw RGB565 data displayed successfully."
elif name == "write_file":
path = str(args.get("path"))
content = str(args.get("content"))