#!/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()