Files
lightweight-cobot/doc/lwc-doc/docs/getting-started/control/rest-api.md
T
2026-09-13 10:06:21 +03:00

31 KiB
Raw Blame History

Управление через REST API

REST API позволяет читать состояние робота и отправлять ему команды по HTTP. Он рассчитан на прикладные скрипты, интеграции с другими системами и быстрые проверки через Swagger UI.

Запросы к перемещению выполняются синхронно: ответ приходит после завершения планирования и выполнения команды либо после внутреннего тайм-аута. Очереди команд в API нет — дождитесь ответа на текущий запрос, прежде чем отправлять следующий.

!!! warning "Безопасность" REST API не заменяет штатную систему безопасности KUKA и кнопку аварийного останова. Перед первым запуском на реальном роботе проверьте программу Sunrise, зоны безопасности, инструмент и рабочую область. Начинать знакомство с API лучше в симуляции.

Запуск и доступ

Веб-сервер запускается вместе со стеком робота, если в корневом файле cobot-setting.yaml включён блок web:

web:
  enabled: true
  host: 0.0.0.0
  port: 8007
  token: "замените-на-длинный-секретный-токен"
  endpoints: pkg://iiwa_config/config/api_endpoints.yaml
  joint_limits: pkg://iiwa_config/config/moveit/joint_limits.yaml

После запуска через cobot run сервер будет доступен по адресу http://адрес-сервера:8007. Swagger UI помогает посмотреть фактическую схему запросов и выполнить одиночный тест:

Отдельного health-check в сервере нет. Если открывается Swagger UI, HTTP-сервер запущен. Готовность ROS-компонентов проверяется при обращении к конкретному маршруту.

Если host отличается от localhost, 127.0.0.1 или другого loopback-адреса, поле token обязательно: без него сервер завершит запуск с ошибкой. Если token заполнен, Bearer-аутентификация применяется ко всем REST-маршрутам и к MCP-маршруту /mcp/mcp, в том числе при локальном host.

Страницы /docs, /redoc и схема /openapi.json доступны без заголовка, чтобы браузер мог загрузить Swagger UI. Это не открывает команды управления. Откройте /docs, нажмите Authorize, вставьте значение web.token без слова Bearer и подтвердите. Swagger сам добавит нужный заголовок к запросам.

Токен хранится в cobot-setting.yaml. Не добавляйте его в документацию, скрипты или публичные репозитории; после утечки замените значение и перезапустите стек. Для удалённого доступа дополнительно ограничьте порт 8007 firewall или VPN.

Аутентификация REST и MCP

Каждый запрос к защищённому REST-маршруту или MCP при заполненном web.token должен содержать:

Authorization: Bearer <значение-web.token>

Примеры подготовки клиентов:

=== "curl"

~~~ bash
HOST=http://localhost:8007
API_TOKEN='скопируйте значение web.token из cobot-setting.yaml'
AUTH_HEADER="Authorization: Bearer ${API_TOKEN}"
curl -sS -H "${AUTH_HEADER}" $HOST/robot/joint_states
~~~

=== "Python"

~~~ python
import httpx

HOST = "http://localhost:8007"
API_TOKEN = "скопируйте значение web.token из cobot-setting.yaml"
HEADERS = {"Authorization": f"Bearer {API_TOKEN}"}
response = httpx.get(f"{HOST}/robot/joint_states", headers=HEADERS, timeout=10)
response.raise_for_status()
~~~

=== "MATLAB"

~~~ matlab
HOST = 'http://localhost:8007';
API_TOKEN = 'скопируйте значение web.token из cobot-setting.yaml';
readOpts = weboptions('Timeout', 10, ...
    'HeaderFields', {'Authorization', ['Bearer ' API_TOKEN]});
jointState = webread([HOST '/robot/joint_states'], readOpts);
~~~

Тот же заголовок передаётся MCP-клиенту при подключении к http://<host>:8007/mcp/mcp. Если клиент поддерживает пользовательские HTTP заголовки, укажите Authorization: Bearer <значение-web.token> в его настройках.

Подготовка к примерам

Вкладки на этой странице синхронизированы: выберите удобный язык один раз, и тот же вариант будет открыт у следующих примеров.

=== "curl"

~~~ bash
HOST=http://localhost:8007
~~~

