FlotillasGPS - Sistema completo de monitoreo de flotillas GPS
Sistema completo para monitoreo y gestion de flotas de vehiculos con: - Backend FastAPI con PostgreSQL/TimescaleDB - Frontend React con TypeScript y TailwindCSS - App movil React Native con Expo - Soporte para dispositivos GPS, Meshtastic y celulares - Video streaming en vivo con MediaMTX - Geocercas, alertas, viajes y reportes - Autenticacion JWT y WebSockets en tiempo real Documentacion completa y guias de usuario incluidas.
This commit is contained in:
14
backend/app/api/websocket/__init__.py
Normal file
14
backend/app/api/websocket/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
Módulo WebSocket - Endpoints para comunicación en tiempo real.
|
||||
"""
|
||||
|
||||
from app.api.websocket.manager import manager, ConnectionManager
|
||||
from app.api.websocket.ubicaciones import router as ubicaciones_router
|
||||
from app.api.websocket.alertas import router as alertas_router
|
||||
|
||||
__all__ = [
|
||||
"manager",
|
||||
"ConnectionManager",
|
||||
"ubicaciones_router",
|
||||
"alertas_router",
|
||||
]
|
||||
125
backend/app/api/websocket/alertas.py
Normal file
125
backend/app/api/websocket/alertas.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""
|
||||
WebSocket endpoint para alertas en tiempo real.
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Query, WebSocket, WebSocketDisconnect
|
||||
|
||||
from app.api.websocket.manager import manager
|
||||
from app.core.security import decode_token
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
async def get_user_from_token(token: Optional[str]) -> Optional[int]:
|
||||
"""
|
||||
Obtiene el ID de usuario desde un token JWT.
|
||||
|
||||
Args:
|
||||
token: Token JWT.
|
||||
|
||||
Returns:
|
||||
ID del usuario o None.
|
||||
"""
|
||||
if not token:
|
||||
return None
|
||||
try:
|
||||
payload = decode_token(token)
|
||||
return int(payload.get("sub"))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
@router.websocket("/ws/alertas")
|
||||
async def websocket_alertas(
|
||||
websocket: WebSocket,
|
||||
token: Optional[str] = Query(None),
|
||||
):
|
||||
"""
|
||||
WebSocket para recibir alertas en tiempo real.
|
||||
|
||||
Recibe todas las alertas generadas en el sistema.
|
||||
|
||||
Args:
|
||||
websocket: Conexión WebSocket.
|
||||
token: Token JWT para autenticación (opcional).
|
||||
"""
|
||||
user_id = await get_user_from_token(token)
|
||||
|
||||
await manager.connect(websocket, "alertas", user_id)
|
||||
|
||||
# Enviar confirmación
|
||||
await websocket.send_json({
|
||||
"type": "connected",
|
||||
"channel": "alerts",
|
||||
})
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
|
||||
try:
|
||||
message = json.loads(data)
|
||||
|
||||
if message.get("action") == "ping":
|
||||
await websocket.send_json({"type": "pong"})
|
||||
|
||||
elif message.get("action") == "acknowledge":
|
||||
# Cliente confirma recepción de alerta
|
||||
alert_id = message.get("alert_id")
|
||||
await websocket.send_json({
|
||||
"type": "acknowledged",
|
||||
"alert_id": alert_id,
|
||||
})
|
||||
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
except WebSocketDisconnect:
|
||||
await manager.disconnect(websocket, "alertas", user_id)
|
||||
|
||||
|
||||
@router.websocket("/ws/alertas/vehiculo/{vehiculo_id}")
|
||||
async def websocket_alertas_vehiculo(
|
||||
websocket: WebSocket,
|
||||
vehiculo_id: int,
|
||||
token: Optional[str] = Query(None),
|
||||
):
|
||||
"""
|
||||
WebSocket para alertas de un vehículo específico.
|
||||
|
||||
Args:
|
||||
websocket: Conexión WebSocket.
|
||||
vehiculo_id: ID del vehículo.
|
||||
token: Token JWT para autenticación (opcional).
|
||||
"""
|
||||
user_id = await get_user_from_token(token)
|
||||
|
||||
await manager.connect(websocket, "alertas", user_id)
|
||||
await manager.subscribe_vehicle(websocket, vehiculo_id)
|
||||
|
||||
# Enviar confirmación
|
||||
await websocket.send_json({
|
||||
"type": "connected",
|
||||
"channel": "vehicle_alerts",
|
||||
"vehicle_id": vehiculo_id,
|
||||
})
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
|
||||
try:
|
||||
message = json.loads(data)
|
||||
|
||||
if message.get("action") == "ping":
|
||||
await websocket.send_json({"type": "pong"})
|
||||
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
except WebSocketDisconnect:
|
||||
await manager.unsubscribe_vehicle(websocket, vehiculo_id)
|
||||
await manager.disconnect(websocket, "alertas", user_id)
|
||||
266
backend/app/api/websocket/manager.py
Normal file
266
backend/app/api/websocket/manager.py
Normal file
@@ -0,0 +1,266 @@
|
||||
"""
|
||||
Gestor de conexiones WebSocket.
|
||||
|
||||
Maneja las conexiones de clientes WebSocket para
|
||||
actualizaciones en tiempo real.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, List, Optional, Set
|
||||
|
||||
from fastapi import WebSocket
|
||||
|
||||
|
||||
class ConnectionManager:
|
||||
"""
|
||||
Gestor de conexiones WebSocket.
|
||||
|
||||
Mantiene un registro de conexiones activas y permite
|
||||
enviar mensajes a clientes específicos o a todos.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Inicializa el gestor de conexiones."""
|
||||
# Conexiones activas por tipo de suscripción
|
||||
self._connections: Dict[str, Set[WebSocket]] = {
|
||||
"ubicaciones": set(),
|
||||
"alertas": set(),
|
||||
"vehiculos": set(),
|
||||
}
|
||||
# Conexiones por usuario
|
||||
self._user_connections: Dict[int, Set[WebSocket]] = {}
|
||||
# Suscripciones a vehículos específicos
|
||||
self._vehicle_subscriptions: Dict[int, Set[WebSocket]] = {}
|
||||
# Lock para operaciones thread-safe
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def connect(
|
||||
self,
|
||||
websocket: WebSocket,
|
||||
channel: str = "ubicaciones",
|
||||
user_id: Optional[int] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Acepta una nueva conexión WebSocket.
|
||||
|
||||
Args:
|
||||
websocket: Conexión WebSocket.
|
||||
channel: Canal de suscripción.
|
||||
user_id: ID del usuario (opcional).
|
||||
"""
|
||||
await websocket.accept()
|
||||
|
||||
async with self._lock:
|
||||
# Agregar a conexiones del canal
|
||||
if channel in self._connections:
|
||||
self._connections[channel].add(websocket)
|
||||
|
||||
# Agregar a conexiones del usuario
|
||||
if user_id:
|
||||
if user_id not in self._user_connections:
|
||||
self._user_connections[user_id] = set()
|
||||
self._user_connections[user_id].add(websocket)
|
||||
|
||||
async def disconnect(
|
||||
self,
|
||||
websocket: WebSocket,
|
||||
channel: str = "ubicaciones",
|
||||
user_id: Optional[int] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Desconecta un WebSocket.
|
||||
|
||||
Args:
|
||||
websocket: Conexión WebSocket.
|
||||
channel: Canal de suscripción.
|
||||
user_id: ID del usuario (opcional).
|
||||
"""
|
||||
async with self._lock:
|
||||
# Remover de conexiones del canal
|
||||
if channel in self._connections:
|
||||
self._connections[channel].discard(websocket)
|
||||
|
||||
# Remover de conexiones del usuario
|
||||
if user_id and user_id in self._user_connections:
|
||||
self._user_connections[user_id].discard(websocket)
|
||||
if not self._user_connections[user_id]:
|
||||
del self._user_connections[user_id]
|
||||
|
||||
# Remover de suscripciones de vehículos
|
||||
for vehicle_id in list(self._vehicle_subscriptions.keys()):
|
||||
self._vehicle_subscriptions[vehicle_id].discard(websocket)
|
||||
if not self._vehicle_subscriptions[vehicle_id]:
|
||||
del self._vehicle_subscriptions[vehicle_id]
|
||||
|
||||
async def subscribe_vehicle(
|
||||
self,
|
||||
websocket: WebSocket,
|
||||
vehicle_id: int,
|
||||
) -> None:
|
||||
"""
|
||||
Suscribe un WebSocket a actualizaciones de un vehículo específico.
|
||||
|
||||
Args:
|
||||
websocket: Conexión WebSocket.
|
||||
vehicle_id: ID del vehículo.
|
||||
"""
|
||||
async with self._lock:
|
||||
if vehicle_id not in self._vehicle_subscriptions:
|
||||
self._vehicle_subscriptions[vehicle_id] = set()
|
||||
self._vehicle_subscriptions[vehicle_id].add(websocket)
|
||||
|
||||
async def unsubscribe_vehicle(
|
||||
self,
|
||||
websocket: WebSocket,
|
||||
vehicle_id: int,
|
||||
) -> None:
|
||||
"""
|
||||
Desuscribe un WebSocket de un vehículo.
|
||||
|
||||
Args:
|
||||
websocket: Conexión WebSocket.
|
||||
vehicle_id: ID del vehículo.
|
||||
"""
|
||||
async with self._lock:
|
||||
if vehicle_id in self._vehicle_subscriptions:
|
||||
self._vehicle_subscriptions[vehicle_id].discard(websocket)
|
||||
|
||||
async def broadcast(
|
||||
self,
|
||||
message: dict,
|
||||
channel: str = "ubicaciones",
|
||||
) -> None:
|
||||
"""
|
||||
Envía un mensaje a todos los clientes de un canal.
|
||||
|
||||
Args:
|
||||
message: Mensaje a enviar.
|
||||
channel: Canal de destino.
|
||||
"""
|
||||
if channel not in self._connections:
|
||||
return
|
||||
|
||||
message_json = json.dumps(message, default=str)
|
||||
disconnected = []
|
||||
|
||||
for websocket in self._connections[channel]:
|
||||
try:
|
||||
await websocket.send_text(message_json)
|
||||
except Exception:
|
||||
disconnected.append(websocket)
|
||||
|
||||
# Limpiar conexiones desconectadas
|
||||
for ws in disconnected:
|
||||
await self.disconnect(ws, channel)
|
||||
|
||||
async def broadcast_vehicle_update(
|
||||
self,
|
||||
vehicle_id: int,
|
||||
data: dict,
|
||||
) -> None:
|
||||
"""
|
||||
Envía actualización a suscriptores de un vehículo específico.
|
||||
|
||||
Args:
|
||||
vehicle_id: ID del vehículo.
|
||||
data: Datos a enviar.
|
||||
"""
|
||||
message = {
|
||||
"type": "vehicle_update",
|
||||
"vehicle_id": vehicle_id,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"data": data,
|
||||
}
|
||||
message_json = json.dumps(message, default=str)
|
||||
|
||||
# Enviar a suscriptores del vehículo
|
||||
if vehicle_id in self._vehicle_subscriptions:
|
||||
disconnected = []
|
||||
for websocket in self._vehicle_subscriptions[vehicle_id]:
|
||||
try:
|
||||
await websocket.send_text(message_json)
|
||||
except Exception:
|
||||
disconnected.append(websocket)
|
||||
|
||||
for ws in disconnected:
|
||||
await self.unsubscribe_vehicle(ws, vehicle_id)
|
||||
|
||||
# También enviar al canal general de ubicaciones
|
||||
await self.broadcast(message, "ubicaciones")
|
||||
|
||||
async def send_to_user(
|
||||
self,
|
||||
user_id: int,
|
||||
message: dict,
|
||||
) -> None:
|
||||
"""
|
||||
Envía un mensaje a todas las conexiones de un usuario.
|
||||
|
||||
Args:
|
||||
user_id: ID del usuario.
|
||||
message: Mensaje a enviar.
|
||||
"""
|
||||
if user_id not in self._user_connections:
|
||||
return
|
||||
|
||||
message_json = json.dumps(message, default=str)
|
||||
disconnected = []
|
||||
|
||||
for websocket in self._user_connections[user_id]:
|
||||
try:
|
||||
await websocket.send_text(message_json)
|
||||
except Exception:
|
||||
disconnected.append(websocket)
|
||||
|
||||
# Limpiar conexiones desconectadas
|
||||
for ws in disconnected:
|
||||
await self.disconnect(ws, user_id=user_id)
|
||||
|
||||
async def send_alert(
|
||||
self,
|
||||
alert_data: dict,
|
||||
) -> None:
|
||||
"""
|
||||
Envía una alerta a todos los clientes suscritos.
|
||||
|
||||
Args:
|
||||
alert_data: Datos de la alerta.
|
||||
"""
|
||||
message = {
|
||||
"type": "alert",
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"data": alert_data,
|
||||
}
|
||||
await self.broadcast(message, "alertas")
|
||||
|
||||
def get_connection_count(self) -> dict:
|
||||
"""
|
||||
Obtiene el conteo de conexiones activas.
|
||||
|
||||
Returns:
|
||||
Dict con conteo por canal.
|
||||
"""
|
||||
return {
|
||||
channel: len(connections)
|
||||
for channel, connections in self._connections.items()
|
||||
}
|
||||
|
||||
def get_vehicle_subscribers(self, vehicle_id: int) -> int:
|
||||
"""
|
||||
Obtiene el número de suscriptores de un vehículo.
|
||||
|
||||
Args:
|
||||
vehicle_id: ID del vehículo.
|
||||
|
||||
Returns:
|
||||
Número de suscriptores.
|
||||
"""
|
||||
if vehicle_id in self._vehicle_subscriptions:
|
||||
return len(self._vehicle_subscriptions[vehicle_id])
|
||||
return 0
|
||||
|
||||
|
||||
# Instancia global del gestor de conexiones
|
||||
manager = ConnectionManager()
|
||||
187
backend/app/api/websocket/ubicaciones.py
Normal file
187
backend/app/api/websocket/ubicaciones.py
Normal file
@@ -0,0 +1,187 @@
|
||||
"""
|
||||
WebSocket endpoint para ubicaciones en tiempo real.
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, WebSocket, WebSocketDisconnect
|
||||
from jose import JWTError
|
||||
|
||||
from app.api.websocket.manager import manager
|
||||
from app.core.security import decode_token
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
async def get_user_from_token(token: Optional[str]) -> Optional[int]:
|
||||
"""
|
||||
Obtiene el ID de usuario desde un token JWT.
|
||||
|
||||
Args:
|
||||
token: Token JWT.
|
||||
|
||||
Returns:
|
||||
ID del usuario o None.
|
||||
"""
|
||||
if not token:
|
||||
return None
|
||||
try:
|
||||
payload = decode_token(token)
|
||||
return int(payload.get("sub"))
|
||||
except (JWTError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
@router.websocket("/ws/ubicaciones")
|
||||
async def websocket_ubicaciones(
|
||||
websocket: WebSocket,
|
||||
token: Optional[str] = Query(None),
|
||||
):
|
||||
"""
|
||||
WebSocket para recibir actualizaciones de ubicaciones.
|
||||
|
||||
Permite suscribirse a:
|
||||
- Todas las ubicaciones de la flota
|
||||
- Vehículos específicos
|
||||
|
||||
Args:
|
||||
websocket: Conexión WebSocket.
|
||||
token: Token JWT para autenticación (opcional).
|
||||
"""
|
||||
user_id = await get_user_from_token(token)
|
||||
|
||||
await manager.connect(websocket, "ubicaciones", user_id)
|
||||
|
||||
try:
|
||||
while True:
|
||||
# Recibir mensajes del cliente
|
||||
data = await websocket.receive_text()
|
||||
|
||||
try:
|
||||
message = json.loads(data)
|
||||
action = message.get("action")
|
||||
|
||||
if action == "subscribe_vehicle":
|
||||
# Suscribirse a un vehículo específico
|
||||
vehicle_id = message.get("vehicle_id")
|
||||
if vehicle_id:
|
||||
await manager.subscribe_vehicle(websocket, vehicle_id)
|
||||
await websocket.send_json({
|
||||
"type": "subscribed",
|
||||
"vehicle_id": vehicle_id,
|
||||
})
|
||||
|
||||
elif action == "unsubscribe_vehicle":
|
||||
# Desuscribirse de un vehículo
|
||||
vehicle_id = message.get("vehicle_id")
|
||||
if vehicle_id:
|
||||
await manager.unsubscribe_vehicle(websocket, vehicle_id)
|
||||
await websocket.send_json({
|
||||
"type": "unsubscribed",
|
||||
"vehicle_id": vehicle_id,
|
||||
})
|
||||
|
||||
elif action == "ping":
|
||||
# Responder ping para keepalive
|
||||
await websocket.send_json({"type": "pong"})
|
||||
|
||||
except json.JSONDecodeError:
|
||||
await websocket.send_json({
|
||||
"type": "error",
|
||||
"message": "Invalid JSON",
|
||||
})
|
||||
|
||||
except WebSocketDisconnect:
|
||||
await manager.disconnect(websocket, "ubicaciones", user_id)
|
||||
|
||||
|
||||
@router.websocket("/ws/vehiculo/{vehiculo_id}")
|
||||
async def websocket_vehiculo(
|
||||
websocket: WebSocket,
|
||||
vehiculo_id: int,
|
||||
token: Optional[str] = Query(None),
|
||||
):
|
||||
"""
|
||||
WebSocket para seguir un vehículo específico.
|
||||
|
||||
Args:
|
||||
websocket: Conexión WebSocket.
|
||||
vehiculo_id: ID del vehículo a seguir.
|
||||
token: Token JWT para autenticación (opcional).
|
||||
"""
|
||||
user_id = await get_user_from_token(token)
|
||||
|
||||
await manager.connect(websocket, "ubicaciones", user_id)
|
||||
await manager.subscribe_vehicle(websocket, vehiculo_id)
|
||||
|
||||
# Enviar confirmación de suscripción
|
||||
await websocket.send_json({
|
||||
"type": "connected",
|
||||
"vehicle_id": vehiculo_id,
|
||||
})
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
|
||||
try:
|
||||
message = json.loads(data)
|
||||
|
||||
if message.get("action") == "ping":
|
||||
await websocket.send_json({"type": "pong"})
|
||||
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
except WebSocketDisconnect:
|
||||
await manager.unsubscribe_vehicle(websocket, vehiculo_id)
|
||||
await manager.disconnect(websocket, "ubicaciones", user_id)
|
||||
|
||||
|
||||
@router.websocket("/ws/flota")
|
||||
async def websocket_flota(
|
||||
websocket: WebSocket,
|
||||
token: Optional[str] = Query(None),
|
||||
):
|
||||
"""
|
||||
WebSocket para monitoreo de toda la flota.
|
||||
|
||||
Recibe actualizaciones de todos los vehículos activos.
|
||||
|
||||
Args:
|
||||
websocket: Conexión WebSocket.
|
||||
token: Token JWT para autenticación (opcional).
|
||||
"""
|
||||
user_id = await get_user_from_token(token)
|
||||
|
||||
await manager.connect(websocket, "ubicaciones", user_id)
|
||||
|
||||
# Enviar confirmación
|
||||
await websocket.send_json({
|
||||
"type": "connected",
|
||||
"channel": "fleet",
|
||||
})
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
|
||||
try:
|
||||
message = json.loads(data)
|
||||
|
||||
if message.get("action") == "ping":
|
||||
await websocket.send_json({"type": "pong"})
|
||||
|
||||
elif message.get("action") == "request_status":
|
||||
# Enviar estado actual de conexiones
|
||||
await websocket.send_json({
|
||||
"type": "status",
|
||||
"connections": manager.get_connection_count(),
|
||||
})
|
||||
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
except WebSocketDisconnect:
|
||||
await manager.disconnect(websocket, "ubicaciones", user_id)
|
||||
Reference in New Issue
Block a user