Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f0f05df87 | ||
|
|
1780532f51 | ||
|
|
29a1601061 | ||
|
|
fd66c8c7ec | ||
|
|
97e3a5f60c | ||
|
|
e7df25c36f | ||
|
|
7f9b120fe8 | ||
|
|
36d620317a | ||
|
|
48e842656b | ||
|
|
ca4bb76008 | ||
|
|
392036caf8 | ||
|
|
d51c3737b9 | ||
|
|
b1fc2c5ff5 | ||
|
|
db2751bc78 | ||
|
|
9e65a2720c | ||
|
|
047e1b190e | ||
|
|
2ba4975471 | ||
|
|
e388e7d168 | ||
|
|
ff4b2e30ce | ||
|
|
0a7eb067ac | ||
|
|
1b6e211b88 |
@@ -188,165 +188,4 @@ Java-программы для KUKA Sunrise Cabinet находятся отде
|
|||||||
| Организация | Примечание |
|
| Организация | Примечание |
|
||||||
|---|---|
|
|---|---|
|
||||||
| [Комсомольский-на-Амуре государственный университет](https://knastu.ru/) | Исследования проводились на базе КнАГУ |
|
| [Комсомольский-на-Амуре государственный университет](https://knastu.ru/) | Исследования проводились на базе КнАГУ |
|
||||||
| [Российский научный фонд](https://rscf.ru/) | Работа выполнена при поддержке Российского научного фонда |
|
| [Российский научный фонд](https://rscf.ru/) | Работа выполнена при поддержке Российского научного фонда |
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<!-- ============================================================ -->
|
|
||||||
<!-- Черновые команды — будут удалены в следующих версиях -->
|
|
||||||
<!-- ============================================================ -->
|
|
||||||
|
|
||||||
## Черновые команды
|
|
||||||
|
|
||||||
> Вроде уже не обязательно
|
|
||||||
Подмена файла по пути обязательна: `/opt/ros/rolling/lib/webots_ros2_driver/ros2_supervisor.py`
|
|
||||||
необходимо `warn` заменить на `warning` в логере
|
|
||||||
|
|
||||||
```bash
|
|
||||||
|
|
||||||
sudo apt install -y ros-${ROS_DISTRO}-webots-ros2 \
|
|
||||||
ros-${ROS_DISTRO}-ros2-control \
|
|
||||||
ros-${ROS_DISTRO}-ros2-controllers \
|
|
||||||
ros-${ROS_DISTRO}-moveit \
|
|
||||||
ros-${ROS_DISTRO}-moveit-py \
|
|
||||||
ros-${ROS_DISTRO}-rmw-cyclonedds-cpp \
|
|
||||||
ros-${ROS_DISTRO}-ament-cmake-clang-format \
|
|
||||||
ros-${ROS_DISTRO}-rosbag2-storage-mcap \
|
|
||||||
ros-${ROS_DISTRO}-librealsense2 \
|
|
||||||
ros-${ROS_DISTRO}-realsense2* \
|
|
||||||
libportaudio2 \
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Установка moveit2 (внимательно проверяй)
|
|
||||||
```bash
|
|
||||||
sudo apt install -y build-essential \
|
|
||||||
cmake \
|
|
||||||
git \
|
|
||||||
python3-colcon-common-extensions \
|
|
||||||
python3-flake8 \
|
|
||||||
python3-rosdep \
|
|
||||||
python3-setuptools \
|
|
||||||
python3-vcstool \
|
|
||||||
wget
|
|
||||||
|
|
||||||
# Not Using
|
|
||||||
LC_ALL=C ros2 launch iiwa_moveit demo...
|
|
||||||
|
|
||||||
sudo pip3 install transforms3d --break-system-packages
|
|
||||||
```
|
|
||||||
Отправка робота в точку:
|
|
||||||
```bash
|
|
||||||
ros2 action send_goal /iiwa/move_to_pose iiwa_msgs/action/MoveToPose \
|
|
||||||
"{x: 0.5, y: 0.0, z: 0.5, a: 3.14, b: 0, c: 0, speed: 0.1, planner: 'ptp'}"
|
|
||||||
|
|
||||||
ros2 action send_goal --feedback /iiwa/move_to_joints iiwa_msgs/action/MoveToJoints \
|
|
||||||
"{joints: [0.0, 0.0, 0.0, -1.57, 0.0, 1.57, 0.0], speed: 0.4}"
|
|
||||||
|
|
||||||
ros2 service call /iiwa/move_to_named iiwa_msgs/srv/MoveToNamedPose \
|
|
||||||
"{name: 'home', speed: 0.5}"
|
|
||||||
|
|
||||||
ros2 service call /iiwa/move_to_named iiwa_msgs/srv/MoveToNamedPose \
|
|
||||||
"{name: 'work', speed: 0.3}"
|
|
||||||
|
|
||||||
ros2 service call /iiwa/stop std_srvs/srv/Trigger "{}"
|
|
||||||
```
|
|
||||||
|
|
||||||
Примеры использования `test_motion_sequence`:
|
|
||||||
```bash
|
|
||||||
ros2 run iiwa_planning motion_sequence_runner \
|
|
||||||
--ros-args \
|
|
||||||
-p config_path:=/path/to/config.json \
|
|
||||||
-p n_iterations:=3 \
|
|
||||||
-p delay_between_iterations:=5.0 \
|
|
||||||
-p bag_path:=/tmp/my_bag \
|
|
||||||
-p joints_action:=my_ns/move_to_joints \
|
|
||||||
-p pose_action:=my_ns/move_to_pose
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Спавн объекта:
|
|
||||||
```bash
|
|
||||||
{
|
|
||||||
"data": "Solid { name \"test_box2\" translation 0 1 0.5 children [ Shape { appearance PBRAppearance { baseColor 0.901961 0.380392 0 } geometry Box { size 0.1 0.1 0.1 } } ] boundingObject Box { size 0.1 0.1 0.1 } physics Physics { } }"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Спавн `.proto`:
|
|
||||||
|
|
||||||
|
|
||||||
Можно заготовить готовые `.proto` файлы, и потом случайно спавнить объект по такому принципу + создать Node который будет вызываться и спавнить этот объекты. Может быть шаблон куда потом подставятся данные через `.format()`. По такому же принципу спавн человека. Остается понять, только задать область спавна относительно робта
|
|
||||||
|
|
||||||
Установка зависимостей для сборки пакетов:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rosdep install --from-paths src --ignore-src -r -y --os=ubuntu:jammy
|
|
||||||
```
|
|
||||||
|
|
||||||
Запись данных в rosbag:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ sudo apt-get install ros-$ROS_DISTRO-rosbag2-storage-mcap
|
|
||||||
|
|
||||||
# Record all topics
|
|
||||||
ros2 bag record -a --storage mcap -o my_session
|
|
||||||
|
|
||||||
# Record specific topics
|
|
||||||
ros2 bag record \
|
|
||||||
/camera/image_raw \
|
|
||||||
/lidar/points \
|
|
||||||
/imu/data \
|
|
||||||
--storage mcap \
|
|
||||||
-o robot_drive_session
|
|
||||||
|
|
||||||
|
|
||||||
# LZ4 = faster write, moderate compression (good for real-time recording)
|
|
||||||
ros2 bag record -a --storage mcap \
|
|
||||||
--compression-mode file \
|
|
||||||
--compression-format lz4 \
|
|
||||||
-o compressed_session
|
|
||||||
|
|
||||||
# Zstandard = slower write, better compression (good for post-processing)
|
|
||||||
ros2 bag record -a --storage mcap \
|
|
||||||
--compression-mode file \
|
|
||||||
--compression-format zstd \
|
|
||||||
-o compressed_zstd_session
|
|
||||||
|
|
||||||
# convert existing rosbag2 to mcap format
|
|
||||||
ros2 bag convert \
|
|
||||||
-i robot_drive_old/ \
|
|
||||||
-o robot_drive_mcap/ \
|
|
||||||
--output-options '{"storage_id": "mcap"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Generate Doc -->
|
|
||||||
```bash
|
|
||||||
chmod +x doc/build_docs.sh
|
|
||||||
|
|
||||||
./build_docs.sh #Собирает статику в mkdocs/site/
|
|
||||||
./build_docs.sh build # То же самое явно
|
|
||||||
./build_docs.sh serve # Live-preview с авто-перезагрузкой
|
|
||||||
```
|
|
||||||
|
|
||||||
<!-- iiwa controller - docker -->
|
|
||||||
<!-- Lightweight cobot -->
|
|
||||||
```bash
|
|
||||||
docker build -t evilfisru/lwc:... -f docker/jazzy/.../Dockerfile .
|
|
||||||
|
|
||||||
docker run -it --rm --network host evilfisru/lwa:jazzy-lwa7-noble
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# release - src/build/log удаляются (по умолчанию)
|
|
||||||
`docker build -t my-image .`
|
|
||||||
|
|
||||||
# dev - src остаётся для отладки
|
|
||||||
`docker build --build-arg BUILD_TYPE=dev -t my-image .`
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
# URL для доступа к MCP LLM
|
|
||||||
http://localhost:8007/mcp/mcp
|
|
||||||
|
|
||||||
```
|
|
||||||
+2
-4
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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())
|
||||||
|
|||||||
@@ -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 внутри пакетов |
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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).
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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 # Не падать, если нода не отвечает на запросы параметров (защита от зависания при старте)
|
||||||
|
|||||||
@@ -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};
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 = ⌖
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user