13 Commits
30 changed files with 1662 additions and 457 deletions
+2 -4
View File
@@ -1,11 +1,9 @@
robot: robot:
name: "iiwa7" name: "iiwa7"
ip: "192.170.10.2" ip: "192.168.21.147"
port: 30200 port: 30200
fri_cycle_ms: 10 # период FRI-цикла: 5 мс (200 Гц) или 10 мс (100 Гц) fri_cycle_ms: 10 # период FRI-цикла: 5 мс (200 Гц) или 10 мс (100 Гц)
joint_position_tau: 0.04 # EMA фильтр позиций [с]: сглаживает команды перед отправкой в FRI joint_position_tau: 0.04 # EMA позиций FRI [с]; 0 отключает сглаживание
joint_velocity_tau: 0.01 # EMA фильтр скорости [с]: убирает выбросы конечных разностей
active_controller: "jtc" # "jtc" = MoveIt/JointTrajectoryController, "forward" = ForwardCommandController
description: pkg://iiwa_description/urdf/iiwa7.urdf.xacro description: pkg://iiwa_description/urdf/iiwa7.urdf.xacro
-7
View File
@@ -143,13 +143,6 @@ _BLOCKS: List[_Block] = [
_Field("fri_cycle_ms", "Цикл FRI (мс):", "10", _Field("fri_cycle_ms", "Цикл FRI (мс):", "10",
note="5 мс = 200 Гц, 10 мс = 100 Гц", note="5 мс = 200 Гц, 10 мс = 100 Гц",
options=["10", "5"]), options=["10", "5"]),
_Field("active_controller", "Активный ROS-контроллер:", "jtc",
note="jtc = JointTrajectoryController (MoveIt), forward = ForwardCommandController",
options=["jtc", "forward"]),
_Field("joint_position_tau", "EMA tau фильтра положения (с):", "0.04",
note="Сглаживает команды положения перед отправкой в FRI"),
_Field("joint_velocity_tau", "EMA tau фильтра скорости (с):", "0.01",
note="Убирает выбросы из оценки скорости конечной разностью"),
], ],
), ),
] ]
+50 -34
View File
@@ -4,13 +4,23 @@ import argparse
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from cobot import process, ui from cobot import process
from cobot.ui import done from cobot.ui import done
from cobot.process import StepProgress from cobot.process import StepProgress
_PROJECT_DIR = Path(__file__).parent.parent.parent _PROJECT_DIR = Path(__file__).parent.parent.parent
def _plain(message: str) -> None:
"""Print after replacing this command's own uv environment.
Rich modules loaded before ``uv tool install`` can no longer be complete after
uv replaces the editable tool environment. Do not render with Rich after that
command returns.
"""
print(message, flush=True)
def _git(*args: str) -> str: def _git(*args: str) -> str:
"""Run a git command in the project dir and return its stripped stdout. """Run a git command in the project dir and return its stripped stdout.
Запускает git-команду в директории проекта и возвращает обрезанный stdout. Запускает git-команду в директории проекта и возвращает обрезанный stdout.
@@ -18,13 +28,12 @@ def _git(*args: str) -> str:
return subprocess.check_output(["git", *args], cwd=_PROJECT_DIR, text=True).strip() return subprocess.check_output(["git", *args], cwd=_PROJECT_DIR, text=True).strip()
def _update() -> None: def _update() -> int:
"""Fetch the current branch, show incoming commits, pull, and reinstall the cobot CLI. """Fetch the current branch, show incoming commits, pull, and reinstall the cobot CLI.
Progress: fetch (0-30 %), pull (30-80 %), reinstall (80-100 %). Progress: fetch (0-30 %), pull (30-80 %), reinstall (80-100 %).
Получает текущую ветку, показывает входящие коммиты, делает pull и переустанавливает CLI. Получает текущую ветку, показывает входящие коммиты, делает pull и переустанавливает CLI.
Прогресс: fetch (0-30 %), pull (30-80 %), переустановка (80-100 %). Прогресс: fetch (0-30 %), pull (30-80 %), переустановка (80-100 %).
""" """
ok, fail_msg = True, ""
with StepProgress("Обновление проекта") as p: with StepProgress("Обновление проекта") as p:
try: try:
branch = _git("rev-parse", "--abbrev-ref", "HEAD") branch = _git("rev-parse", "--abbrev-ref", "HEAD")
@@ -32,42 +41,49 @@ def _update() -> None:
p.set(0, "Получение с удалённого репозитория...") p.set(0, "Получение с удалённого репозитория...")
rc = process.stream(["git", "fetch", "origin"], cwd=str(_PROJECT_DIR), on_line=p.log) rc = process.stream(["git", "fetch", "origin"], cwd=str(_PROJECT_DIR), on_line=p.log)
if rc not in (0, -9, -15): if rc != 0:
done(False, "git fetch завершился с ошибкой") done(False, f"git fetch завершился с кодом {rc}")
return return 1
p.set(30) p.set(30)
behind = _git("rev-list", f"HEAD..origin/{branch}", "--count") behind = _git("rev-list", f"HEAD..origin/{branch}", "--count")
if behind == "0": if behind == "0":
p.set(100, "Уже актуально") p.log("Исходники уже актуальны; проверка установки CLI...")
done(True, "Уже актуальная версия")
return
p.raw(f"\n[bold]{behind} новых коммит(ов):[/bold]")
for line in _git("log", f"HEAD..origin/{branch}", "--oneline").splitlines():
p.log(line)
p.set(30, "Применение изменений...")
rc = process.stream(["git", "pull", "origin", branch],
cwd=str(_PROJECT_DIR), on_line=p.log)
if rc not in (0, -9, -15):
done(False, "git pull завершился с ошибкой")
return
p.set(80)
p.set(80, "Переустановка cobot CLI...")
p.raw("\n[cyan]▸[/cyan] Переустановка cobot CLI...")
rc = process.stream(["uv", "tool", "install", "--editable", str(_PROJECT_DIR)],
on_line=p.log)
if rc in (0, -9, -15):
p.raw("[green]✓[/green] cobot переустановлен")
else: else:
p.raw("[yellow]Предупреждение:[/yellow] переустановка не удалась") p.raw(f"\n[bold]{behind} новых коммит(ов):[/bold]")
p.set(100, "Готово") for line in _git("log", f"HEAD..origin/{branch}", "--oneline").splitlines():
except subprocess.CalledProcessError as exc: p.log(line)
ok, fail_msg = False, str(exc)
done(ok, "Проект обновлён" if ok else fail_msg) p.set(30, "Применение изменений...")
rc = process.stream(["git", "pull", "origin", branch],
cwd=str(_PROJECT_DIR), on_line=p.log)
if rc != 0:
done(False, f"git pull завершился с кодом {rc}")
return 1
p.set(80)
except (subprocess.CalledProcessError, OSError) as exc:
done(False, str(exc))
return 1
# uv can replace the Rich package of this running CLI. Keep this stage outside
# StepProgress and only use built-in print afterwards.
_plain("\n▸ Переустановка cobot CLI...")
try:
rc = process.stream(
["uv", "tool", "install", "--editable", str(_PROJECT_DIR)],
on_line=lambda line: _plain(f" {line}"),
)
except OSError as exc:
_plain(f"✗ Не удалось запустить установщик: {exc}")
return 1
if rc != 0:
_plain(
f"✗ Установка CLI завершилась с кодом {rc}. Исходники обновлены, "
"но установка не завершена. Сохраните вывод выше и повторите cobot update."
)
return 1
_plain("✓ Проект обновлён и cobot переустановлен")
return 0
def register(subparsers: argparse._SubParsersAction) -> None: def register(subparsers: argparse._SubParsersAction) -> None:
@@ -76,4 +92,4 @@ def register(subparsers: argparse._SubParsersAction) -> None:
def run(args: argparse.Namespace) -> None: def run(args: argparse.Namespace) -> None:
_update() raise SystemExit(_update())
+71
View File
@@ -0,0 +1,71 @@
"""Update recovery checks; external git and installer operations are isolated."""
import io
import unittest
from unittest.mock import patch
from rich.console import Console
from cobot.commands import update
class UpdateTests(unittest.TestCase):
def run_update(self, codes, behind="1"):
output = io.StringIO()
commands = []
def stream(command, **kwargs):
commands.append(command)
result = next(codes, 0)
if isinstance(result, Exception):
raise result
return result
def git(*args):
return {"rev-parse": "controller", "rev-list": behind,
"log": "abc123 Update CLI"}[args[0]]
console = Console(file=output, color_system=None)
with patch.object(update, "_git", side_effect=git), \
patch.object(update.process, "stream", side_effect=stream), \
patch.object(update, "_plain"), \
patch("cobot.process.console", console), \
patch("cobot.ui.console", console):
code = update._update()
return code, output.getvalue(), commands
def test_install_failure_reports_failure(self):
for install_code in (1, -9, -15):
with self.subTest(install_code=install_code):
code, output, _ = self.run_update(iter([0, 0, install_code]))
self.assertEqual(code, 1)
self.assertNotIn("Проект обновлён", output)
def test_current_checkout_still_retries_installation(self):
code, _, commands = self.run_update(iter([0, 0]), behind="0")
self.assertEqual([command[:2] for command in commands],
[["git", "fetch"], ["uv", "tool"]])
self.assertEqual(code, 0)
def test_killed_git_stops_before_next_step(self):
for codes, expected_count in [([-9], 1), ([0, -15], 2)]:
with self.subTest(codes=codes):
code, output, commands = self.run_update(iter(codes))
self.assertNotEqual(code, 0)
self.assertEqual(len(commands), expected_count)
self.assertNotIn("Проект обновлён", output)
def test_missing_installer_reports_failure_without_traceback(self):
code, _, _ = self.run_update(
iter([0, 0, FileNotFoundError("uv not found")]))
self.assertNotEqual(code, 0)
def test_command_exits_nonzero_after_failed_installation(self):
with patch.object(update, "_update", return_value=1):
with self.assertRaises(SystemExit) as result:
update.run(None)
self.assertEqual(result.exception.code, 1)
if __name__ == "__main__":
unittest.main()
@@ -26,9 +26,6 @@ robot:
ip: "192.170.10.2" ip: "192.170.10.2"
port: 30200 port: 30200
fri_cycle_ms: 10 fri_cycle_ms: 10
joint_position_tau: 0.04
joint_velocity_tau: 0.01
active_controller: "jtc"
description: pkg://iiwa_description/urdf/iiwa7.urdf.xacro description: pkg://iiwa_description/urdf/iiwa7.urdf.xacro
``` ```
@@ -38,11 +35,11 @@ robot:
| `ip` | KUKA controller IP address | **Change** to the actual controller address | | `ip` | KUKA controller IP address | **Change** to the actual controller address |
| `port` | FRI UDP port | Default: `30200`; change only if the port conflicts | | `port` | FRI UDP port | Default: `30200`; change only if the port conflicts |
| `fri_cycle_ms` | FRI cycle: `5` ms = 200 Hz, `10` ms = 100 Hz | Use `10` for stable operation or `5` for high-precision tasks | | `fri_cycle_ms` | FRI cycle: `5` ms = 200 Hz, `10` ms = 100 Hz | Use `10` for stable operation or `5` for high-precision tasks |
| `joint_position_tau` | Position EMA filter [s], smoothing commands before transmission | Decrease for a faster response; increase if vibration occurs | | `joint_position_tau` | FRI command EMA time constant, s | `0.04`; `0` disables smoothing |
| `joint_velocity_tau` | Velocity EMA filter [s], removing finite-difference spikes | Tune in the same way as `joint_position_tau` |
| `active_controller` | Control mode: `jtc` (MoveIt / JointTrajectory) or `forward` (direct control) | Use `jtc` for most tasks |
| `description` | Path to the robot URDF | Do not change | | `description` | Path to the robot URDF | Do not change |
Motion always uses JointTrajectoryController. The position command is smoothed before FRI transmission with `robot.joint_position_tau`; no velocity filter is applied.
--- ---
## `digital_twin` section — simulator ## `digital_twin` section — simulator
@@ -70,6 +67,7 @@ digital_twin:
| `webots.cameras` | List of YAML configurations for connected cameras | | `webots.cameras` | List of YAML configurations for connected cameras |
| `rviz.config` | Path to the RViz configuration | | `rviz.config` | Path to the RViz configuration |
--- ---
## `tool` section — active tool ## `tool` section — active tool
@@ -169,7 +167,6 @@ The remaining parameters (`tls`, `topic_whitelist`, `min_qos_depth`, and others)
| ✅ | `robot.fri_cycle_ms` | Select `10` (standard) or `5` (high frequency) | | ✅ | `robot.fri_cycle_ms` | Select `10` (standard) or `5` (high frequency) |
| ✅ | `tool.active` | Set the active tool | | ✅ | `tool.active` | Set the active tool |
| ✅ | `web.enabled` | Set to `false` if the web interface is not needed | | ✅ | `web.enabled` | Set to `false` if the web interface is not needed |
| ⚠️ | `robot.active_controller` | Change only when intentionally switching the control mode |
| ⚠️ | `planning.*` | Change only when another planner or other parameters are required | | ⚠️ | `planning.*` | Change only when another planner or other parameters are required |
| ❌ | `robot.description` | Do not change; this is the URDF path | | ❌ | `robot.description` | Do not change; this is the URDF path |
| ❌ | `controller.moveit.*` | Do not change; these are package-internal MoveIt configuration paths | | ❌ | `controller.moveit.*` | Do not change; these are package-internal MoveIt configuration paths |
@@ -28,9 +28,6 @@ robot:
ip: "192.170.10.2" ip: "192.170.10.2"
port: 30200 port: 30200
fri_cycle_ms: 10 fri_cycle_ms: 10
joint_position_tau: 0.04
joint_velocity_tau: 0.01
active_controller: "jtc"
description: pkg://iiwa_description/urdf/iiwa7.urdf.xacro description: pkg://iiwa_description/urdf/iiwa7.urdf.xacro
``` ```
@@ -40,11 +37,11 @@ robot:
| `ip` | IP-адрес контроллера KUKA | **Изменить** на реальный IP вашего контроллера | | `ip` | IP-адрес контроллера KUKA | **Изменить** на реальный IP вашего контроллера |
| `port` | UDP-порт FRI | По умолчанию `30200` — менять только при конфликте портов | | `port` | UDP-порт FRI | По умолчанию `30200` — менять только при конфликте портов |
| `fri_cycle_ms` | Период цикла FRI: `5` мс = 200 Гц, `10` мс = 100 Гц | `10` для стабильной работы, `5` для высокоточных задач | | `fri_cycle_ms` | Период цикла FRI: `5` мс = 200 Гц, `10` мс = 100 Гц | `10` для стабильной работы, `5` для высокоточных задач |
| `joint_position_tau` | EMA-фильтр позиций [с] — сглаживает команды перед отправкой | Уменьшать для более быстрой реакции, увеличивать при вибрациях | | `joint_position_tau` | Постоянная времени EMA команд FRI, с | `0.04`; `0` отключает сглаживание |
| `joint_velocity_tau` | EMA-фильтр скорости [с] — убирает выбросы конечных разностей | Аналогично `joint_position_tau` |
| `active_controller` | Режим управления: `jtc` (MoveIt / JointTrajectory) или `forward` (прямое управление) | `jtc` для большинства задач |
| `description` | Путь к URDF-описанию робота | Не менять | | `description` | Путь к URDF-описанию робота | Не менять |
Управление движением всегда выполняет JointTrajectoryController. Перед отправкой в FRI позиционная команда сглаживается EMA с постоянной времени `robot.joint_position_tau`; фильтр скорости не применяется.
--- ---
## Блок `digital_twin` — симулятор ## Блок `digital_twin` — симулятор
@@ -72,6 +69,7 @@ digital_twin:
| `webots.cameras` | Список YAML-конфигов подключённых камер | | `webots.cameras` | Список YAML-конфигов подключённых камер |
| `rviz.config` | Путь к конфигурации RViz | | `rviz.config` | Путь к конфигурации RViz |
--- ---
## Блок `tool` — активный инструмент ## Блок `tool` — активный инструмент
@@ -171,7 +169,6 @@ foxglove:
| ✅ | `robot.fri_cycle_ms` | Выбрать `10` (стандарт) или `5` (высокая частота) | | ✅ | `robot.fri_cycle_ms` | Выбрать `10` (стандарт) или `5` (высокая частота) |
| ✅ | `tool.active` | Указать активный инструмент | | ✅ | `tool.active` | Указать активный инструмент |
| ✅ | `web.enabled` | Выключить (`false`), если веб-интерфейс не нужен | | ✅ | `web.enabled` | Выключить (`false`), если веб-интерфейс не нужен |
| ⚠️ | `robot.active_controller` | Менять только при намеренном переключении режима управления |
| ⚠️ | `planning.*` | Менять при необходимости другого планировщика или параметров | | ⚠️ | `planning.*` | Менять при необходимости другого планировщика или параметров |
| ❌ | `robot.description` | Не трогать — путь к URDF | | ❌ | `robot.description` | Не трогать — путь к URDF |
| ❌ | `controller.moveit.*` | Не трогать — пути к конфигам MoveIt внутри пакетов | | ❌ | `controller.moveit.*` | Не трогать — пути к конфигам MoveIt внутри пакетов |
+84
View File
@@ -0,0 +1,84 @@
# FIFO priority for FRI
## Why it is needed
FRI exchanges a position command on every cycle. With a 10 ms period, a new packet should be handled about every 10 ms. The regular Linux scheduler can delay the control thread behind other processes. The log then contains messages such as:
```text
Could not enable FIFO RT scheduling: Operation not permitted
Overrun might occur ... Read time: 11500 us
```
`SCHED_FIFO` is Linux scheduling for threads with fixed priority. This project requests priority 50 for `controller_manager` and priority 80 for the FRI worker. The FRI worker receives the higher priority so it can receive a KUKA packet and send the reply on time.
Configure this on the **ROS 2 computer** that runs `cobot run`. It does not require changes to the Sunrise Cabinet or `cobot-setting.yaml`.
## Ubuntu 24.04 setup
On the ROS 2 computer, run these commands as the user who starts `cobot`:
```bash
sudo groupadd -f realtime
sudo usermod -aG realtime "$USER"
sudo tee /etc/security/limits.d/99-ros2-realtime.conf >/dev/null <<'EOF'
@realtime soft rtprio 99
@realtime soft priority 99
@realtime soft memlock unlimited
@realtime hard rtprio 99
@realtime hard priority 99
@realtime hard memlock unlimited
EOF
```
Log out of the desktop session completely and log in again, or reboot. An already open terminal does not receive the new group and limits.
## Verification
In a new terminal, verify the group and limits:
```bash
id -nG | tr ' ' '\n' | grep -x realtime
ulimit -r
ulimit -l
```
The expected output is `realtime`, then `99`, then `unlimited`.
Start FRI without commanding robot motion and check the log for:
```text
FRI worker uses FIFO priority 80
```
You can also inspect the policies of all threads in the process:
```bash
PID=$(pgrep -n ros2_control_node)
ps -L -p "$PID" -o pid,tid,cls,rtprio,pri,comm
```
Realtime threads have `FF` in the `CLS` column. The FRI worker has `RTPRIO` 80 and the `controller_manager` main loop has 50.
!!! warning "Before the first motion"
First confirm that FRI connects and that the log contains `FRI worker uses FIFO priority 80`. Then make a short move at speed 0.1 with an operator supervising. FIFO reduces jitter on the computer; it does not replace network checks, robot limits, or a clear work area.
## If setup does not work
- If `realtime` is missing from `id`, log out and log in again.
- If `ulimit -r` is less than 80, check `/etc/security/limits.d/99-ros2-realtime.conf` and that PAM loads `pam_limits.so`.
- If the log still says `Operation not permitted`, make sure `cobot run` is started by the same user passed to `usermod`.
- A Python virtualenv needs no extra setup: permissions are assigned to the user and inherited by Python.
- Docker requires `--cap-add=sys_nice --ulimit rtprio=99 --ulimit memlock=-1`.
## Rollback
To remove the setup, run:
```bash
sudo rm /etc/security/limits.d/99-ros2-realtime.conf
sudo gpasswd -d "$USER" realtime
```
After logging in again, processes use the regular Linux scheduler.
See the official [ros2_control documentation](https://control.ros.org/jazzy/doc/ros2_control/controller_manager/doc/userdoc.html#determinism) for controller-manager priorities and limits.
+84
View File
@@ -0,0 +1,84 @@
# FIFO-приоритет для FRI
## Зачем это нужно
FRI передаёт позиционную команду каждый цикл. При выбранном периоде 10 мс новый пакет должен обрабатываться примерно каждые 10 мс. Обычный планировщик Linux может задержать поток другим процессом. В журнале это видно по сообщениям:
```text
Could not enable FIFO RT scheduling: Operation not permitted
Overrun might occur ... Read time: 11500 us
```
`SCHED_FIFO` — политика планировщика Linux для потоков с фиксированным приоритетом. В проекте `controller_manager` запрашивает приоритет 50, а FRI-поток — 80. Поток FRI получает более высокий приоритет, чтобы вовремя принять пакет KUKA и отправить ответ.
Настройку выполняют **на компьютере с ROS 2**, с которого запускается `cobot run`. Sunrise Cabinet и `cobot-setting.yaml` менять для этого не нужно.
## Настройка Ubuntu 24.04
Выполните на компьютере с ROS 2 под пользователем, который запускает `cobot`:
```bash
sudo groupadd -f realtime
sudo usermod -aG realtime "$USER"
sudo tee /etc/security/limits.d/99-ros2-realtime.conf >/dev/null <<'EOF'
@realtime soft rtprio 99
@realtime soft priority 99
@realtime soft memlock unlimited
@realtime hard rtprio 99
@realtime hard priority 99
@realtime hard memlock unlimited
EOF
```
Затем полностью выйдите из графического сеанса и войдите снова либо перезагрузите компьютер. Открытый терминал не получает новую группу и лимиты.
## Проверка
В новом терминале проверьте группу и лимиты:
```bash
id -nG | tr ' ' '\n' | grep -x realtime
ulimit -r
ulimit -l
```
Ожидаемый результат: первая команда выводит `realtime`, вторая — `99`, третья — `unlimited`.
Запустите FRI без движения робота и убедитесь, что в журнале есть:
```text
FRI worker uses FIFO priority 80
```
После запуска можно дополнительно посмотреть политики всех потоков процесса:
```bash
PID=$(pgrep -n ros2_control_node)
ps -L -p "$PID" -o pid,tid,cls,rtprio,pri,comm
```
Для потоков реального времени в столбце `CLS` будет `FF`; у FRI-потока `RTPRIO` равен 80, у основного цикла `controller_manager` — 50.
!!! warning "Перед первым движением"
Сначала проверьте успешное подключение FRI и строку `FRI worker uses FIFO priority 80`. Затем выполните короткое перемещение на скорости 0.1 при наблюдении оператора. FIFO уменьшает jitter компьютера, но не отменяет контроль сети, ограничений робота и рабочей зоны.
## Если настройка не сработала
- Если `realtime` отсутствует в выводе `id`, выйдите из системы и войдите снова.
- Если `ulimit -r` меньше 80, проверьте файл `/etc/security/limits.d/99-ros2-realtime.conf` и наличие `pam_limits.so` в конфигурации PAM.
- Если лог всё ещё сообщает `Operation not permitted`, убедитесь, что `cobot run` запущен тем же пользователем, для которого выполнен `usermod`.
- В Python virtualenv дополнительных действий не требуется: права задаются пользователю и наследуются процессом Python.
- Для Docker нужны права контейнера: `--cap-add=sys_nice --ulimit rtprio=99 --ulimit memlock=-1`.
## Откат
Чтобы удалить настройку, выполните:
```bash
sudo rm /etc/security/limits.d/99-ros2-realtime.conf
sudo gpasswd -d "$USER" realtime
```
После повторного входа процессы снова будут работать с обычным планировщиком Linux.
Подробнее о лимитах и приоритете `controller_manager` — в [документации ros2_control](https://control.ros.org/jazzy/doc/ros2_control/controller_manager/doc/userdoc.html#determinism).
+1
View File
@@ -141,5 +141,6 @@ nav:
- Меню IO Group: sunrise/kuka/features/io-group.md - Меню IO Group: sunrise/kuka/features/io-group.md
- Решение проблем: - Решение проблем:
- Реальное время FRI: realtime-fri.md
- Ошибка конфигурации: troubleshooting/config-error.md - Ошибка конфигурации: troubleshooting/config-error.md
- Ошибка SSL (rosdep/setup): troubleshooting/ssl-error.md - Ошибка SSL (rosdep/setup): troubleshooting/ssl-error.md
+13 -3
View File
@@ -4,7 +4,7 @@ import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from launch import LaunchDescription from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription, OpaqueFunction from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription, LogInfo, OpaqueFunction
from launch.launch_description_sources import PythonLaunchDescriptionSource from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node from launch_ros.actions import Node
@@ -50,8 +50,9 @@ def _runtime_setup(context, *args, **kwargs):
"initial_positions_file": settings.controller.moveit.initial_positions, "initial_positions_file": settings.controller.moveit.initial_positions,
"robot_ip": settings.robot.ip, "robot_ip": settings.robot.ip,
"fri_port": str(settings.robot.port), "fri_port": str(settings.robot.port),
"simulate": "false", "fri_cycle_ms": str(settings.robot.fri_cycle_ms),
"joint_position_tau": str(settings.robot.joint_position_tau), "joint_position_tau": str(settings.robot.joint_position_tau),
"simulate": "false",
} }
robot_description = converter.load_robot_description( robot_description = converter.load_robot_description(
@@ -118,9 +119,18 @@ def _runtime_setup(context, *args, **kwargs):
"controller_timer": str(settings.digital_twin.webots.controller_timer), "controller_timer": str(settings.digital_twin.webots.controller_timer),
"fri_cycle_ms": str(settings.robot.fri_cycle_ms), "fri_cycle_ms": str(settings.robot.fri_cycle_ms),
"joint_position_tau": str(settings.robot.joint_position_tau), "joint_position_tau": str(settings.robot.joint_position_tau),
"controller": settings.robot.active_controller, "robot_ip": settings.robot.ip,
"fri_port": str(settings.robot.port),
} }
if not simulate:
setup.append(LogInfo(msg=(
"FRI configured from cobot-setting.yaml: "
f"period={settings.robot.fri_cycle_ms} ms, "
f"position_tau={settings.robot.joint_position_tau:.3f} s, "
f"endpoint={settings.robot.ip}:{settings.robot.port}"
)))
setup.append(IncludeLaunchDescription( setup.append(IncludeLaunchDescription(
PythonLaunchDescriptionSource( PythonLaunchDescriptionSource(
PathJoinSubstitution([ PathJoinSubstitution([
@@ -1,10 +1,15 @@
import math
from launch import LaunchDescription from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, OpaqueFunction from launch.actions import DeclareLaunchArgument, LogInfo, OpaqueFunction
from launch.substitutions import LaunchConfiguration from launch.substitutions import LaunchConfiguration
from launch.event_handlers import OnProcessExit from launch.event_handlers import OnProcessExit
from launch.actions import RegisterEventHandler from launch.actions import RegisterEventHandler
from launch_ros.actions import Node from launch_ros.actions import Node
from webots_ros2_driver.urdf_spawner import URDFSpawner from webots_ros2_driver.urdf_spawner import URDFSpawner
from ament_index_python.packages import get_package_share_directory
from pathlib import Path
from xml.etree import ElementTree
from iiwa_utils import converter from iiwa_utils import converter
@@ -19,9 +24,12 @@ def _setup_controllers(context, *args, **kwargs):
controller_timer = LaunchConfiguration("controller_timer").perform(context) controller_timer = LaunchConfiguration("controller_timer").perform(context)
controller_path = LaunchConfiguration("controller_path").perform(context) controller_path = LaunchConfiguration("controller_path").perform(context)
simulate = LaunchConfiguration("simulate").perform(context).lower() in ("true", "1", "yes") simulate = LaunchConfiguration("simulate").perform(context).lower() in ("true", "1", "yes")
controller = LaunchConfiguration("controller").perform(context) # "jtc" | "forward"
fri_cycle_ms = int(LaunchConfiguration("fri_cycle_ms").perform(context)) fri_cycle_ms = int(LaunchConfiguration("fri_cycle_ms").perform(context))
joint_position_tau = LaunchConfiguration("joint_position_tau").perform(context) if not 1 <= fri_cycle_ms <= 100:
raise ValueError("fri_cycle_ms must be within the FRI 1.16 range 1..100 ms")
joint_position_tau = float(LaunchConfiguration("joint_position_tau").perform(context))
if not math.isfinite(joint_position_tau) or joint_position_tau < 0.0:
raise ValueError("joint_position_tau must be finite and non-negative")
update_rate = 1000 // fri_cycle_ms update_rate = 1000 // fri_cycle_ms
xacro_args = {"initial_positions_file": initial_positions_file} xacro_args = {"initial_positions_file": initial_positions_file}
@@ -29,7 +37,10 @@ def _setup_controllers(context, *args, **kwargs):
if simulate: if simulate:
xacro_args["simulate"] = "true" xacro_args["simulate"] = "true"
else: else:
xacro_args["joint_position_tau"] = joint_position_tau xacro_args["robot_ip"] = LaunchConfiguration("robot_ip").perform(context)
xacro_args["fri_port"] = LaunchConfiguration("fri_port").perform(context)
xacro_args["fri_cycle_ms"] = str(fri_cycle_ms)
xacro_args["joint_position_tau"] = str(joint_position_tau)
robot_description = converter.load_robot_description( robot_description = converter.load_robot_description(
model_path=description, model_path=description,
@@ -75,6 +86,13 @@ def _setup_controllers(context, *args, **kwargs):
# FRI # FRI
else: else:
manifest = Path(get_package_share_directory("controller_manager")) / "package.xml"
version = ElementTree.parse(manifest).findtext("version", "0.0.0")
if tuple(int(part) for part in version.split(".")[:3]) < (4, 48, 0):
raise RuntimeError(
"FRI synchronization requires controller_manager >= 4.48.0 (ROS 2 Jazzy); "
f"installed: {version}"
)
ros2_control_node = Node( ros2_control_node = Node(
package="controller_manager", package="controller_manager",
executable="ros2_control_node", executable="ros2_control_node",
@@ -83,6 +101,7 @@ def _setup_controllers(context, *args, **kwargs):
{"robot_description": robot_description}, {"robot_description": robot_description},
controller_path, controller_path,
{"update_rate": update_rate}, {"update_rate": update_rate},
{"hardware_synchronization.expect_blocking_read_write": True},
], ],
) )
@@ -98,37 +117,23 @@ def _setup_controllers(context, *args, **kwargs):
cm = ["--controller-manager", "/controller_manager"] cm = ["--controller-manager", "/controller_manager"]
jtc_args = ["iiwa_arm_controller"] + cm
if controller == "forward":
jtc_args += ["--inactive"]
# ForwardCommandController: активен если controller=forward, иначе --inactive
forward_args = ["forward_position_controller"] + cm
if controller != "forward":
forward_args += ["--inactive"]
jtc = Node( jtc = Node(
package="controller_manager", package="controller_manager",
executable="spawner", executable="spawner",
output="screen", output="screen",
arguments=jtc_args, arguments=["iiwa_arm_controller"] + cm,
)
forward_controller = Node(
package="controller_manager",
executable="spawner",
output="screen",
arguments=forward_args,
) )
jtc_after_jsb = RegisterEventHandler( jtc_after_jsb = RegisterEventHandler(
OnProcessExit( OnProcessExit(
target_action=jsb, target_action=jsb,
on_exit=[jtc, forward_controller], on_exit=[jtc],
) )
) )
return [ return [
LogInfo(msg=(f"FRI controller period={fri_cycle_ms} ms; Controller Manager rate={update_rate} Hz; "
f"position smoothing tau={joint_position_tau:.3f} s")),
ros2_control_node, ros2_control_node,
jsb, jsb,
jtc_after_jsb, jtc_after_jsb,
@@ -139,6 +144,7 @@ def generate_launch_description():
return LaunchDescription([ return LaunchDescription([
DeclareLaunchArgument("fri_cycle_ms", default_value="5"), DeclareLaunchArgument("fri_cycle_ms", default_value="5"),
DeclareLaunchArgument("joint_position_tau", default_value="0.04"), DeclareLaunchArgument("joint_position_tau", default_value="0.04"),
DeclareLaunchArgument("controller", default_value="jtc"), DeclareLaunchArgument("robot_ip", default_value="192.170.10.2"),
DeclareLaunchArgument("fri_port", default_value="30200"),
OpaqueFunction(function=_setup_controllers), OpaqueFunction(function=_setup_controllers),
]) ])
@@ -39,6 +39,13 @@ def _spawn_setup(context, *args, **kwargs):
with open(controller, "r") as f: with open(controller, "r") as f:
controller_cfg = yaml.safe_load(f) or {} controller_cfg = yaml.safe_load(f) or {}
# FRI sets the hardware cycle dynamically in controllers.launch.py. Webots
# has no FRI period, so preserve its established 200 Hz controller cycle.
manager_params = controller_cfg.setdefault("controller_manager", {}).setdefault(
"ros__parameters", {}
)
manager_params["update_rate"] = 200
combined = { combined = {
"/**": {"ros__parameters": { "/**": {"ros__parameters": {
"robot_description": robot_description, "robot_description": robot_description,
+1 -1
View File
@@ -26,7 +26,7 @@
<exec_depend>rviz2</exec_depend> <exec_depend>rviz2</exec_depend>
<!-- ros2_control --> <!-- ros2_control -->
<exec_depend>controller_manager</exec_depend> <exec_depend version_gte="4.48.0">controller_manager</exec_depend>
<exec_depend>joint_state_broadcaster</exec_depend> <exec_depend>joint_state_broadcaster</exec_depend>
<exec_depend>joint_trajectory_controller</exec_depend> <exec_depend>joint_trajectory_controller</exec_depend>
<exec_depend>forward_command_controller</exec_depend> <exec_depend>forward_command_controller</exec_depend>
@@ -1,6 +1,8 @@
controller_manager: controller_manager:
ros__parameters: ros__parameters:
update_rate: 200 # резервное значение; при запуске через launch: 1000 // fri_cycle_ms # `update_rate` задаётся FRI launch-файлом как 1000 / robot.fri_cycle_ms.
# Не фиксировать его здесь: KLI Position всегда выдаёт 10 мс (100 Гц),
# а KONI может работать с 5 мс (200 Гц).
joint_state_broadcaster: joint_state_broadcaster:
type: "joint_state_broadcaster/JointStateBroadcaster" type: "joint_state_broadcaster/JointStateBroadcaster"
@@ -8,8 +10,6 @@ controller_manager:
iiwa_arm_controller: iiwa_arm_controller:
type: "joint_trajectory_controller/JointTrajectoryController" type: "joint_trajectory_controller/JointTrajectoryController"
forward_position_controller:
type: "forward_command_controller/ForwardCommandController"
joint_state_broadcaster: joint_state_broadcaster:
ros__parameters: ros__parameters:
@@ -58,15 +58,3 @@ iiwa_arm_controller:
# joint5: { trajectory: 0.1, goal: 0.01 } # joint5: { trajectory: 0.1, goal: 0.01 }
# joint6: { trajectory: 0.1, goal: 0.01 } # joint6: { trajectory: 0.1, goal: 0.01 }
# joint7: { trajectory: 0.1, goal: 0.01 } # joint7: { trajectory: 0.1, goal: 0.01 }
forward_position_controller:
ros__parameters:
joints:
- joint1
- joint2
- joint3
- joint4
- joint5
- joint6
- joint7
interface_name: position
+3 -5
View File
@@ -1,11 +1,9 @@
robot: robot:
name: "iiwa7" name: "iiwa7"
ip: "192.170.10.2" ip: "192.168.21.147"
port: 30200 port: 30200
fri_cycle_ms: 10 # период FRI-цикла: 5 мс (200 Гц) или 10 мс (100 Гц) fri_cycle_ms: 10 # период FRI-цикла: 5 мс (200 Гц) или 10 мс (100 Гц)
joint_position_tau: 0.04 # EMA фильтр позиций [с]: сглаживает команды перед отправкой в FRI joint_position_tau: 0.04 # EMA позиций FRI [с]; 0 отключает сглаживание
joint_velocity_tau: 0.01 # EMA фильтр скорости [с]: убирает выбросы конечных разностей
active_controller: "jtc" # "jtc" = MoveIt/JointTrajectoryController, "forward" = ForwardCommandController
description: pkg://iiwa_description/urdf/iiwa7.urdf.xacro description: pkg://iiwa_description/urdf/iiwa7.urdf.xacro
@@ -79,4 +77,4 @@ foxglove:
- assets - assets
include_hidden: false # Показывать клиенту скрытые топики и сервисы (начинаются с _) include_hidden: false # Показывать клиенту скрытые топики и сервисы (начинаются с _)
asset_uri_allowlist: ['^package://(?:[-\w%]+/)*[-\w%.]+\.(?:dae|fbx|glb|gltf|jpeg|jpg|mtl|obj|png|stl|tif|tiff|urdf|webp|xacro)$'] # Regex-список URI вида package://..., из которых bridge разрешает отдавать файлы-ассеты (URDF, mesh и т.п.) asset_uri_allowlist: ['^package://(?:[-\w%]+/)*[-\w%.]+\.(?:dae|fbx|glb|gltf|jpeg|jpg|mtl|obj|png|stl|tif|tiff|urdf|webp|xacro)$'] # Regex-список URI вида package://..., из которых bridge разрешает отдавать файлы-ассеты (URDF, mesh и т.п.)
ignore_unresponsive_param_nodes: true # Не падать, если нода не отвечает на запросы параметров (защита от зависания при старте) ignore_unresponsive_param_nodes: true # Не падать, если нода не отвечает на запросы параметров (защита от зависания при старте)
+15 -2
View File
@@ -33,7 +33,7 @@ file(GLOB_RECURSE FRI_SOURCES
add_library(fri_client_sdk STATIC ${FRI_SOURCES}) add_library(fri_client_sdk STATIC ${FRI_SOURCES})
target_include_directories(fri_client_sdk PUBLIC target_include_directories(fri_client_sdk SYSTEM PUBLIC
${FRI_SDK_DIR}/include ${FRI_SDK_DIR}/include
${FRI_SDK_DIR}/src/nanopb-0.2.8 ${FRI_SDK_DIR}/src/nanopb-0.2.8
${FRI_SDK_DIR}/src/protobuf ${FRI_SDK_DIR}/src/protobuf
@@ -93,4 +93,17 @@ ament_export_targets(export_${PROJECT_NAME})
ament_export_dependencies( ament_export_dependencies(
controller_interface hardware_interface pluginlib rclcpp rclcpp_lifecycle realtime_tools std_msgs) controller_interface hardware_interface pluginlib rclcpp rclcpp_lifecycle realtime_tools std_msgs)
ament_package() if(BUILD_TESTING)
# Manual bounded loopback integration harness; no physical hardware access.
add_executable(fake_fri_server test/fake_fri_server.cpp)
target_link_libraries(fake_fri_server fri_client_sdk)
add_library(fri_loopback_bind SHARED test/loopback_bind.cpp)
target_link_libraries(fri_loopback_bind dl)
find_package(ament_cmake_gtest REQUIRED)
ament_add_gtest(test_fri_client test/test_fri_client.cpp)
target_link_libraries(test_fri_client ${PROJECT_NAME} fri_client_sdk)
ament_add_gtest(test_hardware test/test_hardware.cpp)
target_link_libraries(test_hardware ${PROJECT_NAME} fri_client_sdk hardware_interface::hardware_interface)
endif()
ament_package()
@@ -2,6 +2,7 @@
#include <array> #include <array>
#include <atomic> #include <atomic>
#include <chrono>
#include <mutex> #include <mutex>
#include "friClientApplication.h" #include "friClientApplication.h"
@@ -10,62 +11,70 @@
namespace iiwa_controller namespace iiwa_controller
{ {
// Снимок состояния робота захватывается атомарно за один lock в FRI-потоке
// и так же за один lock читается из read() в потоке управления.
struct IIWAStateSnapshot struct IIWAStateSnapshot
{ {
std::array<double, 7> measured_pos{}; // в Commanding = filtered_pos_ (open-loop) std::array<double, 7> measured_pos{}; // encoder position [rad], never a command
std::array<double, 7> measured_tau{}; // измеренные моменты [Нм] std::array<double, 7> measured_tau{}; // measured torque [Nm]
std::array<double, 7> external_tau{}; // внешние моменты без компенсации модели [Нм] std::array<double, 7> external_tau{}; // estimated external torque [Nm]
std::array<double, 7> ipo_pos{}; // позиция интерполятора [рад], только в Commanding std::array<double, 7> ipo_pos{}; // interpolator position [rad]
double sample_time{0.005}; // период цикла FRI [с] std::array<double, 7> command_pos{}; // position sent in the current FRI reply [rad]
double sample_time{0.0};
KUKA::FRI::ESessionState session{KUKA::FRI::IDLE};
KUKA::FRI::EConnectionQuality quality{KUKA::FRI::POOR}; KUKA::FRI::EConnectionQuality quality{KUKA::FRI::POOR};
bool ipo_valid{false}; // в Monitor-режиме IPO недоступна KUKA::FRI::ESafetyState safety{KUKA::FRI::NORMAL_OPERATION};
unsigned int time_stamp_sec{0}; // Unix-время пакета [с] KUKA::FRI::EDriveState drives{KUKA::FRI::OFF};
unsigned int time_stamp_nano_sec{0}; // наносекундная часть [нс] KUKA::FRI::EOperationMode operation{KUKA::FRI::TEST_MODE_1};
KUKA::FRI::EControlMode control{KUKA::FRI::NO_CONTROL};
KUKA::FRI::EClientCommandMode command_mode{KUKA::FRI::NO_COMMAND_MODE};
KUKA::FRI::EOverlayType overlay{KUKA::FRI::NO_OVERLAY};
double tracking_performance{0.0};
bool valid{false};
bool ipo_valid{false};
unsigned int time_stamp_sec{0};
unsigned int time_stamp_nano_sec{0};
std::chrono::steady_clock::time_point received_at{};
}; };
class FRIClient : public KUKA::FRI::LBRClient class FRIClient : public KUKA::FRI::LBRClient
{ {
public: public:
static constexpr size_t N_JOINTS = 7; static constexpr size_t N_JOINTS = KUKA::FRI::LBRState::NUMBER_OF_JOINTS;
using Joints = std::array<double, N_JOINTS>;
// joint_position_tau — постоянная времени экспоненциального фильтра позиций [с]. explicit FRIClient(double position_smoothing_tau = 0.04);
// Аналог joint_position_tau из lbr_fri_ros2_stack (по умолчанию 0.04 с = 40 мс).
// Сглаживает скачки команд перед отправкой роботу → убирает писк и стук суставов.
explicit FRIClient(double joint_position_tau = 0.04);
~FRIClient() override = default;
// Коллбэки FRI SDK, вызываются из friThreadFunc через ClientApplication::step()
void monitor() override; void monitor() override;
void waitForCommand() override; void waitForCommand() override;
void command() override; void command() override;
void onStateChange( void onStateChange(KUKA::FRI::ESessionState oldState,
KUKA::FRI::ESessionState oldState, KUKA::FRI::ESessionState newState) override; KUKA::FRI::ESessionState newState) override;
// Потокобезопасное API для ros2_control, вызывается из read() и write() // Configure before starting the FRI thread; limits come from the robot URDF.
void setTargetJointPositions(const std::array<double, N_JOINTS> & q); void setLimits(const Joints & lower, const Joints & upper, const Joints & velocity);
IIWAStateSnapshot getStateSnapshot() const; void setExpectedSampleTime(double seconds) {expected_sample_time_ = seconds;}
void setTargetJointPositions(const Joints & q);
IIWAStateSnapshot getStateSnapshot() const; // lifecycle/non-RT use
bool tryGetStateSnapshot(IIWAStateSnapshot & snapshot) const;
bool isCommandingActive() const; bool isCommandingActive() const;
KUKA::FRI::ESessionState getSessionState() const; KUKA::FRI::ESessionState getSessionState() const;
private: private:
double joint_position_tau_;
std::atomic<KUKA::FRI::ESessionState> session_state_{KUKA::FRI::IDLE}; std::atomic<KUKA::FRI::ESessionState> session_state_{KUKA::FRI::IDLE};
mutable std::mutex state_mutex_;
mutable std::mutex data_mutex_; std::mutex command_mutex_;
std::array<double, N_JOINTS> target_pos_{}; Joints target_pos_{};
// Сглаженная позиция, которую реально отправляем роботу. Joints filtered_pos_{};
// Инициализируется IPO-позицией в waitForCommand(), чтобы не было скачка при старте. Joints requested_pos_{};
std::array<double, N_JOINTS> filtered_pos_{}; Joints sent_pos_{};
IIWAStateSnapshot snapshot_{}; Joints lower_, upper_, max_velocity_;
bool initialized_{false};
// Обновить snapshot_ без поля ipo_pos (в Monitor-режиме getIpoJointPosition() недоступна) bool requested_{false};
void captureMonitoringData(); double position_smoothing_tau_{0.04};
// Обновить snapshot_ вместе с ipo_pos (в Commanding-режиме) double expected_sample_time_{0.0};
void captureCommandingData(); std::chrono::steady_clock::time_point requested_at_{};
std::chrono::steady_clock::time_point last_command_at_{};
IIWAStateSnapshot snapshot_{}; // published under state_mutex_
IIWAStateSnapshot current_{}; // owned exclusively by FRI thread
void captureData(bool commanding);
void publishState();
void validateCommanding() const;
}; };
} // namespace iiwa_controller } // namespace iiwa_controller
@@ -0,0 +1,67 @@
#pragma once
#include <chrono>
#include <condition_variable>
#include <mutex>
namespace iiwa_controller
{
// One SDK step -> one read/update/write -> next SDK step. No queued commands,
// no independent ROS timer and no overwrite of an unconsumed setpoint.
class FRICycleGate
{
public:
void reset()
{
std::lock_guard<std::mutex> lock(mutex_);
stopped_ = available_ = acquired_ = completed_ = false;
}
void stop()
{
std::lock_guard<std::mutex> lock(mutex_);
stopped_ = true;
cv_.notify_all();
}
bool publishAndWait()
{
std::unique_lock<std::mutex> lock(mutex_);
if (stopped_) {return false;}
available_ = true;
completed_ = false;
cv_.notify_all();
if (!cv_.wait_for(lock, timeout_, [&] {return stopped_ || completed_;})) {
stopped_ = true;
cv_.notify_all();
}
return !stopped_;
}
bool waitReady(std::chrono::milliseconds timeout)
{
std::unique_lock<std::mutex> lock(mutex_);
return cv_.wait_for(lock, timeout, [&] {return stopped_ || available_;}) && !stopped_;
}
bool acquire()
{
std::unique_lock<std::mutex> lock(mutex_);
if (acquired_ || !cv_.wait_for(lock, timeout_, [&] {return stopped_ || available_;}) ||
stopped_) {return false;}
available_ = false;
acquired_ = true;
return true;
}
bool complete()
{
std::lock_guard<std::mutex> lock(mutex_);
if (stopped_ || !acquired_) {return false;}
acquired_ = false;
completed_ = true;
cv_.notify_all();
return true;
}
private:
static constexpr std::chrono::milliseconds timeout_{100};
std::mutex mutex_;
std::condition_variable cv_;
bool stopped_{true}, available_{false}, acquired_{false}, completed_{false};
};
} // namespace iiwa_controller
@@ -8,10 +8,7 @@
#include <thread> #include <thread>
#include <vector> #include <vector>
// В Jazzy 4.44.0+ нельзя переопределять export_state_interfaces() и export_command_interfaces(). // Jazzy 4.48: framework-managed interfaces and handle API.
// Устаревший конструктор не регистрирует introspection-callback pal_statistics,
// из-за чего падает с segfault. Базовый класс сам создаёт интерфейсы из URDF.
// Данные читаем и пишем через handle-API: set_state() и get_command().
#include "hardware_interface/hardware_info.hpp" #include "hardware_interface/hardware_info.hpp"
#include "hardware_interface/system_interface.hpp" #include "hardware_interface/system_interface.hpp"
#include "hardware_interface/types/hardware_component_interface_params.hpp" #include "hardware_interface/types/hardware_component_interface_params.hpp"
@@ -23,6 +20,7 @@
#include "rclcpp_lifecycle/state.hpp" #include "rclcpp_lifecycle/state.hpp"
#include "iiwa_controller/FRIClient.h" #include "iiwa_controller/FRIClient.h"
#include "iiwa_controller/FRICycleGate.hpp"
namespace iiwa_controller namespace iiwa_controller
{ {
@@ -31,6 +29,7 @@ class IIWAHardwareInterface : public hardware_interface::SystemInterface
{ {
public: public:
RCLCPP_SHARED_PTR_DEFINITIONS(IIWAHardwareInterface) RCLCPP_SHARED_PTR_DEFINITIONS(IIWAHardwareInterface)
~IIWAHardwareInterface() override;
CallbackReturn on_init( CallbackReturn on_init(
const hardware_interface::HardwareComponentInterfaceParams & params) override; const hardware_interface::HardwareComponentInterfaceParams & params) override;
@@ -39,13 +38,16 @@ public:
std::vector<hardware_interface::InterfaceDescription> std::vector<hardware_interface::InterfaceDescription>
export_unlisted_state_interface_descriptions() override; export_unlisted_state_interface_descriptions() override;
// Полный lifecycle: configure открывает сокет, activate запускает поток, // Полный lifecycle: configure создаёт SDK, activate открывает сокет и запускает поток,
// deactivate останавливает поток, cleanup освобождает FRI-объекты. // deactivate останавливает поток, cleanup освобождает FRI-объекты.
CallbackReturn on_configure(const rclcpp_lifecycle::State & previous_state) override; CallbackReturn on_configure(const rclcpp_lifecycle::State & previous_state) override;
CallbackReturn on_activate(const rclcpp_lifecycle::State & previous_state) override; CallbackReturn on_activate(const rclcpp_lifecycle::State & previous_state) override;
CallbackReturn on_deactivate(const rclcpp_lifecycle::State & previous_state) override; CallbackReturn on_deactivate(const rclcpp_lifecycle::State & previous_state) override;
CallbackReturn on_cleanup(const rclcpp_lifecycle::State & previous_state) override; CallbackReturn on_cleanup(const rclcpp_lifecycle::State & previous_state) override;
CallbackReturn on_shutdown(const rclcpp_lifecycle::State & previous_state) override;
CallbackReturn on_error(const rclcpp_lifecycle::State & previous_state) override;
hardware_interface::return_type read( hardware_interface::return_type read(
const rclcpp::Time & time, const rclcpp::Duration & period) override; const rclcpp::Time & time, const rclcpp::Duration & period) override;
@@ -58,44 +60,48 @@ private:
// Параметры из секции <hardware><param> в URDF // Параметры из секции <hardware><param> в URDF
std::string robot_ip_; std::string robot_ip_;
int fri_port_{30200}; int fri_port_{30200};
bool simulate_{false}; int fri_cycle_ms_{5};
double joint_position_tau_{0.04}; double joint_position_tau_{0.04};
// EMA-фильтр скорости: сглаживает одиночные выбросы конечных разностей. int rt_prio_{80};
// joint_velocity_tau = 0 отключает фильтр (raw finite difference). bool simulate_{false};
double joint_velocity_tau_{0.01};
// Объекты FRI SDK // Объекты FRI SDK
std::unique_ptr<FRIClient> fri_client_; std::unique_ptr<FRIClient> fri_client_;
std::unique_ptr<KUKA::FRI::UdpConnection> connection_; std::unique_ptr<KUKA::FRI::UdpConnection> connection_;
std::unique_ptr<KUKA::FRI::ClientApplication> app_; std::unique_ptr<KUKA::FRI::ClientApplication> app_;
// FRI работает в отдельном потоке: step() блокируется в recvfrom(). // FRI работает в отдельном потоке: step() блокируется в recvfrom().
// read() лишь читает готовый снимок — без блокировки RT-потока. // read/write rendezvous once per SDK step; network I/O stays in this worker.
// Requires controller_manager hardware_synchronization mode (Jazzy 4.48+).
std::thread fri_thread_; std::thread fri_thread_;
FRICycleGate cycle_gate_;
std::atomic<bool> fri_running_{false}; std::atomic<bool> fri_running_{false};
void friThreadFunc(); void friThreadFunc();
void stopFRI();
void releaseFRI();
std::atomic<bool> fri_fault_{false};
bool active_{false};
IIWAStateSnapshot last_snapshot_{};
std::array<double, N_JOINTS> lower_, upper_, max_velocity_;
// Хэндлы интерфейсов состояния, заполняются в on_activate // Хэндлы интерфейсов состояния, заполняются в on_activate
std::array<hardware_interface::StateInterface::SharedPtr, N_JOINTS> h_pos_; std::array<hardware_interface::StateInterface::SharedPtr, N_JOINTS> h_pos_;
std::array<hardware_interface::StateInterface::SharedPtr, N_JOINTS> h_vel_; std::array<hardware_interface::StateInterface::SharedPtr, N_JOINTS> h_vel_;
std::array<hardware_interface::StateInterface::SharedPtr, N_JOINTS> h_eff_; std::array<hardware_interface::StateInterface::SharedPtr, N_JOINTS> h_eff_;
std::array<hardware_interface::StateInterface::SharedPtr, N_JOINTS> h_ext_; std::array<hardware_interface::StateInterface::SharedPtr, N_JOINTS> h_ext_;
std::array<hardware_interface::StateInterface::SharedPtr, N_JOINTS> h_ipo_;
std::array<hardware_interface::StateInterface::SharedPtr, N_JOINTS> h_fri_cmd_;
std::array<hardware_interface::CommandInterface::SharedPtr, N_JOINTS> h_cmd_pos_; std::array<hardware_interface::CommandInterface::SharedPtr, N_JOINTS> h_cmd_pos_;
// Вычисление скорости: конечные разности + EMA-фильтр // Вычисление скорости по измеренной позиции и меткам времени FRI.
std::array<double, N_JOINTS> prev_pos_{}; std::array<double, N_JOINTS> prev_pos_{};
std::array<double, N_JOINTS> velocity_{}; // отфильтрованная скорость, публикуется в JTC std::array<double, N_JOINTS> velocity_{}; // измеренная скорость [рад/с]
std::array<double, N_JOINTS> velocity_raw_{}; // сырая скорость до фильтра
unsigned int last_ts_sec_{0}; unsigned int last_ts_sec_{0};
unsigned int last_ts_nsec_{0}; unsigned int last_ts_nsec_{0};
bool velocity_initialized_{false}; bool velocity_initialized_{false};
void compute_velocity_(const IIWAStateSnapshot & snap); void compute_velocity_(const IIWAStateSnapshot & snap);
// Отслеживание сессии FRI для обнаружения потери управления
KUKA::FRI::ESessionState previous_session_state_{KUKA::FRI::IDLE};
rclcpp::Clock throttle_clock_{RCL_STEADY_TIME};
}; };
+1
View File
@@ -17,6 +17,7 @@
<depend>realtime_tools</depend> <depend>realtime_tools</depend>
<depend>std_msgs</depend> <depend>std_msgs</depend>
<test_depend>ament_cmake_gtest</test_depend>
<test_depend>ament_lint_auto</test_depend> <test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend> <test_depend>ament_lint_common</test_depend>
+186 -121
View File
@@ -1,155 +1,220 @@
#include "iiwa_controller/FRIClient.h" #include "iiwa_controller/FRIClient.h"
#include <algorithm>
#include <cmath> #include <cmath>
#include <cstring> #include <limits>
#include <stdexcept>
#include <rclcpp/rclcpp.hpp>
namespace iiwa_controller namespace iiwa_controller
{ {
using namespace KUKA::FRI;
static const char * friStateName(KUKA::FRI::ESessionState s) FRIClient::FRIClient(double position_smoothing_tau)
: position_smoothing_tau_(position_smoothing_tau)
{ {
switch (s) { if (!std::isfinite(position_smoothing_tau_) || position_smoothing_tau_ < 0.0) {
case KUKA::FRI::IDLE: return "IDLE"; throw std::invalid_argument("Position smoothing tau must be finite and non-negative");
case KUKA::FRI::MONITORING_WAIT: return "MONITORING_WAIT";
case KUKA::FRI::MONITORING_READY: return "MONITORING_READY";
case KUKA::FRI::COMMANDING_WAIT: return "COMMANDING_WAIT";
case KUKA::FRI::COMMANDING_ACTIVE: return "COMMANDING_ACTIVE";
default: return "UNKNOWN";
} }
lower_.fill(-std::numeric_limits<double>::infinity());
upper_.fill(std::numeric_limits<double>::infinity());
max_velocity_.fill(std::numeric_limits<double>::infinity());
} }
FRIClient::FRIClient(double joint_position_tau) void FRIClient::setLimits(const Joints & lower, const Joints & upper, const Joints & velocity)
: joint_position_tau_(joint_position_tau)
{ {
target_pos_.fill(0.0); lower_ = lower;
filtered_pos_.fill(0.0); upper_ = upper;
max_velocity_ = velocity;
} }
// Вызывается только в Monitor-состояниях. void FRIClient::captureData(bool commanding)
// В Monitor-режиме getIpoJointPosition() бросает FRIException, поэтому здесь не зовём.
void FRIClient::captureMonitoringData()
{ {
std::memcpy( const auto & state = robotState();
snapshot_.measured_pos.data(), std::copy_n(state.getMeasuredJointPosition(), N_JOINTS, current_.measured_pos.begin());
robotState().getMeasuredJointPosition(), N_JOINTS * sizeof(double)); std::copy_n(state.getMeasuredTorque(), N_JOINTS, current_.measured_tau.begin());
std::memcpy( std::copy_n(state.getExternalTorque(), N_JOINTS, current_.external_tau.begin());
snapshot_.measured_tau.data(), if (!commanding) {
robotState().getMeasuredTorque(), N_JOINTS * sizeof(double)); // No application command exists outside a commanding FRI state.
std::memcpy( current_.command_pos = current_.measured_pos;
snapshot_.external_tau.data(),
robotState().getExternalTorque(), N_JOINTS * sizeof(double));
snapshot_.sample_time = robotState().getSampleTime();
snapshot_.quality = robotState().getConnectionQuality();
snapshot_.ipo_valid = false;
snapshot_.time_stamp_sec = robotState().getTimestampSec();
snapshot_.time_stamp_nano_sec = robotState().getTimestampNanoSec();
}
// Вызывается из Commanding-состояний (COMMANDING_WAIT и COMMANDING_ACTIVE).
// В отличие от Monitor, здесь getIpoJointPosition() доступна.
void FRIClient::captureCommandingData()
{
captureMonitoringData();
std::memcpy(
snapshot_.ipo_pos.data(),
robotState().getIpoJointPosition(), N_JOINTS * sizeof(double));
snapshot_.ipo_valid = true;
// Open-loop: JTC видит filtered_pos_ как «измеренную» позицию — как в lbr_fri_ros2_stack.
// Благодаря этому JTC не видит расхождения и не генерирует коррекций.
snapshot_.measured_pos = filtered_pos_;
}
// Вызывается в MONITORING_WAIT и MONITORING_READY
void FRIClient::monitor()
{
std::lock_guard<std::mutex> lock(data_mutex_);
captureMonitoringData();
}
// Вызывается в COMMANDING_WAIT.
// По документации FRI (п. 6.2.2) клиент должен отправлять команды в каждом цикле.
// Переход в COMMANDING_ACTIVE происходит только когда разница между commanded_position
// и IPO_position меньше 0.001 рад для всех суставов.
// Важно эхировать именно IPO-позицию, не measured. Если взять measured,
// статическое отклонение не даст выполниться этому условию.
void FRIClient::waitForCommand()
{
std::lock_guard<std::mutex> lock(data_mutex_);
captureCommandingData();
// Инициализируем цель и фильтр IPO-позицией.
// Фильтр стартует с IPO — это гарантирует нулевой скачок при переходе в COMMANDING_ACTIVE.
std::memcpy(target_pos_.data(), snapshot_.ipo_pos.data(), N_JOINTS * sizeof(double));
std::memcpy(filtered_pos_.data(), snapshot_.ipo_pos.data(), N_JOINTS * sizeof(double));
robotCommand().setJointPosition(filtered_pos_.data());
}
// Вызывается в COMMANDING_ACTIVE, основной цикл управления
void FRIClient::command()
{
std::lock_guard<std::mutex> lock(data_mutex_);
// EMA-фильтр применяется ДО захвата снимка — тогда snapshot_.measured_pos = filtered_pos_
// будет содержать то, что реально отправлено роботу в этом цикле (не прошлом).
// Это соответствует lbr_fri_ros2_stack: снимок захватывается post-EMA.
const double dt = robotState().getSampleTime();
const double alpha = (joint_position_tau_ > 0.0) ? dt / (joint_position_tau_ + dt) : 1.0;
for (size_t i = 0; i < N_JOINTS; ++i) {
filtered_pos_[i] = alpha * target_pos_[i] + (1.0 - alpha) * filtered_pos_[i];
} }
current_.sample_time = state.getSampleTime();
robotCommand().setJointPosition(filtered_pos_.data()); current_.session = state.getSessionState();
current_.quality = state.getConnectionQuality();
// Захватываем снимок ПОСЛЕ EMA: measured_pos = filtered_pos_ = что робот только что получил. current_.safety = state.getSafetyState();
captureCommandingData(); current_.drives = state.getDriveState();
} current_.operation = state.getOperationMode();
current_.control = state.getControlMode();
void FRIClient::onStateChange( current_.command_mode = commanding ? state.getClientCommandMode() : NO_COMMAND_MODE;
KUKA::FRI::ESessionState oldState, KUKA::FRI::ESessionState newState) current_.overlay = commanding ? state.getOverlayType() : NO_OVERLAY;
{ current_.tracking_performance = commanding ? state.getTrackingPerformance() : 0.0;
session_state_.store(newState, std::memory_order_relaxed); current_.ipo_valid = commanding;
if (commanding) {
RCLCPP_INFO( std::copy_n(state.getIpoJointPosition(), N_JOINTS, current_.ipo_pos.begin());
rclcpp::get_logger("FRIClient"), }
"FRI смена состояния: %s, теперь %s", friStateName(oldState), friStateName(newState)); const auto sec = state.getTimestampSec();
const auto nsec = state.getTimestampNanoSec();
if (newState == KUKA::FRI::IDLE || if (current_.valid && (sec < current_.time_stamp_sec ||
newState == KUKA::FRI::MONITORING_WAIT || (sec == current_.time_stamp_sec && nsec <= current_.time_stamp_nano_sec)))
newState == KUKA::FRI::MONITORING_READY)
{ {
throw std::runtime_error("FRI timestamp did not advance");
} }
} if (nsec >= 1000000000 || !std::isfinite(current_.sample_time) || current_.sample_time <= 0.0) {
throw std::runtime_error("Invalid FRI timestamp/sample period");
void FRIClient::setTargetJointPositions(const std::array<double, N_JOINTS> & q) }
{ if (expected_sample_time_ > 0.0 &&
// До первой команды контроллера интерфейс содержит NaN. std::abs(current_.sample_time - expected_sample_time_) > 1e-9)
// Если отправить NaN роботу в COMMANDING_ACTIVE, получим CK_COMPOUND_RETURN_ERROR. {
for (const auto & v : q) { throw std::runtime_error(
if (!std::isfinite(v)) { "Sunrise FRI period is " + std::to_string(current_.sample_time * 1000.0) +
return; " ms, but robot.fri_cycle_ms is " +
std::to_string(expected_sample_time_ * 1000.0) + " ms");
}
if (current_.valid && current_.session == COMMANDING_ACTIVE) {
const double elapsed = static_cast<double>(sec - current_.time_stamp_sec) +
(static_cast<double>(nsec) - current_.time_stamp_nano_sec) * 1e-9;
if (std::abs(elapsed - current_.sample_time) > 1e-6) {
throw std::runtime_error("FRI commanding packet gap; refusing to catch up trajectory");
} }
} }
std::lock_guard<std::mutex> lock(data_mutex_); for (size_t i = 0; i < N_JOINTS; ++i) {
target_pos_ = q; if (!std::isfinite(current_.measured_pos[i]) || !std::isfinite(current_.measured_tau[i]) ||
!std::isfinite(current_.external_tau[i]) ||
(commanding && !std::isfinite(current_.ipo_pos[i])))
{
throw std::runtime_error("Non-finite FRI telemetry");
}
}
current_.time_stamp_sec = sec;
current_.time_stamp_nano_sec = nsec;
current_.received_at = std::chrono::steady_clock::now();
current_.valid = true;
}
void FRIClient::publishState()
{
std::unique_lock<std::mutex> lock(state_mutex_, std::try_to_lock);
if (lock.owns_lock()) {snapshot_ = current_;}
}
void FRIClient::validateCommanding() const
{
if (current_.command_mode != POSITION || current_.overlay != JOINT ||
(current_.control != POSITION_CONTROL_MODE && current_.control != JOINT_IMP_CONTROL_MODE &&
current_.control != CART_IMP_CONTROL_MODE))
{
throw std::runtime_error("Expected FRI POSITION command mode with JOINT overlay");
}
if (current_.quality < GOOD || current_.safety != NORMAL_OPERATION ||
current_.drives != ACTIVE)
{
throw std::runtime_error("FRI commanding unavailable: quality, safety or drives");
}
}
void FRIClient::monitor()
{
captureData(false);
LBRClient::monitor();
initialized_ = false;
publishState();
}
void FRIClient::waitForCommand()
{
captureData(true);
validateCommanding();
LBRClient::waitForCommand(); // SDK 1.16 mirrors IPO, not measured position.
sent_pos_ = target_pos_ = filtered_pos_ = current_.ipo_pos;
current_.command_pos = sent_pos_;
initialized_ = true;
last_command_at_ = current_.received_at;
publishState();
}
void FRIClient::command()
{
captureData(true);
validateCommanding();
if (!initialized_) {
sent_pos_ = target_pos_ = filtered_pos_ = current_.ipo_pos;
last_command_at_ = current_.received_at;
initialized_ = true;
}
{
std::lock_guard<std::mutex> lock(command_mutex_);
if (requested_ && requested_at_ > last_command_at_) {
target_pos_ = requested_pos_;
last_command_at_ = requested_at_;
}
requested_ = false;
}
// Local watchdog: a stalled ros2_control loop must not leave a moving target active.
if (current_.received_at - last_command_at_ > std::chrono::milliseconds(100)) {
throw std::runtime_error("FRI command watchdog expired");
}
const double dt = current_.sample_time;
const double alpha = position_smoothing_tau_ == 0.0 ? 1.0 :
dt / (position_smoothing_tau_ + dt);
for (size_t i = 0; i < N_JOINTS; ++i) {
if (!std::isfinite(target_pos_[i]) || target_pos_[i] < lower_[i] ||
target_pos_[i] > upper_[i])
{
throw std::runtime_error("FRI position command outside configured limits");
}
filtered_pos_[i] = alpha * target_pos_[i] + (1.0 - alpha) * filtered_pos_[i];
const double step = filtered_pos_[i] - sent_pos_[i];
sent_pos_[i] += std::clamp(step, -max_velocity_[i] * dt, max_velocity_[i] * dt);
}
robotCommand().setJointPosition(sent_pos_.data());
current_.command_pos = sent_pos_;
publishState();
}
void FRIClient::onStateChange(ESessionState oldState, ESessionState newState)
{
session_state_.store(newState);
if (oldState == COMMANDING_ACTIVE && newState != COMMANDING_ACTIVE) {
throw std::runtime_error("FRI left COMMANDING_ACTIVE; explicit recovery required");
}
}
void FRIClient::setTargetJointPositions(const Joints & q)
{
for (size_t i = 0; i < N_JOINTS; ++i) {
if (!std::isfinite(q[i]) || q[i] < lower_[i] || q[i] > upper_[i]) {
throw std::invalid_argument("Invalid FRI position command");
}
}
std::lock_guard<std::mutex> lock(command_mutex_);
if (requested_) {
throw std::logic_error("Previous FRI command has not been consumed");
}
requested_pos_ = q;
requested_at_ = std::chrono::steady_clock::now();
requested_ = true;
} }
IIWAStateSnapshot FRIClient::getStateSnapshot() const IIWAStateSnapshot FRIClient::getStateSnapshot() const
{ {
std::lock_guard<std::mutex> lock(data_mutex_); std::lock_guard<std::mutex> lock(state_mutex_);
return snapshot_; return snapshot_;
} }
bool FRIClient::tryGetStateSnapshot(IIWAStateSnapshot & snapshot) const
{
std::unique_lock<std::mutex> lock(state_mutex_, std::try_to_lock);
if (!lock.owns_lock()) {return false;}
snapshot = snapshot_;
return true;
}
bool FRIClient::isCommandingActive() const bool FRIClient::isCommandingActive() const
{ {
return session_state_.load(std::memory_order_relaxed) == KUKA::FRI::COMMANDING_ACTIVE; return getSessionState() == COMMANDING_ACTIVE;
} }
KUKA::FRI::ESessionState FRIClient::getSessionState() const ESessionState FRIClient::getSessionState() const
{ {
return session_state_.load(std::memory_order_relaxed); return session_state_.load();
} }
} // namespace iiwa_controller } // namespace iiwa_controller
+293 -164
View File
@@ -1,5 +1,10 @@
#include "iiwa_controller/IIWAHardwareInterface.hpp" #include "iiwa_controller/IIWAHardwareInterface.hpp"
#include <algorithm>
#include <arpa/inet.h>
#include <limits>
#include <stdexcept>
#include "friException.h"
#include <chrono> #include <chrono>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
@@ -10,6 +15,7 @@
#include "hardware_interface/types/hardware_interface_type_values.hpp" #include "hardware_interface/types/hardware_interface_type_values.hpp"
#include "pluginlib/class_list_macros.hpp" #include "pluginlib/class_list_macros.hpp"
#include "rclcpp/rclcpp.hpp" #include "rclcpp/rclcpp.hpp"
#include "realtime_tools/realtime_helpers.hpp"
PLUGINLIB_EXPORT_CLASS( PLUGINLIB_EXPORT_CLASS(
iiwa_controller::IIWAHardwareInterface, iiwa_controller::IIWAHardwareInterface,
@@ -21,6 +27,32 @@ namespace iiwa_controller
using CallbackReturn = using CallbackReturn =
rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn; rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn;
// The SDK keeps a 100 ms receive bound after the first packet. During startup
// only, allow 15 s for the operator to start Sunrise without retrying failed SDK steps.
class StartupConnection : public KUKA::FRI::UdpConnection
{
public:
explicit StartupConnection(const std::atomic<bool> & running)
: KUKA::FRI::UdpConnection(100), running_(running) {}
int receive(char * buffer, int size) override
{
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(15);
do {
const int received = KUKA::FRI::UdpConnection::receive(buffer, size);
if (received >= 0) {
started_ = true;
return received;
}
if (started_) {return received;}
} while (running_.load() && std::chrono::steady_clock::now() < deadline);
return -1;
}
private:
const std::atomic<bool> & running_;
bool started_{false};
};
static const char * LOG = "IIWAHardwareInterface"; static const char * LOG = "IIWAHardwareInterface";
static std::string getParam( static std::string getParam(
@@ -43,18 +75,44 @@ CallbackReturn IIWAHardwareInterface::on_init(
const auto & info = params.hardware_info; const auto & info = params.hardware_info;
robot_ip_ = getParam(info, "robot_ip", "192.170.10.2"); try {
fri_port_ = std::stoi(getParam(info, "fri_port", "30200")); robot_ip_ = getParam(info, "robot_ip", "192.170.10.2");
simulate_ = (getParam(info, "simulate", "false") == "true"); size_t used = 0;
joint_position_tau_ = std::stod(getParam(info, "joint_position_tau", "0.04")); const auto port = getParam(info, "fri_port", "30200");
joint_velocity_tau_ = std::stod(getParam(info, "joint_velocity_tau", "0.01")); fri_port_ = std::stoi(port, &used);
if (used != port.size() || fri_port_ < 1 || fri_port_ > 65535) {
RCLCPP_INFO(rclcpp::get_logger(LOG), throw std::invalid_argument("Invalid fri_port");
"on_init: ip=%s port=%d simulate=%s pos_tau=%.3f vel_tau=%.3f", }
robot_ip_.c_str(), fri_port_, const auto cycle = getParam(info, "fri_cycle_ms", "5");
simulate_ ? "true" : "false", fri_cycle_ms_ = std::stoi(cycle, &used);
joint_position_tau_, if (used != cycle.size() || fri_cycle_ms_ < 1 || fri_cycle_ms_ > 100) {
joint_velocity_tau_); throw std::invalid_argument("Invalid fri_cycle_ms (expected 1..100)");
}
const auto position_tau = getParam(info, "joint_position_tau", "0.04");
joint_position_tau_ = std::stod(position_tau, &used);
if (used != position_tau.size() || !std::isfinite(joint_position_tau_) ||
joint_position_tau_ < 0.0)
{
throw std::invalid_argument("Invalid joint_position_tau (expected >= 0)");
}
const auto rt_prio = getParam(info, "rt_prio", "80");
rt_prio_ = std::stoi(rt_prio, &used);
if (used != rt_prio.size() || rt_prio_ < 1 || rt_prio_ > 99) {
throw std::invalid_argument("Invalid rt_prio (expected 1..99)");
}
const auto simulation = getParam(info, "simulate", "false");
if (simulation != "true" && simulation != "false") {
throw std::invalid_argument("simulate must be true or false");
}
simulate_ = simulation == "true";
in_addr address{};
if (!simulate_ && inet_pton(AF_INET, robot_ip_.c_str(), &address) != 1) {
throw std::invalid_argument("robot_ip must be an IPv4 address");
}
} catch (const std::exception & e) {
RCLCPP_ERROR(rclcpp::get_logger(LOG), "Invalid hardware parameters: %s", e.what());
return CallbackReturn::ERROR;
}
if (info.joints.size() != N_JOINTS) { if (info.joints.size() != N_JOINTS) {
RCLCPP_FATAL(rclcpp::get_logger(LOG), RCLCPP_FATAL(rclcpp::get_logger(LOG),
@@ -62,9 +120,41 @@ CallbackReturn IIWAHardwareInterface::on_init(
return CallbackReturn::ERROR; return CallbackReturn::ERROR;
} }
for (size_t i = 0; i < N_JOINTS; ++i) {
const auto & joint = info.joints[i];
// SDK arrays are ordered A1..A7. Never silently permute axes.
if (joint.name != "joint" + std::to_string(i + 1) ||
joint.command_interfaces.size() != 1 || joint.command_interfaces[0].name != "position")
{
RCLCPP_ERROR(rclcpp::get_logger(LOG), "Expected joint1..joint7 with position commands");
return CallbackReturn::ERROR;
}
for (const auto * name : {"position", "velocity", "effort"}) {
if (std::none_of(joint.state_interfaces.begin(), joint.state_interfaces.end(),
[&](const auto & interface) {return interface.name == name;}))
{
return CallbackReturn::ERROR;
}
}
const auto limits = info.limits.find(joint.name);
if (limits == info.limits.end() || !limits->second.has_position_limits ||
!limits->second.has_velocity_limits)
{
RCLCPP_ERROR(rclcpp::get_logger(LOG), "Missing position/velocity limits for %s", joint.name.c_str());
return CallbackReturn::ERROR;
}
lower_[i] = limits->second.min_position;
upper_[i] = limits->second.max_position;
max_velocity_[i] = limits->second.max_velocity;
if (!std::isfinite(lower_[i]) || !std::isfinite(upper_[i]) || lower_[i] >= upper_[i] ||
!std::isfinite(max_velocity_[i]) || max_velocity_[i] <= 0.0)
{
return CallbackReturn::ERROR;
}
}
prev_pos_.fill(0.0); prev_pos_.fill(0.0);
velocity_.fill(0.0); velocity_.fill(0.0);
velocity_raw_.fill(0.0);
return CallbackReturn::SUCCESS; return CallbackReturn::SUCCESS;
} }
@@ -74,21 +164,23 @@ std::vector<hardware_interface::InterfaceDescription>
IIWAHardwareInterface::export_unlisted_state_interface_descriptions() IIWAHardwareInterface::export_unlisted_state_interface_descriptions()
{ {
std::vector<hardware_interface::InterfaceDescription> descs; std::vector<hardware_interface::InterfaceDescription> descs;
descs.reserve(N_JOINTS); descs.reserve(3 * N_JOINTS);
for (size_t i = 0; i < N_JOINTS; ++i) { for (size_t i = 0; i < N_JOINTS; ++i) {
hardware_interface::InterfaceInfo if_info; for (const auto * name : {"external_torque", "ipo_position", "fri_command_position"}) {
if_info.name = "external_torque"; hardware_interface::InterfaceInfo if_info;
if_info.data_type = "double"; if_info.name = name;
if_info.initial_value = "0.0"; if_info.data_type = "double";
descs.emplace_back(info_.joints[i].name, if_info); if_info.initial_value = "0.0";
descs.emplace_back(info_.joints[i].name, if_info);
}
} }
return descs; return descs;
} }
// ── on_configure ─────────────────────────────────────────────────────────────── // ── on_configure ───────────────────────────────────────────────────────────────
// Открывает UDP-сокет и создаёт FRI-объекты. Не запускает поток. // Создаёт FRI-объекты. Сокет и поток запускаются при активации.
CallbackReturn IIWAHardwareInterface::on_configure(const rclcpp_lifecycle::State &) CallbackReturn IIWAHardwareInterface::on_configure(const rclcpp_lifecycle::State &)
{ {
@@ -97,143 +189,171 @@ CallbackReturn IIWAHardwareInterface::on_configure(const rclcpp_lifecycle::State
return CallbackReturn::SUCCESS; return CallbackReturn::SUCCESS;
} }
releaseFRI();
fri_client_ = std::make_unique<FRIClient>(joint_position_tau_); fri_client_ = std::make_unique<FRIClient>(joint_position_tau_);
// 100 мс таймаут recvfrom — поток корректно завершится после disconnect(). fri_client_->setLimits(lower_, upper_, max_velocity_);
connection_ = std::make_unique<KUKA::FRI::UdpConnection>(100); fri_client_->setExpectedSampleTime(fri_cycle_ms_ * 0.001);
app_ = std::make_unique<KUKA::FRI::ClientApplication>(*connection_, *fri_client_); connection_ = std::make_unique<StartupConnection>(fri_running_);
app_ = std::make_unique<KUKA::FRI::ClientApplication>(*connection_, *fri_client_);
return CallbackReturn::SUCCESS;
}
if (!app_->connect(fri_port_, robot_ip_.c_str())) { CallbackReturn IIWAHardwareInterface::on_activate(const rclcpp_lifecycle::State & state)
RCLCPP_FATAL(rclcpp::get_logger(LOG), {
"Не удалось открыть UDP-сокет на порту %d (робот: %s)", fri_port_, robot_ip_.c_str()); stopFRI();
velocity_initialized_ = false;
last_snapshot_ = {};
try {
for (size_t i = 0; i < N_JOINTS; ++i) {
const auto & name = info_.joints[i].name;
h_pos_[i] = get_state_interface_handle(name + "/position");
h_vel_[i] = get_state_interface_handle(name + "/velocity");
h_eff_[i] = get_state_interface_handle(name + "/effort");
h_ext_[i] = get_state_interface_handle(name + "/external_torque");
h_ipo_[i] = get_state_interface_handle(name + "/ipo_position");
h_fri_cmd_[i] = get_state_interface_handle(name + "/fri_command_position");
h_cmd_pos_[i] = get_command_interface_handle(name + "/position");
}
if (!simulate_) {
// Recreate SDK sequence counters and buffers for every new session.
if (on_configure(state) != CallbackReturn::SUCCESS ||
!app_->connect(fri_port_, robot_ip_.c_str()))
{
releaseFRI();
return CallbackReturn::ERROR;
}
fri_fault_.store(false);
fri_running_.store(true);
cycle_gate_.reset();
fri_thread_ = std::thread(&IIWAHardwareInterface::friThreadFunc, this);
const bool ready = cycle_gate_.waitReady(std::chrono::seconds(15));
last_snapshot_ = fri_client_->getStateSnapshot();
if (!ready || fri_fault_.load() || !last_snapshot_.valid ||
last_snapshot_.quality < KUKA::FRI::GOOD ||
last_snapshot_.session < KUKA::FRI::MONITORING_READY ||
std::chrono::steady_clock::now() - last_snapshot_.received_at > std::chrono::milliseconds(100))
{
stopFRI();
RCLCPP_ERROR(rclcpp::get_logger(LOG), "FRI activation failed: no fresh ready session");
return CallbackReturn::ERROR;
}
}
for (size_t i = 0; i < N_JOINTS; ++i) {
double position = 0.0;
if (simulate_) {
get_state(h_pos_[i], position, true);
if (!std::isfinite(position)) {position = 0.0;}
} else {
position = last_snapshot_.measured_pos[i];
}
set_state(h_pos_[i], position, true);
set_state(h_vel_[i], 0.0, true);
set_state(h_eff_[i], simulate_ ? 0.0 : last_snapshot_.measured_tau[i], true);
set_state(h_ext_[i], simulate_ ? 0.0 : last_snapshot_.external_tau[i], true);
set_state(h_ipo_[i], position, true);
set_state(h_fri_cmd_[i], position, true);
set_command(h_cmd_pos_[i], position, true);
}
} catch (const std::exception & e) {
stopFRI();
RCLCPP_ERROR(rclcpp::get_logger(LOG), "Activation failed: %s", e.what());
return CallbackReturn::ERROR; return CallbackReturn::ERROR;
} }
active_ = true;
RCLCPP_INFO(rclcpp::get_logger(LOG),
"UDP-порт %d открыт. Запустите ServerFriRos2 на роботе (%s)...",
fri_port_, robot_ip_.c_str());
return CallbackReturn::SUCCESS; return CallbackReturn::SUCCESS;
} }
// ── on_activate ──────────────────────────────────────────────────────────────── void IIWAHardwareInterface::stopFRI()
// Получает хэндлы интерфейсов, запускает FRI-поток и ждёт установки сессии.
CallbackReturn IIWAHardwareInterface::on_activate(const rclcpp_lifecycle::State &)
{ {
RCLCPP_INFO(rclcpp::get_logger(LOG), "Активация..."); active_ = false;
fri_running_.store(false);
for (size_t i = 0; i < N_JOINTS; ++i) { cycle_gate_.stop();
const std::string & jn = info_.joints[i].name; // Receive has a finite timeout. Never close a socket while step() uses it.
if (fri_thread_.joinable()) {fri_thread_.join();}
h_pos_[i] = get_state_interface_handle(jn + "/" + hardware_interface::HW_IF_POSITION); if (app_) {app_->disconnect();}
h_vel_[i] = get_state_interface_handle(jn + "/" + hardware_interface::HW_IF_VELOCITY);
h_eff_[i] = get_state_interface_handle(jn + "/" + hardware_interface::HW_IF_EFFORT);
h_ext_[i] = get_state_interface_handle(jn + "/external_torque");
h_cmd_pos_[i] = get_command_interface_handle(jn + "/" + hardware_interface::HW_IF_POSITION);
if (!h_pos_[i] || !h_vel_[i] || !h_eff_[i] || !h_ext_[i] || !h_cmd_pos_[i])
{
RCLCPP_FATAL(rclcpp::get_logger(LOG),
"Не удалось получить хэндл интерфейса для сустава '%s'. "
"Проверьте объявление <state_interface>/<command_interface> в URDF.",
jn.c_str());
return CallbackReturn::ERROR;
}
}
if (simulate_) {
return CallbackReturn::SUCCESS;
}
fri_running_.store(true, std::memory_order_relaxed);
fri_thread_ = std::thread(&IIWAHardwareInterface::friThreadFunc, this);
constexpr int kTimeoutMs = 15000;
constexpr int kPollMs = 200;
for (int elapsed = 0;
fri_client_->getSessionState() == KUKA::FRI::IDLE && elapsed < kTimeoutMs;
elapsed += kPollMs)
{
RCLCPP_INFO_THROTTLE(rclcpp::get_logger(LOG), throttle_clock_, 2000,
"Ожидание FRI-сессии... (%d мс)", elapsed);
std::this_thread::sleep_for(std::chrono::milliseconds(kPollMs));
}
if (fri_client_->getSessionState() == KUKA::FRI::IDLE) {
RCLCPP_ERROR(rclcpp::get_logger(LOG),
"FRI не подключился за %d с. Проверьте ServerFriRos2 на %s",
kTimeoutMs / 1000, robot_ip_.c_str());
} else {
RCLCPP_INFO(rclcpp::get_logger(LOG), "FRI сессия установлена!");
const auto snap = fri_client_->getStateSnapshot();
prev_pos_ = snap.measured_pos;
last_ts_sec_ = snap.time_stamp_sec;
last_ts_nsec_ = snap.time_stamp_nano_sec;
velocity_.fill(0.0);
velocity_initialized_ = true;
}
previous_session_state_ = fri_client_->getSessionState();
return CallbackReturn::SUCCESS;
} }
// ── on_deactivate ────────────────────────────────────────────────────────────── void IIWAHardwareInterface::releaseFRI()
// Останавливает FRI-поток. FRI-объекты остаются — их очищает on_cleanup(). {
stopFRI();
// ClientApplication::~ClientApplication calls the connection by reference.
app_.reset();
connection_.reset();
fri_client_.reset();
}
IIWAHardwareInterface::~IIWAHardwareInterface() {releaseFRI();}
CallbackReturn IIWAHardwareInterface::on_deactivate(const rclcpp_lifecycle::State &) CallbackReturn IIWAHardwareInterface::on_deactivate(const rclcpp_lifecycle::State &)
{ {
RCLCPP_INFO(rclcpp::get_logger(LOG), "Деактивация..."); stopFRI();
if (!simulate_ && fri_running_.load()) {
fri_running_.store(false, std::memory_order_relaxed);
// Сначала закрываем сокет — это разблокирует recvfrom() в FRI-потоке.
// Только потом join(), иначе он зависнет навсегда.
if (app_) { app_->disconnect(); }
if (fri_thread_.joinable()) { fri_thread_.join(); }
RCLCPP_INFO(rclcpp::get_logger(LOG), "FRI поток остановлен");
}
for (size_t i = 0; i < N_JOINTS; ++i) {
h_pos_[i] = h_vel_[i] = h_eff_[i] = h_ext_[i] = nullptr;
h_cmd_pos_[i] = nullptr;
}
velocity_initialized_ = false; velocity_initialized_ = false;
return CallbackReturn::SUCCESS; return CallbackReturn::SUCCESS;
} }
// ── on_cleanup ─────────────────────────────────────────────────────────────────
// Освобождает FRI-объекты. Вызывается после on_deactivate().
CallbackReturn IIWAHardwareInterface::on_cleanup(const rclcpp_lifecycle::State &) CallbackReturn IIWAHardwareInterface::on_cleanup(const rclcpp_lifecycle::State &)
{ {
fri_client_.reset(); releaseFRI();
connection_.reset();
app_.reset();
return CallbackReturn::SUCCESS; return CallbackReturn::SUCCESS;
} }
// ── friThreadFunc ────────────────────────────────────────────────────────────── CallbackReturn IIWAHardwareInterface::on_shutdown(const rclcpp_lifecycle::State & state)
{
return on_cleanup(state);
}
CallbackReturn IIWAHardwareInterface::on_error(const rclcpp_lifecycle::State & state)
{
return on_cleanup(state);
}
void IIWAHardwareInterface::friThreadFunc() void IIWAHardwareInterface::friThreadFunc()
{ {
RCLCPP_INFO(rclcpp::get_logger(LOG), "FRI поток запущен"); try {
if (!realtime_tools::configure_sched_fifo(rt_prio_)) {
while (fri_running_.load(std::memory_order_relaxed)) { RCLCPP_WARN(rclcpp::get_logger(LOG),
if (!app_->step()) { "FRI worker could not enable FIFO priority %d; configure realtime permissions for this user",
RCLCPP_WARN_THROTTLE(rclcpp::get_logger(LOG), throttle_clock_, 2000, rt_prio_);
"FRI: step() вернул false, возможно потеряли соединение"); } else {
RCLCPP_INFO(rclcpp::get_logger(LOG), "FRI worker uses FIFO priority %d", rt_prio_);
} }
bool synchronized = false;
const auto startup_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(15);
while (fri_running_.load()) {
if (!app_->step()) {
throw std::runtime_error("FRI step failed");
}
if (fri_client_->getSessionState() == KUKA::FRI::IDLE) {
throw std::runtime_error("FRI session ended");
}
const auto snapshot = fri_client_->getStateSnapshot();
synchronized = synchronized || (snapshot.quality >= KUKA::FRI::GOOD &&
snapshot.session >= KUKA::FRI::MONITORING_READY);
if (synchronized) {
if (!cycle_gate_.publishAndWait()) {
if (!fri_running_.load()) {break;}
throw std::runtime_error("FRI cycle was not completed by ros2_control within 100 ms");
}
} else if (std::chrono::steady_clock::now() >= startup_deadline) {
throw std::runtime_error("FRI did not reach MONITORING_READY within 15 s");
}
}
} catch (const KUKA::FRI::FRIException & e) {
fri_fault_.store(true);
RCLCPP_ERROR(rclcpp::get_logger(LOG), "FRI SDK: %s", e.getErrorMessage());
} catch (const std::exception & e) {
fri_fault_.store(true);
RCLCPP_ERROR(rclcpp::get_logger(LOG), "FRI: %s", e.what());
} catch (...) {
fri_fault_.store(true);
RCLCPP_ERROR(rclcpp::get_logger(LOG), "Unknown FRI failure");
} }
fri_running_.store(false);
RCLCPP_INFO(rclcpp::get_logger(LOG), "FRI поток завершён"); cycle_gate_.stop();
} }
// ── compute_velocity_ ────────────────────────────────────────────────────────── // ── compute_velocity_ ──────────────────────────────────────────────────────────
// Конечные разности + EMA-фильтр. // Velocity is the finite difference of measured positions at robot timestamps.
// int64-вычитание timestamp'ов предотвращает потерю точности при больших Unix-значениях.
// EMA-фильтр (joint_velocity_tau) убирает одиночные выбросы, которые видит JTC как
// скачки состояния и компенсирует агрессивными командами → хруст двигателей.
void IIWAHardwareInterface::compute_velocity_(const IIWAStateSnapshot & snap) void IIWAHardwareInterface::compute_velocity_(const IIWAStateSnapshot & snap)
{ {
@@ -242,7 +362,6 @@ void IIWAHardwareInterface::compute_velocity_(const IIWAStateSnapshot & snap)
last_ts_sec_ = snap.time_stamp_sec; last_ts_sec_ = snap.time_stamp_sec;
last_ts_nsec_ = snap.time_stamp_nano_sec; last_ts_nsec_ = snap.time_stamp_nano_sec;
velocity_.fill(0.0); velocity_.fill(0.0);
velocity_raw_.fill(0.0);
velocity_initialized_ = true; velocity_initialized_ = true;
return; return;
} }
@@ -258,23 +377,10 @@ void IIWAHardwareInterface::compute_velocity_(const IIWAStateSnapshot & snap)
(static_cast<double>(snap.time_stamp_nano_sec) - (static_cast<double>(snap.time_stamp_nano_sec) -
static_cast<double>(last_ts_nsec_)) * 1e-9; static_cast<double>(last_ts_nsec_)) * 1e-9;
static constexpr std::array<double, N_JOINTS> kMaxVel =
{1.71, 1.71, 1.75, 2.27, 2.44, 3.14, 3.14};
static constexpr double kVelDeadband = 1e-4;
if (dt > 0.0) { if (dt > 0.0) {
// EMA alpha для фильтра скорости: tau=0 → alpha=1 (без фильтра)
const double vel_alpha = (joint_velocity_tau_ > 0.0)
? dt / (joint_velocity_tau_ + dt)
: 1.0;
for (size_t i = 0; i < N_JOINTS; ++i) { for (size_t i = 0; i < N_JOINTS; ++i) {
const double raw = (snap.measured_pos[i] - prev_pos_[i]) / dt; velocity_[i] = (snap.measured_pos[i] - prev_pos_[i]) / dt;
const double clamped = std::clamp(raw, -kMaxVel[i], kMaxVel[i]);
velocity_raw_[i] = (std::abs(clamped) < kVelDeadband) ? 0.0 : clamped;
// EMA: velocity_[i] = alpha * raw + (1 - alpha) * prev_filtered
velocity_[i] = vel_alpha * velocity_raw_[i] + (1.0 - vel_alpha) * velocity_[i];
} }
} }
@@ -288,31 +394,40 @@ void IIWAHardwareInterface::compute_velocity_(const IIWAStateSnapshot & snap)
hardware_interface::return_type IIWAHardwareInterface::read( hardware_interface::return_type IIWAHardwareInterface::read(
const rclcpp::Time &, const rclcpp::Duration &) const rclcpp::Time &, const rclcpp::Duration &)
{ {
if (!active_) {return hardware_interface::return_type::OK;}
if (simulate_) { if (simulate_) {
for (size_t i = 0; i < N_JOINTS; ++i) { for (size_t i = 0; i < N_JOINTS; ++i) {
double pos = 0.0; double pos = 0.0;
get_command(h_cmd_pos_[i], pos, false); if (!get_command(h_cmd_pos_[i], pos, false) || !std::isfinite(pos)) {
return hardware_interface::return_type::ERROR;
}
set_state(h_pos_[i], pos, false); set_state(h_pos_[i], pos, false);
set_state(h_vel_[i], 0.0, false); set_state(h_vel_[i], 0.0, false);
set_state(h_eff_[i], 0.0, false); set_state(h_eff_[i], 0.0, false);
set_state(h_ext_[i], 0.0, false); set_state(h_ext_[i], 0.0, false);
set_state(h_ipo_[i], pos, false);
set_state(h_fri_cmd_[i], pos, false);
} }
return hardware_interface::return_type::OK; return hardware_interface::return_type::OK;
} }
const auto snap = fri_client_->getStateSnapshot(); if (!fri_client_ || fri_fault_.load() || !fri_running_.load()) {
return hardware_interface::return_type::ERROR;
// Обнаружение потери управления: неожиданный выход из COMMANDING_ACTIVE. }
const auto current_state = fri_client_->getSessionState(); if (!cycle_gate_.acquire()) {
if (previous_session_state_ == KUKA::FRI::COMMANDING_ACTIVE && fri_fault_.store(true);
current_state != KUKA::FRI::COMMANDING_ACTIVE) fri_running_.store(false);
cycle_gate_.stop();
return hardware_interface::return_type::ERROR;
}
last_snapshot_ = fri_client_->getStateSnapshot();
const auto & snap = last_snapshot_;
if (!snap.valid || std::chrono::steady_clock::now() - snap.received_at >
std::chrono::milliseconds(100))
{ {
RCLCPP_ERROR(rclcpp::get_logger(LOG), fri_running_.store(false);
"Робот вышел из COMMANDING_ACTIVE! Деактивируйте и повторно активируйте контроллер.");
return hardware_interface::return_type::ERROR; return hardware_interface::return_type::ERROR;
} }
previous_session_state_ = current_state;
compute_velocity_(snap); compute_velocity_(snap);
for (size_t i = 0; i < N_JOINTS; ++i) { for (size_t i = 0; i < N_JOINTS; ++i) {
@@ -320,6 +435,8 @@ hardware_interface::return_type IIWAHardwareInterface::read(
set_state(h_vel_[i], velocity_[i], false); set_state(h_vel_[i], velocity_[i], false);
set_state(h_eff_[i], snap.measured_tau[i], false); set_state(h_eff_[i], snap.measured_tau[i], false);
set_state(h_ext_[i], snap.external_tau[i], false); set_state(h_ext_[i], snap.external_tau[i], false);
set_state(h_ipo_[i], snap.ipo_valid ? snap.ipo_pos[i] : snap.measured_pos[i], false);
set_state(h_fri_cmd_[i], snap.command_pos[i], false);
} }
return hardware_interface::return_type::OK; return hardware_interface::return_type::OK;
@@ -330,22 +447,34 @@ hardware_interface::return_type IIWAHardwareInterface::read(
hardware_interface::return_type IIWAHardwareInterface::write( hardware_interface::return_type IIWAHardwareInterface::write(
const rclcpp::Time &, const rclcpp::Duration &) const rclcpp::Time &, const rclcpp::Duration &)
{ {
if (simulate_) { if (!active_ || simulate_) {return hardware_interface::return_type::OK;}
return hardware_interface::return_type::OK; if (!fri_client_ || fri_fault_.load() || !fri_running_.load()) {
return hardware_interface::return_type::ERROR;
} }
if (!fri_client_->isCommandingActive()) {
if (fri_client_->getSessionState() != KUKA::FRI::COMMANDING_ACTIVE) { return cycle_gate_.complete() ? hardware_interface::return_type::OK :
return hardware_interface::return_type::OK; hardware_interface::return_type::ERROR;
} }
std::array<double, N_JOINTS> pos_cmd{}; std::array<double, N_JOINTS> pos_cmd{};
for (size_t i = 0; i < N_JOINTS; ++i) { for (size_t i = 0; i < N_JOINTS; ++i) {
get_command(h_cmd_pos_[i], pos_cmd[i], false); if (!get_command(h_cmd_pos_[i], pos_cmd[i], false)) {
cycle_gate_.stop();
return hardware_interface::return_type::ERROR;
}
if (!std::isfinite(pos_cmd[i]) || pos_cmd[i] < lower_[i] || pos_cmd[i] > upper_[i]) {
fri_fault_.store(true);
fri_running_.store(false);
return hardware_interface::return_type::ERROR;
}
} }
try {
fri_client_->setTargetJointPositions(pos_cmd); fri_client_->setTargetJointPositions(pos_cmd);
} catch (const std::exception &) {
return hardware_interface::return_type::OK; cycle_gate_.stop();
return hardware_interface::return_type::ERROR;
}
return cycle_gate_.complete() ? hardware_interface::return_type::OK :
hardware_interface::return_type::ERROR;
} }
} // namespace iiwa_controller } // namespace iiwa_controller
@@ -0,0 +1,112 @@
// Offline protocol peer, not a dynamics or Sunrise safety simulator.
// Both endpoints are hardcoded loopback addresses. Never accepts a robot IP.
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <array>
#include <chrono>
#include <cmath>
#include <fstream>
#include <iostream>
#include <thread>
#include "FRIMessages.pb.h"
#include "pb_encode.h"
#include "pb_decode.h"
#include "pb_frimessages_callbacks.h"
int main(int argc, char ** argv)
{
if (argc != 2) {return 2;}
std::ofstream csv(argv[1]);
csv << "sequence,session,receive_seconds,position,delta\n";
csv.flush();
int fd = socket(AF_INET, SOCK_DGRAM, 0);
sockaddr_in local{}, remote{};
local.sin_family = remote.sin_family = AF_INET;
local.sin_port = remote.sin_port = htons(30283);
inet_pton(AF_INET, "127.0.0.2", &local.sin_addr);
inet_pton(AF_INET, "127.0.0.1", &remote.sin_addr);
if (fd < 0 || bind(fd, reinterpret_cast<sockaddr *>(&local), sizeof(local))) {
perror("fake FRI loopback bind"); return 2;
}
timeval timeout{0, 20000};
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
std::array<double, 7> q{}, zero{}, received{};
std::array<int64_t, 7> drives{};
drives.fill(DriveState_ACTIVE);
tRepeatedDoubleArguments positions{7, 7, q.data()}, zeros{7, 7, zero.data()};
tRepeatedIntArguments drive_values{7, 7, drives.data()};
FRIMonitoringMessage m{};
m.header.messageIdentifier = 0x245142; // Bundled SDK LBRMONITORMESSAGEID.
m.has_robotInfo = m.has_monitorData = m.has_connectionInfo = true;
m.robotInfo.has_numberOfJoints = m.robotInfo.has_safetyState = true;
m.robotInfo.has_operationMode = m.robotInfo.has_controlMode = true;
m.robotInfo.numberOfJoints = 7;
m.robotInfo.safetyState = SafetyState_NORMAL_OPERATION;
m.robotInfo.operationMode = OperationMode_AUTOMATIC_MODE;
m.robotInfo.controlMode = ControlMode_POSITION_CONTROLMODE;
m.robotInfo.driveState.funcs.encode = encode_repeatedInt;
m.robotInfo.driveState.arg = &drive_values;
m.connectionInfo.quality = FRIConnectionQuality_GOOD;
m.connectionInfo.has_sendPeriod = m.connectionInfo.has_receiveMultiplier = true;
m.connectionInfo.sendPeriod = 10;
m.connectionInfo.receiveMultiplier = 1;
auto & md = m.monitorData;
md.has_timestamp = md.has_measuredJointPosition = md.has_commandedJointPosition = true;
md.has_measuredTorque = md.has_commandedTorque = md.has_externalTorque = true;
for (auto * values : {&md.measuredJointPosition, &md.commandedJointPosition, &m.ipoData.jointPosition}) {
values->value.funcs.encode = encode_repeatedDouble;
values->value.arg = &positions;
}
for (auto * values : {&md.measuredTorque, &md.commandedTorque, &md.externalTorque}) {
values->value.funcs.encode = encode_repeatedDouble;
values->value.arg = &zeros;
}
m.ipoData.has_jointPosition = m.ipoData.has_clientCommandMode = true;
m.ipoData.has_overlayType = m.ipoData.has_trackingPerformance = true;
m.ipoData.clientCommandMode = ClientCommandMode_POSITION;
m.ipoData.overlayType = OverlayType_JOINT;
m.ipoData.trackingPerformance = 1.0;
using Clock = std::chrono::steady_clock;
const auto start = Clock::now();
auto next = start;
unsigned replies = 0;
for (unsigned seq = 0; Clock::now() - start < std::chrono::seconds(45); ++seq) {
m.header.sequenceCounter = seq;
m.connectionInfo.sessionState = replies < 20 ? FRISessionState_MONITORING_READY :
replies < 40 ? FRISessionState_COMMANDING_WAIT : FRISessionState_COMMANDING_ACTIVE;
m.has_ipoData = replies >= 20;
md.timestamp.sec = 1 + seq / 100;
md.timestamp.nanosec = (seq % 100) * 10000000;
uint8_t buffer[2048];
auto output = pb_ostream_from_buffer(buffer, sizeof(buffer));
if (!pb_encode(&output, FRIMonitoringMessage_fields, &m)) {return 3;}
sendto(fd, buffer, output.bytes_written, 0, reinterpret_cast<sockaddr *>(&remote), sizeof(remote));
auto count = recv(fd, buffer, sizeof(buffer), 0);
if (count > 0) {
FRICommandMessage command{};
tRepeatedDoubleArguments target{0, 7, received.data()};
command.commandData.jointPosition.value.funcs.decode = decode_repeatedDouble;
command.commandData.jointPosition.value.arg = &target;
auto input = pb_istream_from_buffer(buffer, count);
if (!pb_decode(&input, FRICommandMessage_fields, &command) ||
!command.has_commandData || !command.commandData.has_jointPosition) {return 4;}
for (double value : received) {if (!std::isfinite(value)) {return 5;}}
const double delta = received[0] - q[0];
csv << seq << ',' << m.connectionInfo.sessionState << ',' <<
std::chrono::duration<double>(Clock::now() - start).count() << ',' << received[0] << ',' << delta << '\n';
csv.flush();
q = received;
m.header.reflectedSequenceCounter = command.header.sequenceCounter;
++replies;
} else if (replies) {
std::cerr << "Reply timeout after " << replies << " frames\n";
close(fd); return 6;
}
// Before connection, avoid catch-up bursts after receive timeouts.
next = replies ? next + std::chrono::milliseconds(10) : Clock::now();
std::this_thread::sleep_until(next);
}
close(fd);
return 0;
}
@@ -0,0 +1,20 @@
// Test-only interposer: allow the unmodified SDK and fake robot to share one host.
// The SDK otherwise binds all local addresses at the robot's source port.
#include <arpa/inet.h>
#include <dlfcn.h>
#include <sys/socket.h>
extern "C" int bind(int fd, const sockaddr * address, socklen_t length)
{
using Bind = int (*)(int, const sockaddr *, socklen_t);
static auto real_bind = reinterpret_cast<Bind>(dlsym(RTLD_NEXT, "bind"));
if (address->sa_family == AF_INET && length == sizeof(sockaddr_in)) {
auto local = *reinterpret_cast<const sockaddr_in *>(address);
// Dedicated smoke-test port only; never alter DDS or other application binds.
if (local.sin_port == htons(30283) && local.sin_addr.s_addr == htonl(INADDR_ANY)) {
inet_pton(AF_INET, "127.0.0.1", &local.sin_addr);
return real_bind(fd, reinterpret_cast<const sockaddr *>(&local), sizeof(local));
}
}
return real_bind(fd, address, length);
}
+83
View File
@@ -0,0 +1,83 @@
"""Manual bounded real-SDK/JTC integration check over loopback only.
Run after building/sourcing iiwa_controller; never connects physical hardware.
"""
import csv
import sys
import time
import os
import signal
import subprocess
import tempfile
from pathlib import Path
import xacro
import yaml
root = Path(__file__).resolve().parents[3]
work = Path(tempfile.mkdtemp(prefix='iiwa-fri-smoke-'))
env = dict(os.environ, ROS_DOMAIN_ID='83', ROS_LOCALHOST_ONLY='1', ROS_LOG_DIR=str(work / 'logs'), ROS_HOME=str(work / 'ros'))
env['CYCLONEDDS_URI'] = '<CycloneDDS><Domain><General><Interfaces><NetworkInterface name="lo"/></Interfaces><AllowMulticast>false</AllowMulticast></General><Discovery><Peers><Peer Address="127.0.0.1"/></Peers></Discovery></Domain></CycloneDDS>'
xml = xacro.process_file(str(root / 'src/iiwa_description/urdf/iiwa7.urdf.xacro'), mappings={'robot_ip': '127.0.0.2', 'fri_port': '30283', 'fri_cycle_ms': '10'}).toxml()
assert '<param name="simulate">false</param>' in xml
assert '<param name="robot_ip">127.0.0.2</param>' in xml
assert '<param name="fri_port">30283</param>' in xml
build = Path(sys.argv[1]) if len(sys.argv) > 1 else root / 'build/iiwa_controller'
server = build / 'fake_fri_server'
shim = build / 'libfri_loopback_bind.so'
assert server.is_file() and shim.is_file(), 'Build the test harness first'
trace = work / 'commands.csv'
params = work / 'robot.yaml'
params.write_text(yaml.safe_dump({'/**': {'ros__parameters': {'robot_description': xml}}}))
processes = []
handles = []
try:
server_log = (work / 'fake_fri_server.log').open('w')
handles.append(server_log)
processes.append(subprocess.Popen([str(server), str(trace)], stdout=server_log, stderr=subprocess.STDOUT, start_new_session=True))
time.sleep(0.1)
assert processes[0].poll() is None, 'Fake peer failed to bind'
for package, executable, extra in [
('robot_state_publisher', 'robot_state_publisher', []),
('controller_manager', 'ros2_control_node', ['--params-file', str(root / 'src/iiwa_config/config/moveit/iiwa_controller.yaml'), '-p', 'update_rate:=100', '-p', 'hardware_synchronization.expect_blocking_read_write:=true']),
]:
log = (work / (executable + '.log')).open('w')
handles.append(log)
processes.append(subprocess.Popen(['ros2', 'run', package, executable, '--ros-args', '--params-file', str(params)] + extra,
env=dict(env, **({'LD_PRELOAD': str(shim)} if executable == 'ros2_control_node' else {})), stdout=log, stderr=subprocess.STDOUT, start_new_session=True))
for name in ['joint_state_broadcaster', 'iiwa_arm_controller']:
result = subprocess.run(['ros2', 'run', 'controller_manager', 'spawner', name, '--controller-manager-timeout', '15'],
env=env, text=True, capture_output=True, timeout=25)
print(result.stdout, result.stderr, flush=True)
assert result.returncode == 0, name
result = subprocess.run(['ros2', 'control', 'list_controllers'], env=env, text=True, capture_output=True, timeout=10)
print(result.stdout, result.stderr, flush=True)
assert result.returncode == 0 and 'iiwa_arm_controller' in result.stdout and 'active' in result.stdout
initial = yaml.safe_load((root / 'src/iiwa_config/config/moveit/initial_positions.yaml').read_text())['initial_positions']
positions = [float(initial[f'joint{i}']) for i in range(1, 8)]
positions[0] += 0.02
goal = {'trajectory': {'joint_names': [f'joint{i}' for i in range(1, 8)],
'points': [{'positions': positions, 'time_from_start': {'sec': 2, 'nanosec': 0}}]}}
result = subprocess.run(['ros2', 'action', 'send_goal', '/iiwa_arm_controller/follow_joint_trajectory',
'control_msgs/action/FollowJointTrajectory', yaml.safe_dump(goal)],
env=env, text=True, capture_output=True, timeout=15)
print(result.stdout, result.stderr, flush=True)
assert result.returncode == 0 and 'SUCCEEDED' in result.stdout
rows = list(csv.DictReader(trace.open()))
assert len(rows) > 100 and processes[0].poll() is None, 'FRI stream stopped'
active = [r for r in rows if int(r['session']) == 4]
assert active and abs(float(active[-1]['position']) - 0.02) < 0.001
deltas = [abs(float(r['delta'])) for r in active]
intervals = [float(b['receive_seconds']) - float(a['receive_seconds']) for a, b in zip(active, active[1:])]
assert max(deltas) <= 1.71 * 0.01 + 1e-6, 'Command exceeds per-frame velocity limit'
print(f'SMOKE PASS: real SDK loopback + actual CM/JTC action succeeded; {len(active)} active frames, max delta={max(deltas):.8f} rad; receive interval min/max={min(intervals):.6f}/{max(intervals):.6f} s.', flush=True)
finally:
for process in processes:
if process.poll() is None:
os.killpg(process.pid, signal.SIGINT)
for process in processes:
try: process.wait(timeout=5)
except subprocess.TimeoutExpired: os.killpg(process.pid, signal.SIGKILL)
for handle in handles: handle.close()
for log in work.glob('*.log'):
print('\n' + log.name + '\n' + log.read_text()[-4500:])
print('Logs:', work)
+59
View File
@@ -0,0 +1,59 @@
"""Manual local JTC integration check; never opens a FRI connection."""
import os
import signal
import subprocess
import tempfile
from pathlib import Path
import xacro
import yaml
root = Path(__file__).resolve().parents[3]
work = Path(tempfile.mkdtemp(prefix='iiwa-controller-smoke-'))
env = dict(os.environ, ROS_DOMAIN_ID='83', ROS_LOCALHOST_ONLY='1', ROS_LOG_DIR=str(work / 'logs'), ROS_HOME=str(work / 'ros'))
env['CYCLONEDDS_URI'] = '<CycloneDDS><Domain><General><Interfaces><NetworkInterface name="lo"/></Interfaces><AllowMulticast>false</AllowMulticast></General><Discovery><Peers><Peer Address="127.0.0.1"/></Peers></Discovery></Domain></CycloneDDS>'
xml = xacro.process_file(str(root / 'src/iiwa_description/urdf/iiwa7.urdf.xacro')).toxml()
assert '<param name="simulate">false</param>' in xml
# Exercise our plugin's explicit local simulation path, with no FRI socket.
xml = xml.replace('<param name="simulate">false</param>', '<param name="simulate">true</param>')
params = work / 'robot.yaml'
params.write_text(yaml.safe_dump({'/**': {'ros__parameters': {'robot_description': xml}}}))
processes = []
handles = []
try:
for package, executable, extra in [
('robot_state_publisher', 'robot_state_publisher', []),
('controller_manager', 'ros2_control_node', ['--params-file', str(root / 'src/iiwa_config/config/moveit/iiwa_controller.yaml')]),
]:
log = (work / (executable + '.log')).open('w')
handles.append(log)
processes.append(subprocess.Popen(['ros2', 'run', package, executable, '--ros-args', '--params-file', str(params)] + extra,
env=env, stdout=log, stderr=subprocess.STDOUT, start_new_session=True))
for name in ['joint_state_broadcaster', 'iiwa_arm_controller']:
result = subprocess.run(['ros2', 'run', 'controller_manager', 'spawner', name, '--controller-manager-timeout', '15'],
env=env, text=True, capture_output=True, timeout=25)
print(result.stdout, result.stderr, flush=True)
assert result.returncode == 0, name
result = subprocess.run(['ros2', 'control', 'list_controllers'], env=env, text=True, capture_output=True, timeout=10)
print(result.stdout, result.stderr, flush=True)
assert result.returncode == 0 and 'iiwa_arm_controller' in result.stdout and 'active' in result.stdout
initial = yaml.safe_load((root / 'src/iiwa_config/config/moveit/initial_positions.yaml').read_text())['initial_positions']
positions = [float(initial[f'joint{i}']) for i in range(1, 8)]
positions[0] += 0.02
goal = {'trajectory': {'joint_names': [f'joint{i}' for i in range(1, 8)],
'points': [{'positions': positions, 'time_from_start': {'sec': 2, 'nanosec': 0}}]}}
result = subprocess.run(['ros2', 'action', 'send_goal', '/iiwa_arm_controller/follow_joint_trajectory',
'control_msgs/action/FollowJointTrajectory', yaml.safe_dump(goal)],
env=env, text=True, capture_output=True, timeout=15)
print(result.stdout, result.stderr, flush=True)
assert result.returncode == 0 and 'SUCCEEDED' in result.stdout
print('SMOKE PASS: actual URDF, iiwa hardware plugin, broadcaster and JTC active; no FRI connection.', flush=True)
finally:
for process in processes:
os.killpg(process.pid, signal.SIGINT)
for process in processes:
try: process.wait(timeout=5)
except subprocess.TimeoutExpired: os.killpg(process.pid, signal.SIGKILL)
for handle in handles: handle.close()
for log in work.glob('*.log'):
print('\n' + log.name + '\n' + log.read_text()[-4500:])
print('Logs:', work)
@@ -0,0 +1,279 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <limits>
#include <memory>
#include <stdexcept>
#include <thread>
#include "iiwa_controller/FRIClient.h"
#include "iiwa_controller/FRICycleGate.hpp"
#include <atomic>
#include <vector>
#include "friClientData.h"
#include "pb_frimessages_callbacks.h"
using namespace KUKA::FRI;
class TestApplication : public ClientApplication
{
public:
using ClientApplication::ClientApplication;
ClientData * data() {return _data;}
};
class ClientFixture : public iiwa_controller::FRIClient
{
public:
explicit ClientFixture(double position_smoothing_tau = 0.04)
: FRIClient(position_smoothing_tau), app(connection, *this), data(app.data())
{
auto & m = data->monitoringMsg;
m.connectionInfo.sendPeriod = 10;
m.connectionInfo.quality = FRIConnectionQuality_GOOD;
m.robotInfo.safetyState = SafetyState_NORMAL_OPERATION;
m.robotInfo.operationMode = OperationMode_AUTOMATIC_MODE;
m.robotInfo.controlMode = ControlMode_POSITION_CONTROLMODE;
auto * drives = static_cast<tRepeatedIntArguments *>(m.robotInfo.driveState.arg);
std::fill_n(drives->value, 7, DriveState_ACTIVE);
m.ipoData.clientCommandMode = ClientCommandMode_POSITION;
m.ipoData.overlayType = OverlayType_JOINT;
m.ipoData.has_trackingPerformance = false;
m.monitorData.timestamp.sec = 1;
m.monitorData.timestamp.nanosec = 0;
fill(m.monitorData.measuredJointPosition, 0.2);
fill(m.monitorData.commandedJointPosition, 0.3);
fill(m.monitorData.measuredTorque, 1.0);
fill(m.monitorData.externalTorque, 0.1);
fill(m.ipoData.jointPosition, 0.4);
}
static void fill(JointValues & values, double value)
{
auto * args = static_cast<tRepeatedDoubleArguments *>(values.value.arg);
std::fill_n(args->value, 7, value);
args->size = 7;
}
void cycle(ESessionState state)
{
data->resetCommandMessage();
data->monitoringMsg.ipoData.has_jointPosition = state >= COMMANDING_WAIT;
data->monitoringMsg.connectionInfo.sessionState = static_cast<FRISessionState>(state);
onStateChange(data->lastState, state);
data->lastState = state;
data->monitoringMsg.monitorData.timestamp.nanosec += 10000000;
if (state == COMMANDING_WAIT) waitForCommand();
else if (state == COMMANDING_ACTIVE) command();
else monitor();
}
double sentPosition()
{
return static_cast<tRepeatedDoubleArguments *>(
data->commandMsg.commandData.jointPosition.value.arg)->value[0];
}
UdpConnection connection;
TestApplication app;
ClientData * data;
};
TEST(FRIClient, ReportsMeasuredPositionInCommanding)
{
ClientFixture client;
client.cycle(COMMANDING_WAIT);
EXPECT_DOUBLE_EQ(client.sentPosition(), 0.4);
EXPECT_DOUBLE_EQ(client.getStateSnapshot().measured_pos[0], 0.2);
client.cycle(COMMANDING_ACTIVE);
EXPECT_DOUBLE_EQ(client.getStateSnapshot().measured_pos[0], 0.2);
}
TEST(FRIClient, PreservesSdkMonitoringReplyWithoutIpo)
{
ClientFixture client;
client.cycle(MONITORING_READY);
EXPECT_TRUE(client.data->commandMsg.commandData.has_jointPosition);
EXPECT_DOUBLE_EQ(client.sentPosition(), 0.3);
EXPECT_FALSE(client.getStateSnapshot().ipo_valid);
}
TEST(FRIClient, RejectsUnsupportedCommandModeBeforeSending)
{
ClientFixture client;
client.data->monitoringMsg.ipoData.clientCommandMode = ClientCommandMode_TORQUE;
EXPECT_THROW(client.cycle(COMMANDING_WAIT), std::runtime_error);
EXPECT_FALSE(client.data->commandMsg.commandData.has_jointPosition);
}
TEST(FRIClient, PositionCommandsAreSmoothedBeforeSending)
{
ClientFixture client;
client.cycle(COMMANDING_WAIT);
client.cycle(COMMANDING_ACTIVE);
iiwa_controller::FRIClient::Joints q;
q.fill(0.41);
client.setTargetJointPositions(q);
client.cycle(COMMANDING_ACTIVE);
// tau=40 ms, dt=10 ms: alpha=dt/(tau+dt)=0.2.
EXPECT_DOUBLE_EQ(client.sentPosition(), 0.402);
EXPECT_DOUBLE_EQ(client.getStateSnapshot().measured_pos[0], 0.2);
}
TEST(FRIClient, ZeroPositionSmoothingTauForwardsStepImmediately)
{
ClientFixture client(0.0);
client.cycle(COMMANDING_WAIT);
client.cycle(COMMANDING_ACTIVE);
iiwa_controller::FRIClient::Joints q;
q.fill(0.41);
client.setTargetJointPositions(q);
client.cycle(COMMANDING_ACTIVE);
EXPECT_DOUBLE_EQ(client.sentPosition(), 0.41);
}
TEST(FRIClient, RejectsInvalidCommandsAndBoundsCommandRate)
{
ClientFixture client;
iiwa_controller::FRIClient::Joints lower, upper, velocity, q;
lower.fill(-1.0); upper.fill(1.0); velocity.fill(1.0);
client.setLimits(lower, upper, velocity);
client.cycle(COMMANDING_WAIT);
client.cycle(COMMANDING_ACTIVE);
q.fill(0.8);
client.setTargetJointPositions(q);
client.cycle(COMMANDING_ACTIVE);
EXPECT_NEAR(client.sentPosition(), 0.41, 1e-12); // 1 rad/s * 10 ms
q[0] = 1.1;
EXPECT_THROW(client.setTargetJointPositions(q), std::invalid_argument);
q[0] = std::numeric_limits<double>::quiet_NaN();
EXPECT_THROW(client.setTargetJointPositions(q), std::invalid_argument);
}
TEST(FRIClient, SafetyStopCannotSendApplicationCommand)
{
ClientFixture client;
client.cycle(COMMANDING_WAIT);
client.data->monitoringMsg.robotInfo.safetyState = SafetyState_SAFETY_STOP_LEVEL_1;
EXPECT_THROW(client.cycle(COMMANDING_ACTIVE), std::runtime_error);
EXPECT_FALSE(client.data->commandMsg.commandData.has_jointPosition);
}
TEST(FRIClient, DuplicateTimestampIsNotFreshTelemetry)
{
ClientFixture client;
client.cycle(MONITORING_READY);
EXPECT_THROW(client.monitor(), std::runtime_error);
}
TEST(FRIClient, RejectsMismatchedPeriodAndMissingCommandingPackets)
{
ClientFixture mismatch;
mismatch.setExpectedSampleTime(0.005);
EXPECT_THROW(mismatch.cycle(MONITORING_READY), std::runtime_error);
ClientFixture gap;
gap.cycle(COMMANDING_WAIT);
gap.data->monitoringMsg.monitorData.timestamp.nanosec += 10000000;
EXPECT_THROW(gap.cycle(COMMANDING_ACTIVE), std::runtime_error);
EXPECT_FALSE(gap.data->commandMsg.commandData.has_jointPosition);
}
TEST(FRIClient, LeavingCommandingRequiresExplicitRecovery)
{
ClientFixture client;
client.cycle(COMMANDING_WAIT);
client.cycle(COMMANDING_ACTIVE);
EXPECT_THROW(client.cycle(MONITORING_READY), std::runtime_error);
}
TEST(FRIClient, PositionOverlaySupportsSunriseImpedanceModes)
{
for (auto mode : {ControlMode_JOINT_IMPEDANCE_CONTROLMODE,
ControlMode_CARTESIAN_IMPEDANCE_CONTROLMODE})
{
ClientFixture client;
client.data->monitoringMsg.robotInfo.controlMode = mode;
EXPECT_NO_THROW(client.cycle(COMMANDING_WAIT));
EXPECT_DOUBLE_EQ(client.sentPosition(), 0.4);
}
}
TEST(FRIClient, StalledRosCommandStreamStopsBeforeSending)
{
ClientFixture client;
client.cycle(COMMANDING_WAIT);
client.cycle(COMMANDING_ACTIVE);
std::this_thread::sleep_for(std::chrono::milliseconds(110));
EXPECT_THROW(client.cycle(COMMANDING_ACTIVE), std::runtime_error);
EXPECT_FALSE(client.data->commandMsg.commandData.has_jointPosition);
}
TEST(FRIClient, RejectsOverwritingAnUnconsumedPoint)
{
ClientFixture client;
client.cycle(COMMANDING_WAIT);
client.cycle(COMMANDING_ACTIVE);
iiwa_controller::FRIClient::Joints q;
q.fill(0.401);
client.setTargetJointPositions(q);
q.fill(0.402);
EXPECT_THROW(client.setTargetJointPositions(q), std::logic_error);
client.cycle(COMMANDING_ACTIVE);
EXPECT_DOUBLE_EQ(client.sentPosition(), 0.4002);
}
TEST(FRIClient, SynchronizedCyclesPreserveEveryPointDespiteSchedulingJitter)
{
ClientFixture client;
client.cycle(COMMANDING_WAIT);
iiwa_controller::FRICycleGate gate;
gate.reset();
std::atomic<bool> worker_ok{true};
std::vector<double> sent;
std::thread worker([&] {
for (int frame = 0; frame <= 8; ++frame) {
client.cycle(COMMANDING_ACTIVE);
sent.push_back(client.sentPosition());
if (frame < 8 && !gate.publishAndWait()) {worker_ok = false; break;}
}
});
for (int frame = 1; frame <= 8; ++frame) {
if (!gate.acquire()) {worker_ok = false; break;}
// Variable controller execution time cannot cause a point to be overwritten.
std::this_thread::sleep_for(std::chrono::milliseconds(frame % 3));
iiwa_controller::FRIClient::Joints q;
q.fill(0.4 + frame * 0.001);
client.setTargetJointPositions(q);
if (!gate.complete()) {worker_ok = false; break;}
}
worker.join();
ASSERT_TRUE(worker_ok);
ASSERT_EQ(sent.size(), 9u);
double expected = 0.4;
for (size_t frame = 1; frame < sent.size(); ++frame) {
const double target = 0.4 + static_cast<double>(frame) * 0.001;
expected = 0.2 * target + 0.8 * expected;
EXPECT_NEAR(sent[frame], expected, 1e-12);
}
}
TEST(FRICycleGate, DuplicateReadOrWriteCannotAdvanceWorker)
{
iiwa_controller::FRICycleGate gate;
gate.reset();
EXPECT_FALSE(gate.complete());
std::thread worker([&] {EXPECT_TRUE(gate.publishAndWait());});
EXPECT_TRUE(gate.acquire());
EXPECT_FALSE(gate.acquire());
EXPECT_TRUE(gate.complete());
EXPECT_FALSE(gate.complete());
worker.join();
}
TEST(FRICycleGate, StopWakesWorkerAndMissingWriteTimesOut)
{
iiwa_controller::FRICycleGate gate;
gate.reset();
std::thread worker([&] {EXPECT_FALSE(gate.publishAndWait());});
EXPECT_TRUE(gate.acquire());
gate.stop();
worker.join();
gate.reset();
EXPECT_FALSE(gate.publishAndWait());
EXPECT_FALSE(gate.acquire());
EXPECT_FALSE(gate.complete());
}
+109
View File
@@ -0,0 +1,109 @@
#include <gtest/gtest.h>
#include "iiwa_controller/IIWAHardwareInterface.hpp"
using Hardware = iiwa_controller::IIWAHardwareInterface;
using Return = hardware_interface::CallbackReturn;
hardware_interface::HardwareComponentInterfaceParams parameters()
{
hardware_interface::HardwareComponentInterfaceParams params;
auto & info = params.hardware_info;
info.name = "iiwa_test";
info.type = "system";
info.hardware_parameters["simulate"] = "true";
for (int i = 1; i <= 7; ++i) {
hardware_interface::ComponentInfo joint;
joint.name = "joint" + std::to_string(i);
for (const auto * name : {"position", "velocity", "effort"}) {
hardware_interface::InterfaceInfo interface;
interface.name = name;
interface.data_type = "double";
interface.initial_value = "0.2";
joint.state_interfaces.push_back(interface);
}
hardware_interface::InterfaceInfo command;
command.name = "position";
command.data_type = "double";
joint.command_interfaces.push_back(command);
joint_limits::JointLimits limits;
limits.has_position_limits = true;
limits.min_position = -2.0;
limits.max_position = 2.0;
limits.has_velocity_limits = true;
limits.max_velocity = 1.0;
info.limits[joint.name] = limits;
info.joints.push_back(joint);
}
return params;
}
TEST(Hardware, RejectsMalformedPortWithoutThrowing)
{
Hardware hardware;
auto params = parameters();
params.hardware_info.hardware_parameters["fri_port"] = "30200oops";
EXPECT_EQ(hardware.on_init(params), Return::ERROR);
}
TEST(Hardware, RejectsInvalidRealtimePriority)
{
Hardware hardware;
auto params = parameters();
params.hardware_info.hardware_parameters["rt_prio"] = "0";
EXPECT_EQ(hardware.on_init(params), Return::ERROR);
}
TEST(Hardware, RejectsInvalidPositionSmoothingTau)
{
Hardware hardware;
auto params = parameters();
params.hardware_info.hardware_parameters["joint_position_tau"] = "-0.01";
EXPECT_EQ(hardware.on_init(params), Return::ERROR);
}
TEST(Hardware, RejectsWrongJointOrderAndMissingInterfaces)
{
auto params = parameters();
std::swap(params.hardware_info.joints[0], params.hardware_info.joints[1]);
Hardware wrong_order;
EXPECT_EQ(wrong_order.on_init(params), Return::ERROR);
params = parameters();
params.hardware_info.joints[0].state_interfaces.pop_back();
Hardware missing_interface;
EXPECT_EQ(missing_interface.on_init(params), Return::ERROR);
}
TEST(Hardware, SimulationSeedsCommandsAndSupportsRepeatedLifecycle)
{
Hardware hardware;
ASSERT_EQ(hardware.on_init(parameters()), Return::SUCCESS);
auto states = hardware.on_export_state_interfaces();
auto commands = hardware.on_export_command_interfaces();
const rclcpp_lifecycle::State state;
ASSERT_EQ(hardware.on_configure(state), Return::SUCCESS);
for (int cycle = 0; cycle < 2; ++cycle) {
ASSERT_EQ(hardware.on_activate(state), Return::SUCCESS);
const double command = hardware.get_command<double>("joint1/position");
EXPECT_DOUBLE_EQ(command, 0.2);
EXPECT_EQ(hardware.read(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01)),
hardware_interface::return_type::OK);
EXPECT_EQ(hardware.on_deactivate(state), Return::SUCCESS);
EXPECT_EQ(hardware.read(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01)),
hardware_interface::return_type::OK);
}
EXPECT_EQ(hardware.on_cleanup(state), Return::SUCCESS);
EXPECT_EQ(hardware.on_shutdown(state), Return::SUCCESS);
}
TEST(Hardware, ConfiguredRealHardwareCleansUpInDependencyOrder)
{
Hardware hardware;
auto params = parameters();
params.hardware_info.hardware_parameters["simulate"] = "false";
params.hardware_info.hardware_parameters["robot_ip"] = "127.0.0.1";
ASSERT_EQ(hardware.on_init(params), Return::SUCCESS);
const rclcpp_lifecycle::State state;
ASSERT_EQ(hardware.on_configure(state), Return::SUCCESS);
EXPECT_EQ(hardware.on_cleanup(state), Return::SUCCESS);
EXPECT_EQ(hardware.on_error(state), Return::SUCCESS);
}
+5 -4
View File
@@ -6,8 +6,8 @@
<xacro:arg name="simulate" default="false"/> <xacro:arg name="simulate" default="false"/>
<xacro:arg name="robot_ip" default="192.170.10.2"/> <xacro:arg name="robot_ip" default="192.170.10.2"/>
<xacro:arg name="fri_port" default="30200"/> <xacro:arg name="fri_port" default="30200"/>
<xacro:arg name="fri_cycle_ms" default="5"/>
<xacro:arg name="joint_position_tau" default="0.04"/> <xacro:arg name="joint_position_tau" default="0.04"/>
<xacro:arg name="joint_velocity_tau" default="0.01"/>
<xacro:property name="initial_positions" <xacro:property name="initial_positions"
value="${xacro.load_yaml('$(arg initial_positions_file)')['initial_positions']}"/> value="${xacro.load_yaml('$(arg initial_positions_file)')['initial_positions']}"/>
@@ -145,9 +145,10 @@
<plugin>iiwa_controller/IIWAHardwareInterface</plugin> <plugin>iiwa_controller/IIWAHardwareInterface</plugin>
<param name="robot_ip">$(arg robot_ip)</param> <param name="robot_ip">$(arg robot_ip)</param>
<param name="fri_port">$(arg fri_port)</param> <param name="fri_port">$(arg fri_port)</param>
<param name="simulate">false</param> <param name="fri_cycle_ms">$(arg fri_cycle_ms)</param>
<param name="joint_position_tau">$(arg joint_position_tau)</param> <param name="joint_position_tau">$(arg joint_position_tau)</param>
<param name="joint_velocity_tau">$(arg joint_velocity_tau)</param> <param name="rt_prio">80</param>
<param name="simulate">false</param>
</hardware> </hardware>
<joint name="joint1"> <joint name="joint1">
@@ -245,4 +246,4 @@
</xacro:unless> </xacro:unless>
</robot> </robot>
+5 -3
View File
@@ -1,3 +1,4 @@
import math
import os import os
from dataclasses import dataclass, fields, is_dataclass from dataclasses import dataclass, fields, is_dataclass
from typing import Any, Dict, List, Optional, Type, TypeVar from typing import Any, Dict, List, Optional, Type, TypeVar
@@ -16,7 +17,6 @@ class RobotCfg:
description: str description: str
fri_cycle_ms: int fri_cycle_ms: int
joint_position_tau: float joint_position_tau: float
active_controller: str # "jtc" | "forward"
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -284,14 +284,16 @@ def build_settings(settings_path: str, check_files: bool = True) -> Settings:
# robot # robot
robot_raw = require(raw, "robot") robot_raw = require(raw, "robot")
joint_position_tau = float(robot_raw.get("joint_position_tau", 0.04))
if not math.isfinite(joint_position_tau) or joint_position_tau < 0.0:
raise ValueError("robot.joint_position_tau must be a finite value >= 0")
robot = RobotCfg( robot = RobotCfg(
name=str(require(robot_raw, "name")), name=str(require(robot_raw, "name")),
ip=str(require(robot_raw, "ip")), ip=str(require(robot_raw, "ip")),
port=int(require(robot_raw, "port")), port=int(require(robot_raw, "port")),
description=resolve_path(str(require(robot_raw, "description")), settings_dir), description=resolve_path(str(require(robot_raw, "description")), settings_dir),
fri_cycle_ms=int(robot_raw.get("fri_cycle_ms", 5)), fri_cycle_ms=int(robot_raw.get("fri_cycle_ms", 5)),
joint_position_tau=float(robot_raw.get("joint_position_tau", 0.04)), joint_position_tau=joint_position_tau,
active_controller=str(robot_raw.get("active_controller", "jtc")),
) )
# digital_twin # digital_twin