1
02-websocket.md
hack_23031391_8ff9d8 edited this page 2026-05-23 02:37:51 +00:00

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