a8f0d6ccba
Fonctionnalités : - Lecture RS485 Modbus Epever Tracer 4210N (115200 bps, FC03/FC04/FC16) - Moteur de règles JSON (LittleFS) — commande automatique des relais - Interface web mobile-first (dashboard, règles, config, historique, EPEVER, debug) - WiFi AP+STA simultanés avec reconnexion automatique et portail captif - mDNS configurable (pv.local par défaut) - Configuration registres EPEVER depuis l'UI (18 registres holding) - Historique basse/haute résolution avec graphes canvas - VPN WireGuard optionnel (désactivé par défaut, config via UI) - OTA firmware + filesystem via ElegantOTA - Deep sleep / économie d'énergie Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
112 lines
3.9 KiB
Python
112 lines
3.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Serveur HTTP pour l'interface de débogage 3 volets.
|
|
|
|
Port 8888 :
|
|
GET / → page HTML 3 volets
|
|
GET /serial → SSE (Server-Sent Events) du port série QEMU
|
|
GET /api/* → proxy transparent vers le webserver ESP32 (port 10080)
|
|
POST /api/* → idem
|
|
"""
|
|
import http.server
|
|
import socketserver
|
|
import time
|
|
import urllib.request
|
|
import urllib.error
|
|
from urllib.parse import urlparse
|
|
|
|
ESP_URL = 'http://127.0.0.1:10080'
|
|
SERIAL_LOG = '/tmp/serial.log'
|
|
PORT = 8888
|
|
|
|
|
|
class Handler(http.server.BaseHTTPRequestHandler):
|
|
|
|
def do_GET(self):
|
|
if self.path in ('/', '/index.html'):
|
|
self._serve_file('/emulator/ui/index.html', 'text/html; charset=utf-8')
|
|
|
|
elif self.path == '/serial':
|
|
self._sse_serial()
|
|
|
|
elif self.path.startswith('/api/'):
|
|
self._proxy('GET')
|
|
|
|
else:
|
|
self.send_error(404)
|
|
|
|
def do_POST(self):
|
|
if self.path.startswith('/api/'):
|
|
self._proxy('POST')
|
|
else:
|
|
self.send_error(404)
|
|
|
|
# ------------------------------------------------------------------
|
|
def _serve_file(self, path, content_type):
|
|
try:
|
|
with open(path, 'rb') as f:
|
|
data = f.read()
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', content_type)
|
|
self.send_header('Content-Length', str(len(data)))
|
|
self.end_headers()
|
|
self.wfile.write(data)
|
|
except FileNotFoundError:
|
|
self.send_error(404)
|
|
|
|
def _sse_serial(self):
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'text/event-stream')
|
|
self.send_header('Cache-Control', 'no-cache')
|
|
self.send_header('Access-Control-Allow-Origin', '*')
|
|
self.end_headers()
|
|
try:
|
|
# Ouvrir le fichier log et se positionner à la fin
|
|
with open(SERIAL_LOG, 'r', errors='replace') as f:
|
|
f.seek(0, 2)
|
|
while True:
|
|
line = f.readline()
|
|
if line:
|
|
msg = line.rstrip().replace('\n', ' ')
|
|
self.wfile.write(f'data: {msg}\n\n'.encode())
|
|
self.wfile.flush()
|
|
else:
|
|
time.sleep(0.1)
|
|
except (BrokenPipeError, ConnectionResetError):
|
|
pass
|
|
except FileNotFoundError:
|
|
# Log pas encore créé — attendre
|
|
time.sleep(1)
|
|
|
|
def _proxy(self, method):
|
|
target = ESP_URL + self.path
|
|
try:
|
|
length = int(self.headers.get('Content-Length', 0))
|
|
body = self.rfile.read(length) if length else None
|
|
req = urllib.request.Request(target, data=body, method=method)
|
|
if body:
|
|
req.add_header('Content-Type', self.headers.get('Content-Type', 'application/json'))
|
|
with urllib.request.urlopen(req, timeout=3) as resp:
|
|
data = resp.read()
|
|
self.send_response(resp.status)
|
|
self.send_header('Content-Type', resp.headers.get('Content-Type', 'application/json'))
|
|
self.send_header('Content-Length', str(len(data)))
|
|
self.send_header('Access-Control-Allow-Origin', '*')
|
|
self.end_headers()
|
|
self.wfile.write(data)
|
|
except urllib.error.URLError:
|
|
self.send_response(503)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.end_headers()
|
|
self.wfile.write(b'{"error":"ESP32 hors ligne"}')
|
|
|
|
def log_message(self, *_):
|
|
pass # supprimer les logs d'accès
|
|
|
|
|
|
if __name__ == '__main__':
|
|
with socketserver.ThreadingTCPServer(('', PORT), Handler) as httpd:
|
|
httpd.allow_reuse_address = True
|
|
print(f'[UI] Interface de débogage sur http://0.0.0.0:{PORT}', flush=True)
|
|
httpd.serve_forever()
|