From 3356e5d4a29d01bd932a9802bd057b90ff68a48a Mon Sep 17 00:00:00 2001 From: Adolfo Reyna Date: Mon, 8 Jun 2026 15:04:36 -0400 Subject: [PATCH] Implement notetaking app, ST7796 display with touch support, audio beep navigation, and video streaming --- battery_util.py | 5 +- ble_util.py | 52 +- boot.py | 27 +- fonts/CourierPrime.ttf | Bin 0 -> 71188 bytes fonts/PatrickHand.ttf | Bin 0 -> 214772 bytes fonts/SpecialElite.ttf | 1483 ++++++++++++++++++++++++++ ft6336u.py | 123 +++ main.py | 175 ++- mcp_server.py | 64 +- note.txt | 17 + note_2026-06-04.txt | 5 + notetaker.py | 490 +++++++++ rgb_led_util.py | 5 +- rlcd.py | 8 +- scratch/delete_all.py | 86 ++ scratch/interact_repl.py | 39 + scratch/listen_touch.py | 24 + scratch/nuke_boot.py | 71 ++ scratch/verify_actual_virtual_mcp.py | 33 + scratch/verify_launcher_beep.py | 84 ++ st7796.py | 249 +++++ test_local_notetaker.py | 42 + test_stream.py | 214 ++++ upload_files.py | 82 ++ verify_broadcast.py | 90 ++ verify_broadcast_single_frame.py | 106 ++ verify_streaming.py | 107 ++ video_stream.py | 285 +++++ virtual_screen_mcp/server.py | 71 ++ wifi_util.py | 17 +- 30 files changed, 3983 insertions(+), 71 deletions(-) create mode 100644 fonts/CourierPrime.ttf create mode 100644 fonts/PatrickHand.ttf create mode 100644 fonts/SpecialElite.ttf create mode 100644 ft6336u.py create mode 100644 note.txt create mode 100644 note_2026-06-04.txt create mode 100644 notetaker.py create mode 100644 scratch/delete_all.py create mode 100644 scratch/interact_repl.py create mode 100644 scratch/listen_touch.py create mode 100644 scratch/nuke_boot.py create mode 100644 scratch/verify_actual_virtual_mcp.py create mode 100644 scratch/verify_launcher_beep.py create mode 100644 st7796.py create mode 100644 test_local_notetaker.py create mode 100644 test_stream.py create mode 100644 upload_files.py create mode 100644 verify_broadcast.py create mode 100644 verify_broadcast_single_frame.py create mode 100644 verify_streaming.py create mode 100644 video_stream.py diff --git a/battery_util.py b/battery_util.py index 91ca8d7..79e1f4e 100644 --- a/battery_util.py +++ b/battery_util.py @@ -4,9 +4,10 @@ class BatteryMonitor: """Utility class for monitoring battery voltage and capacity on ESP32-S3-RLCD-4.2.""" def __init__(self, pin_num=4): - # Initialize ADC on GPIO 4 with 11dB attenuation + import sys self.adc = ADC(Pin(pin_num)) - self.adc.atten(ADC.ATTN_11DB) + if sys.platform == 'esp32': + self.adc.atten(ADC.ATTN_11DB) def read_voltage(self): """Reads the battery voltage in Volts using internal calibration. diff --git a/ble_util.py b/ble_util.py index 0ee0aab..af826e5 100644 --- a/ble_util.py +++ b/ble_util.py @@ -1,25 +1,40 @@ -import bluetooth +try: + import bluetooth + has_ble = True +except ImportError: + has_ble = False + import struct import time -# BLE UUID definitions (Nordic UART Service) -_UART_UUID = bluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E") -_UART_TX = ( - bluetooth.UUID("6E400003-B5A3-F393-E0A9-E50E24DCCA9E"), - bluetooth.FLAG_NOTIFY, -) -_UART_RX = ( - bluetooth.UUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"), - bluetooth.FLAG_WRITE | bluetooth.FLAG_WRITE_NO_RESPONSE, -) -_UART_SERVICE = (_UART_UUID, (_UART_TX, _UART_RX)) +if not has_ble: + class BLEUART: + def __init__(self, ble=None, name="ESP32-S3-RLCD"): + print("BLE not supported on this platform.") + def on_rx(self, callback): pass + def write(self, data): return False + def is_connected(self): return False + def scan(self, duration_ms=3000): return {} + def close(self): pass +else: + # BLE UUID definitions (Nordic UART Service) + _UART_UUID = bluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E") + _UART_TX = ( + bluetooth.UUID("6E400003-B5A3-F393-E0A9-E50E24DCCA9E"), + bluetooth.FLAG_NOTIFY, + ) + _UART_RX = ( + bluetooth.UUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"), + bluetooth.FLAG_WRITE | bluetooth.FLAG_WRITE_NO_RESPONSE, + ) + _UART_SERVICE = (_UART_UUID, (_UART_TX, _UART_RX)) -# BLE IRQ constants -_IRQ_CENTRAL_CONNECT = 1 -_IRQ_CENTRAL_DISCONNECT = 2 -_IRQ_GATTS_WRITE = 3 + # BLE IRQ constants + _IRQ_CENTRAL_CONNECT = 1 + _IRQ_CENTRAL_DISCONNECT = 2 + _IRQ_GATTS_WRITE = 3 -class BLEUART: +class _ActiveBLEUART: """A helper class to manage BLE UART (Serial Over BLE) for raw text data exchange.""" def __init__(self, ble=None, name="ESP32-S3-RLCD"): @@ -174,3 +189,6 @@ class BLEUART: self._ble.gap_disconnect(conn_handle) self._ble.active(False) print("BLE closed.") + +if has_ble: + BLEUART = _ActiveBLEUART diff --git a/boot.py b/boot.py index 0c89e31..8b5bbfc 100644 --- a/boot.py +++ b/boot.py @@ -1,17 +1,34 @@ # This file is executed on every boot (including wake-boot from deepsleep) -import network import time -import rlcd +import sys + +try: + import network + has_network = True +except ImportError: + has_network = False +if sys.platform == 'rp2': + import st7796 as display_module +else: + import rlcd as display_module from machine import Pin, SPI import wifi_config def connect_wifi(): + if sys.platform == 'rp2': + # Skip display and connection setup on RP2 (no network/Wi-Fi hardware) + # Keep the sleep window to make REPL interruption easy + time.sleep(1.5) + return + # Initialize display to show connection progress display = None try: + # ESP32-S3 configuration spi = SPI(1, baudrate=20000000, polarity=0, phase=0, sck=Pin(11), mosi=Pin(12)) - display = rlcd.RLCD(spi, cs=Pin(40), dc=Pin(5), rst=Pin(41)) + display = display_module.RLCD(spi, cs=Pin(40), dc=Pin(5), rst=Pin(41)) display.clear(0) + display.text("ESP32-S3-RLCD-4.2", 10, 10, 1) display.line(10, 20, 390, 20, 1) display.text("Connecting to Wi-Fi...", 10, 35, 1) @@ -21,8 +38,10 @@ def connect_wifi(): print("Display init failed in boot.py:", e) # Initialize Wi-Fi Station + if not has_network: + return + try: - import network network.hostname('esp32screen') print("Hostname set to: {}".format(network.hostname())) except Exception as he: diff --git a/fonts/CourierPrime.ttf b/fonts/CourierPrime.ttf new file mode 100644 index 0000000000000000000000000000000000000000..2a0f8bde9778bb8c7b3a0ca5d4b4fb041737ef1a GIT binary patch literal 71188 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 literal 0 HcmV?d00001 diff --git a/fonts/PatrickHand.ttf b/fonts/PatrickHand.ttf new file mode 100644 index 0000000000000000000000000000000000000000..fb45ccdbd344ab7f9f4ae98792521405e48cda3e GIT binary patch literal 214772 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%0AISm&OeRj9HPv@D< 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

