Table of Contents
- WebSocket Protocol
- Endpoint
- Conexión
- Tipos de eventos
- 1. Ruta Iniciada
- 2. Aproximándose
- 3. Falla Mecánica
- 4. Ruta Tarde
- 5. Completado
- Filtrado por preferencias
- Manejo de desconexiones
- Provider Riverpod para WebSocket
- Testing del WebSocket
- CORS y seguridad
- Troubleshooting
- "WebSocket connection failed"
- "Connection closed immediately"
- "No events received"
- Eventos duplicados en Flutter
- Siguiente paso
WebSocket Protocol
Documentación del protocolo WebSocket para notificaciones en tiempo real.
Endpoint
ws://localhost:8000/ws/{address_id}
Parámetros:
{address_id}— ID del domicilio registrado por el usuario
Ejemplo:
ws://localhost:8000/ws/1
Conexión
Desde línea de comandos (wscat)
# Instalar wscat
npm install -g wscat
# Conectar
wscat -c ws://localhost:8000/ws/1
Desde Flutter
import 'package:web_socket_channel/web_socket_channel.dart';
final channel = WebSocketChannel.connect(
Uri.parse('ws://10.0.2.2:8000/ws/1'), // Android emulator
);
// Escuchar eventos
channel.stream.listen((message) {
final json = jsonDecode(message);
print('Evento recibido: ${json['tipo']}');
});
Tipos de eventos
El backend envía eventos en formato JSON. Todos los eventos tienen esta estructura base:
{
"tipo": "string",
"address_id": int,
"eta_minutos": int | null,
"mensaje": "string",
"hora_utc": "ISO-8601 timestamp"
}
1. Ruta Iniciada
Enviado cuando el simulador arranca.
Cuándo: POST /admin/route/{id}/start
JSON:
{
"tipo": "ruta_iniciada",
"address_id": 1,
"eta_minutos": null,
"mensaje": "El camión de tu zona comenzó su recorrido.",
"hora_utc": "2026-05-22T19:00:00.123456"
}
Acción en Flutter:
- Mostrar badge "En camino" en home
- Opcional: notificación push local
2. Aproximándose
Enviado cuando el ETA <= umbral (default 10 min).
Cuándo: El camión está cerca del domicilio.
JSON:
{
"tipo": "aproximandose",
"address_id": 1,
"eta_minutos": 8,
"mensaje": "El camión llega en ~8 minutos. Saca tu basura ahora.",
"hora_utc": "2026-05-22T19:35:00.123456"
}
Acción en Flutter:
- Mostrar alerta destacada (SnackBar, Dialog)
- Notificación push local con sonido
- Actualizar badge a "¡Llega pronto!"
3. Falla Mecánica
Enviado cuando hay una avería.
Cuándo: POST /alerts/breakdown
JSON:
{
"tipo": "falla_mecanica",
"address_id": 1,
"eta_minutos": null,
"mensaje": "El camión de tu zona presenta una falla mecánica. Te avisaremos cuando se resuelva.",
"hora_utc": "2026-05-22T20:15:00.123456"
}
Acción en Flutter:
- Cambiar badge a "Avería"
- Mostrar mensaje de disculpa
- Opcional: botón "Reportar problema"
4. Ruta Tarde
Enviado cuando hay un retraso significativo.
Cuándo: POST /admin/route/{id}/delay
JSON:
{
"tipo": "ruta_tarde",
"address_id": 1,
"eta_minutos": 45,
"mensaje": "La ruta se ha retrasado debido a tráfico. Nueva estimación: 45 minutos.",
"hora_utc": "2026-05-22T19:10:00.123456"
}
Acción en Flutter:
- Actualizar ventana horaria
- Mostrar ícono de reloj
- Badge "Retrasado"
5. Completado
Enviado cuando el camión pasa por el domicilio.
Cuándo: El camión alcanza el punto correspondiente al domicilio.
JSON:
{
"tipo": "completado",
"address_id": 1,
"eta_minutos": 0,
"mensaje": "El camión pasó por tu zona. ¡Gracias por separar tu basura!",
"hora_utc": "2026-05-22T19:43:00.123456"
}
Acción en Flutter:
- Badge "Completado" (verde)
- Opcional: pedir feedback (calificar servicio)
Filtrado por preferencias
El backend consulta notification_preferences antes de enviar eventos. Si el usuario desactivó una categoría, no recibe ese tipo de notificación.
Ejemplo:
-- Usuario 1 tiene notify_proximity = False
SELECT notify_proximity FROM notification_preferences WHERE user_id = 1;
-- Resultado: False
-- Backend NO envía evento "aproximandose" a address_id 1
Manejo de desconexiones
Reconexión automática (Flutter)
class WSManager {
WebSocketChannel? _channel;
Timer? _reconnectTimer;
final int addressId;
void connect() {
try {
_channel = WebSocketChannel.connect(
Uri.parse('ws://10.0.2.2:8000/ws/$addressId'),
);
_channel!.stream.listen(
_onMessage,
onError: _onError,
onDone: _onDone,
);
} catch (e) {
print('Error al conectar: $e');
_scheduleReconnect();
}
}
void _onError(error) {
print('WebSocket error: $error');
_scheduleReconnect();
}
void _onDone() {
print('WebSocket cerrado');
_scheduleReconnect();
}
void _scheduleReconnect() {
_reconnectTimer?.cancel();
_reconnectTimer = Timer(Duration(seconds: 5), connect);
}
void _onMessage(dynamic message) {
final json = jsonDecode(message);
// Procesar evento
}
}
Provider Riverpod para WebSocket
// lib/features/eta/presentation/providers/ws_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
class NotificationEvent {
final String tipo;
final int addressId;
final int? etaMinutos;
final String mensaje;
final DateTime hora;
NotificationEvent.fromJson(Map<String, dynamic> json)
: tipo = json['tipo'],
addressId = json['address_id'],
etaMinutos = json['eta_minutos'],
mensaje = json['mensaje'],
hora = DateTime.parse(json['hora_utc']);
}
final wsProvider = StreamProvider.family<NotificationEvent, int>(
(ref, addressId) {
final baseUrl = ref.watch(wsBaseUrlProvider);
final channel = WebSocketChannel.connect(
Uri.parse('$baseUrl/ws/$addressId'),
);
return channel.stream.map((data) {
final json = jsonDecode(data);
return NotificationEvent.fromJson(json);
});
},
);
Usar en la UI
class ETAHomeScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final addressId = 1; // Del usuario autenticado
// Escuchar WebSocket
ref.listen(wsProvider(addressId), (prev, next) {
next.when(
data: (event) {
switch (event.tipo) {
case 'aproximandose':
_showProximityAlert(context, event);
break;
case 'falla_mecanica':
_showBreakdownAlert(context, event);
break;
case 'completado':
_showCompletedAlert(context, event);
break;
}
},
error: (e, _) => _showError(context, 'Error de conexión'),
loading: () {},
);
});
return Scaffold(/* ... */);
}
void _showProximityAlert(BuildContext context, NotificationEvent event) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(event.mensaje),
backgroundColor: Colors.orange,
duration: Duration(seconds: 10),
action: SnackBarAction(
label: 'OK',
onPressed: () {},
),
),
);
}
}
Testing del WebSocket
1. Conectar con wscat
wscat -c ws://localhost:8000/ws/1
2. Arrancar simulador (en otra terminal)
curl -X POST http://localhost:8000/admin/route/RUTA-01/start
3. Observar eventos
En la terminal de wscat, deberías ver:
{"tipo":"ruta_iniciada","address_id":1,"eta_minutos":null,...}
{"tipo":"aproximandose","address_id":1,"eta_minutos":9,...}
{"tipo":"aproximandose","address_id":1,"eta_minutos":8,...}
...
4. Forzar avería
curl -X POST http://localhost:8000/alerts/breakdown \
-H "Content-Type: application/json" \
-d '{"route_id":"RUTA-01"}'
En wscat:
{"tipo":"falla_mecanica","address_id":1,"eta_minutos":null,...}
CORS y seguridad
El backend ya tiene CORS habilitado para desarrollo:
# app/main.py
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # En producción, especificar dominios
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
En producción, cambiar allow_origins a dominios específicos.
Troubleshooting
"WebSocket connection failed"
Causa: Backend no está corriendo o URL incorrecta.
Solución:
# Verificar que el backend esté up
curl http://localhost:8000/health
# Verificar la URL según tu entorno:
# - Emulador Android: ws://10.0.2.2:8000/ws/1
# - Emulador iOS: ws://localhost:8000/ws/1
# - Dispositivo físico: ws://192.168.x.x:8000/ws/1
"Connection closed immediately"
Causa: El address_id no existe en la BD.
Solución:
# Verificar que el address_id 1 existe
curl http://localhost:8000/eta/1
# Si falla, usa otro address_id válido
"No events received"
Causa: El simulador no está corriendo o el camión ya pasó.
Solución:
# Verificar estado del camión
curl http://localhost:8000/admin/route/RUTA-01/status
# Reiniciar simulador si está detenido
curl -X POST http://localhost:8000/admin/route/RUTA-01/start
Eventos duplicados en Flutter
Causa: Múltiples listeners al mismo stream.
Solución:
// Usar StreamProvider de Riverpod (automáticamente maneja múltiples listeners)
final wsProvider = StreamProvider.family<NotificationEvent, int>(...);
// NO crear múltiples WebSocketChannels manualmente
Siguiente paso
- Modelos de datos — schemas completos de eventos
- Integración entre módulos — conectar A, B, C, D
- Troubleshooting WebSocket