Migrate MCP and utilities to CircuitPython, including color GIF and volume fixes
This commit is contained in:
+406
-82
@@ -1,6 +1,7 @@
|
||||
"""CircuitPython entrypoint for Waveshare ESP32-S3-RLCD-4.2 migration.
|
||||
"""CircuitPython entrypoint for Waveshare ESP32-S3-RLCD-4.2 and Hosyond ESP32-S3 Touchscreen.
|
||||
|
||||
Copy this file to CIRCUITPY/code.py after flashing CircuitPython.
|
||||
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
|
||||
@@ -10,106 +11,429 @@ import time
|
||||
import board
|
||||
import busio
|
||||
import digitalio
|
||||
import wifi
|
||||
import socketpool
|
||||
import adafruit_requests
|
||||
|
||||
# Import drivers
|
||||
from audio_cp import BoardAudio
|
||||
from gif_player import GIFPlayer
|
||||
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"
|
||||
|
||||
# Keep pin choices in one place because CircuitPython board builds differ in
|
||||
# how they expose ESP32-S3 pins. If board.IO## is missing, run `dir(board)` on
|
||||
# the serial console and update these names.
|
||||
PINS = {
|
||||
"display_sck": board.IO11,
|
||||
"display_mosi": board.IO12,
|
||||
"display_cs": board.IO40,
|
||||
"display_dc": board.IO5,
|
||||
"display_rst": board.IO41,
|
||||
"i2c_sda": board.IO13,
|
||||
"i2c_scl": board.IO14,
|
||||
"i2s_bclk": board.IO9,
|
||||
"i2s_lrck": board.IO45,
|
||||
"i2s_dout": board.IO8,
|
||||
"audio_mclk": board.IO16,
|
||||
"audio_amp": board.IO46,
|
||||
}
|
||||
def detect_and_init():
|
||||
global board_type, display, touch, audio, sensor, rtc_chip, battery, led, buttons, ble_uart
|
||||
|
||||
print("Scanning for board type...")
|
||||
|
||||
def exists(path):
|
||||
# Try Hosyond pins first (SDA=IO16, SCL=IO15)
|
||||
try:
|
||||
os.stat(path)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
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'
|
||||
|
||||
def init_display():
|
||||
spi = busio.SPI(clock=PINS["display_sck"], MOSI=PINS["display_mosi"])
|
||||
# Match the working MicroPython code's 20 MHz SPI if the build supports it.
|
||||
while not spi.try_lock():
|
||||
pass
|
||||
# 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:
|
||||
spi.configure(baudrate=20000000, phase=0, polarity=0)
|
||||
finally:
|
||||
spi.unlock()
|
||||
display = RLCD(
|
||||
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(PINS["display_cs"]),
|
||||
dc=digitalio.DigitalInOut(PINS["display_dc"]),
|
||||
rst=digitalio.DigitalInOut(PINS["display_rst"]),
|
||||
cs=digitalio.DigitalInOut(board.IO10),
|
||||
dc=digitalio.DigitalInOut(board.IO46),
|
||||
bl=board.IO45,
|
||||
rst=None,
|
||||
)
|
||||
return display
|
||||
|
||||
|
||||
def init_audio():
|
||||
i2c = busio.I2C(scl=PINS["i2c_scl"], sda=PINS["i2c_sda"])
|
||||
return BoardAudio(
|
||||
i2c,
|
||||
bit_clock=PINS["i2s_bclk"],
|
||||
word_select=PINS["i2s_lrck"],
|
||||
data=PINS["i2s_dout"],
|
||||
mclk=PINS["audio_mclk"],
|
||||
amp=PINS["audio_amp"],
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
def draw_boot_screen(display, message="CircuitPython RLCD"):
|
||||
display.clear(0)
|
||||
display.text("ESP32-S3-RLCD", 10, 10, 1)
|
||||
display.line(10, 22, 390, 22, 1)
|
||||
display.text_large("CIRCUITPY", 18, 45, scale=3, color=1)
|
||||
display.text(message, 18, 86, 1)
|
||||
display.text("MP3: /demo.mp3", 18, 116, 1)
|
||||
display.text("GIF: /demo.gif", 18, 132, 1)
|
||||
display.text("If visible, ST7305 init works", 18, 170, 1)
|
||||
display.show()
|
||||
|
||||
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():
|
||||
display = init_display()
|
||||
draw_boot_screen(display, "Display initialized")
|
||||
time.sleep(1)
|
||||
global ip_addr
|
||||
detect_and_init()
|
||||
|
||||
# Try GIF first because it verifies display animation support.
|
||||
if exists("/demo.gif"):
|
||||
draw_boot_screen(display, "Playing GIF...")
|
||||
GIFPlayer(display).play("/demo.gif", loops=1)
|
||||
draw_boot_screen(display, "GIF done")
|
||||
gc.collect()
|
||||
# Draw initial dashboard
|
||||
display.clear(0)
|
||||
display.text("CircuitPython Active", 10, 10, 1)
|
||||
display.text("Connecting Wi-Fi...", 10, 30, 1)
|
||||
display.show()
|
||||
|
||||
# Try MP3 if present. If this fails silently, verify MCLK on IO16 and codec
|
||||
# register sequence against a known-good WAV/tone first.
|
||||
if exists("/demo.mp3"):
|
||||
draw_boot_screen(display, "Playing MP3...")
|
||||
# Sync system clock from RTC chip if available
|
||||
if rtc_chip:
|
||||
try:
|
||||
audio = init_audio()
|
||||
ok = audio.play_mp3("/demo.mp3", volume=65)
|
||||
draw_boot_screen(display, "MP3 ok" if ok else "MP3 failed")
|
||||
except Exception as exc:
|
||||
draw_boot_screen(display, "MP3 error: " + str(exc)[:28])
|
||||
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:
|
||||
time.sleep(5)
|
||||
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
|
||||
|
||||
main()
|
||||
# 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()
|
||||
|
||||
Reference in New Issue
Block a user