24 Emotionen, HTTP-API, HeatRing-Temperaturanzeige mit Farbverlauf, Trapez-Lider (Cozmo-inspiriert), Tests per curl. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
139 lines
4.6 KiB
Python
139 lines
4.6 KiB
Python
from machine import Pin, SPI
|
|
import gc9a01
|
|
import network
|
|
import time
|
|
from eyes import Eyes
|
|
from mouth import Mouth
|
|
from heat_ring import HeatRing
|
|
from api_server import ApiServer
|
|
|
|
# --- WLAN verbinden ---
|
|
# Zugangsdaten liegen in secrets.py (nicht committen). Fehlt die
|
|
# Datei, laeuft das Gesicht offline weiter -- nur ohne API.
|
|
try:
|
|
from secrets import WIFI_SSID, WIFI_PASSWORD
|
|
except ImportError:
|
|
WIFI_SSID = None
|
|
WIFI_PASSWORD = None
|
|
print("secrets.py fehlt - starte ohne WLAN")
|
|
|
|
wlan = network.WLAN(network.STA_IF)
|
|
if WIFI_SSID:
|
|
wlan.active(True)
|
|
# Power-Save aus: der ESP32 sonst zeitweise komplett unerreichbar
|
|
# (kein ARP, kein Ping), bis ein Paket den Funk wieder aufweckt --
|
|
# bei Discovery-Probes reicht das, bei der Gesichts-API stoerts.
|
|
wlan.config(pm=wlan.PM_NONE)
|
|
wlan.connect(WIFI_SSID, WIFI_PASSWORD)
|
|
|
|
print("Verbinde mit WLAN...")
|
|
timeout_ms = 15000
|
|
start = time.ticks_ms()
|
|
while not wlan.isconnected():
|
|
if time.ticks_diff(time.ticks_ms(), start) > timeout_ms:
|
|
print("WLAN-Verbindung fehlgeschlagen, fahre ohne API fort")
|
|
break
|
|
time.sleep_ms(200)
|
|
|
|
if wlan.isconnected():
|
|
print("WLAN verbunden:", wlan.ifconfig()[0])
|
|
else:
|
|
print("Kein WLAN - API-Server startet nicht")
|
|
|
|
# --- Display-Init (ESP32-C3 Super Mini, siehe agents.md fuer Pinbelegung) ---
|
|
rst = Pin(0, Pin.OUT)
|
|
rst.value(1)
|
|
time.sleep_ms(50)
|
|
rst.value(0)
|
|
time.sleep_ms(50)
|
|
rst.value(1)
|
|
time.sleep_ms(150)
|
|
|
|
spi = SPI(1, baudrate=40000000, sck=Pin(2), mosi=Pin(4))
|
|
|
|
tft = gc9a01.GC9A01(
|
|
spi,
|
|
240, 240,
|
|
reset=Pin(0, Pin.OUT),
|
|
dc=Pin(1, Pin.OUT),
|
|
cs=Pin(5, Pin.OUT),
|
|
)
|
|
tft.init()
|
|
tft.fill(0x0000)
|
|
|
|
# --- Augen und Mund starten ---
|
|
face_color = gc9a01.color565(80, 200, 255) # helles Techno-Blau
|
|
eyes = Eyes(tft, eye_color=face_color)
|
|
mouth = Mouth(tft, y_offset=50, color=face_color)
|
|
heat_ring = HeatRing(tft)
|
|
|
|
|
|
def set_face_emotion(name):
|
|
"""Setzt Emotion auf Augen UND Mund gleichzeitig -- zentrale
|
|
Stelle, damit API und lokale Steuerung nicht getrennt aufgerufen
|
|
werden muessen."""
|
|
eyes.set_emotion(name)
|
|
mouth.set_emotion(name)
|
|
|
|
|
|
set_face_emotion("NEUTRAL")
|
|
|
|
# --- API-Server starten (nur falls WLAN verbunden) ---
|
|
api = None
|
|
if wlan.isconnected():
|
|
try:
|
|
api = ApiServer(eyes, mouth_ref=mouth, heat_ring_ref=heat_ring, port=80)
|
|
print("API laeuft auf http://%s/" % wlan.ifconfig()[0])
|
|
print("Beispiel: http://%s/emotion/happy" % wlan.ifconfig()[0])
|
|
except Exception as e:
|
|
print("API-Server konnte nicht gestartet werden:", e)
|
|
|
|
import gc
|
|
gc.collect()
|
|
print("RAM frei nach Init: %d Bytes (~%.1f KB)" % (gc.mem_free(), gc.mem_free() / 1024))
|
|
|
|
# --- Hauptschleife ---
|
|
# WLAN-Reconnect: wlan.connect() verbindet sich nur EINMAL beim Boot.
|
|
# Bricht die Verbindung spaeter ab (Funkstoerung, AP-Roaming, ...),
|
|
# bleibt das Board sonst dauerhaft offline bis zum naechsten Reset.
|
|
# Pruefung alle 5s statt jeden Frame -- wlan.isconnected() ist billig,
|
|
# aber taeglich zigtausendfach pro Sekunde unnoetig.
|
|
RECONNECT_INTERVAL_MS = 5000
|
|
next_wlan_check = time.ticks_add(time.ticks_ms(), RECONNECT_INTERVAL_MS)
|
|
|
|
while True:
|
|
eyes.update()
|
|
mouth.update()
|
|
heat_ring.update()
|
|
# Schweiss folgt dem aktuellen Ringstand direkt (nicht dem Ziel-
|
|
# wert), damit es zusammen mit dem Ring langsam ansteigt statt
|
|
# sofort zu springen. Sonnenbrille an, sobald der aktuelle Stand
|
|
# die per /heat/peak gesetzte Zieltemperatur erreicht (oder 1.0,
|
|
# falls kein Peak gesetzt ist).
|
|
eyes.set_heat(heat_ring.cur_level)
|
|
peak_target = heat_ring.peak_level if heat_ring.peak_level is not None else 1.0
|
|
eyes.set_sunglasses_auto(heat_ring.cur_level >= peak_target - 0.01)
|
|
if api is not None:
|
|
api.poll()
|
|
|
|
if WIFI_SSID:
|
|
now = time.ticks_ms()
|
|
if time.ticks_diff(now, next_wlan_check) >= 0:
|
|
next_wlan_check = time.ticks_add(now, RECONNECT_INTERVAL_MS)
|
|
if not wlan.isconnected():
|
|
print("WLAN getrennt, versuche erneut zu verbinden...")
|
|
try:
|
|
wlan.connect(WIFI_SSID, WIFI_PASSWORD)
|
|
except Exception as e:
|
|
print("Reconnect-Fehler:", e)
|
|
elif api is None:
|
|
# (Wieder-)Verbunden, aber API lief noch nicht (z.B.
|
|
# weil sie beim Boot mangels WLAN nicht starten konnte)
|
|
try:
|
|
api = ApiServer(eyes, mouth_ref=mouth, heat_ring_ref=heat_ring, port=80)
|
|
print("API laeuft auf http://%s/" % wlan.ifconfig()[0])
|
|
except Exception as e:
|
|
print("API-Server konnte nicht gestartet werden:", e)
|
|
|
|
time.sleep_ms(12)
|