←

Comandos remotos a vehiculos con confirmacion 2-step

Contexto

En un sistema de fleet tracking puedes enviar comandos remotos a los dispositivos GPS (apagar motor, activar alarma, solicitar ubicacion). El problema es que el dispositivo no responde inmediatamente: el comando viaja del servidor al proveedor GPS, del proveedor al dispositivo celular, y la respuesta regresa por el mismo camino. Puede tardar de 5 segundos a 5 minutos.

Lo que aprendi

El patron 2-step separa el envio del resultado. Paso 1: enviar el comando. Paso 2: pollear el resultado.

async def send_command(self, imei: str, instruction_id: int, template: str, params: list[str] = None) -> dict:
    """Envia comando al dispositivo. Retorna ID de instruccion."""
    result = await self.provider.send_instruction(
        imei=imei,
        inst_id=str(instruction_id),
        inst_template=template,
        params=params or [],
    )
    # Guardar en DB con status "pending"
    command = await self.repo.create({
        "imei": imei,
        "instruction_id": instruction_id,
        "status": "pending",
        "sent_at": datetime.utcnow(),
    })
    return command

Para obtener el resultado, pollear con timeout de 5 minutos:

@celery_app.task(bind=True, max_retries=10)
def check_command_result(self, command_id: int):
    """Pollea el resultado del comando cada 30 segundos."""
    command = repo.get(command_id)
    if not command or command.status != "pending":
        return

    try:
        result = provider.get_instruction_result(command.imei)
        if result.get("status") == "completed":
            command.status = "completed"
            command.result = result
            command.completed_at = datetime.utcnow()
            repo.update(command)
            return
    except ProviderAPIError:
        pass

    # Timeout: 5 minutos desde envio
    elapsed = (datetime.utcnow() - command.sent_at).total_seconds()
    if elapsed > 300:
        command.status = "timeout"
        repo.update(command)
        return

    # Reintentar en 30 segundos
    raise self.retry(countdown=30)

El endpoint REST expone ambos pasos:

@router.post("/devices/{imei}/commands")
async def send(imei: str, body: CommandRequest):
    return await service.send_command(imei, body.instruction_id, body.template)

@router.get("/commands/{command_id}")
async def get_result(command_id: int):
    return await service.get_command(command_id)

Por que importa

Sin el patron 2-step, tu endpoint HTTP se queda bloqueado esperando la respuesta del dispositivo (que puede nunca llegar). Con 2-step, el usuario recibe confirmacion inmediata de que el comando se envio y consulta el resultado despues.