"""CircuitPython entrypoint for Waveshare ESP32-S3-RLCD-4.2 and Hosyond ESP32-S3 Touchscreen. Performs I2C scanning to auto-detect the connected board type, dynamically initializes all peripheral drivers, connects to Wi-Fi, and starts the MCP server and video stream servers. """ import gc import os import time import board import busio import digitalio import wifi import socketpool import adafruit_requests # Import drivers from audio_cp import BoardAudio from rlcd_cp import RLCD from ili9341_cp import ILI9341 from ft6336u_cp import FT6336U from battery_cp import BatteryMonitor from shtc3_cp import SHTC3 from rtc_cp import PCF85063 from rgb_led_cp import BoardLED from button_cp import BoardButtons from ble_cp import BLEUART from video_stream_cp import VideoStreamServer from mcp_server_cp import MCPServer import wifi_config # Global variables for hardware board_type = None # 'HOSYOND' or 'WAVESHARE_RLCD' display = None touch = None audio = None sensor = None rtc_chip = None battery = None led = None buttons = None ble_uart = None ip_addr = "Offline" def detect_and_init(): global board_type, display, touch, audio, sensor, rtc_chip, battery, led, buttons, ble_uart print("Scanning for board type...") # Try Hosyond pins first (SDA=IO16, SCL=IO15) try: i2c = busio.I2C(scl=board.IO15, sda=board.IO16) while not i2c.try_lock(): pass devices = i2c.scan() i2c.unlock() i2c.deinit() except Exception as e: devices = [] if 0x38 in devices: print("Detected Board: HOSYOND (Color Screen + Touch)") board_type = 'HOSYOND' # Display: ILI9341 (320x240) spi = busio.SPI(clock=board.IO12, MOSI=board.IO11) display = ILI9341( spi, cs=digitalio.DigitalInOut(board.IO10), dc=digitalio.DigitalInOut(board.IO46), bl=board.IO45, rst=None, ) # Touch: FT6336U i2c_bus = busio.I2C(scl=board.IO15, sda=board.IO16) touch = FT6336U( i2c_bus, rst_pin=digitalio.DigitalInOut(board.IO18), int_pin=digitalio.DigitalInOut(board.IO17), width=320, height=240, swap_xy=True, invert_x=False, invert_y=True, ) # Audio: ES8311 audio = BoardAudio( i2c_bus, bit_clock=board.IO5, word_select=board.IO7, data=board.IO8, mclk=board.IO4, amp=board.IO1, amp_active_level=0, # Active Low ) battery = BatteryMonitor(board.IO9) led = BoardLED(board.IO48) buttons = BoardButtons(board.IO0, key_pin=None) ble_name = "ESP32-S3-Touch" try: ble_uart = BLEUART(name=ble_name) except Exception as ble_err: print(f"BLE init failed: {ble_err}") return # Try Waveshare RLCD pins (SDA=IO13, SCL=IO14) try: i2c = busio.I2C(scl=board.IO14, sda=board.IO13) while not i2c.try_lock(): pass devices = i2c.scan() i2c.unlock() i2c.deinit() except Exception as e: devices = [] if 0x70 in devices or 0x51 in devices: print("Detected Board: WAVESHARE_RLCD (Monochrome RLCD)") board_type = 'WAVESHARE_RLCD' # Display: RLCD (400x300) spi = busio.SPI(clock=board.IO11, MOSI=board.IO12) display = RLCD( spi, cs=digitalio.DigitalInOut(board.IO40), dc=digitalio.DigitalInOut(board.IO5), rst=digitalio.DigitalInOut(board.IO41), ) # Audio: ES8311 i2c_bus = busio.I2C(scl=board.IO14, sda=board.IO13) audio = BoardAudio( i2c_bus, bit_clock=board.IO9, word_select=board.IO45, data=board.IO8, mclk=board.IO16, amp=board.IO46, amp_active_level=1, # Active High ) battery = BatteryMonitor(board.IO9) led = BoardLED(board.IO38) buttons = BoardButtons(board.IO0, key_pin=board.IO47) # Environment & Clock try: sensor = SHTC3(i2c_bus) except Exception as e: print("Failed to init SHTC3:", e) try: rtc_chip = PCF85063(i2c_bus) except Exception as e: print("Failed to init PCF85063:", e) ble_name = "ESP32-S3-RLCD" try: ble_uart = BLEUART(name=ble_name) except Exception as ble_err: print(f"BLE init failed: {ble_err}") return # Fallback to Hosyond print("Board scan failed. Falling back to HOSYOND defaults...") board_type = 'HOSYOND' spi = busio.SPI(clock=board.IO12, MOSI=board.IO11) display = ILI9341( spi, cs=digitalio.DigitalInOut(board.IO10), dc=digitalio.DigitalInOut(board.IO46), bl=board.IO45, rst=None, ) i2c_bus = busio.I2C(scl=board.IO15, sda=board.IO16) try: touch = FT6336U( i2c_bus, rst_pin=digitalio.DigitalInOut(board.IO18), int_pin=digitalio.DigitalInOut(board.IO17), width=320, height=240, swap_xy=True, invert_x=False, invert_y=True, ) except Exception as te: print("Fallback touch init failed:", te) audio = BoardAudio( i2c_bus, bit_clock=board.IO5, word_select=board.IO7, data=board.IO8, mclk=board.IO4, amp=board.IO1, amp_active_level=0, ) battery = BatteryMonitor(board.IO9) led = BoardLED(board.IO48) buttons = BoardButtons(board.IO0) ble_name = "ESP32-S3-Touch" try: ble_uart = BLEUART(name=ble_name) except Exception as ble_err: print(f"BLE init failed: {ble_err}") def main(): global ip_addr detect_and_init() # Draw initial dashboard display.clear(0) display.text("CircuitPython Active", 10, 10, 1) display.text("Connecting Wi-Fi...", 10, 30, 1) display.show() # Sync system clock from RTC chip if available if rtc_chip: try: rtc_chip.sync_to_system() except Exception as e: print("Failed to sync clock:", e) # Connect to Wi-Fi WIFI_SSID = getattr(wifi_config, "WIFI_SSID", "FamReynaMesh") WIFI_PASS = getattr(wifi_config, "WIFI_PASS", "Gloria2020") print(f"Connecting to Wi-Fi SSID '{WIFI_SSID}'...") try: wifi.radio.connect(WIFI_SSID, WIFI_PASS) ip_addr = str(wifi.radio.ipv4_address) print(f"Wi-Fi Connected! IP Address: {ip_addr}") except Exception as wifi_err: print(f"Wi-Fi Connection failed: {wifi_err}") ip_addr = "Disconnected" # Initialize socket pool and request session pool = socketpool.SocketPool(wifi.radio) session = adafruit_requests.Session(pool) # Start Video Stream Server vstream = VideoStreamServer(display, pool, tcp_port=8081, udp_port=8082, color_port=8083) vstream.start() # Start MCP Server mcp = MCPServer( display, led, battery, sensor, rtc_chip, ble_uart, pool=pool, vstream=vstream, touch=touch, session=session, audio=audio ) mcp.start(port=80) # Sync time from NTP if connected if ip_addr != "Disconnected": try: mcp._call_tool("sync_time", {}) except Exception as ntp_err: print(f"NTP Time sync failed: {ntp_err}") # Set default LED mode: Green Breathing led_modes = [ ("Red (Breathing)", lambda l: l.set_color(40, 0, 0), "breath"), ("Green (Breathing)", lambda l: l.set_color(0, 40, 0), "breath"), ("Blue (Breathing)", lambda l: l.set_color(0, 0, 40), "breath"), ("Cyan (Breathing)", lambda l: l.set_color(0, 30, 30), "breath"), ("Magenta (Breathing)", lambda l: l.set_color(30, 0, 30), "breath"), ("Rainbow Cycle", lambda l: l.set_color(30, 30, 30), "rainbow"), ("LED Off", lambda l: l.off(), "off") ] local_led_mode_idx = 1 # Green breathing led_modes[local_led_mode_idx][1](led) mcp.active_led_mode = led_modes[local_led_mode_idx][2] # Button callbacks last_action_str = "Boot finished." force_dashboard_redraw = True def on_key_click(): nonlocal local_led_mode_idx, last_action_str, force_dashboard_redraw local_led_mode_idx = (local_led_mode_idx + 1) % len(led_modes) mode_name, color_fn, mode_type = led_modes[local_led_mode_idx] color_fn(led) mcp.active_led_mode = mode_type print(f"Local Button: Cycle LED -> {mode_name}") last_action_str = f"Local Button: {mode_name}" mcp.override_active = False force_dashboard_redraw = True def on_boot_click(): nonlocal last_action_str, force_dashboard_redraw print("Local Button: Force Dashboard refresh.") last_action_str = "Dashboard Refreshed" mcp.override_active = False force_dashboard_redraw = True if buttons: buttons.boot.on_click(on_boot_click) if buttons.key: buttons.key.on_click(on_key_click) last_dashboard_update = 0 dashboard_update_interval_s = 5 last_led_update = 0 last_wifi_check = 0 print("ESP32 CircuitPython main loop running...") while True: now = time.monotonic() # A. Check Wi-Fi connection and reconnect if lost (every 10s) if now - last_wifi_check >= 10.0: last_wifi_check = now if not wifi.radio.connected: print("Wi-Fi connection lost. Attempting reconnect...") last_action_str = "Wi-Fi Disconnected" if ip_addr != "Disconnected": ip_addr = "Disconnected" force_dashboard_redraw = True try: wifi.radio.connect(WIFI_SSID, WIFI_PASS) except Exception as e: print("Wi-Fi reconnect trigger failed:", e) elif ip_addr == "Disconnected": ip_addr = str(wifi.radio.ipv4_address) print(f"Wi-Fi Connected! IP Address: {ip_addr}") last_action_str = f"Wi-Fi Connected: {ip_addr}" force_dashboard_redraw = True try: mcp._call_tool("sync_time", {}) except: pass # B. Check for incoming MCP JSON-RPC TCP/UDP queries mcp.update() # C. Check for incoming Video Streaming TCP/UDP frames vstream.update() # D. Update button debouncers if buttons: buttons.update() # E. Update BLE UART connection poll if ble_uart: ble_uart.update() # F. Draw Local Dashboard periodically # Do not draw if screen is overridden by MCP commands (e.g. draw_text, draw_image) or active video stream if not mcp.override_active and not vstream.active: if force_dashboard_redraw or (now - last_dashboard_update >= dashboard_update_interval_s): force_dashboard_redraw = False last_dashboard_update = now # SHTC3 Sensor t, h = sensor.read_sensor() if sensor else (None, None) t_str = f"{t:.1f} C" if t is not None else "N/A" h_str = f"{h:.1f} %" if h is not None else "N/A" # Battery bat_v = battery.read_voltage() if battery else None bat_p = battery.read_percentage() if battery else 0 bat_str = f"{bat_v:.2f}V ({bat_p}%)" if bat_v is not None else "N/A" # Time dt = rtc_chip.get_datetime() if rtc_chip else None if dt: time_str = f"{dt[0]:04d}-{dt[1]:02d}-{dt[2]:02d} {dt[4]:02d}:{dt[5]:02d}:{dt[6]:02d}" else: time_str = "N/A (RTC Error)" display.clear(0) line_w = display.width - 10 if board_type == 'WAVESHARE_RLCD': title_text = "Waveshare ESP32-S3-RLCD Server" display.text(title_text, 10, 10, 1) display.line(10, 20, line_w, 20, 1) display.text_large("ENVIRONMENT", 15, 30, scale=2, color=1) display.text(f"Temp : {t_str}", 25, 55, 1) display.text(f"Humid : {h_str}", 25, 70, 1) display.line(10, 95, line_w, 95, 1) display.text_large("MCP NET CONNECTION", 15, 105, scale=2, color=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, line_w, 185, 1) display.text_large("SYSTEM STATUS", 15, 195, scale=2, color=1) display.text(f"Battery : {bat_str}", 25, 220, 1) display.text(f"Time : {time_str}", 25, 235, 1) display.line(10, 255, line_w, 255, 1) display.text(f"Status: {last_action_str}", 15, 265, 1) else: title_text = "Hosyond ESP32-S3 Server" display.text(title_text, 10, 8, 1) display.line(10, 18, line_w, 18, 1) # Left Column display.text("SYSTEM & ENV", 10, 28, 1) display.line(10, 38, 150, 38, 1) display.text(f"Temp : {t_str}", 10, 46, 1) display.text(f"Hum : {h_str}", 10, 58, 1) display.text(f"Bat : {bat_str}", 10, 70, 1) display.text(f"Time : {time_str[11:19]}", 10, 82, 1) display.text(f"Date : {time_str[0:10]}", 10, 94, 1) # Right Column display.text("MCP NETWORK", 170, 28, 1) display.line(170, 38, line_w, 38, 1) display.text(f"IP : {ip_addr}", 170, 46, 1) display.text("Port: 80/api/mcp", 170, 58, 1) display.text("BLE : Touch", 170, 70, 1) # Bottom Status display.line(10, 115, line_w, 115, 1) display.text(f"Status: {last_action_str}", 10, 125, 1) display.show() gc.collect() # G. Update NeoPixel LED animations smoothly (every 50ms) if now - last_led_update >= 0.05: 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() # H. Sleep to prevent CPU hogging, poll faster if streaming is active if vstream.active: time.sleep(0.002) else: time.sleep(0.020) if __name__ == "__main__": main()