=== "Python"

~~~ python
import httpx

HOST = "http://localhost:8007"
T_READ = 10
T_MOVE = 60
~~~

=== "MATLAB"

~~~ matlab
HOST = 'http://localhost:8007';
T_READ = 10;
T_MOVE = 60;

readOpts = weboptions('Timeout', T_READ);
moveOpts = weboptions('MediaType', 'application/json', 'Timeout', T_MOVE);
~~~

Для JSON-запросов MATLAB использует встроенный webwrite. Загрузка CSV и JSON-файлов требует интерфейса matlab.net.http, который есть в современных desktop-версиях MATLAB.

Состав API

Метод Маршрут Назначение
GET /robot/joint_states Текущее состояние суставов
GET /robot/pose Поза TCP относительно base_link
GET /robot/positions Именованные положения из SRDF
POST /robot/move/named Переход в именованное положение
POST /robot/move/pose Декартово перемещение TCP
POST /robot/move/joints Перемещение по углам семи суставов
POST /trajectory/send Публикация траектории из JSON
POST /trajectory/send_csv Загрузка и публикация траектории из CSV
GET /trajectory/logs Последние записи траекторного модуля
POST /sequences/start Запуск последовательности из JSON-файла
GET /sequences/status Статус запущенной последовательности
GET /sequences/logs Вывод процесса последовательности
POST /stop Остановка команд API и планировщика

Получение состояния

Состояние суставов

GET /robot/joint_states возвращает последнее сообщение ROS-топика /joint_states. Поля position, velocity и effort расположены в том же порядке, что и соответствующий массив name. Углы в position заданы в радианах.

=== "curl"

~~~ bash
curl -sS --max-time 10 $HOST/robot/joint_states | python3 -m json.tool
~~~

=== "Python"

~~~ python
response = httpx.get(f"{HOST}/robot/joint_states", headers=HEADERS, timeout=T_READ)
response.raise_for_status()
state = response.json()
print(dict(zip(state["name"], state["position"])))
~~~

=== "MATLAB"

~~~ matlab
jointState = webread([HOST '/robot/joint_states'], readOpts);
disp(jointState.position)
~~~

Типичный ответ:

{
  "name": ["joint1", "joint2", "joint3", "joint4", "joint5", "joint6", "joint7"],
  "position": [0.0, 0.0, 0.0, -1.57, 0.0, 1.57, 0.0],
  "velocity": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
  "effort": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
}

Если сообщения от контроллера не поступают в течение двух секунд, API вернёт 503. Обычно это означает, что контроллер или робот ещё не запущен.

Поза TCP

GET /robot/pose вычисляет прямую кинематику через сервис MoveIt /compute_fk. Положение задаётся в метрах, а ориентация возвращается одновременно кватернионом и углами Эйлера:

  • euler_rad — радианы;
  • euler_deg — градусы;
  • углы A, B, C соответствуют конвенции KUKA ABC: поворот вокруг Z, затем Y и затем X.

=== "curl"

~~~ bash
curl -sS --max-time 10 $HOST/robot/pose | python3 -m json.tool
~~~

=== "Python"

~~~ python
response = httpx.get(f"{HOST}/robot/pose", headers=HEADERS, timeout=T_READ)
response.raise_for_status()
pose = response.json()
print(pose["position"])
~~~

=== "MATLAB"

~~~ matlab
pose = webread([HOST '/robot/pose'], readOpts);
fprintf('TCP: x=%.3f, y=%.3f, z=%.3f м\n', ...
    pose.position.x, pose.position.y, pose.position.z);
fprintf('ABC: A=%.1f, B=%.1f, C=%.1f град\n', ...
    pose.orientation.euler_deg.a, ...
    pose.orientation.euler_deg.b, ...
    pose.orientation.euler_deg.c);
~~~

Маршрут зависит и от /joint_states, и от работающего MoveIt. При недоступности любого из них будет возвращён 503.

Именованные положения

GET /robot/positions читает положения group_state из SRDF. Список не зашит в API: он отражает текущую конфигурацию робота. В стандартной конфигурации есть положения home, work и transport.

=== "curl"

~~~ bash
curl -sS $HOST/robot/positions | python3 -m json.tool
~~~

=== "Python"

