Fix keyword argument decoding issue on MicroPython and finalize raw image endpoint and clock sync changes
This commit is contained in:
+222
-22
@@ -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"))
|
||||
|
||||
Reference in New Issue
Block a user