Files
mcp_screen/circuitpython/code.py
T

440 lines
15 KiB
Python

"""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()