~~~ python
response = httpx.get(f"{HOST}/robot/positions", headers=HEADERS, timeout=T_READ)
response.raise_for_status()
for position in response.json():
    print(position["name"], "—", position["description"])
~~~

=== "MATLAB"

~~~ matlab
namedPositions = webread([HOST '/robot/positions'], readOpts);
for i = 1:numel(namedPositions)
    fprintf('%s — %s\n', namedPositions(i).name, ...
        namedPositions(i).description);
end
~~~

Перед вызовом /robot/move/named всегда полезно получить этот список: он показывает точное имя, группу планирования и целевые углы суставов.

Команды перемещения

Все три команды ниже используют MoveIt. Ответ имеет вид:

{"success": true, "message": "Движение выполнено успешно"}

Поле success: false означает, что планировщик не смог построить или выполнить траекторию. HTTP-статус при этом может остаться 200, поэтому в прикладном коде проверяйте и статус HTTP, и поле success.

Переход в именованное положение

POST /robot/move/named перемещает манипулятор в положение из SRDF.

Поле Обязательное Значение
name да Имя положения из /robot/positions
speed нет Масштаб скорости от 0.01 до 1.0; по умолчанию 0.1
accel_scale нет Масштаб ускорения от 0 до 1.0; 0 означает использовать speed

=== "curl"

~~~ bash
curl -sS --max-time 60 -X POST $HOST/robot/move/named \
  -H "Content-Type: application/json" \
  -d '{"name": "home", "speed": 0.1, "accel_scale": 0.0}'
~~~

=== "Python"

~~~ python
response = httpx.post(
    f"{HOST}/robot/move/named",
    headers=HEADERS,
    json={"name": "home", "speed": 0.1, "accel_scale": 0.0},
    timeout=T_MOVE,
)
response.raise_for_status()
result = response.json()
if not result["success"]:
    raise RuntimeError(result["message"])
~~~

=== "MATLAB"

~~~ matlab
body = struct('name', 'home', 'speed', 0.1, 'accel_scale', 0.0);
reply = webwrite([HOST '/robot/move/named'], body, moveOpts);
assert(reply.success, reply.message)
~~~

Декартово перемещение TCP

POST /robot/move/pose принимает положение TCP в метрах и ориентацию ABC в радианах. Если frame_id пуст, используется фрейм, заданный в настройках планирования; в стандартной конфигурации это base_link.

Поле Обязательное Значение
x, y, z да Координаты TCP, м
a, b, c нет Углы KUKA ABC, рад; по умолчанию 0
speed нет Масштаб скорости от 0.01 до 1.0; по умолчанию 0.1
planner нет ompl, ptp, lin, circ или chomp; по умолчанию ptp
frame_id нет Фрейм целевой позы; пустая строка использует фрейм по умолчанию

Значение planner приводится к нижнему регистру. PTP подходит для переходов между точками, LIN — для прямолинейного движения инструмента. Выбор CIRC имеет смысл только для задач, где он поддерживается вашим планировщиком и целевой позой.

=== "curl"

~~~ bash
curl -sS --max-time 60 -X POST $HOST/robot/move/pose \
  -H "Content-Type: application/json" \
  -d '{
    "x": 0.40, "y": 0.00, "z": 0.50,
    "a": 0.0, "b": 3.14159, "c": 0.0,
    "speed": 0.1, "planner": "ptp", "frame_id": ""
  }'
~~~

=== "Python"

~~~ python
target = {
    "x": 0.40, "y": 0.00, "z": 0.50,
    "a": 0.0, "b": 3.14159, "c": 0.0,
    "speed": 0.1, "planner": "ptp", "frame_id": "",
}
response = httpx.post(f"{HOST}/robot/move/pose", headers=HEADERS, json=target, timeout=T_MOVE)
response.raise_for_status()
print(response.json())
~~~

=== "MATLAB"

~~~ matlab
body = struct( ...
    'x', 0.40, 'y', 0.00, 'z', 0.50, ...
    'a', 0.0, 'b', pi, 'c', 0.0, ...
    'speed', 0.1, 'planner', 'ptp', 'frame_id', '');
reply = webwrite([HOST '/robot/move/pose'], body, moveOpts);
assert(reply.success, reply.message)
~~~

Перемещение по углам суставов

