Connection protocol

How a robot talks to the XPARO server. You only need this to write your own client (for a robot without ROS 2, or in another language); xparo_ros does all of it for you.

Connection sequence: WebSocket with secret key, project sync, ADD_robots_info, ROBOT_CREDENTIAL, heartbeat every 30 seconds.

Addresses

Channel

Address

WebSocket

wss://xparo.in/ws/chatbot_api/<key>/<project-id>/

HTTPS

https://xparo.in/chatbot_api/<key>/<project-id>/

<key> is a project secret key for a robot’s first connection, and the robot’s own credential afterwards. There is no separate login: the key in the address is the authentication. A wrong key is refused during the WebSocket handshake with 403.

Messages

Every message, in both directions, is one JSON object whose keys are dispatch keys (the message names listed in Message catalogue), each with its own payload:

{"ROBOT_HEARTBEAT": {"device_id": "my-robot-0001"}}

Usually there is one key, but one object can carry several. The project sync the server sends on connect is a single object with many keys (title, aiml, custom_aiml, custom_tasks, custom_node_files, …), and the reply to ADD_robots_info combines ROBOT_CREDENTIAL, REST_API_TOKEN, sync_local_database and others. Handle every key you know and ignore the rest.

Some server messages are wrapped in a message key, for example answers from the AI assistant:

{"message": {"bot_response": "...", "responded_by": "llm"}}

A robot’s first connection

  1. Open the WebSocket with the project’s secret key.

  2. The server sends the project sync.

  3. Send ADD_robots_info with the robot’s details (below).

  4. The first time a device ID is seen, the reply contains ROBOT_CREDENTIAL. Save it and use it instead of the secret key from now on; it is sent only once.

  5. Every 30 seconds, send ROBOT_HEARTBEAT (xparo_ros sends it with an HTTPS POST to the HTTPS address). The dashboard shows the robot online while the last heartbeat is less than 90 seconds old.

ADD_robots_info must contain all of these fields, as text: device_id, hostname, os_name, os_version, os_release, mac_address, ip_address, cpu_model, cpu_count, cpu_physical_count, total_memory, available_memory, total_disk, used_disk, free_disk. If any is missing, the robot is not registered properly and never receives its credential. Optional: ros_distro, xparo_git_commit, locations ({"latitude": ..., "longitude": ...}), data (free-form details), width and height (screen size).

device_id identifies the robot: keep it the same across restarts, or the robot appears as a new one.

Dashboard commands (Run now, terminal, teleop, files…) are delivered only to robots that registered over the WebSocket and are connected.

A complete minimal client

This Python client registers, keeps its credential, sends heartbeats and answers Teleop. It was run against an XPARO server: it appeared online on Robots Fleet, and a Teleop command from the dashboard reached it and was acknowledged.

"""A minimal XPARO robot client without ROS 2: joins a project, keeps a
heartbeat, and answers Teleop commands from the dashboard."""
import json
import os
import threading
import time

import requests
import websocket

SERVER = "xparo.in"
PROJECT_ID = "<your-project-id>"
SECRET_KEY = "<your-secret-key>"
DEVICE_ID = "my-robot-0001"            # any ID that stays the same for this robot
CREDENTIAL_FILE = "xparo_credential.txt"


def secret():
    # After the first connection, use the robot's own credential.
    if os.path.exists(CREDENTIAL_FILE):
        return open(CREDENTIAL_FILE).read().strip()
    return SECRET_KEY


def send(ws, key, payload):
    ws.send(json.dumps({key: payload}))


ROBOT_INFO = {
    # All of these are required (as text); use real values where you have them.
    "device_id": DEVICE_ID, "hostname": "my-robot",
    "os_name": "Linux", "os_version": "Ubuntu 24.04", "os_release": "6.8",
    "mac_address": "02:00:00:00:00:01", "ip_address": "192.168.1.50",
    "cpu_model": "arm64", "cpu_count": "4", "cpu_physical_count": "4",
    "total_memory": "8 GB", "available_memory": "6 GB",
    "total_disk": "64 GB", "used_disk": "12 GB", "free_disk": "52 GB",
}


def on_open(ws):
    print("connected")
    send(ws, "ADD_robots_info", ROBOT_INFO)


def on_message(ws, raw):
    message = json.loads(raw)
    for key, value in message.items():
        if key == "ROBOT_CREDENTIAL":
            open(CREDENTIAL_FILE, "w").write(value)
            print("got my own credential")
        elif key == "TELEOP":
            print("drive:", value["axes"], value["buttons"])
            send(ws, "TELEOP_ACK", {"success": True})
        elif key == "custom_tasks":
            print("tasks in the project:", len(value))


def heartbeat():
    while True:
        url = f"https://{SERVER}/chatbot_api/{secret()}/{PROJECT_ID}/"
        requests.post(url, json={"ROBOT_HEARTBEAT": {"device_id": DEVICE_ID}}, timeout=10)
        time.sleep(30)


threading.Thread(target=heartbeat, daemon=True).start()
websocket.WebSocketApp(
    f"wss://{SERVER}/ws/chatbot_api/{secret()}/{PROJECT_ID}/",
    on_open=on_open, on_message=on_message,
).run_forever(reconnect=5)
pip install websocket-client requests
python3 my_robot.py

Output of the test run (the Teleop came from the dashboard):

connected
tasks in the project: 1
got my own credential
drive: [0.5, 0, 0, 0] [1, 0, 0]

What the dashboard received back:

{"message": {"TELEOP_ACK": {"success": true}}}

Reconnecting

xparo_ros reconnects 5 seconds after a drop and re-syncs. If its saved credential is refused (the robot was deleted from the project, for example), it deletes the credential file and connects once more with the secret key it was started with, which registers it again. Messages that must not be lost (task results and task history) are kept in memory while offline and sent after reconnecting.

Connection types

xparo_ros can also poll over HTTPS instead of keeping a WebSocket open (xparo_connection_type): rest polls only; hybrid uses the WebSocket and falls back to polling every 2 seconds during an outage of more than about 25 seconds. In polling mode the robot syncs and reports, but dashboard commands cannot reach it. See Configuration.