Files
led-controller/run_web.py
jimmy 91bd78ab31 Add favicon route and minor cleanup
- Add /favicon.ico route (204) to avoid browser 404
- CSS formatting tweaks
- Pipfile trailing newline

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-20 11:49:11 +13:00

174 lines
5.3 KiB
Python

#!/usr/bin/env python3
"""
Local development web server - imports and runs main.py with port 5000
"""
import sys
import os
import asyncio
# Add src and lib to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'lib'))
# Import the main module
from src import main as main_module
# Override the port in the main function
async def run_local():
"""Run main with port 5000 for local development."""
from settings import Settings
import gc
# Mock MicroPython modules for local development
class MockMachine:
class WDT:
def __init__(self, timeout):
pass
def feed(self):
pass
import sys as sys_module
sys_module.modules['machine'] = MockMachine()
class MockESPNow:
def __init__(self):
self.active_value = False
self.peers = []
def active(self, value):
self.active_value = value
print(f"[MOCK] ESPNow active: {value}")
def add_peer(self, peer):
self.peers.append(peer)
print(f"[MOCK] Added peer: {peer.hex() if hasattr(peer, 'hex') else peer}")
async def asend(self, peer, data):
print(f"[MOCK] Would send to {peer.hex() if hasattr(peer, 'hex') else peer}: {data}")
class MockAIOESPNow:
def __init__(self):
pass
def active(self, value):
return MockESPNow()
def add_peer(self, peer):
pass
class MockNetwork:
class WLAN:
def __init__(self, interface):
self.interface = interface
def active(self, value):
print(f"[MOCK] WLAN({self.interface}) active: {value}")
STA_IF = 0
# Replace MicroPython modules with mocks
sys_module.modules['aioespnow'] = type('module', (), {'AIOESPNow': MockESPNow})()
sys_module.modules['network'] = MockNetwork()
# Mock gc if needed
if not hasattr(gc, 'collect'):
class MockGC:
def collect(self):
pass
gc = MockGC()
settings = Settings()
print("Starting LED Controller Web Server (Local Development)")
print("=" * 60)
# Mock network
import network
network.WLAN(network.STA_IF).active(True)
# Mock ESPNow
import aioespnow
e = aioespnow.AIOESPNow()
e.active(True)
e.add_peer(b"\xbb\xbb\xbb\xbb\xbb\xbb")
from microdot import Microdot, send_file
from microdot.websocket import with_websocket
from microdot.session import Session
import controllers.preset as preset
import controllers.profile as profile
import controllers.group as group
import controllers.sequence as sequence
import controllers.tab as tab
import controllers.palette as palette
import controllers.scene as scene
import controllers.pattern as pattern
import controllers.settings as settings_controller
app = Microdot()
# Initialize sessions with a secret key from settings
secret_key = settings.get('session_secret_key', 'led-controller-secret-key-change-in-production')
Session(app, secret_key=secret_key)
# Mount model controllers as subroutes
app.mount(preset.controller, '/presets')
app.mount(profile.controller, '/profiles')
app.mount(group.controller, '/groups')
app.mount(sequence.controller, '/sequences')
app.mount(tab.controller, '/tabs')
app.mount(palette.controller, '/palettes')
app.mount(scene.controller, '/scenes')
app.mount(pattern.controller, '/patterns')
app.mount(settings_controller.controller, '/settings')
# Serve index.html at root
@app.route('/')
def index(request):
"""Serve the main web UI."""
return send_file('src/templates/index.html')
# Serve settings page
@app.route('/settings')
def settings_page(request):
"""Serve the settings page."""
return send_file('src/templates/settings.html')
# Favicon: avoid 404 in browser console (no file needed)
@app.route('/favicon.ico')
def favicon(request):
return '', 204
# Static file route
@app.route("/static/<path:path>")
def static_handler(request, path):
"""Serve static files."""
if '..' in path:
return 'Not found', 404
return send_file('src/static/' + path)
@app.route('/ws')
@with_websocket
async def ws(request, ws):
while True:
data = await ws.receive()
if data:
await e.asend(b"\xbb\xbb\xbb\xbb\xbb\xbb", data)
print(data)
else:
break
# Use port 5000 for local development
port = 5000
print(f"Starting server on http://0.0.0.0:{port}")
print(f"Open http://localhost:{port} in your browser")
print("=" * 60)
try:
await app.start_server(host="0.0.0.0", port=port, debug=True)
except KeyboardInterrupt:
print("\nShutting down server...")
if __name__ == '__main__':
# Change to project root
os.chdir(os.path.dirname(os.path.abspath(__file__)))
# Override settings path for local development
import settings as settings_module
settings_module.Settings.SETTINGS_FILE = os.path.join(os.getcwd(), 'settings.json')
asyncio.run(run_local())