POST /robot/move/joints принимает ровно семь углов в порядке J1–J7. API проверяет количество значений и текущие границы из файла joint_limits.yaml.

Сустав Допустимый угол, рад
J1 от -2.97 до 2.97
J2 от -2.10 до 2.10
J3 от -2.97 до 2.97
J4 от -2.10 до 2.10
J5 от -2.97 до 2.97
J6 от -2.10 до 2.10
J7 от -3.05 до 3.05

При изменении файла ограничений ориентируйтесь на Swagger UI: значения в этой таблице относятся к поставляемой конфигурации.

=== "curl"

~~~ bash
curl -sS --max-time 60 -X POST $HOST/robot/move/joints \
  -H "Content-Type: application/json" \
  -d '{"joints": [0.0, 0.5, 0.0, -1.57, 0.0, 1.57, 0.0], "speed": 0.1}'
~~~

=== "Python"

~~~ python
response = httpx.post(
    f"{HOST}/robot/move/joints",
    headers=HEADERS,
    json={
        "joints": [0.0, 0.5, 0.0, -1.57, 0.0, 1.57, 0.0],
        "speed": 0.1,
    },
    timeout=T_MOVE,
)
response.raise_for_status()
print(response.json())
~~~

=== "MATLAB"

~~~ matlab
body = struct( ...
    'joints', [0.0, 0.5, 0.0, -1.57, 0.0, 1.57, 0.0], ...
    'speed', 0.1);
reply = webwrite([HOST '/robot/move/joints'], body, moveOpts);
assert(reply.success, reply.message)
~~~

Траектории по суставам

Маршруты раздела /trajectory публикуют сообщение JointTrajectory напрямую в контроллер /iiwa_arm_controller/joint_trajectory. Ответ status: sent подтверждает публикацию сообщения, но не завершение движения и не отсутствие ошибок контроллера. Отслеживайте состояние робота через /robot/joint_states и при необходимости смотрите /trajectory/logs.

Траектория в JSON

POST /trajectory/send принимает одну или несколько точек.

Поле Значение
points Непустой список точек
points[].positions Ровно 7 углов J1–J7 в радианах
points[].time_from_start Время от начала траектории в секундах, не меньше 0
validate_limits Проверять границы суставов; по умолчанию true

Сервер не проверяет возрастание времени между точками, поэтому задавайте его самостоятельно. Для контроллера траектория с возрастающими значениями времени предсказуемее.

=== "curl"

~~~ bash
curl -sS --max-time 20 -X POST $HOST/trajectory/send \
  -H "Content-Type: application/json" \
  -d '{
    "points": [
      {"positions": [0, 0, 0, 0, 0, 0, 0], "time_from_start": 0.0},
      {"positions": [0, 0.5, 0, -1.0, 0, 1.0, 0], "time_from_start": 3.0},
      {"positions": [0, 0, 0, 0, 0, 0, 0], "time_from_start": 6.0}
    ],
    "validate_limits": true
  }'
~~~

=== "Python"

~~~ python
trajectory = {
    "points": [
        {"positions": [0.0] * 7, "time_from_start": 0.0},
        {"positions": [0.0, 0.5, 0.0, -1.0, 0.0, 1.0, 0.0], "time_from_start": 3.0},
        {"positions": [0.0] * 7, "time_from_start": 6.0},
    ],
    "validate_limits": True,
}
response = httpx.post(f"{HOST}/trajectory/send", headers=HEADERS, json=trajectory, timeout=T_READ)
response.raise_for_status()
print(response.json())
~~~

=== "MATLAB"

~~~ matlab
p1 = struct('positions', [0, 0, 0, 0, 0, 0, 0], ...
            'time_from_start', 0.0);
p2 = struct('positions', [0, 0.5, 0, -1.0, 0, 1.0, 0], ...
            'time_from_start', 3.0);
trajectory.points = [p1, p2];
trajectory.validate_limits = true;

opts = weboptions('MediaType', 'application/json', 'Timeout', T_READ);
reply = webwrite([HOST '/trajectory/send'], trajectory, opts);
disp(reply)
~~~

Загрузка CSV

POST /trajectory/send_csv принимает CSV-файл в multipart-поле file. Первая строка должна быть заголовком. Имена колонок суставов могут быть записаны как joint1 или joint_1, регистр не важен; колонка времени называется t, time или time_from_start. Порядок колонок произвольный.