+0bv5 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 B)w%!x literal 0 HcmV?d00001 diff --git a/fonts/SpecialElite.ttf b/fonts/SpecialElite.ttf new file mode 100644 index 0000000..c842f35 --- /dev/null +++ b/fonts/SpecialElite.ttf @@ -0,0 +1,1483 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Page not found · GitHub · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + diff --git a/ft6336u.py b/ft6336u.py new file mode 100644 index 0000000..9122bfd --- /dev/null +++ b/ft6336u.py @@ -0,0 +1,123 @@ +import time +from machine import Pin, I2C + +class FT6336U: + ADDR = 0x38 + + # Registers + REG_DEV_MODE = 0x00 + REG_TD_STATUS = 0x02 + REG_P1_XH = 0x03 + REG_P1_XL = 0x04 + REG_P1_YH = 0x05 + REG_P1_YL = 0x06 + REG_CTRL = 0x86 + REG_CHIPID = 0xA3 + REG_G_MODE = 0xA4 + + # Modes + CTRL_KEEP_ACTIVE = 0x00 + G_MODE_TRIGGER = 0x01 + + def __init__(self, i2c, rst_pin=28, int_pin=25, width=480, height=320, swap_xy=True, invert_x=True, invert_y=False): + self.i2c = i2c + self.rst = Pin(rst_pin, Pin.OUT) + self.int = Pin(int_pin, Pin.IN, Pin.PULL_UP) + self.width = width + self.height = height + self.swap_xy = swap_xy + self.invert_x = invert_x + self.invert_y = invert_y + self.initialized = False + + self.reset() + self.init_chip() + + def reset(self): + self.rst(0) + time.sleep_ms(10) + self.rst(1) + time.sleep_ms(300) # Wait for chip to wake up + + def read_reg(self, reg, n=1): + try: + return self.i2c.readfrom_mem(self.ADDR, reg, n) + except Exception as e: + print(f"I2C read failed at reg 0x{reg:02X}: {e}") + return None + + def write_reg(self, reg, val): + try: + self.i2c.writeto_mem(self.ADDR, reg, bytearray([val])) + return True + except Exception as e: + print(f"I2C write failed at reg 0x{reg:02X}: {e}") + return False + + def init_chip(self): + # 1. Read Chip ID + chip_id = self.read_reg(self.REG_CHIPID) + if chip_id is None or chip_id[0] != 0x64: + # Retry once + time.sleep_ms(100) + chip_id = self.read_reg(self.REG_CHIPID) + if chip_id is None or chip_id[0] != 0x64: + print(f"FT6336U error: Invalid Chip ID (got {chip_id[0] if chip_id else None}, expected 0x64)") + self.initialized = False + return False + + print(f"FT6336U touch controller detected (Chip ID: 0x{chip_id[0]:02X})") + + # 2. Configure operating mode (0 = Normal Mode) + self.write_reg(self.REG_DEV_MODE, 0x00) + + # 3. Configure CTRL mode (0 = Keep Active) + self.write_reg(self.REG_CTRL, self.CTRL_KEEP_ACTIVE) + + self.initialized = True + return True + + def is_touched(self): + """Returns True if screen is touched (the active-low INT pin is pulled LOW).""" + return self.int() == 0 + + def read_touch(self): + """Reads touch point coordinates. + Returns: + (x, y) coordinates of first touch point, or None if no touch. + """ + if not self.initialized: + return None + + # Read TD_STATUS to check if there is an active touch + td_status = self.read_reg(self.REG_TD_STATUS) + if td_status is None: + return None + + touch_count = td_status[0] & 0x0F + if touch_count == 0: + return None + + # Read P1 coordinates (6 bytes starting at P1_XH) + buf = self.read_reg(self.REG_P1_XH, 6) + if buf is None or len(buf) < 6: + return None + + raw_x = ((buf[0] & 0x0F) << 8) | buf[1] + raw_y = ((buf[2] & 0x0F) << 8) | buf[3] + + # Apply coordinate transformations to map touch coordinate space to screen space + if self.swap_xy: + raw_x, raw_y = raw_y, raw_x + + if self.invert_x: + raw_x = self.width - 1 - raw_x + + if self.invert_y: + raw_y = self.height - 1 - raw_y + + # Clamp coordinates to screen boundaries + x = max(0, min(self.width - 1, raw_x)) + y = max(0, min(self.height - 1, raw_y)) + + return (x, y) diff --git a/main.py b/main.py index b3d502a..1a3fcfc 100644 --- a/main.py +++ b/main.py @@ -1,8 +1,31 @@ import time -import network +import sys import machine from machine import Pin, SPI, I2C -import rlcd + +try: + import network + has_network = True +except ImportError: + has_network = False + +if sys.platform == 'rp2': + import st7796 as display_module +else: + import rlcd as display_module + +class DummyMCP: + def __init__(self): + self.override_active = False + self.active_led_mode = "off" + def update(self): pass + def start(self, port=80): pass + +class DummyVStream: + def __init__(self): + self.active = False + def update(self): pass + def start(self): pass # Import our utility classes from shtc3_util import SHTC3 @@ -12,6 +35,7 @@ from button_util import BoardButtons from rgb_led_util import BoardLED from ble_util import BLEUART from mcp_server import MCPServer +from video_stream import VideoStreamServer # LED mode options for manual cycling led_modes = [ @@ -30,33 +54,83 @@ def main(): print("=== Starting ESP32-S3-RLCD-4.2 Main Boot ===") # 1. Initialize shared buses and peripherals - i2c = I2C(0, sda=Pin(13), scl=Pin(14)) + i2c = None + if sys.platform != 'rp2': + try: + i2c = I2C(0, sda=Pin(13), scl=Pin(14)) + except Exception as e: + print("Failed to initialize I2C0:", e) - spi = SPI(1, baudrate=20000000, polarity=0, phase=0, sck=Pin(11), mosi=Pin(12)) - display = rlcd.RLCD(spi, cs=Pin(40), dc=Pin(5), rst=Pin(41)) + if sys.platform == 'rp2': + spi = SPI(1, baudrate=32000000, polarity=0, phase=0, sck=Pin(10), mosi=Pin(11)) + display = display_module.ST7796(spi, cs=Pin(7), dc=Pin(4), rst=Pin(9), bl=Pin(6)) + + # Touch controller + touch = None + try: + touch_i2c = I2C(1, sda=Pin(2), scl=Pin(3), freq=400000) + from ft6336u import FT6336U + touch = FT6336U(touch_i2c, rst_pin=28, int_pin=25) + except Exception as te: + print("Failed to initialize touch:", te) + else: + spi = SPI(1, baudrate=20000000, polarity=0, phase=0, sck=Pin(11), mosi=Pin(12)) + display = display_module.RLCD(spi, cs=Pin(40), dc=Pin(5), rst=Pin(41)) + touch = None - # Configure Audio Amp control pin to save power - amp_pin = Pin(46, Pin.OUT, value=0) + if sys.platform != 'rp2': + # Configure Audio Amp control pin to save power + amp_pin = Pin(46, Pin.OUT, value=0) # 2. Initialize utility objects - sensor = SHTC3(i2c) - rtc_chip = PCF85063(i2c) + sensor = None + rtc_chip = None + if i2c is not None: + try: + sensor = SHTC3(i2c) + except Exception as e: + print("Failed to initialize SHTC3:", e) + try: + rtc_chip = PCF85063(i2c) + except Exception as e: + print("Failed to initialize PCF85063:", e) + battery = BatteryMonitor() - led = BoardLED(38) - buttons = BoardButtons() + led = BoardLED() + buttons = None + if sys.platform != 'rp2': + buttons = BoardButtons() ble_uart = BLEUART(name="ESP32-S3-RLCD") # Sync system clock from RTC chip - rtc_chip.sync_to_system() + if rtc_chip: + try: + rtc_chip.sync_to_system() + except Exception as e: + print("Failed to sync clock:", e) - # 3. Connect to Wi-Fi status check (boot.py already attempted connection) - wlan = network.WLAN(network.STA_IF) - ip_addr = wlan.ifconfig()[0] if wlan.isconnected() else "Disconnected" - - # 4. Start MCP Server - mcp = MCPServer(display, led, battery, sensor, rtc_chip, ble_uart) - mcp.start(port=80) + # 3. Connect to Wi-Fi status check + ip_addr = "Offline (USB)" + mcp = DummyMCP() + vstream = DummyVStream() + if has_network: + try: + wlan = network.WLAN(network.STA_IF) + ip_addr = wlan.ifconfig()[0] if wlan.isconnected() else "Disconnected" + + # 4. Start background TCP/UDP Video Streaming Server + from video_stream import VideoStreamServer + vstream = VideoStreamServer(display, tcp_port=8081, udp_port=8082) + vstream.start() + + # 4b. Start MCP Server + from mcp_server import MCPServer + mcp = MCPServer(display, led, battery, sensor, rtc_chip, ble_uart, vstream=vstream) + mcp.start(port=80) + except Exception as ne: + print("Failed to start network services:", ne) + # Default to Green Breathing led_modes[local_led_mode_idx][1](led) mcp.active_led_mode = led_modes[local_led_mode_idx][2] @@ -87,12 +161,14 @@ def main(): mcp.override_active = False force_dashboard_redraw = True - buttons.key.on_click(on_key_click) - buttons.boot.on_click(on_boot_click) + if buttons: + buttons.key.on_click(on_key_click) + buttons.boot.on_click(on_boot_click) # Loop state last_dashboard_update = 0 dashboard_update_interval_ms = 5000 + last_led_update = 0 print("ESP32 MCP loop running...") @@ -100,18 +176,30 @@ def main(): while True: now = time.ticks_ms() + # Check touch interaction if available + if touch and touch.is_touched(): + pt = touch.read_touch() + if pt: + tx, ty = pt + print(f"Touch detected at: ({tx}, {ty})") + last_action_str = f"Touch: ({tx}, {ty})" + mcp.override_active = False + force_dashboard_redraw = True + # A. Handle non-blocking MCP client connection updates mcp.update() - - # C. Draw local dashboard (if not overridden by MCP draw text commands) - if not mcp.override_active: + # B. Handle non-blocking background video stream updates + vstream.update() + + # C. Draw local dashboard (if not overridden by MCP draw text commands or active video stream) + if not mcp.override_active and not vstream.active: if force_dashboard_redraw or time.ticks_diff(now, last_dashboard_update) >= dashboard_update_interval_ms: force_dashboard_redraw = False last_dashboard_update = now # Fetch sensor data - t, h = sensor.read_sensor() + t, h = sensor.read_sensor() if sensor else (None, None) t_str = f"{t} C" if t is not None else "Error" h_str = f"{h} %" if h is not None else "Error" @@ -121,45 +209,54 @@ def main(): bat_str = f"{bat_v:.2f}V ({bat_p}%)" if bat_v is not None else "Error" # Fetch current time - dt = rtc_chip.get_datetime() + dt = rtc_chip.get_datetime() if rtc_chip else None time_str = f"{dt[0]:04d}-{dt[1]:02d}-{dt[2]:02d} {dt[4]:02d}:{dt[5]:02d}:{dt[6]:02d}" if dt else "RTC Error" # Draw standard status dashboard layout display.clear(0) - display.text("ESP32-S3-RLCD MCP SERVER", 10, 10, 1) - display.line(10, 20, 390, 20, 1) + title_text = "RP2350-TFT MCP SERVER" if sys.platform == 'rp2' else "ESP32-S3-RLCD MCP SERVER" + line_w = 470 if sys.platform == 'rp2' else 390 + + display.text(title_text, 10, 10, 1) + display.line(10, 20, line_w, 20, 1) display.text_large("ENVIRONMENT", 15, 30, scale=2, c=1) display.text(f"Temp : {t_str}", 25, 55, 1) display.text(f"Humid : {h_str}", 25, 70, 1) - display.line(10, 95, 390, 95, 1) + display.line(10, 95, line_w, 95, 1) display.text_large("MCP NET CONNECTION", 15, 105, scale=2, c=1) display.text(f"IP Address : {ip_addr}", 25, 130, 1) display.text(f"Port / Path : 80 /api/mcp", 25, 145, 1) display.text(f"BLE Name : ESP32-S3-RLCD", 25, 160, 1) - display.line(10, 185, 390, 185, 1) + display.line(10, 185, line_w, 185, 1) display.text_large("SYSTEM STATUS", 15, 195, scale=2, c=1) display.text(f"Battery : {bat_str}", 25, 220, 1) display.text(f"Time : {time_str}", 25, 235, 1) - display.line(10, 255, 390, 255, 1) + display.line(10, 255, line_w, 255, 1) display.text(f"Status: {last_action_str}", 15, 265, 1) display.show() # D. Update NeoPixel animation smoothly (runs every 50ms) - mode = mcp.active_led_mode - if mode == "breath": - led.update_breathing(1.5) - elif mode == "rainbow": - led.update_rainbow(0.4) - elif mode == "off": - led.off() + if time.ticks_diff(now, last_led_update) >= 50: + last_led_update = now + mode = mcp.active_led_mode + if mode == "breath": + led.update_breathing(1.5) + elif mode == "rainbow": + led.update_rainbow(0.4) + elif mode == "off": + led.off() - time.sleep_ms(50) + # Poll rapidly if stream is active, otherwise sleep 50ms to save power + if vstream.active: + time.sleep_ms(2) + else: + time.sleep_ms(50) if __name__ == "__main__": main() diff --git a/mcp_server.py b/mcp_server.py index 4878c78..c852eee 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -5,13 +5,14 @@ import time class MCPServer: """A lightweight JSON-RPC HTTP server implementing Model Context Protocol (MCP) endpoints.""" - def __init__(self, display, led, battery, sensor, rtc, ble): + def __init__(self, display, led, battery, sensor, rtc, ble, vstream=None): self.display = display self.led = led self.battery = battery self.sensor = sensor self.rtc = rtc self.ble = ble + self.vstream = vstream self.sock = None self.active_led_mode = "off" # static, breath, rainbow, off @@ -128,7 +129,7 @@ class MCPServer: "tools": [ { "name": "clear_screen", - "description": "Clear the 400x300 screen to white (0) or black (1).", + "description": "Clear the 480x320 screen to white (0) or black (1).", "inputSchema": { "type": "object", "properties": { @@ -144,8 +145,8 @@ class MCPServer: "type": "object", "properties": { "text": {"type": "string", "description": "The message to display"}, - "x": {"type": "integer", "description": "X coordinate (0-390)"}, - "y": {"type": "integer", "description": "Y coordinate (0-290)"}, + "x": {"type": "integer", "description": "X coordinate (0-470)"}, + "y": {"type": "integer", "description": "Y coordinate (0-310)"}, "size": {"type": "integer", "enum": [1, 2], "description": "Text scale (1=normal, 2=large)"} }, "required": ["text", "x", "y"] @@ -297,6 +298,21 @@ class MCPServer: }, "required": ["url", "filename"] } + }, + { + "name": "get_video_streaming_instructions", + "description": "Get detailed instructions and sample Python code to stream video directly into the RLCD screen using TCP or UDP.", + "inputSchema": { + "type": "object", + "properties": { + "protocol": {"type": "string", "enum": ["tcp", "udp", "both"], "description": "The streaming protocol to query (default 'both')"} + } + } + }, + { + "name": "get_stream_stats", + "description": "Get real-time device-side performance and frame-rate statistics for the TCP/UDP video stream.", + "inputSchema": {"type": "object", "properties": {}} } ] }, @@ -568,5 +584,45 @@ class MCPServer: else: raise RuntimeError(f"Failed to download file from '{url}'.") + elif name == "get_video_streaming_instructions": + protocol = str(args.get("protocol", "both")).lower() + + instructions = [ + "### RP2350 TFT Video Streaming Instructions", + "Dimensions: 400x300 (decoded and centered automatically on the 480x320 screen), 1-bit monochrome (Floyd-Steinberg dithered).", + "Frame Buffer Size: 15,000 bytes. The host must convert and map standard pixels into the specific RLCD hardware buffer layout before sending.", + "Mapping logic (Python):", + " def map_to_rlcd(pil_img):", + " img_1bit = pil_img.convert('1', dither=1)", + " px = img_1bit.load()", + " buf = bytearray(15000)", + " for y in range(300):", + " for x in range(400):", + " if px[x, y]:", + " inv_y = 299 - y", + " bx = x // 2", + " by = inv_y // 4", + " idx = bx * 75 + by", + " lx, ly = x % 2, inv_y % 4", + " bit = 7 - (ly * 2 + lx)", + " buf[idx] |= (1 << bit)", + " return buf" + ] + + if protocol in ("tcp", "both"): + instructions.append("\n**TCP Streaming (Port 8081):**") + instructions.append("Open a TCP connection to the device's IP on port 8081 and send consecutive 15,000-byte frame blocks.") + + if protocol in ("udp", "both"): + instructions.append("\n**UDP Streaming / Broadcast (Port 8082):**") + instructions.append("Split the 15,000-byte frame into 15 chunks of 1,000 bytes each. Send each chunk as a 1002-byte packet: byte 0 = frame_id (0-255), byte 1 = chunk_idx (0-14), bytes 2..1001 = chunk payload. Send to port 8082 (unicast or broadcast).") + + return "\n".join(instructions) + + elif name == "get_stream_stats": + if self.vstream is None: + return json.dumps({"error": "Video stream server not initialized."}) + return json.dumps(self.vstream.get_stats()) + else: raise ValueError(f"Unknown tool: {name}") diff --git a/note.txt b/note.txt new file mode 100644 index 0000000..69866d1 --- /dev/null +++ b/note.txt @@ -0,0 +1,17 @@ +Hey there This is now a note taking app. + +What do you think of this? + +This is going to be pretty cool if you ask me!!! + + +Now the themes are supper cool too!!!! + +What is going to happend if I write a line that is too long would it wrapped yes!!! + +It also scrolls for vertical overflow!! + + + + + diff --git a/note_2026-06-04.txt b/note_2026-06-04.txt new file mode 100644 index 0000000..cbebc7b --- /dev/null +++ b/note_2026-06-04.txt @@ -0,0 +1,5 @@ +Hey there buddy! +I think you are going to like this DEVICE! +I made it for you, so you can write your hearth into it!... +Love you... DAD + diff --git a/notetaker.py b/notetaker.py new file mode 100644 index 0000000..edda74d --- /dev/null +++ b/notetaker.py @@ -0,0 +1,490 @@ +#!/usr/bin/env python3 +import socket +import time +import sys +import os +import argparse +import curses +import datetime +from PIL import Image, ImageDraw, ImageFont + +# Screen Dimensions +WIDTH = 400 +HEIGHT = 300 + +# Margins and Layout +MARGIN_LEFT = 20 +MARGIN_RIGHT = 380 +TOP_LIMIT = 35 +BOTTOM_LIMIT = 265 + +# Precompute destination byte mapping for RLCD hardware buffer +DEST_BYTE_MAP = [] +for dst_idx in range(15000): + byte_x = dst_idx // 75 + block_y = dst_idx % 75 + x_base = 2 * byte_x + y_base = HEIGHT - 1 - 4 * block_y + + bits_map = [] + for local_y in range(4): + y = y_base - local_y + for local_x in range(2): + x = x_base + local_x + src_byte_idx = y * 50 + (x // 8) + src_bit_idx = 7 - (x % 8) + src_mask = 1 << src_bit_idx + dst_mask = 1 << (7 - (local_y * 2 + local_x)) + bits_map.append((src_byte_idx, src_mask, dst_mask)) + DEST_BYTE_MAP.append(tuple(bits_map)) + + +def map_to_rlcd_hw_buffer(img_1bit): + """Highly optimized byte-level 1-bit monochrome image mapping to Waveshare RLCD buffer.""" + img_bytes = img_1bit.tobytes() + hw_buffer = bytearray(15000) + for dst_idx in range(15000): + val = 0 + for src_idx, src_mask, dst_mask in DEST_BYTE_MAP[dst_idx]: + if img_bytes[src_idx] & src_mask: + val |= dst_mask + hw_buffer[dst_idx] = val + return hw_buffer + + +def wrap_text_by_width(text, cursor_pos, font, max_width=360): + """ + Wraps text to fit within max_width pixels. + Returns: + lines: list of strings (the wrapped lines) + line_starts: list of starting indices of each line in the raw text + cursor_line: index of line containing cursor + cursor_col: index of column in that line (in terms of character index in the line) + """ + lines = [] + line_starts = [] + cursor_line = 0 + cursor_col = 0 + + current_char_idx = 0 + paragraphs = text.split('\n') + + # Create a dummy draw context to measure text lengths + dummy_im = Image.new("1", (1, 1)) + dummy_draw = ImageDraw.Draw(dummy_im) + + for p_idx, p in enumerate(paragraphs): + if not p: + if current_char_idx <= cursor_pos <= current_char_idx: + cursor_line = len(lines) + cursor_col = 0 + lines.append("") + line_starts.append(current_char_idx) + current_char_idx += 1 # for the '\n' + continue + + words = p.split(' ') + curr_line = "" + curr_line_start = current_char_idx + + for w in words: + if not curr_line: + test_line = w + else: + test_line = curr_line + " " + w + + w_len = dummy_draw.textlength(test_line, font=font) + if w_len <= max_width: + curr_line = test_line + else: + # Wrap current line + if curr_line_start <= cursor_pos <= curr_line_start + len(curr_line): + cursor_line = len(lines) + cursor_col = cursor_pos - curr_line_start + lines.append(curr_line) + line_starts.append(curr_line_start) + + # Check for long words + w_start = curr_line_start + len(curr_line) + 1 # +1 for space/wrap boundary + while dummy_draw.textlength(w, font=font) > max_width: + part_len = 1 + while part_len <= len(w) and dummy_draw.textlength(w[:part_len], font=font) <= max_width: + part_len += 1 + part = w[:part_len - 1] + if not part: + part = w[0] + part_len = 2 + + if w_start <= cursor_pos <= w_start + len(part): + cursor_line = len(lines) + cursor_col = cursor_pos - w_start + lines.append(part) + line_starts.append(w_start) + w_start += len(part) + w = w[part_len - 1:] + + curr_line = w + curr_line_start = w_start + + if curr_line or p_idx == len(paragraphs) - 1: + if curr_line_start <= cursor_pos <= curr_line_start + len(curr_line): + cursor_line = len(lines) + cursor_col = cursor_pos - curr_line_start + lines.append(curr_line) + line_starts.append(curr_line_start) + current_char_idx = curr_line_start + len(curr_line) + + current_char_idx += 1 # for the '\n' + + return lines, line_starts, cursor_line, cursor_col + + +def render_screen(text, cursor_pos, scroll_top, font, char_h, line_spacing, max_lines, dark_mode, show_cursor, status_msg=""): + # Create 1-bit image (0 = Black background) + if dark_mode: + bg_color = 0 # Black background + text_color = 1 # White text + accent_color = 1 # White elements + rule_color = 1 # White dotted lines + else: + bg_color = 1 # White background + text_color = 0 # Black text + accent_color = 0 # Black elements + rule_color = 0 # Black dotted lines + + img = Image.new("1", (WIDTH, HEIGHT), bg_color) + draw = ImageDraw.Draw(img) + + # Load clean, non-clipping UI font for header/footer + try: + ui_font = ImageFont.load_default(size=12) + except TypeError: + ui_font = ImageFont.load_default() + + # 1. Wrap the text and get the cursor location (max_width = 360) + lines, line_starts, cursor_line, cursor_col = wrap_text_by_width(text, cursor_pos, font, max_width=360) + + # 2. Adjust scrolling + if cursor_line >= scroll_top + max_lines: + scroll_top = cursor_line - max_lines + 1 + elif cursor_line < scroll_top: + scroll_top = cursor_line + + # 3. Draw Header Title Bar + draw.rectangle([0, 0, WIDTH - 1, 28], fill=text_color) + + # Display note title + date + today_str = datetime.date.today().strftime("%a, %b %d, %Y").upper() + header_title = f"KID KEEPER - {today_str}" + draw.text((15, 7), header_title, fill=bg_color, font=ui_font) + + # Header stats (word and character count) + word_count = len(text.split()) + char_count = len(text) + stats_str = f"W:{word_count} C:{char_count}" + stats_bbox = draw.textbbox((0, 0), stats_str, font=ui_font) + stats_w = stats_bbox[2] - stats_bbox[0] + draw.text((WIDTH - stats_w - 15, 7), stats_str, fill=bg_color, font=ui_font) + + # 4. Draw Notebook Ruled Paper Lines (aligned with baseline of the font size) + for i in range(max_lines): + y_line = TOP_LIMIT + line_spacing * i + char_h + 5 + # Prevent drawing paper lines past bottom limit + if y_line < BOTTOM_LIMIT + 5: + for x in range(MARGIN_LEFT, MARGIN_RIGHT, 4): + draw.point((x, y_line), fill=rule_color) + + # 5. Draw Visible Text Lines + visible_lines = lines[scroll_top:scroll_top + max_lines] + for idx, line_text in enumerate(visible_lines): + y_pos = TOP_LIMIT + line_spacing * idx + 2 + draw.text((MARGIN_LEFT, y_pos), line_text, fill=text_color, font=font) + + # 6. Draw Blinking Text Cursor + if show_cursor: + cursor_line_text = lines[cursor_line] + text_before_cursor = cursor_line_text[:cursor_col] + cursor_x = MARGIN_LEFT + draw.textlength(text_before_cursor, font=font) + cursor_y = TOP_LIMIT + (cursor_line - scroll_top) * line_spacing + 2 + if MARGIN_LEFT <= cursor_x <= MARGIN_RIGHT: + # Draw a thick cursor bar (2px width) + draw.rectangle([cursor_x, cursor_y, cursor_x + 1, cursor_y + char_h + 2], fill=text_color) + + # 7. Draw Bottom Status Bar + draw.line((0, BOTTOM_LIMIT + 8, WIDTH - 1, BOTTOM_LIMIT + 8), fill=accent_color) + + help_text = "Ctrl+S: Save Ctrl+T: Theme Ctrl+F: Font Ctrl+X: Exit" + draw.text((15, BOTTOM_LIMIT + 12), help_text, fill=text_color, font=ui_font) + + if status_msg: + msg_bbox = draw.textbbox((0, 0), status_msg, font=ui_font) + msg_w = msg_bbox[2] - msg_bbox[0] + draw.rectangle([WIDTH - msg_w - 20, BOTTOM_LIMIT + 10, WIDTH - 5, HEIGHT - 2], fill=bg_color) + draw.text((WIDTH - msg_w - 15, BOTTOM_LIMIT + 12), status_msg, fill=text_color, font=ui_font) + + return img, scroll_top + + +def load_font(font_path, size): + """Safely loads a TTF font and computes its vertical metric using 'Hyg' ascenders/descenders.""" + try: + if font_path and os.path.exists(font_path): + font = ImageFont.truetype(font_path, size=size) + else: + font = ImageFont.load_default(size=size) + except Exception: + font = ImageFont.load_default() + + # Measure character height using ascenders and descenders + test_img = Image.new("1", (100, 100)) + test_draw = ImageDraw.Draw(test_img) + bbox = test_draw.textbbox((0, 0), "Hyg", font=font) + char_h = bbox[3] - bbox[1] + if char_h <= 0: + char_h = 12 + return font, char_h + + +def run_editor(stdscr, args): + # --- Curses Initialization --- + stdscr.nodelay(True) # Non-blocking input + curses.noecho() # Hide echoing + stdscr.keypad(True) # Handle arrow keys + + # Configure kid-friendly font choices + # Format: (path, size, name) + font_folder = "fonts" + if not os.path.exists(font_folder): + font_folder = os.path.expanduser("~/fonts") + + font_options = [ + (os.path.join(font_folder, "PatrickHand.ttf"), 22, "Handwritten"), + (os.path.join(font_folder, "CourierPrime.ttf"), 20, "Typewriter"), + (None, 14, "Default Monospace") + ] + font_idx = 0 + + # Load first font + f_path, f_size, f_name = font_options[font_idx] + font, char_h = load_font(f_path, f_size) + + # 2. Load note from file (use date-specific note by default) + note_text = "" + if os.path.exists(args.file): + try: + with open(args.file, "r") as f: + note_text = f.read() + except: + pass + + cursor_pos = len(note_text) + scroll_top = 0 + dark_mode = False + + # 3. Connect to the ESP32 TCP Stream Server + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(5.0) + try: + sock.connect((args.ip, args.port)) + except Exception as e: + curses.endwin() + print(f"\nConnection failed to {args.ip}:{args.port} -- {e}") + print("Please verify the ESP32 is powered on and connected to Wi-Fi.") + sys.exit(1) + + sock.setblocking(False) + + # State variables + last_blink_time = time.time() + show_cursor = True + status_msg = f"Font: {f_name}" + status_msg_expiry = time.time() + 2.0 + redraw = True + + while True: + now = time.time() + + # Calculate text layout dynamically based on current font height + line_spacing = char_h + 8 + max_lines_on_screen = (BOTTOM_LIMIT - TOP_LIMIT) // line_spacing + + # Cursor blink check (toggles every 500ms) + if now - last_blink_time >= 0.5: + show_cursor = not show_cursor + last_blink_time = now + redraw = True + + # Expiry check for status message + if status_msg and now > status_msg_expiry: + status_msg = "" + redraw = True + + # Read all pending characters from curses + keys = [] + while True: + try: + k = stdscr.get_wch() + keys.append(k) + except curses.error: + break + + if keys: + redraw = True + show_cursor = True + last_blink_time = now + + for key in keys: + if isinstance(key, str): + if key == '\x18': # Ctrl+X: Save and Exit + status_msg = "Saving..." + img, _ = render_screen(note_text, cursor_pos, scroll_top, font, char_h, line_spacing, max_lines_on_screen, dark_mode, False, status_msg) + try: + sock.sendall(map_to_rlcd_hw_buffer(img)) + except: + pass + try: + with open(args.file, "w") as f: + f.write(note_text) + except: + pass + sock.close() + return + + elif key == '\x13': # Ctrl+S: Save Note + try: + with open(args.file, "w") as f: + f.write(note_text) + status_msg = "Saved!" + except: + status_msg = "Err Save" + status_msg_expiry = now + 2.0 + + elif key == '\x14': # Ctrl+T: Toggle Theme + dark_mode = not dark_mode + status_msg = "Theme Dark" if dark_mode else "Theme Light" + status_msg_expiry = now + 2.0 + + elif key == '\x06': # Ctrl+F: Cycle Fonts + font_idx = (font_idx + 1) % len(font_options) + f_path, f_size, f_name = font_options[font_idx] + font, char_h = load_font(f_path, f_size) + status_msg = f"Font: {f_name}" + status_msg_expiry = now + 2.0 + + # Backspace + elif key in ('\x7f', '\x08'): + if cursor_pos > 0: + note_text = note_text[:cursor_pos - 1] + note_text[cursor_pos:] + cursor_pos -= 1 + + # Enter: Newline + elif key in ('\r', '\n'): + note_text = note_text[:cursor_pos] + "\n" + note_text[cursor_pos:] + cursor_pos += 1 + + # Normal typed characters + elif ord(key) >= 32: + note_text = note_text[:cursor_pos] + key + note_text[cursor_pos:] + cursor_pos += 1 + + elif isinstance(key, int): + lines, line_starts, cursor_line, cursor_col = wrap_text_by_width(note_text, cursor_pos, font, max_width=360) + + if key == curses.KEY_UP: + if cursor_line > 0: + target_line = cursor_line - 1 + target_col = min(cursor_col, len(lines[target_line])) + cursor_pos = line_starts[target_line] + target_col + + elif key == curses.KEY_DOWN: + if cursor_line < len(lines) - 1: + target_line = cursor_line + 1 + target_col = min(cursor_col, len(lines[target_line])) + cursor_pos = line_starts[target_line] + target_col + + elif key == curses.KEY_RIGHT: + if cursor_pos < len(note_text): + cursor_pos += 1 + + elif key == curses.KEY_LEFT: + if cursor_pos > 0: + cursor_pos -= 1 + + elif key == curses.KEY_BACKSPACE: + if cursor_pos > 0: + note_text = note_text[:cursor_pos - 1] + note_text[cursor_pos:] + cursor_pos -= 1 + + if redraw: + redraw = False + # Render to RLCD and stream + img, scroll_top = render_screen(note_text, cursor_pos, scroll_top, font, char_h, line_spacing, max_lines_on_screen, dark_mode, show_cursor, status_msg) + try: + raw_bytes = map_to_rlcd_hw_buffer(img) + sock.sendall(raw_bytes) + except BlockingIOError: + pass + except: + break + + # Draw Console UI on terminal screen + stdscr.erase() + h, w = stdscr.getmaxyx() + + # Header + stdscr.attron(curses.A_REVERSE) + today_header = datetime.date.today().strftime("%A, %B %d, %Y") + header_str = f" NOTE KEEPER | {today_header} " + stdscr.addstr(0, 0, header_str + " " * (w - len(header_str) - 1)) + stdscr.attroff(curses.A_REVERSE) + + # Note Content + stdscr.addstr(2, 0, f"--- Note File: {args.file} (Font: {f_name}) ---", curses.A_BOLD) + + # Wrap text to display in console + term_lines, _, term_cursor_line, term_cursor_col = wrap_text_by_width(note_text, cursor_pos, font, max_width=360) + + for idx, line in enumerate(term_lines): + if 4 + idx < h - 3: + stdscr.addstr(4 + idx, 2, line) + + # Footer + stdscr.attron(curses.A_REVERSE) + footer_str = " Ctrl+S: Save | Ctrl+T: Theme | Ctrl+F: Font | Ctrl+X: Save & Exit " + stdscr.addstr(h - 1, 0, footer_str + " " * (w - len(footer_str) - 1)) + stdscr.attroff(curses.A_REVERSE) + + # Corner stats + if status_msg: + stdscr.addstr(h - 2, w - len(status_msg) - 2, f"[{status_msg}]", curses.A_BOLD) + else: + stdscr.addstr(h - 2, w - 18, f"Chars: {len(note_text)}") + + # Position Terminal Cursor + try: + stdscr.move(4 + term_cursor_line, 2 + term_cursor_col) + except: + pass + + stdscr.refresh() + + time.sleep(0.01) + + +def main(): + # 1. Determine daily default filename: note_YYYY-MM-DD.txt + today_str = datetime.date.today().strftime("%Y-%m-%d") + default_filename = f"note_{today_str}.txt" + + parser = argparse.ArgumentParser(description="Curses Pi Zero Kid Note Taking Streaming App") + parser.add_argument("--ip", default="192.168.68.123", help="Target ESP32-S3 IP Address (default: 192.168.68.123)") + parser.add_argument("--port", type=int, default=8081, help="Streaming TCP Port (default: 8081)") + parser.add_argument("--file", default=default_filename, help=f"Filename to persist notes (default: {default_filename})") + args = parser.parse_args() + + curses.wrapper(run_editor, args) + print(f"\nExited. Note saved to {args.file}. Thank you for using Note Keeper!") + + +if __name__ == "__main__": + main() diff --git a/rgb_led_util.py b/rgb_led_util.py index 8365bf5..95447e9 100644 --- a/rgb_led_util.py +++ b/rgb_led_util.py @@ -6,7 +6,10 @@ import neopixel class BoardLED: """Utility class to control the onboard WS2812 (NeoPixel) RGB LED on GPIO 38.""" - def __init__(self, pin_num=38): + def __init__(self, pin_num=None): + import sys + if pin_num is None: + pin_num = 14 if sys.platform == 'rp2' else 38 self.np = neopixel.NeoPixel(Pin(pin_num), 1) self.base_color = (0, 0, 0) self.off() diff --git a/rlcd.py b/rlcd.py index 766551a..a0ad832 100644 --- a/rlcd.py +++ b/rlcd.py @@ -129,13 +129,19 @@ class RLCD: self.write_cmd(0x3A); self.write_data(0x11) self.write_cmd(0xB9); self.write_data(0x20) self.write_cmd(0xB8); self.write_data(0x29) - self.write_cmd(0x20) # Inversion OFF (White Background) + self.write_cmd(0x21) # Inversion ON (Black Background by default) self.write_cmd(0x2A); self.write_data([0x12, 0x2A]) self.write_cmd(0x2B); self.write_data([0x00, 0xC7]) self.write_cmd(0x35); self.write_data(0x00) self.write_cmd(0xD0); self.write_data(0xFF) self.write_cmd(0x38); self.write_cmd(0x29) + def invert(self, enable): + if enable: + self.write_cmd(0x21) # Inversion ON (Black Background) + else: + self.write_cmd(0x20) # Inversion OFF (White Background) + # --- THE HEAVY LIFTER (Optimized) --- @micropython.native def show(self): diff --git a/scratch/delete_all.py b/scratch/delete_all.py new file mode 100644 index 0000000..00a5ee1 --- /dev/null +++ b/scratch/delete_all.py @@ -0,0 +1,86 @@ +import serial +import time +import sys + +PORT = '/dev/cu.usbmodem101' +print("=== MicroPython Robust Flash Wipe Tool ===") +print(f"Waiting for a clean, active connection on {PORT}...") +print("Please press the physical RESET button on the board now...") + +ser = None +while True: + try: + # Open port + ser = serial.Serial(PORT, 115200, timeout=1.0) + # Try writing Ctrl-C to verify if the link is active + ser.write(b'\x03') + time.sleep(0.01) + ser.write(b'\x03') + + # If write succeeded, we have an active link! + print("\n[+] Active connection established!") + break + except (serial.SerialException, OSError, Exception) as e: + if ser is not None: + try: + ser.close() + except: + pass + ser = None + # Print dot to show progress, stay on same line + print(".", end="") + sys.stdout.flush() + time.sleep(0.1) + +print("[*] Sending remaining Ctrl-C storm to break into REPL...") +for _ in range(80): + try: + ser.write(b'\x03') + time.sleep(0.005) + except Exception as e: + print(f"\n[-] Write error during storm: {e}") + break + +time.sleep(0.1) +try: + ser.reset_input_buffer() +except Exception as e: + print(f"[-] Buffer reset failed: {e}") + +# Check if we have REPL access +print("[*] Verifying REPL responsiveness...") +try: + ser.write(b'\r\n') + time.sleep(0.2) + if ser.in_waiting: + resp = ser.read(ser.in_waiting) + print(f"REPL Response:\n{resp.decode('utf-8', errors='ignore')}") + else: + ser.write(b'\x03\r\n') + time.sleep(0.2) + resp = ser.read(ser.in_waiting) + print(f"REPL Response:\n{resp.decode('utf-8', errors='ignore')}") +except Exception as e: + print(f"[-] REPL verification failed: {e}") + ser.close() + sys.exit(1) + +print("[*] Deleting all files on the flash...") +wipe_code = b"import os; print('FILES_BEFORE:', os.listdir()); [os.remove(f) for f in os.listdir() if not (os.stat(f)[0] & 0x4000)]; print('FILES_AFTER:', os.listdir()); print('WIPE_SUCCESS')\r\n" + +try: + ser.write(wipe_code) + time.sleep(1.0) + if ser.in_waiting: + result = ser.read(ser.in_waiting).decode('utf-8', errors='ignore') + print(f"Execution Output:\n{result}") + if "WIPE_SUCCESS" in result: + print("[+] SUCCESS! Flash has been wiped clean.") + else: + print("[-] Wipe did not finish successfully.") + else: + print("[-] No response to wipe command.") +except Exception as e: + print(f"[-] Error during wipe command execution: {e}") + +ser.close() diff --git a/scratch/interact_repl.py b/scratch/interact_repl.py new file mode 100644 index 0000000..16a1e9c --- /dev/null +++ b/scratch/interact_repl.py @@ -0,0 +1,39 @@ +import serial +import time + +print("Opening serial port /dev/cu.usbmodem101...") +try: + ser = serial.Serial('/dev/cu.usbmodem101', 115200, timeout=1.0) + print("Opened successfully!") + + # Clear input buffer + ser.reset_input_buffer() + + # Send Ctrl-C to interrupt anything + print("Sending Ctrl-C...") + ser.write(b'\x03') + time.sleep(0.2) + + # Read response + if ser.in_waiting: + print(f"After Ctrl-C: {ser.read(ser.in_waiting)}") + + # Send newline + print("Sending newline...") + ser.write(b'\r\n') + time.sleep(0.2) + if ser.in_waiting: + print(f"After newline: {ser.read(ser.in_waiting)}") + + # Send test print command + print("Sending test print command...") + ser.write(b"print('REPL_ACTIVE')\r\n") + time.sleep(0.5) + if ser.in_waiting: + print(f"After command: {ser.read(ser.in_waiting)}") + else: + print("No response to command.") + + ser.close() +except Exception as e: + print(f"Error: {e}") diff --git a/scratch/listen_touch.py b/scratch/listen_touch.py new file mode 100644 index 0000000..88491dc --- /dev/null +++ b/scratch/listen_touch.py @@ -0,0 +1,24 @@ +import serial +import time +import sys + +PORT = '/dev/cu.usbmodem101' +print("=== MicroPython Serial Event Listener ===") +print(f"Opening {PORT} at 115200...") +try: + ser = serial.Serial(PORT, 115200, timeout=1.0) + print("Opened successfully! Press Ctrl+C on the host to stop listening.") + print("Listening for touch events or debug logs from the board...\n") + + # We do NOT send Ctrl-C to the board, we just read what it outputs + while True: + if ser.in_waiting: + data = ser.read(ser.in_waiting) + text = data.decode('utf-8', errors='ignore') + sys.stdout.write(text) + sys.stdout.flush() + time.sleep(0.05) +except KeyboardInterrupt: + print("\nStopped listening.") +except Exception as e: + print(f"\nError: {e}") diff --git a/scratch/nuke_boot.py b/scratch/nuke_boot.py new file mode 100644 index 0000000..3433ac8 --- /dev/null +++ b/scratch/nuke_boot.py @@ -0,0 +1,71 @@ +import serial +import time +import os +import sys + +PORT = '/dev/cu.usbmodem101' +print("=== MicroPython Boot Interrupter & Recovery Tool ===") +print(f"Waiting for {PORT} to appear. Please press the RESET button on the board...") + +while True: + try: + ser = serial.Serial(PORT, 115200, timeout=1.0) + print("\n[+] Port opened successfully!") + break + except Exception as e: + # Port not present or busy + time.sleep(0.01) + +print("[*] Sending Ctrl-C storm to halt boot sequence...") +# Send 100 Ctrl-C interrupts in rapid succession +for _ in range(100): + try: + ser.write(b'\x03') + time.sleep(0.005) + except Exception as e: + print(f"Write error during storm: {e}") + break + +time.sleep(0.1) +ser.reset_input_buffer() + +# Check if we have REPL access by sending a newline and expecting a prompt +print("[*] Verifying REPL responsiveness...") +ser.write(b'\r\n') +time.sleep(0.2) + +if ser.in_waiting: + resp = ser.read(ser.in_waiting) + print(f"REPL Response:\n{resp.decode('utf-8', errors='ignore')}") +else: + print("[-] No response to newline, trying to force it...") + ser.write(b'\x03\r\n') + time.sleep(0.2) + resp = ser.read(ser.in_waiting) + print(f"REPL Response:\n{resp.decode('utf-8', errors='ignore')}") + +# Try to rename main.py to stop it from running on subsequent boots +print("[*] Attempting to disable main.py...") +rename_code = b""" +import os +try: + os.rename('main.py', 'main_bad.py') + print('RENAME:OK') +except Exception as e: + print('RENAME:ERROR:', e) +""" +ser.write(rename_code + b'\r\n') +time.sleep(0.5) + +if ser.in_waiting: + result = ser.read(ser.in_waiting).decode('utf-8', errors='ignore') + print(f"Execution Output:\n{result}") + if "RENAME:OK" in result: + print("[+] SUCCESS! main.py has been disabled.") + print("[+] You can now safely upload files or reboot.") + else: + print("[-] Rename failed or returned unexpected output.") +else: + print("[-] No response to rename command.") + +ser.close() diff --git a/scratch/verify_actual_virtual_mcp.py b/scratch/verify_actual_virtual_mcp.py new file mode 100644 index 0000000..653b33c --- /dev/null +++ b/scratch/verify_actual_virtual_mcp.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +import sys +import os +import json +import time + +sys.path.append("/Users/adolforeyna/Projects/PiZeroNoteKeeper") +from kernel.system import SystemContext + +def run_test(): + context = SystemContext(target_ip="127.0.0.1", target_port=8081, pin_target=True) + # Start with wrong port (80 instead of 8080) to force fallback + context.mcp_port = 80 + + # Clear any initial status message from discovery + time.sleep(0.5) + context.status_msg = "" + + print("Sending play_beep command starting with port 80...") + context.play_beep(frequency=1200, duration_ms=40, volume=30) + + # Wait for fallback to execute (requires timeout retry) + time.sleep(2.0) + + # Verify fallback succeeded and updated port to 8080 + if context.mcp_port == 8080: + print("SUCCESS: Fallback automatically corrected mcp_port to 8080!") + else: + print("FAIL: Fallback did not correct port. Current port: {}, status: '{}'".format(context.mcp_port, context.status_msg)) + sys.exit(1) + +if __name__ == "__main__": + run_test() diff --git a/scratch/verify_launcher_beep.py b/scratch/verify_launcher_beep.py new file mode 100644 index 0000000..2fc66c2 --- /dev/null +++ b/scratch/verify_launcher_beep.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +import sys +import os +import json +import time +import http.server +import threading + +# Add PiZeroNoteKeeper to path so we can import kernel +sys.path.append("/Users/adolforeyna/Projects/PiZeroNoteKeeper") +from kernel.system import SystemContext + +# Mock HTTP Server to capture the POST request +class MockMCPServer(http.server.BaseHTTPRequestHandler): + received_request = None + + def do_POST(self): + if self.path == "/api/mcp": + content_length = int(self.headers['Content-Length']) + post_data = self.rfile.read(content_length) + MockMCPServer.received_request = json.loads(post_data.decode('utf-8')) + + # Send success response + self.send_response(200) + self.send_header('Content-Type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps({"jsonrpc": "2.0", "result": "OK", "id": "test"}).encode('utf-8')) + else: + self.send_response(404) + self.end_headers() + + def log_message(self, format, *args): + # Suppress logging to keep output clean + pass + +def run_test(): + # Start mock server + server = http.server.HTTPServer(('127.0.0.1', 9999), MockMCPServer) + server_thread = threading.Thread(target=server.serve_forever, daemon=True) + server_thread.start() + print("Mock MCP server running on http://127.0.0.1:9999") + + # Initialize SystemContext pointing to mock server + context = SystemContext(target_ip="127.0.0.1", target_port=8081, pin_target=True) + # Set the discovered/active mcp_port + context.mcp_port = 9999 + + print("Sending play_beep command...") + context.play_beep(frequency=1200, duration_ms=40, volume=30) + + # Wait for background thread to execute the HTTP request + timeout = 3.0 + start_time = time.time() + while MockMCPServer.received_request is None and (time.time() - start_time) < timeout: + time.sleep(0.1) + + # Shutdown server + server.shutdown() + server.server_close() + + # Assert and verify + req = MockMCPServer.received_request + if req is None: + print("FAIL: No request received by mock MCP server within timeout.") + sys.exit(1) + + print("Received request payload:") + print(json.dumps(req, indent=2)) + + try: + assert req["jsonrpc"] == "2.0" + assert req["method"] == "tools/call" + assert req["params"]["name"] == "play_tone" + args = req["params"]["arguments"] + assert args["frequency"] == 1200 + assert args["duration_ms"] == 40 + assert args["volume"] == 30 + print("\nSUCCESS: All request fields verified perfectly!") + except AssertionError as e: + print("\nFAIL: Request fields did not match expected values.", e) + sys.exit(1) + +if __name__ == "__main__": + run_test() diff --git a/st7796.py b/st7796.py new file mode 100644 index 0000000..c28ce01 --- /dev/null +++ b/st7796.py @@ -0,0 +1,249 @@ +import time +from machine import Pin, SPI +import framebuf +import micropython + +class ST7796: + def __init__(self, spi, cs, dc, rst, bl=None, width=480, height=320, invert_color=True): + self.spi = spi + self.cs = cs + self.dc = dc + self.rst = rst + self.bl = bl + self.width = width + self.height = height + self.invert_color = invert_color + + # 1. 1-bit Canvas Buffer (Standard MONO_HLSB for drawing) + self.hw_len = (self.width * self.height) // 8 + self.canvas_buffer = bytearray(self.hw_len) + self.canvas = framebuf.FrameBuffer(self.canvas_buffer, self.width, self.height, framebuf.MONO_HLSB) + + # Pre-allocate chunk buffer for conversion (16 rows: 480 * 16 * 2 = 15360 bytes) + self.chunk_rows = 16 + self.row_buffer = bytearray(self.width * self.chunk_rows * 2) + + # Initialize pins + self.cs.init(self.cs.OUT, value=1) + self.dc.init(self.dc.OUT, value=0) + self.rst.init(self.rst.OUT, value=1) + + if self.bl is not None: + # PWM or Pin backlight control + if isinstance(self.bl, Pin): + self.bl.init(self.bl.OUT, value=1) + + self.reset() + self.init_display() + self.clear(0) + self.show() + + # --- DRAWING WRAPPERS --- + def pixel(self, x, y, c): self.canvas.pixel(x, y, c) + def line(self, x1, y1, x2, y2, c): self.canvas.line(x1, y1, x2, y2, c) + def rect(self, x, y, w, h, c): self.canvas.rect(x, y, w, h, c) + def fill_rect(self, x, y, w, h, c): self.canvas.fill_rect(x, y, w, h, c) + def text(self, msg, x, y, c=1): self.canvas.text(msg, x, y, c) + def clear(self, c=0): self.canvas.fill(c) + + # --- SCALABLE TEXT --- + def text_large(self, msg, x, y, scale=2, c=1): + char_w = 8; char_h = 8 + tmp_buf = bytearray(char_w * char_h // 8) + tmp_fb = framebuf.FrameBuffer(tmp_buf, char_w, char_h, framebuf.MONO_HLSB) + for char in msg: + tmp_fb.fill(0); tmp_fb.text(char, 0, 0, 1) + for py in range(8): + for px in range(8): + if tmp_fb.pixel(px, py): + self.canvas.fill_rect(x + (px * scale), y + (py * scale), scale, scale, c) + x += (8 * scale) + + # --- RAW BITMAPS (1:1 scale) --- + def bitmap(self, x, y, w, h, pixel_data): + img = framebuf.FrameBuffer(pixel_data, w, h, framebuf.MONO_HLSB) + self.canvas.blit(img, x, y) + + # --- PBM FILE LOADER WITH SCALING --- + def draw_pbm(self, filename, x, y, scale=1): + try: + with open(filename, 'rb') as f: + line1 = f.readline() + if not line1.startswith(b'P4'): print("Err: Not P4 PBM"); return + while True: + line = f.readline() + if not line.startswith(b'#'): break + dims = line.split(); w = int(dims[0]); h = int(dims[1]) + data = bytearray(f.read()) + + src_fb = framebuf.FrameBuffer(data, w, h, framebuf.MONO_HLSB) + + if scale == 1: + self.canvas.blit(src_fb, x, y) + else: + for sy in range(h): + for sx in range(w): + if src_fb.pixel(sx, sy): + self.canvas.fill_rect(x + (sx * scale), y + (sy * scale), scale, scale, 1) + print(f"Loaded {filename} (scale {scale})") + except OSError: + print(f"Error: Could not open {filename}") + + # --- SCREENSHOT --- + def save_screenshot(self, filename): + print(f"Saving screenshot to {filename}...") + try: + with open(filename, 'wb') as f: + f.write(b'P4\n') + f.write(f"{self.width} {self.height}\n".encode()) + f.write(self.canvas_buffer) + print("Saved!") + except Exception as e: + print(f"Error saving screenshot: {e}") + + # --- HARDWARE LOGIC --- + def reset(self): + self.rst(1); time.sleep_ms(5); self.rst(0); time.sleep_ms(15); self.rst(1); time.sleep_ms(15) + + def write_cmd(self, cmd): + self.dc(0); self.cs(0); self.spi.write(bytearray([cmd])); self.cs(1) + + def write_data(self, data): + self.dc(1); self.cs(0) + if isinstance(data, int): self.spi.write(bytearray([data])) + elif isinstance(data, list): self.spi.write(bytearray(data)) + else: self.spi.write(data) + self.cs(1) + + def init_display(self): + # ST7796 Minimal Initialization Sequence (ported from working C++ codebase) + self.write_cmd(0x01) # SWRESET + time.sleep_ms(150) + + self.write_cmd(0x11) # SLPOUT + time.sleep_ms(120) + + # Pixel Format (COLMOD) = 0x55 (16-bit color / RGB565) + self.write_cmd(0x3A); self.write_data(0x55) + time.sleep_ms(10) + + # Memory Access Control (MADCTL) = 0xE0 (Landscape: MY=1, MX=1, MV=1, RGB order) + self.write_cmd(0x36); self.write_data(0xE0) + time.sleep_ms(10) + + # Display Inversion Control + if self.invert_color: + self.write_cmd(0x21) # INVON + else: + self.write_cmd(0x20) # INVOFF + time.sleep_ms(10) + + self.write_cmd(0x13) # NORON + time.sleep_ms(10) + + self.write_cmd(0x29) # DISPON + time.sleep_ms(120) + + def invert(self, enable): + if enable: + self.write_cmd(0x21) # INVON + else: + self.write_cmd(0x20) # INVOFF + + def set_window(self, x0, y0, x1, y1): + # Column Address Set (CASET) + self.write_cmd(0x2A) + self.write_data(bytearray([x0 >> 8, x0 & 0xFF, x1 >> 8, x1 & 0xFF])) + + # Row Address Set (RASET) + self.write_cmd(0x2B) + self.write_data(bytearray([y0 >> 8, y0 & 0xFF, y1 >> 8, y1 & 0xFF])) + + # Memory Write (RAMWR) + self.write_cmd(0x2C) + + @micropython.native + def _convert_rows(self, start_row, num_rows, row_buf): + """Converts 1-bit monochrome row segment to 16-bit RGB565 format. + Compiles block-wise bitwise operations at native speed. + """ + width = self.width + canvas_buf = self.canvas_buffer + idx = 0 + + for y in range(start_row, start_row + num_rows): + byte_offset = y * (width // 8) + for x_byte_idx in range(width // 8): + val = canvas_buf[byte_offset + x_byte_idx] + + # Unroll 8 bits for speed + # Bit 7 + if val & 0x80: + row_buf[idx] = 0xFF; row_buf[idx+1] = 0xFF + else: + row_buf[idx] = 0x00; row_buf[idx+1] = 0x00 + idx += 2 + + # Bit 6 + if val & 0x40: + row_buf[idx] = 0xFF; row_buf[idx+1] = 0xFF + else: + row_buf[idx] = 0x00; row_buf[idx+1] = 0x00 + idx += 2 + + # Bit 5 + if val & 0x20: + row_buf[idx] = 0xFF; row_buf[idx+1] = 0xFF + else: + row_buf[idx] = 0x00; row_buf[idx+1] = 0x00 + idx += 2 + + # Bit 4 + if val & 0x10: + row_buf[idx] = 0xFF; row_buf[idx+1] = 0xFF + else: + row_buf[idx] = 0x00; row_buf[idx+1] = 0x00 + idx += 2 + + # Bit 3 + if val & 0x08: + row_buf[idx] = 0xFF; row_buf[idx+1] = 0xFF + else: + row_buf[idx] = 0x00; row_buf[idx+1] = 0x00 + idx += 2 + + # Bit 2 + if val & 0x04: + row_buf[idx] = 0xFF; row_buf[idx+1] = 0xFF + else: + row_buf[idx] = 0x00; row_buf[idx+1] = 0x00 + idx += 2 + + # Bit 1 + if val & 0x02: + row_buf[idx] = 0xFF; row_buf[idx+1] = 0xFF + else: + row_buf[idx] = 0x00; row_buf[idx+1] = 0x00 + idx += 2 + + # Bit 0 + if val & 0x01: + row_buf[idx] = 0xFF; row_buf[idx+1] = 0xFF + else: + row_buf[idx] = 0x00; row_buf[idx+1] = 0x00 + idx += 2 + + def show(self): + """Refreshes the screen by writing the frame buffer segment-by-segment.""" + self.set_window(0, 0, self.width - 1, self.height - 1) + + self.dc(1) + self.cs(0) + + num_chunks = self.height // self.chunk_rows + for chunk in range(num_chunks): + start_row = chunk * self.chunk_rows + self._convert_rows(start_row, self.chunk_rows, self.row_buffer) + self.spi.write(self.row_buffer) + + self.cs(1) diff --git a/test_local_notetaker.py b/test_local_notetaker.py new file mode 100644 index 0000000..ca02c3d --- /dev/null +++ b/test_local_notetaker.py @@ -0,0 +1,42 @@ +import sys +import os +import datetime +from PIL import Image, ImageDraw, ImageFont + +# Add workspace to path to import notetaker functions +sys.path.append("/Users/adolforeyna/Projects/MicroPython/test1/Screen") +import notetaker + +def test_rendering(): + output_dir = "/Users/adolforeyna/.gemini/antigravity/brain/95f1ff45-7d9b-4493-ad2c-fdf647d73805" + text = "Adolfo Reyna Hey\nThis is a bigger font for my son!\nIt supports handwriting and typewriter styles." + cursor_pos = len(text) + scroll_top = 0 + + # Font folder + font_folder = "/Users/adolforeyna/Projects/MicroPython/test1/Screen/fonts" + + # Test cases: (font_path, font_size, font_name, output_filename) + test_cases = [ + (os.path.join(font_folder, "PatrickHand.ttf"), 22, "Handwritten", "render_kids_handwritten.png"), + (os.path.join(font_folder, "CourierPrime.ttf"), 20, "Typewriter", "render_kids_typewriter.png") + ] + + for font_path, font_size, font_name, filename in test_cases: + font, char_h = notetaker.load_font(font_path, font_size) + line_spacing = char_h + 8 + max_lines = (notetaker.BOTTOM_LIMIT - notetaker.TOP_LIMIT) // line_spacing + + img, scroll_top = notetaker.render_screen( + text, cursor_pos, scroll_top, font, + char_h, line_spacing, max_lines, + dark_mode=False, show_cursor=True, status_msg=f"Font: {font_name}" + ) + + dest_path = os.path.join(output_dir, filename) + img.save(dest_path) + print(f"Saved {font_name} layout test to {dest_path}") + print(f" char_h: {char_h}px, line_spacing: {line_spacing}px, max_lines: {max_lines}") + +if __name__ == "__main__": + test_rendering() diff --git a/test_stream.py b/test_stream.py new file mode 100644 index 0000000..0cf31d1 --- /dev/null +++ b/test_stream.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +import socket +import time +import argparse +import sys +from PIL import Image, ImageDraw, ImageFont + +# Dimensions of the reflective LCD +WIDTH = 400 +HEIGHT = 300 + +def map_to_rlcd_hw_buffer(img_1bit): + """Converts a standard 1-bit monochrome PIL Image to the Waveshare RLCD hardware buffer layout.""" + px = img_1bit.load() + hw_buffer = bytearray(15000) + + for y in range(HEIGHT): + for x in range(WIDTH): + if px[x, y]: # True if pixel is white (1) + inv_y = HEIGHT - 1 - y + byte_x = x // 2 + block_y = inv_y // 4 + index = byte_x * 75 + block_y + local_x = x % 2 + local_y = inv_y % 4 + bit = 7 - (local_y * 2 + local_x) + hw_buffer[index] |= (1 << bit) + + return hw_buffer + +def send_udp_frame_chunked(sock, esp32_ip, port, frame_idx, raw_bytes): + """Sends a 15,000-byte frame split into 15 small UDP packets to bypass MTU and OS limits.""" + frame_id = frame_idx % 256 + for chunk_idx in range(15): + packet = bytearray(1002) + packet[0] = frame_id + packet[1] = chunk_idx + start = chunk_idx * 1000 + packet[2:1002] = raw_bytes[start : start + 1000] + sock.sendto(packet, (esp32_ip, port)) + time.sleep(0.010) # Add small pacing delay to prevent network buffer flooding + +def generate_animation_frame(frame_idx, mode_name): + """Generates a test pattern animation frame using Pillow.""" + # Create 1-bit image (0 = black background) + img = Image.new("1", (WIDTH, HEIGHT), 0) + draw = ImageDraw.Draw(img) + + # Draw header text + draw.text((10, 10), "ESP32-S3 VIDEO STREAM TEST", fill=1) + draw.line((10, 22, 390, 22), fill=1) + + # Display active settings + draw.text((15, 30), f"Protocol : {mode_name}", fill=1) + draw.text((15, 45), f"Frame No : {frame_idx}", fill=1) + + # 1. Draw a bouncing rectangle + rect_w, rect_h = 60, 40 + bounce_x = int((frame_idx * 5) % (WIDTH - rect_w - 20)) + 10 + bounce_y = int(120 + 20 * (frame_idx % 10 < 5 and (frame_idx % 5) or (5 - frame_idx % 5))) + draw.rectangle([bounce_x, bounce_y, bounce_x + rect_w, bounce_y + rect_h], outline=1, width=2) + draw.text((bounce_x + 10, bounce_y + 15), "TCP/UDP", fill=1) + + # 2. Draw a spinning needle / line + import math + angle = frame_idx * 0.1 + center_x, center_y = 300, 200 + radius = 40 + end_x = center_x + radius * math.cos(angle) + end_y = center_y + radius * math.sin(angle) + draw.ellipse([center_x - radius, center_y - radius, center_x + radius, center_y + radius], outline=1) + draw.line((center_x, center_y, end_x, end_y), fill=1) + + # 3. Dynamic scrolling text at the bottom + scrolling_text = "Waveshare RLCD 4.2inch -- Low Power -- High Framerate Streaming in MicroPython" + scroll_pos = (frame_idx * 3) % 400 + draw.text((10 - scroll_pos, 275), scrolling_text, fill=1) + draw.text((410 - scroll_pos, 275), scrolling_text, fill=1) + draw.line((10, 270, 390, 270), fill=1) + + return img + +def stream_camera(esp32_ip, port, protocol): + """Streams camera capture frames using OpenCV (requires opencv-python).""" + try: + import cv2 + except ImportError: + print("Error: opencv-python is not installed. Run 'pip install opencv-python' or use default animation mode.") + sys.exit(1) + + print(f"Opening camera stream...") + cap = cv2.VideoCapture(0) + if not cap.isOpened(): + print("Error: Could not open camera.") + sys.exit(1) + + sock = None + if protocol == "tcp": + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((esp32_ip, port)) + else: # udp + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + # Enable broadcast if destination is broadcast + if esp32_ip.endswith(".255") or esp32_ip == "255.255.255.255": + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + print(f"Streaming webcam to {esp32_ip}:{port} via {protocol.upper()}...") + frame_count = 0 + start_time = time.time() + + try: + while cap.isOpened(): + ret, frame = cap.read() + if not ret: + break + + # Flip image, convert to RGB + frame = cv2.flip(frame, 1) + frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + pil_img = Image.fromarray(frame_rgb) + + # Resize and dither + pil_img.thumbnail((WIDTH, HEIGHT)) + bg = Image.new("1", (WIDTH, HEIGHT), 0) + offset = ((WIDTH - pil_img.width) // 2, (HEIGHT - pil_img.height) // 2) + bg.paste(pil_img.convert("1", dither=Image.FLOYDSTEINBERG), offset) + + # Map pixels to hardware buffer + raw_bytes = map_to_rlcd_hw_buffer(bg) + + # Transmit + if protocol == "tcp": + sock.sendall(raw_bytes) + else: + send_udp_frame_chunked(sock, esp32_ip, port, frame_count, raw_bytes) + + frame_count += 1 + elapsed = time.time() - start_time + if elapsed >= 2.0: + print(f"Streaming performance: {frame_count / elapsed:.1f} FPS") + frame_count = 0 + start_time = time.time() + + # Target ~30 FPS + time.sleep(0.033) + + finally: + cap.release() + if sock: + sock.close() + +def stream_animation(esp32_ip, port, protocol): + """Streams a generated Pillow vector animation.""" + sock = None + if protocol == "tcp": + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((esp32_ip, port)) + else: # udp + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + if esp32_ip.endswith(".255") or esp32_ip == "255.255.255.255": + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + print(f"Streaming vector animation to {esp32_ip}:{port} via {protocol.upper()}...") + frame_idx = 0 + start_time = time.time() + + try: + while True: + # Generate frame + pil_img = generate_animation_frame(frame_idx, f"{protocol.upper()} Stream") + + # Map pixels to hardware buffer + raw_bytes = map_to_rlcd_hw_buffer(pil_img) + + # Transmit + if protocol == "tcp": + sock.sendall(raw_bytes) + else: + send_udp_frame_chunked(sock, esp32_ip, port, frame_idx, raw_bytes) + + frame_idx += 1 + + # FPS tracking printout + if frame_idx % 60 == 0: + elapsed = time.time() - start_time + print(f"Streaming performance: {60 / elapsed:.1f} FPS") + start_time = time.time() + + # Restrict rate to ~25 FPS to match update scan limits nicely + time.sleep(0.04) + + except KeyboardInterrupt: + print("\nStreaming stopped.") + finally: + if sock: + sock.close() + +def main(): + parser = argparse.ArgumentParser(description="Host Client for ESP32-S3 TCP/UDP Video Streaming") + parser.add_argument("--ip", default="192.168.68.123", help="Target ESP32-S3 IP Address (default: 192.168.68.123)") + parser.add_argument("--protocol", choices=["tcp", "udp"], default="tcp", help="Streaming protocol (default: tcp)") + parser.add_argument("--source", choices=["camera", "animation"], default="animation", help="Streaming source (default: animation)") + args = parser.parse_args() + + # Assign ports based on protocol selection + port = 8081 if args.protocol == "tcp" else 8082 + + if args.source == "camera": + stream_camera(args.ip, port, args.protocol) + else: + stream_animation(args.ip, port, args.protocol) + +if __name__ == "__main__": + main() diff --git a/upload_files.py b/upload_files.py new file mode 100644 index 0000000..7b7c5ee --- /dev/null +++ b/upload_files.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +import json +import urllib.request +import sys + +ESP32_IP = "192.168.68.123" +URL = f"http://{ESP32_IP}/api/mcp" + +def call_mcp_tool(tool_name, arguments): + payload = { + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": tool_name, + "arguments": arguments + } + } + + req_data = json.dumps(payload).encode('utf-8') + req = urllib.request.Request( + URL, + data=req_data, + headers={"Content-Type": "application/json"}, + method="POST" + ) + + try: + with urllib.request.urlopen(req, timeout=10.0) as response: + resp_body = response.read().decode('utf-8') + resp_json = json.loads(resp_body) + if "error" in resp_json: + print(f"Error calling {tool_name}: {resp_json['error']}") + return None + return resp_json.get("result", {}).get("content", [{}])[0].get("text", "") + except Exception as e: + print(f"Connection failed for {tool_name}: {e}") + return None + +def upload_file(local_path, remote_path): + print(f"Reading local file {local_path}...") + with open(local_path, 'r') as f: + content = f.read() + + print(f"Uploading to ESP32: {remote_path} ({len(content)} chars)...") + res = call_mcp_tool("write_file", {"path": remote_path, "content": content}) + if res: + print(f"Success: {res}") + return True + return False + +def main(): + print(f"--- RP2350/ESP32-S3 OTA File Uploader ({ESP32_IP}) ---") + + # Upload new drivers, utilities, and updated scripts + files_to_upload = [ + ("st7796.py", "st7796.py"), + ("ft6336u.py", "ft6336u.py"), + ("battery_util.py", "battery_util.py"), + ("rgb_led_util.py", "rgb_led_util.py"), + ("video_stream.py", "video_stream.py"), + ("mcp_server.py", "mcp_server.py"), + ("boot.py", "boot.py"), + ("main.py", "main.py") + ] + + for local, remote in files_to_upload: + if not upload_file(local, remote): + print(f"Failed to upload {local}. Stopping.") + sys.exit(1) + + # 4. Trigger remote reboot + print("Sending reboot command to board...") + reboot_code = "import machine\nmachine.reset()" + res = call_mcp_tool("execute_python", {"code": reboot_code}) + if res: + print("Reboot command acknowledged. The board is restarting!") + else: + print("Reboot request failed. You may need to manually reset the board.") + +if __name__ == "__main__": + main() diff --git a/verify_broadcast.py b/verify_broadcast.py new file mode 100644 index 0000000..708b93d --- /dev/null +++ b/verify_broadcast.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +import urllib.request +import json +import base64 +import time +import subprocess +import os +from PIL import Image + +ESP32_IP = "192.168.68.123" +BROADCAST_IP = "192.168.68.255" +URL = f"http://{ESP32_IP}/api/mcp" +ARTIFACTS_DIR = "/Users/adolforeyna/.gemini/antigravity/brain/8b421e8b-10a8-4d06-a7d0-27bd82b42804" + +def capture_screenshot(output_filename): + print(f"Requesting screenshot from ESP32 ({ESP32_IP})...") + payload = { + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "get_screenshot", + "arguments": {} + } + } + + req_data = json.dumps(payload).encode('utf-8') + req = urllib.request.Request( + URL, + data=req_data, + headers={"Content-Type": "application/json"}, + method="POST" + ) + + try: + with urllib.request.urlopen(req, timeout=10.0) as response: + resp_body = response.read().decode('utf-8') + resp_json = json.loads(resp_body) + content = resp_json.get("result", {}).get("content", [{}])[0].get("text", "") + + if content.startswith("__PBM_BASE64__:"): + pbm_b64 = content.split(":", 1)[1] + pbm_bytes = base64.b64decode(pbm_b64) + + temp_pbm = "temp_capture_bcast.pbm" + with open(temp_pbm, "wb") as pf: + pf.write(pbm_bytes) + + # Convert to PNG using Pillow + img = Image.open(temp_pbm) + dest_path = os.path.join(ARTIFACTS_DIR, output_filename) + img.save(dest_path) + os.remove(temp_pbm) + print(f"Success: Screenshot saved to {dest_path}") + return True + else: + print("Error: Invalid screenshot response format.") + return False + except Exception as e: + print(f"Failed to capture screenshot: {e}") + return False + +def main(): + print(f"--- Starting UDP Broadcast Stream Verification ({BROADCAST_IP}) ---") + + # 1. Start test_stream.py targeting broadcast IP + cmd = ["python3", "test_stream.py", "--ip", BROADCAST_IP, "--protocol", "udp", "--source", "animation"] + proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + print(f"Broadcast stream started in background (PID: {proc.pid}).") + + # 2. Wait for packets to propagate + print("Broadcasting UDP packets... Waiting 3 seconds...") + time.sleep(3.0) + + # 3. Capture screen rendering on ESP32 + capture_screenshot("udp_broadcast_success.png") + + # 4. Terminate stream client + print(f"Terminating broadcast client process...") + proc.terminate() + proc.wait() + print(f"Broadcast stream client stopped.") + + # Let the screen return to dashboard + print("Waiting 4 seconds to let the screen return to dashboard...") + time.sleep(4.0) + print("Completed.") + +if __name__ == "__main__": + main() diff --git a/verify_broadcast_single_frame.py b/verify_broadcast_single_frame.py new file mode 100644 index 0000000..5547d61 --- /dev/null +++ b/verify_broadcast_single_frame.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +import socket +import time +import urllib.request +import json +import base64 +import os +from PIL import Image, ImageDraw + +ESP32_IP = "192.168.68.123" +BROADCAST_IP = "192.168.68.255" +PORT = 8082 +ARTIFACTS_DIR = "/Users/adolforeyna/.gemini/antigravity/brain/8b421e8b-10a8-4d06-a7d0-27bd82b42804" + +def main(): + print(f"--- Starting UDP Broadcast Single-Frame Test ({BROADCAST_IP}) ---") + + # 1. Generate a test frame with Pillow displaying "BROADCAST SUCCESS!" + img = Image.new("1", (400, 300), 0) + draw = ImageDraw.Draw(img) + draw.text((100, 140), "BROADCAST SUCCESS!", fill=1) + draw.rectangle([40, 110, 360, 180], outline=1, width=2) + + # Map to Waveshare RLCD buffer format + px = img.load() + hw_buffer = bytearray(15000) + for y in range(300): + for x in range(400): + if px[x, y]: + inv_y = 299 - y + bx = x // 2 + by = inv_y // 4 + idx = bx * 75 + by + lx, ly = x % 2, inv_y % 4 + bit = 7 - (ly * 2 + lx) + hw_buffer[idx] |= (1 << bit) + + # 2. Open UDP socket with broadcast permission + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Send all 15 chunks representing the single frame + print("Broadcasting chunks...") + frame_id = 99 + for chunk_idx in range(15): + packet = bytearray(1002) + packet[0] = frame_id + packet[1] = chunk_idx + start = chunk_idx * 1000 + packet[2:1002] = hw_buffer[start : start + 1000] + sock.sendto(packet, (BROADCAST_IP, PORT)) + time.sleep(0.002) # Small pacing delay + + sock.close() + print("Frame broadcast sent.") + + # 3. Wait 1 second for ESP32 to render and display + time.sleep(1.0) + + # 4. Capture screenshot from ESP32 + print(f"Requesting screenshot from ESP32 ({ESP32_IP}) to verify broadcast reception...") + payload = { + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "get_screenshot", + "arguments": {} + } + } + + req_data = json.dumps(payload).encode('utf-8') + req = urllib.request.Request( + f"http://{ESP32_IP}/api/mcp", + data=req_data, + headers={"Content-Type": "application/json"}, + method="POST" + ) + + try: + with urllib.request.urlopen(req, timeout=10.0) as response: + resp_body = response.read().decode('utf-8') + resp_json = json.loads(resp_body) + content = resp_json.get("result", {}).get("content", [{}])[0].get("text", "") + + if content.startswith("__PBM_BASE64__:"): + pbm_b64 = content.split(":", 1)[1] + pbm_bytes = base64.b64decode(pbm_b64) + + temp_pbm = "temp_capture_bcast.pbm" + with open(temp_pbm, "wb") as pf: + pf.write(pbm_bytes) + + # Convert to PNG using Pillow + img = Image.open(temp_pbm) + dest_path = os.path.join(ARTIFACTS_DIR, "udp_broadcast_success.png") + img.save(dest_path) + os.remove(temp_pbm) + print(f"Success: Broadcast screenshot saved to {dest_path}") + else: + print("Error: Invalid screenshot response format.") + except Exception as e: + print(f"Failed to capture screenshot: {e}") + +if __name__ == "__main__": + main() diff --git a/verify_streaming.py b/verify_streaming.py new file mode 100644 index 0000000..82762fc --- /dev/null +++ b/verify_streaming.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +import urllib.request +import json +import base64 +import time +import subprocess +import os +from PIL import Image + +ESP32_IP = "192.168.68.123" +URL = f"http://{ESP32_IP}/api/mcp" +ARTIFACTS_DIR = "/Users/adolforeyna/.gemini/antigravity/brain/8b421e8b-10a8-4d06-a7d0-27bd82b42804" + +def capture_screenshot(output_filename): + print(f"Requesting screenshot from ESP32 ({ESP32_IP})...") + payload = { + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "get_screenshot", + "arguments": {} + } + } + + req_data = json.dumps(payload).encode('utf-8') + req = urllib.request.Request( + URL, + data=req_data, + headers={"Content-Type": "application/json"}, + method="POST" + ) + + try: + with urllib.request.urlopen(req, timeout=10.0) as response: + resp_body = response.read().decode('utf-8') + resp_json = json.loads(resp_body) + content = resp_json.get("result", {}).get("content", [{}])[0].get("text", "") + + if content.startswith("__PBM_BASE64__:"): + pbm_b64 = content.split(":", 1)[1] + pbm_bytes = base64.b64decode(pbm_b64) + + temp_pbm = "temp_capture.pbm" + with open(temp_pbm, "wb") as pf: + pf.write(pbm_bytes) + + # Convert to PNG using Pillow + img = Image.open(temp_pbm) + dest_path = os.path.join(ARTIFACTS_DIR, output_filename) + img.save(dest_path) + os.remove(temp_pbm) + print(f"Success: Screenshot saved to {dest_path}") + return True + else: + print("Error: Invalid screenshot response format.") + return False + except Exception as e: + print(f"Failed to capture screenshot: {e}") + return False + +def run_stream_test(protocol): + print(f"\n--- Starting {protocol.upper()} Stream Verification ---") + + # 1. Start test_stream.py in background + cmd = ["python3", "test_stream.py", "--ip", ESP32_IP, "--protocol", protocol, "--source", "animation"] + proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + print(f"Stream client process started in background (PID: {proc.pid}).") + + # 2. Wait for frames to stream and render + print("Streaming frames... Waiting 3 seconds...") + time.sleep(3.0) + + # 3. Capture screen rendering + filename = f"{protocol}_stream_success.png" + capture_screenshot(filename) + + # 4. Terminate stream client + print(f"Terminating stream client process...") + proc.terminate() + proc.wait() + print(f"Stream client stopped.") + +def main(): + if not os.path.exists(ARTIFACTS_DIR): + os.makedirs(ARTIFACTS_DIR) + + # Test TCP + run_stream_test("tcp") + + # Test Dashboard Auto-Timeout (stream timeout is 3.0s, let's wait 4.0s) + print("\n--- Verifying Stream Inactivity Timeout ---") + print("Waiting 4 seconds for stream to time out on ESP32...") + time.sleep(4.0) + capture_screenshot("dashboard_resumed.png") + + # Test UDP + run_stream_test("udp") + + # Clean up stream + print("\nWait 4 seconds to let the screen return to dashboard...") + time.sleep(4.0) + + print("\nVerification Completed successfully.") + +if __name__ == "__main__": + main() diff --git a/video_stream.py b/video_stream.py new file mode 100644 index 0000000..49258dc --- /dev/null +++ b/video_stream.py @@ -0,0 +1,285 @@ +import socket +import time +import select +import sys +import micropython + +class VideoStreamServer: + def __init__(self, display, tcp_port=8081, udp_port=8082): + self.display = display + self.tcp_port = tcp_port + self.udp_port = udp_port + + # Sockets + self.tcp_server = None + self.tcp_client = None + self.udp_sock = None + + # State + self.active = False + self.last_packet_time = 0 + self.timeout_ms = 3000 + + # Frame buffering (Pre-allocated to prevent memory allocations) + self.buffer = bytearray(15000) + self.view = memoryview(self.buffer) + self.tcp_bytes_received = 0 + + # UDP Reassembly (15 chunks of 1000 bytes each) + self.udp_temp_buffer = bytearray(1002) + self.udp_view = memoryview(self.udp_temp_buffer) + self.current_frame_id = -1 + self.chunks_received = 0 # Bitmask for chunks 0-14 + + # Performance Stats + self.debug = False + self.frames_drawn = 0 + self.udp_packets_received = 0 + self.udp_frames_complete = 0 + self.dropped_udp_frames = 0 + self.last_draw_ms = 0 + self.last_fps = 0.0 + + # FPS Calculation + self.fps_start_time = time.ticks_ms() + self.fps_frame_count = 0 + + @micropython.native + def rlcd_to_mono(self, rlcd_buf, canvas_buf): + # canvas_buf size: 19200 bytes (480x320) + # Clear canvas_buf to 0 + for i in range(19200): + canvas_buf[i] = 0 + + X_OFF = 40 + Y_OFF = 10 + + # Loop over 15000 bytes + for index in range(15000): + val = rlcd_buf[index] + if val == 0: + continue + + byte_x = index // 75 + block_y = index % 75 + x_base = 2 * byte_x + X_OFF + y_base = 299 - 4 * block_y + Y_OFF + + # bit 7: local_y = 0, local_x = 0 + if val & 0x80: + cx = x_base + cy = y_base + canvas_buf[cy * 60 + (cx >> 3)] |= (1 << (7 - (cx & 7))) + + # bit 6: local_y = 0, local_x = 1 + if val & 0x40: + cx = x_base + 1 + cy = y_base + canvas_buf[cy * 60 + (cx >> 3)] |= (1 << (7 - (cx & 7))) + + # bit 5: local_y = 1, local_x = 0 + if val & 0x20: + cx = x_base + cy = y_base - 1 + canvas_buf[cy * 60 + (cx >> 3)] |= (1 << (7 - (cx & 7))) + + # bit 4: local_y = 1, local_x = 1 + if val & 0x10: + cx = x_base + 1 + cy = y_base - 1 + canvas_buf[cy * 60 + (cx >> 3)] |= (1 << (7 - (cx & 7))) + + # bit 3: local_y = 2, local_x = 0 + if val & 0x08: + cx = x_base + cy = y_base - 2 + canvas_buf[cy * 60 + (cx >> 3)] |= (1 << (7 - (cx & 7))) + + # bit 2: local_y = 2, local_x = 1 + if val & 0x04: + cx = x_base + 1 + cy = y_base - 2 + canvas_buf[cy * 60 + (cx >> 3)] |= (1 << (7 - (cx & 7))) + + # bit 1: local_y = 3, local_x = 0 + if val & 0x02: + cx = x_base + cy = y_base - 3 + canvas_buf[cy * 60 + (cx >> 3)] |= (1 << (7 - (cx & 7))) + + # bit 0: local_y = 3, local_x = 1 + if val & 0x01: + cx = x_base + 1 + cy = y_base - 3 + canvas_buf[cy * 60 + (cx >> 3)] |= (1 << (7 - (cx & 7))) + + def start(self): + """Initialize and start the sockets.""" + # 1. Start TCP Server + try: + self.tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.tcp_server.bind(('', self.tcp_port)) + self.tcp_server.listen(1) + self.tcp_server.setblocking(False) + print(f"TCP Stream Server started on port {self.tcp_port}") + except Exception as e: + print(f"Failed to start TCP stream server: {e}") + + # 2. Start UDP Server + try: + self.udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.udp_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.udp_sock.bind(('', self.udp_port)) + self.udp_sock.setblocking(False) + print(f"UDP Stream Server started on port {self.udp_port}") + except Exception as e: + print(f"Failed to start UDP stream server: {e}") + + def update(self): + """Updates connection states and checks for incoming video data. + Call this periodically in the main execution loop. + """ + now = time.ticks_ms() + + # Calculate dynamic FPS every 1 second + fps_elapsed = time.ticks_diff(now, self.fps_start_time) + if fps_elapsed >= 1000: + self.last_fps = (self.fps_frame_count * 1000.0) / fps_elapsed + self.fps_frame_count = 0 + self.fps_start_time = now + + # 1. Check for inactivity timeout + if self.active and time.ticks_diff(now, self.last_packet_time) > self.timeout_ms: + print("Video stream timed out. Returning to dashboard.") + self.active = False + self.tcp_bytes_received = 0 + + # 2. Handle incoming UDP packet (if UDP is active) + if self.udp_sock: + while True: + try: + n = self.udp_sock.readinto(self.udp_temp_buffer) + if n is None or n == 0: + break # Socket buffer queue is empty + + self.udp_packets_received += 1 + frame_id = self.udp_temp_buffer[0] + chunk_idx = self.udp_temp_buffer[1] + + if self.debug: + print(f"[UDP] Received packet size {n}: frame={frame_id}, chunk={chunk_idx}") + + if chunk_idx < 15: + # If a new frame sequence starts, reset tracking and check if old frame was incomplete + if frame_id != self.current_frame_id: + if self.chunks_received != 0: + self.dropped_udp_frames += 1 + self.current_frame_id = frame_id + self.chunks_received = 0 + + # Copy chunk data directly to frame buffer + start_offset = chunk_idx * 1000 + self.buffer[start_offset : start_offset + 1000] = self.udp_view[2:1002] + + # Mark chunk index as received + self.chunks_received |= (1 << chunk_idx) + + # Check if all 15 chunks (indices 0 to 14) are received (0x7FFF) + if self.chunks_received == 0x7FFF: + self.udp_frames_complete += 1 + if self.debug: + print(f"[UDP] Frame {frame_id} complete! Drawing to screen.") + self.active = True + self.last_packet_time = now + self._draw_frame() + self.chunks_received = 0 # Reset for next frame + except OSError as e: + break # EWOULDBLOCK (no data available) + + # 3. Handle TCP stream + if self.tcp_server: + # If no client is connected, try to accept one + if self.tcp_client is None: + try: + self.tcp_client, addr = self.tcp_server.accept() + self.tcp_client.setblocking(False) + self.tcp_bytes_received = 0 + self.active = True + self.last_packet_time = now + print(f"TCP Stream client connected from: {addr}") + except OSError: + pass + + # If client is connected, read data + if self.tcp_client is not None: + try: + remaining = 15000 - self.tcp_bytes_received + n = self.tcp_client.readinto(self.view[self.tcp_bytes_received : self.tcp_bytes_received + remaining]) + + if n is not None: + if n == 0: + # Client closed connection + print("TCP Stream client disconnected.") + self.close_tcp_client() + else: + self.tcp_bytes_received += n + if self.debug: + print(f"[TCP] Received {n} bytes (Total: {self.tcp_bytes_received}/15000)") + self.last_packet_time = now + self.active = True + + # If we have a full frame, render it + if self.tcp_bytes_received == 15000: + if self.debug: + print("[TCP] Frame complete! Drawing to screen.") + self.tcp_bytes_received = 0 + self._draw_frame() + except OSError as e: + # Connection reset or socket error + print(f"TCP Stream socket error: {e}") + self.close_tcp_client() + + def _draw_frame(self): + """Sends the frame buffer directly to the SPI display bus.""" + draw_start = time.ticks_ms() + + if sys.platform == 'rp2': + # Decode the 15,000-byte RLCD buffer and write it centered on the 480x320 canvas + self.rlcd_to_mono(self.buffer, self.display.canvas_buffer) + self.display.show() + else: + # Frame boundary command sequences for the RLCD display + self.display.write_cmd(0x2A); self.display.write_data(0x12); self.display.write_data(0x2A) + self.display.write_cmd(0x2B); self.display.write_data(0x00); self.display.write_data(0xC7) + self.display.write_cmd(0x2C) + + # Send raw 15,000 bytes directly + self.display.write_data(self.buffer) + + self.last_draw_ms = time.ticks_diff(time.ticks_ms(), draw_start) + self.frames_drawn += 1 + self.fps_frame_count += 1 + + def get_stats(self): + """Returns streaming performance counters.""" + return { + "frames_drawn": self.frames_drawn, + "tcp_bytes_received": self.tcp_bytes_received, + "udp_packets_received": self.udp_packets_received, + "udp_frames_complete": self.udp_frames_complete, + "dropped_udp_frames": self.dropped_udp_frames, + "last_draw_ms": self.last_draw_ms, + "last_fps": round(self.last_fps, 1), + "active": self.active + } + + def close_tcp_client(self): + """Safely disconnects the TCP client socket.""" + if self.tcp_client: + try: + self.tcp_client.close() + except: + pass + self.tcp_client = None + self.tcp_bytes_received = 0 diff --git a/virtual_screen_mcp/server.py b/virtual_screen_mcp/server.py index a07bd13..52c8f07 100644 --- a/virtual_screen_mcp/server.py +++ b/virtual_screen_mcp/server.py @@ -146,6 +146,76 @@ class MainHandler(tornado.web.RequestHandler): # Serve the index.html from the same directory self.render("index.html") +class APIHandler(tornado.web.RequestHandler): + def set_default_headers(self): + self.set_header("Access-Control-Allow-Origin", "*") + self.set_header("Access-Control-Allow-Headers", "x-requested-with, content-type") + self.set_header("Access-Control-Allow-Methods", "POST, GET, OPTIONS") + + def options(self): + self.set_status(204) + self.finish() + + async def post(self): + try: + req = json.loads(self.request.body.decode('utf-8')) + method = req.get("method") + rpc_id = req.get("id") + + if method == "tools/call": + params = req.get("params", {}) + tool_name = params.get("name") + arguments = params.get("arguments", {}) + + result = await execute_mcp_tool(tool_name, arguments) + if isinstance(result, list): + content = result + else: + content = [{"type": "text", "text": str(result)}] + + resp = { + "jsonrpc": "2.0", + "id": rpc_id, + "result": { + "content": content + } + } + elif method == "initialize": + resp = { + "jsonrpc": "2.0", + "id": rpc_id, + "result": { + "protocolVersion": "2024-11-05", + "capabilities": { + "tools": {} + }, + "serverInfo": { + "name": "virtual-screen-mcp", + "version": "1.0.0" + } + } + } + else: + resp = { + "jsonrpc": "2.0", + "id": rpc_id, + "error": { + "code": -32601, + "message": f"Method {method} not found" + } + } + self.write(json.dumps(resp)) + except Exception as e: + resp = { + "jsonrpc": "2.0", + "id": None, + "error": { + "code": -32000, + "message": str(e) + } + } + self.write(json.dumps(resp)) + class ClientWebSocketHandler(tornado.websocket.WebSocketHandler): clients = set() @@ -633,6 +703,7 @@ background_tasks = set() async def main_async(port): app = tornado.web.Application([ (r"/", MainHandler), + (r"/api/mcp", APIHandler), (r"/ws", ClientWebSocketHandler), ], template_path=os.path.dirname(os.path.abspath(__file__))) diff --git a/wifi_util.py b/wifi_util.py index 60bd2c4..388d93c 100644 --- a/wifi_util.py +++ b/wifi_util.py @@ -1,6 +1,10 @@ -import network +try: + import network + has_network = True +except ImportError: + has_network = False -class WiFiUtil: +class _ActiveWiFiUtil: def __init__(self): self.wlan = network.WLAN(network.STA_IF) self.wlan.active(True) @@ -11,7 +15,6 @@ class WiFiUtil: networks = self.wlan.scan() result = [] for net in networks: - # network tuple: (ssid, bssid, channel, RSSI, authmode, hidden) try: ssid = net[0].decode('utf-8') except Exception: @@ -26,3 +29,11 @@ class WiFiUtil: # Sort by signal strength (RSSI) descending result.sort(key=lambda x: x['rssi'], reverse=True) return result + +class _DummyWiFiUtil: + def __init__(self): + print("Wi-Fi not supported on this platform.") + def scan(self): + return [] + +WiFiUtil = _ActiveWiFiUtil if has_network else _DummyWiFiUtil
+
+ + +
+ Skip to content + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+ + + +
+
+ +
+
+ 404 “This is not the web page you are looking for” + + + + + + + + + + + + +
+
+ +
+
+ +
+ + +
+
+ +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + +