WebSocket API - iapunto/carousel_api GitHub Wiki

🔌 WebSocket API

La API WebSocket de carousel_api proporciona comunicación en tiempo real para recibir actualizaciones de estado y enviar comandos de manera asíncrona.


📋 Información General

  • URL: ws://localhost:8765
  • Protocolo: WebSocket RFC 6455
  • Formato: JSON
  • Autenticación: No requerida (configurable)
  • Heartbeat: Ping/Pong automático cada 30s

🔗 Conexión

Establecer Conexión

const ws = new WebSocket('ws://localhost:8765');

ws.onopen = function(event) {
    console.log('Conectado al WebSocket');
};

ws.onmessage = function(event) {
    const data = JSON.parse(event.data);
    console.log('Mensaje recibido:', data);
};

ws.onerror = function(error) {
    console.error('Error WebSocket:', error);
};

ws.onclose = function(event) {
    console.log('Conexión cerrada:', event.code, event.reason);
};

Python con websockets

import asyncio
import websockets
import json

async def connect_websocket():
    uri = "ws://localhost:8765"
    async with websockets.connect(uri) as websocket:
        # Recibir mensaje de bienvenida
        welcome = await websocket.recv()
        print(f"Bienvenida: {welcome}")
        
        # Enviar mensaje
        message = {
            "type": "get_status",
            "machine_id": "machine_1",
            "timestamp": "2025-01-27T10:30:00.000Z"
        }
        await websocket.send(json.dumps(message))
        
        # Escuchar respuestas
        async for message in websocket:
            data = json.loads(message)
            print(f"Recibido: {data}")

# Ejecutar
asyncio.run(connect_websocket())

📨 Tipos de Mensaje

1. Mensaje de Bienvenida (Automático)

Enviado por: Servidor (automático al conectar)

{
  "type": "welcome",
  "timestamp": "2025-01-27T10:30:00.000Z",
  "mode": "multi-plc",
  "server_info": {
    "version": "2.6.0",
    "capabilities": [
      "status_updates", 
      "command_execution", 
      "real_time_notifications"
    ]
  },
  "machines": [
    {
      "id": "machine_1",
      "name": "Carrusel Principal",
      "ip": "192.168.1.50",
      "port": 3200,
      "type": "Real PLC"
    },
    {
      "id": "machine_2",
      "name": "Carrusel Secundario",
      "ip": "192.168.1.51",
      "port": 3200,
      "type": "Simulator"
    }
  ]
}

2. Solicitar Estado

Enviado por: Cliente

{
  "type": "get_status",
  "machine_id": "machine_1",
  "timestamp": "2025-01-27T10:30:00.000Z"
}

Respuesta del servidor:

{
  "type": "machine_status",
  "machine_id": "machine_1",
  "status": {
    "READY": "OK",
    "RUN": "Detenido",
    "MODO_OPERACION": "Remoto",
    "ALARMA": "Desactivada"
  },
  "position": 15,
  "raw_status": 218,
  "connection_status": "connected",
  "timestamp": "2025-01-27T10:30:00.000Z"
}

3. Enviar Comando

Enviado por: Cliente

{
  "type": "send_command",
  "machine_id": "machine_1",
  "command": {
    "command": 1,
    "argument": 25
  },
  "timestamp": "2025-01-27T10:30:00.000Z"
}

Respuesta del servidor:

{
  "type": "command_result",
  "machine_id": "machine_1",
  "success": true,
  "data": {
    "command_sent": 1,
    "argument": 25,
    "result": "Comando ejecutado correctamente"
  },
  "timestamp": "2025-01-27T10:30:00.000Z"
}

4. Suscribirse a Actualizaciones

Enviado por: Cliente

{
  "type": "subscribe",
  "subscription_type": "status_updates",
  "machine_ids": ["machine_1", "machine_2"],
  "timestamp": "2025-01-27T10:30:00.000Z"
}

Confirmación del servidor:

{
  "type": "subscription_confirmed",
  "subscription_type": "status_updates",
  "machine_ids": ["machine_1", "machine_2"],
  "timestamp": "2025-01-27T10:30:00.000Z"
}

5. Broadcast de Estado (Automático)

Enviado por: Servidor (automático cuando cambia el estado)

{
  "type": "status_broadcast",
  "timestamp": "2025-01-27T10:30:00.000Z",
  "status": {
    "machine_1": {
      "status": {
        "READY": "OK",
        "RUN": "En movimiento",
        "MODO_OPERACION": "Remoto",
        "ALARMA": "Desactivada"
      },
      "position": 25,
      "raw_status": 219,
      "connection_status": "connected"
    },
    "machine_2": {
      "status": {
        "READY": "OK",
        "RUN": "Detenido",
        "MODO_OPERACION": "Remoto",
        "ALARMA": "Desactivada"
      },
      "position": 10,
      "raw_status": 218,
      "connection_status": "connected"
    }
  }
}

6. Notificación de Error

Enviado por: Servidor

{
  "type": "error",
  "error": "Machine not found",
  "code": "MACHINE_NOT_FOUND",
  "machine_id": "invalid_machine",
  "timestamp": "2025-01-27T10:30:00.000Z"
}

🔔 Tipos de Suscripción

Status Updates

Recibe actualizaciones automáticas cuando cambia el estado de las máquinas.

{
  "type": "subscribe",
  "subscription_type": "status_updates",
  "machine_ids": ["machine_1", "machine_2"]
}

Command Results

Recibe notificaciones cuando se ejecutan comandos en las máquinas.

{
  "type": "subscribe",
  "subscription_type": "command_results",
  "machine_ids": ["machine_1"]
}

All Events

Recibe todas las notificaciones disponibles.

{
  "type": "subscribe",
  "subscription_type": "all_events"
}

🔧 Ejemplos de Implementación

Cliente JavaScript Completo

class CarouselWebSocketClient {
    constructor(url = 'ws://localhost:8765') {
        this.url = url;
        this.ws = null;
        this.reconnectInterval = 5000;
        this.maxReconnectAttempts = 5;
        this.reconnectAttempts = 0;
    }

    connect() {
        this.ws = new WebSocket(this.url);
        
        this.ws.onopen = (event) => {
            console.log('Conectado al WebSocket');
            this.reconnectAttempts = 0;
            this.subscribeToUpdates();
        };

        this.ws.onmessage = (event) => {
            const data = JSON.parse(event.data);
            this.handleMessage(data);
        };

        this.ws.onclose = (event) => {
            console.log('Conexión cerrada:', event.code);
            this.reconnect();
        };

        this.ws.onerror = (error) => {
            console.error('Error WebSocket:', error);
        };
    }

    handleMessage(data) {
        switch(data.type) {
            case 'welcome':
                console.log('Bienvenida recibida:', data);
                break;
            case 'status_broadcast':
                this.onStatusUpdate(data.status);
                break;
            case 'command_result':
                this.onCommandResult(data);
                break;
            case 'error':
                console.error('Error del servidor:', data);
                break;
        }
    }

    subscribeToUpdates() {
        this.send({
            type: 'subscribe',
            subscription_type: 'status_updates',
            timestamp: new Date().toISOString()
        });
    }

    sendCommand(machineId, command, argument) {
        this.send({
            type: 'send_command',
            machine_id: machineId,
            command: {
                command: command,
                argument: argument
            },
            timestamp: new Date().toISOString()
        });
    }

    getStatus(machineId) {
        this.send({
            type: 'get_status',
            machine_id: machineId,
            timestamp: new Date().toISOString()
        });
    }

    send(data) {
        if (this.ws && this.ws.readyState === WebSocket.OPEN) {
            this.ws.send(JSON.stringify(data));
        } else {
            console.warn('WebSocket no está conectado');
        }
    }

    reconnect() {
        if (this.reconnectAttempts < this.maxReconnectAttempts) {
            this.reconnectAttempts++;
            console.log(`Intentando reconectar... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
            setTimeout(() => this.connect(), this.reconnectInterval);
        } else {
            console.error('Máximo número de intentos de reconexión alcanzado');
        }
    }

    onStatusUpdate(status) {
        // Implementar lógica de actualización de UI
        console.log('Estado actualizado:', status);
    }

    onCommandResult(result) {
        // Implementar lógica de respuesta a comandos
        console.log('Resultado de comando:', result);
    }

    disconnect() {
        if (this.ws) {
            this.ws.close();
        }
    }
}

// Uso
const client = new CarouselWebSocketClient();
client.connect();

// Enviar comando
client.sendCommand('machine_1', 1, 25);

// Obtener estado
client.getStatus('machine_1');

Cliente Python Asíncrono

import asyncio
import websockets
import json
from datetime import datetime

class CarouselWebSocketClient:
    def __init__(self, uri="ws://localhost:8765"):
        self.uri = uri
        self.websocket = None
        self.running = False

    async def connect(self):
        """Conectar al WebSocket"""
        try:
            self.websocket = await websockets.connect(self.uri)
            self.running = True
            print("Conectado al WebSocket")
            
            # Suscribirse a actualizaciones
            await self.subscribe_to_updates()
            
            # Escuchar mensajes
            await self.listen()
            
        except Exception as e:
            print(f"Error de conexión: {e}")

    async def listen(self):
        """Escuchar mensajes del servidor"""
        try:
            async for message in self.websocket:
                data = json.loads(message)
                await self.handle_message(data)
        except websockets.exceptions.ConnectionClosed:
            print("Conexión cerrada")
            self.running = False

    async def handle_message(self, data):
        """Manejar mensajes recibidos"""
        message_type = data.get('type')
        
        if message_type == 'welcome':
            print(f"Bienvenida: {data}")
        elif message_type == 'status_broadcast':
            await self.on_status_update(data['status'])
        elif message_type == 'command_result':
            await self.on_command_result(data)
        elif message_type == 'error':
            print(f"Error del servidor: {data}")

    async def subscribe_to_updates(self):
        """Suscribirse a actualizaciones de estado"""
        message = {
            "type": "subscribe",
            "subscription_type": "status_updates",
            "timestamp": datetime.now().isoformat()
        }
        await self.send(message)

    async def send_command(self, machine_id, command, argument):
        """Enviar comando a una máquina"""
        message = {
            "type": "send_command",
            "machine_id": machine_id,
            "command": {
                "command": command,
                "argument": argument
            },
            "timestamp": datetime.now().isoformat()
        }
        await self.send(message)

    async def get_status(self, machine_id):
        """Obtener estado de una máquina"""
        message = {
            "type": "get_status",
            "machine_id": machine_id,
            "timestamp": datetime.now().isoformat()
        }
        await self.send(message)

    async def send(self, data):
        """Enviar mensaje al servidor"""
        if self.websocket and not self.websocket.closed:
            await self.websocket.send(json.dumps(data))
        else:
            print("WebSocket no está conectado")

    async def on_status_update(self, status):
        """Manejar actualizaciones de estado"""
        print(f"Estado actualizado: {status}")

    async def on_command_result(self, result):
        """Manejar resultados de comandos"""
        print(f"Resultado de comando: {result}")

    async def disconnect(self):
        """Desconectar del WebSocket"""
        self.running = False
        if self.websocket:
            await self.websocket.close()

# Uso
async def main():
    client = CarouselWebSocketClient()
    
    # Conectar
    await client.connect()
    
    # Enviar comando
    await client.send_command('machine_1', 1, 25)
    
    # Obtener estado
    await client.get_status('machine_1')
    
    # Mantener conexión
    while client.running:
        await asyncio.sleep(1)

# Ejecutar
if __name__ == "__main__":
    asyncio.run(main())

🛡️ Manejo de Errores

Códigos de Error Comunes

Código Descripción Solución
MACHINE_NOT_FOUND Máquina no existe Verificar machine_id
INVALID_COMMAND Comando inválido Verificar formato del comando
CONNECTION_ERROR Error de conexión con PLC Verificar red y configuración
BUSY Máquina ocupada Esperar y reintentar
INVALID_MESSAGE_FORMAT Formato de mensaje inválido Verificar estructura JSON

Reconexión Automática

function setupReconnection(ws, url, maxAttempts = 5) {
    let attempts = 0;
    
    ws.onclose = function(event) {
        if (attempts < maxAttempts) {
            attempts++;
            console.log(`Reconectando... (${attempts}/${maxAttempts})`);
            setTimeout(() => {
                const newWs = new WebSocket(url);
                setupReconnection(newWs, url, maxAttempts);
            }, 2000 * attempts); // Backoff exponencial
        }
    };
}

🔗 Enlaces Relacionados