Пример файла:

joint1,joint2,joint3,joint4,joint5,joint6,joint7,t
0,0,0,0,0,0,0,0.0
0,0.5,0,-1.0,0,1.0,0,3.0

Параметры separator и validate_limits передаются в строке запроса, а не как поля формы. По умолчанию разделитель — запятая, проверка ограничений включена.

=== "curl"

~~~ bash
curl -sS --max-time 20 -X POST \
  "$HOST/trajectory/send_csv?separator=%2C&validate_limits=true" \
  -F "file=@trajectory.csv;type=text/csv"
~~~

=== "Python"

~~~ python
with open("trajectory.csv", "rb") as csv_file:
    response = httpx.post(
        f"{HOST}/trajectory/send_csv",
        headers=HEADERS,
        params={"separator": ",", "validate_limits": True},
        files={"file": ("trajectory.csv", csv_file, "text/csv")},
        timeout=T_READ,
    )
response.raise_for_status()
print(response.json())
~~~

=== "MATLAB"

~~~ matlab
import matlab.net.http.*
import matlab.net.http.io.*

uri = URI([HOST '/trajectory/send_csv?separator=%2C&validate_limits=true']);
form = MultipartFormProvider('file', FileProvider('trajectory.csv'));
request = RequestMessage('post', [], form);
httpOpts = HTTPOptions('ConnectTimeout', T_READ, 'ResponseTimeout', T_READ);
response = request.send(uri, httpOpts);

disp(response.Body.Data)
~~~

Для файла с точкой с запятой замените %2C на %3B.

Лог траекторного модуля

GET /trajectory/logs?n=50 возвращает до 300 последних записей. Параметр n должен быть в диапазоне от 1 до 300.

=== "curl"

~~~ bash
curl -sS "$HOST/trajectory/logs?n=20" | python3 -m json.tool
~~~

=== "Python"

~~~ python
response = httpx.get(f"{HOST}/trajectory/logs", headers=HEADERS, params={"n": 20}, timeout=T_READ)
response.raise_for_status()
for line in response.json()["lines"]:
    print(line)
~~~

=== "MATLAB"

~~~ matlab
logs = webread([HOST '/trajectory/logs?n=20'], readOpts);
disp(logs.lines)
~~~

Чтобы прервать траекторию, используйте общий маршрут POST /stop. Отдельного маршрута /trajectory/stop в API нет.

Последовательности движений

POST /sequences/start запускает отдельный процесс motion_sequence_runner. Он читает загруженный JSON-файл и поочерёдно отправляет цели MoveToJoints или MoveToPose.

Поле формы Значение по умолчанию Назначение
config JSON-файл последовательности, обязательное поле
n_iterations 3 Число повторений, не меньше 1
delay_between_iterations 5.0 Пауза между итерациями, с
bag_path пусто Путь для записи rosbag; пустая строка отключает запись
topics пусто Топики для rosbag через запятую; пусто означает все обнаруженные топики
joints_action cobot/move_to_joints Имя action для суставных целей
pose_action cobot/move_to_pose Имя action для декартовых целей

Минимальная структура конфигурации:

{
  "home": {
    "joints": [0, 0, 0, -1.57, 0, 1.57, 0],
    "speed": 0.1
  },
  "waypoints": [
    {
      "x": 0.6, "y": 0.1, "z": 0.55,
      "a": 3.14, "b": 0.31, "c": 2.79,
      "speed": 0.2, "planner": "lin"
    },
    {
      "joints": [0.5, 0.3, 0, -1.2, 0, 1.4, 0],
      "speed": 0.2
    }
  ]
}

Если в точке есть поле joints, она считается суставной. Иначе runner ожидает декартовы поля x, y, z, a, b и c.

Запуск последовательности

=== "curl"

~~~ bash
curl -sS --max-time 10 -X POST $HOST/sequences/start \
  -F "config=@motion_sequence_config.json;type=application/json" \
  -F "n_iterations=3" \
  -F "delay_between_iterations=5.0"
~~~

=== "Python"

