import os import glob from datetime import datetime from typing import List, Optional from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException from fastapi.responses import HTMLResponse, FileResponse from fastapi.middleware.cors import CORSMiddleware app = FastAPI(title="ESP32 Solar Tracker Remote Hub") app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) LOGS_DIR = os.path.join(os.path.dirname(__file__), "data_logs") os.makedirs(LOGS_DIR, exist_ok=True) class HubManager: def __init__(self): self.esp32_socket: Optional[WebSocket] = None self.client_sockets: List[WebSocket] = [] self.last_pos_x: int = 0 self.last_pos_y: int = 0 self.esp32_connected: bool = False self.last_seen: Optional[datetime] = None async def connect_esp32(self, websocket: WebSocket): await websocket.accept() self.esp32_socket = websocket self.esp32_connected = True self.last_seen = datetime.now() print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] ESP32 Conectado con exito.") await self.broadcast_status_to_clients() def disconnect_esp32(self): self.esp32_socket = None self.esp32_connected = False print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] ESP32 Desconectado.") async def connect_client(self, websocket: WebSocket): await websocket.accept() self.client_sockets.append(websocket) # Enviar estado actual y última posición al cliente recién conectado status_msg = f"STATUS:{'ONLINE' if self.esp32_connected else 'OFFLINE'}" await websocket.send_text(status_msg) pos_msg = f"POS:{self.last_pos_x}:{self.last_pos_y}" await websocket.send_text(pos_msg) # Enviar últimas 50 líneas del archivo de hoy today_filename = f"log_{datetime.now().strftime('%Y-%m-%d')}.txt" today_path = os.path.join(LOGS_DIR, today_filename) if os.path.exists(today_path): try: with open(today_path, "r", encoding="utf-8", errors="ignore") as f: lines = f.readlines() tail = "".join(lines[-50:]) if tail: await websocket.send_text(f"LOG:{tail}") except Exception as e: print(f"Error leyendo archivo de hoy: {e}") def disconnect_client(self, websocket: WebSocket): if websocket in self.client_sockets: self.client_sockets.remove(websocket) async def handle_esp32_message(self, message: str): self.last_seen = datetime.now() today_filename = f"log_{self.last_seen.strftime('%Y-%m-%d')}.txt" today_path = os.path.join(LOGS_DIR, today_filename) # 1. Registrar en el archivo diario en disco with open(today_path, "a", encoding="utf-8") as f: f.write(message.strip() + "\n") # 2. Si es actualización de posición POS:X:Y if message.startswith("POS:"): parts = message.split(":") if len(parts) >= 3: try: self.last_pos_x = int(parts[1]) self.last_pos_y = int(parts[2]) except ValueError: pass # 3. Retransmitir en tiempo real a todos los clientes web for client in list(self.client_sockets): try: await client.send_text(message) except Exception: self.disconnect_client(client) async def send_command_to_esp32(self, command: str) -> bool: if self.esp32_socket and self.esp32_connected: try: await self.esp32_socket.send_text(command) return True except Exception as e: print(f"Error enviando comando a ESP32: {e}") self.disconnect_esp32() return False async def broadcast_status_to_clients(self): status_msg = f"STATUS:{'ONLINE' if self.esp32_connected else 'OFFLINE'}" for client in list(self.client_sockets): try: await client.send_text(status_msg) except Exception: self.disconnect_client(client) hub = HubManager() # ==================== WEBSOCKETS ==================== @app.websocket("/ws/esp32") async def esp32_ws_endpoint(websocket: WebSocket): await hub.connect_esp32(websocket) try: while True: data = await websocket.receive_text() await hub.handle_esp32_message(data) except WebSocketDisconnect: hub.disconnect_esp32() await hub.broadcast_status_to_clients() except Exception as e: print(f"Excepcion en conexion ESP32: {e}") hub.disconnect_esp32() await hub.broadcast_status_to_clients() @app.websocket("/ws/client") async def client_ws_endpoint(websocket: WebSocket): await hub.connect_client(websocket) try: while True: cmd = await websocket.receive_text() # Reenviar comando al ESP32 success = await hub.send_command_to_esp32(cmd) if not success: await websocket.send_text("LOG:[ERROR] ESP32 no conectado en este momento\n") except WebSocketDisconnect: hub.disconnect_client(websocket) except Exception as e: print(f"Excepcion en cliente web: {e}") hub.disconnect_client(websocket) # ==================== REST APIS ==================== @app.get("/api/status") async def get_status(): return { "esp32_connected": hub.esp32_connected, "last_seen": hub.last_seen.isoformat() if hub.last_seen else None, "pos_x": hub.last_pos_x, "pos_y": hub.last_pos_y, "active_clients": len(hub.client_sockets) } @app.get("/api/logs") async def list_logs(): files = glob.glob(os.path.join(LOGS_DIR, "log_*.txt")) result = [] for f in sorted(files, reverse=True): basename = os.path.basename(f) size_bytes = os.path.getsize(f) mtime = os.path.getmtime(f) result.append({ "filename": basename, "size_kb": round(size_bytes / 1024, 2), "date": basename.replace("log_", "").replace(".txt", ""), "last_modified": datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S") }) return result @app.get("/api/logs/{filename}") async def download_log(filename: str): # Seguridad básica contra traversal attacks clean_name = os.path.basename(filename) path = os.path.join(LOGS_DIR, clean_name) if not os.path.exists(path): raise HTTPException(status_code=404, detail="Archivo de log no encontrado") return FileResponse(path, filename=clean_name, media_type="text/plain") # ==================== DASHBOARD WEB ==================== DASHBOARD_HTML = r"""
| Fecha | Tamaño | Acción |
|---|---|---|
| Cargando archivos... | ||