440 lines
15 KiB
Python
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()
|