~~~ python
with open("motion_sequence_config.json", "rb") as config:
    response = httpx.post(
        f"{HOST}/sequences/start",
        headers=HEADERS,
        files={"config": ("motion_sequence_config.json", config, "application/json")},
        data={"n_iterations": "3", "delay_between_iterations": "5.0"},
        timeout=T_READ,
    )
response.raise_for_status()
print(response.json())
~~~

=== "MATLAB"

~~~ matlab
import matlab.net.http.*
import matlab.net.http.io.*

form = MultipartFormProvider( ...
    'config', FileProvider('motion_sequence_config.json'), ...
    'n_iterations', '3', ...
    'delay_between_iterations', '5.0');
request = RequestMessage('post', [], form);
httpOpts = HTTPOptions('ConnectTimeout', T_READ, 'ResponseTimeout', T_READ);
response = request.send(URI([HOST '/sequences/start']), httpOpts);

disp(response.Body.Data)
~~~

Ответ status: started подтверждает запуск процесса, но не корректность содержимого JSON и не успешность каждого движения. Если runner завершится с ошибкой, проверьте его состояние и лог.

Статус и журнал последовательности

=== "curl"

~~~ bash
curl -sS $HOST/sequences/status | python3 -m json.tool
curl -sS "$HOST/sequences/logs?n=50" | python3 -m json.tool
~~~

=== "Python"

~~~ python
status = httpx.get(f"{HOST}/sequences/status", headers=HEADERS, timeout=T_READ)
status.raise_for_status()
print(status.json())

logs = httpx.get(f"{HOST}/sequences/logs", headers=HEADERS, params={"n": 50}, timeout=T_READ)
logs.raise_for_status()
for line in logs.json()["lines"]:
    print(line)
~~~

=== "MATLAB"

~~~ matlab
status = webread([HOST '/sequences/status'], readOpts);
logs = webread([HOST '/sequences/logs?n=50'], readOpts);
disp(status)
disp(logs.lines)
~~~

Статусы:

  • idle — последовательность ещё не запускалась;
  • running — процесс выполняется;
  • finished — процесс завершён; в ответе будет код returncode.

Одновременно может работать только одна последовательность. Повторный POST /sequences/start во время её выполнения вернёт 409. Для остановки используйте POST /stop: отдельного /sequences/stop нет.

Общая остановка

POST /stop останавливает запущенный runner, публикует точку удержания текущей позиции для траекторного контроллера и вызывает сервис MoveIt cobot/stop. Если текущие состояния суставов недоступны, вместо точки удержания публикуется пустая траектория.

=== "curl"

~~~ bash
curl -sS --max-time 10 -X POST $HOST/stop | python3 -m json.tool
~~~

=== "Python"

~~~ python
response = httpx.post(f"{HOST}/stop", headers=HEADERS, timeout=T_READ)
response.raise_for_status()
print(response.json())
~~~

=== "MATLAB"

~~~ matlab
reply = webwrite([HOST '/stop'], struct(), ...
    weboptions('MediaType', 'application/json', 'Timeout', T_READ));
disp(reply)
~~~

Команда отменяет программные операции, но не снимает питание с робота и не заменяет штатную аварийную остановку. После её вызова проверьте сообщение в ответе и реальное состояние робота.

Ошибки и диагностика

Код Когда возникает
200 Запрос обработан; для команд движения дополнительно проверьте поле success
409 Уже запущена последовательность движений
422 Некорректная структура запроса, число суставов, скорость, планировщик или лимиты суставов
503 ROS-топик, сервис, action-сервер или MoveIt недоступен; также возможен тайм-аут ожидания

При проблемах идите от простого к сложному:

  1. Откройте /docs и убедитесь, что сервер запущен и маршрут присутствует в схеме.
  2. Проверьте /robot/joint_states. Без него не будет работать получение позы, а остановка траектории не сможет сформировать точку удержания.
  3. Убедитесь, что стек запущен полностью: controller_manager, MoveIt и iiwa_motion_server.
  4. После запуска последовательности посмотрите /sequences/logs; после публикации траектории — /trajectory/logs.

MCP-сервер работает в том же процессе, но это отдельный интерфейс: его адрес — http://адрес-сервера:8007/mcp/mcp. Он использует тот же Bearer-токен; передайте заголовок Authorization при подключении MCP-клиента. Для обычных HTTP-интеграций используйте маршруты из этой страницы.