7 Commits
26 changed files with 1381 additions and 461 deletions
+1 -4
View File
@@ -1,11 +1,8 @@
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_velocity_tau: 0.01 # EMA фильтр скорости [с]: убирает выбросы конечных разностей
active_controller: "jtc" # "jtc" = MoveIt/JointTrajectoryController, "forward" = ForwardCommandController
description: pkg://iiwa_description/urdf/iiwa7.urdf.xacro description: pkg://iiwa_description/urdf/iiwa7.urdf.xacro
-7
View File
@@ -143,13 +143,6 @@ _BLOCKS: List[_Block] = [
_Field("fri_cycle_ms", "Цикл FRI (мс):", "10", _Field("fri_cycle_ms", "Цикл FRI (мс):", "10",
note="5 мс = 200 Гц, 10 мс = 100 Гц", note="5 мс = 200 Гц, 10 мс = 100 Гц",
options=["10", "5"]), options=["10", "5"]),
_Field("active_controller", "Активный ROS-контроллер:", "jtc",
note="jtc = JointTrajectoryController (MoveIt), forward = ForwardCommandController",
options=["jtc", "forward"]),
_Field("joint_position_tau", "EMA tau фильтра положения (с):", "0.04",
note="Сглаживает команды положения перед отправкой в FRI"),
_Field("joint_velocity_tau", "EMA tau фильтра скорости (с):", "0.01",
note="Убирает выбросы из оценки скорости конечной разностью"),
], ],
), ),
] ]
+43 -27
View File
@@ -4,13 +4,23 @@ import argparse
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from cobot import process, ui from cobot import process
from cobot.ui import done from cobot.ui import done
from cobot.process import StepProgress from cobot.process import StepProgress
_PROJECT_DIR = Path(__file__).parent.parent.parent _PROJECT_DIR = Path(__file__).parent.parent.parent
def _plain(message: str) -> None:
"""Print after replacing this command's own uv environment.
Rich modules loaded before ``uv tool install`` can no longer be complete after
uv replaces the editable tool environment. Do not render with Rich after that
command returns.
"""
print(message, flush=True)
def _git(*args: str) -> str: def _git(*args: str) -> str:
"""Run a git command in the project dir and return its stripped stdout. """Run a git command in the project dir and return its stripped stdout.
Запускает git-команду в директории проекта и возвращает обрезанный stdout. Запускает git-команду в директории проекта и возвращает обрезанный stdout.
@@ -18,13 +28,12 @@ def _git(*args: str) -> str:
return subprocess.check_output(["git", *args], cwd=_PROJECT_DIR, text=True).strip() return subprocess.check_output(["git", *args], cwd=_PROJECT_DIR, text=True).strip()
def _update() -> None: def _update() -> int:
"""Fetch the current branch, show incoming commits, pull, and reinstall the cobot CLI. """Fetch the current branch, show incoming commits, pull, and reinstall the cobot CLI.
Progress: fetch (0-30 %), pull (30-80 %), reinstall (80-100 %). Progress: fetch (0-30 %), pull (30-80 %), reinstall (80-100 %).
Получает текущую ветку, показывает входящие коммиты, делает pull и переустанавливает CLI. Получает текущую ветку, показывает входящие коммиты, делает pull и переустанавливает CLI.
Прогресс: fetch (0-30 %), pull (30-80 %), переустановка (80-100 %). Прогресс: fetch (0-30 %), pull (30-80 %), переустановка (80-100 %).
""" """
ok, fail_msg = True, ""
with StepProgress("Обновление проекта") as p: with StepProgress("Обновление проекта") as p:
try: try:
branch = _git("rev-parse", "--abbrev-ref", "HEAD") branch = _git("rev-parse", "--abbrev-ref", "HEAD")
@@ -32,17 +41,15 @@ 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, "Уже актуальная версия") else:
return
p.raw(f"\n[bold]{behind} новых коммит(ов):[/bold]") p.raw(f"\n[bold]{behind} новых коммит(ов):[/bold]")
for line in _git("log", f"HEAD..origin/{branch}", "--oneline").splitlines(): for line in _git("log", f"HEAD..origin/{branch}", "--oneline").splitlines():
p.log(line) p.log(line)
@@ -50,24 +57,33 @@ def _update() -> None:
p.set(30, "Применение изменений...") p.set(30, "Применение изменений...")
rc = process.stream(["git", "pull", "origin", branch], rc = process.stream(["git", "pull", "origin", branch],
cwd=str(_PROJECT_DIR), on_line=p.log) cwd=str(_PROJECT_DIR), on_line=p.log)
if rc not in (0, -9, -15): if rc != 0:
done(False, "git pull завершился с ошибкой") done(False, f"git pull завершился с кодом {rc}")
return return 1
p.set(80) p.set(80)
except (subprocess.CalledProcessError, OSError) as exc:
done(False, str(exc))
return 1
p.set(80, "Переустановка cobot CLI...") # uv can replace the Rich package of this running CLI. Keep this stage outside
p.raw("\n[cyan]▸[/cyan] Переустановка cobot CLI...") # StepProgress and only use built-in print afterwards.
rc = process.stream(["uv", "tool", "install", "--editable", str(_PROJECT_DIR)], _plain("\n▸ Переустановка cobot CLI...")
on_line=p.log) try:
if rc in (0, -9, -15): rc = process.stream(
p.raw("[green]✓[/green] cobot переустановлен") ["uv", "tool", "install", "--editable", str(_PROJECT_DIR)],
else: on_line=lambda line: _plain(f" {line}"),
p.raw("[yellow]Предупреждение:[/yellow] переустановка не удалась") )
p.set(100, "Готово") except OSError as exc:
except subprocess.CalledProcessError as exc: _plain(f"✗ Не удалось запустить установщик: {exc}")
ok, fail_msg = False, str(exc) return 1
if rc != 0:
done(ok, "Проект обновлён" if ok else fail_msg) _plain(
f"✗ Установка CLI завершилась с кодом {rc}. Исходники обновлены, "
"но установка не завершена. Сохраните вывод выше и повторите cobot update."
)
return 1
_plain("✓ Проект обновлён и cobot переустановлен")
return 0
def register(subparsers: argparse._SubParsersAction) -> None: def register(subparsers: argparse._SubParsersAction) -> None:
@@ -76,4 +92,4 @@ def register(subparsers: argparse._SubParsersAction) -> None:
def run(args: argparse.Namespace) -> None: def run(args: argparse.Namespace) -> None:
_update() raise SystemExit(_update())
+71
View File
@@ -0,0 +1,71 @@
"""Update recovery checks; external git and installer operations are isolated."""
import io
import unittest
from unittest.mock import patch
from rich.console import Console
from cobot.commands import update
class UpdateTests(unittest.TestCase):
def run_update(self, codes, behind="1"):
output = io.StringIO()
commands = []
def stream(command, **kwargs):
commands.append(command)
result = next(codes, 0)
if isinstance(result, Exception):
raise result
return result
def git(*args):
return {"rev-parse": "controller", "rev-list": behind,
"log": "abc123 Update CLI"}[args[0]]
console = Console(file=output, color_system=None)
with patch.object(update, "_git", side_effect=git), \
patch.object(update.process, "stream", side_effect=stream), \
patch.object(update, "_plain"), \
patch("cobot.process.console", console), \
patch("cobot.ui.console", console):
code = update._update()
return code, output.getvalue(), commands
def test_install_failure_reports_failure(self):
for install_code in (1, -9, -15):
with self.subTest(install_code=install_code):
code, output, _ = self.run_update(iter([0, 0, install_code]))
self.assertEqual(code, 1)
self.assertNotIn("Проект обновлён", output)
def test_current_checkout_still_retries_installation(self):
code, _, commands = self.run_update(iter([0, 0]), behind="0")
self.assertEqual([command[:2] for command in commands],
[["git", "fetch"], ["uv", "tool"]])
self.assertEqual(code, 0)
def test_killed_git_stops_before_next_step(self):
for codes, expected_count in [([-9], 1), ([0, -15], 2)]:
with self.subTest(codes=codes):
code, output, commands = self.run_update(iter(codes))
self.assertNotEqual(code, 0)
self.assertEqual(len(commands), expected_count)
self.assertNotIn("Проект обновлён", output)
def test_missing_installer_reports_failure_without_traceback(self):
code, _, _ = self.run_update(
iter([0, 0, FileNotFoundError("uv not found")]))
self.assertNotEqual(code, 0)
def test_command_exits_nonzero_after_failed_installation(self):
with patch.object(update, "_update", return_value=1):
with self.assertRaises(SystemExit) as result:
update.run(None)
self.assertEqual(result.exception.code, 1)
if __name__ == "__main__":
unittest.main()
@@ -26,9 +26,6 @@ robot:
ip: "192.170.10.2" ip: "192.170.10.2"
port: 30200 port: 30200
fri_cycle_ms: 10 fri_cycle_ms: 10
joint_position_tau: 0.04
joint_velocity_tau: 0.01
active_controller: "jtc"
description: pkg://iiwa_description/urdf/iiwa7.urdf.xacro description: pkg://iiwa_description/urdf/iiwa7.urdf.xacro
``` ```
@@ -38,11 +35,10 @@ 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_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. Position and velocity EMA filters are not applied.
--- ---
## `digital_twin` section — simulator ## `digital_twin` section — simulator
@@ -70,6 +66,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 +166,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,10 @@ 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_velocity_tau` | EMA-фильтр скорости [с] — убирает выбросы конечных разностей | Аналогично `joint_position_tau` |
| `active_controller` | Режим управления: `jtc` (MoveIt / JointTrajectory) или `forward` (прямое управление) | `jtc` для большинства задач |
| `description` | Путь к URDF-описанию робота | Не менять | | `description` | Путь к URDF-описанию робота | Не менять |
Управление движением всегда выполняет JointTrajectoryController. EMA-фильтры положения и скорости не применяются.
--- ---
## Блок `digital_twin` — симулятор ## Блок `digital_twin` — симулятор
@@ -72,6 +68,7 @@ digital_twin:
| `webots.cameras` | Список YAML-конфигов подключённых камер | | `webots.cameras` | Список YAML-конфигов подключённых камер |
| `rviz.config` | Путь к конфигурации RViz | | `rviz.config` | Путь к конфигурации RViz |
--- ---
## Блок `tool` — активный инструмент ## Блок `tool` — активный инструмент
@@ -171,7 +168,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 внутри пакетов |
+11 -4
View File
@@ -4,7 +4,7 @@ import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from launch import LaunchDescription from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription, OpaqueFunction from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription, LogInfo, OpaqueFunction
from launch.launch_description_sources import PythonLaunchDescriptionSource from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node from launch_ros.actions import Node
@@ -50,8 +50,8 @@ 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),
"fri_cycle_ms": str(settings.robot.fri_cycle_ms),
"simulate": "false", "simulate": "false",
"joint_position_tau": str(settings.robot.joint_position_tau),
} }
robot_description = converter.load_robot_description( robot_description = converter.load_robot_description(
@@ -117,10 +117,17 @@ def _runtime_setup(context, *args, **kwargs):
"rotation": str(settings.digital_twin.webots.rotation), "rotation": str(settings.digital_twin.webots.rotation),
"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), "robot_ip": settings.robot.ip,
"controller": settings.robot.active_controller, "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"endpoint={settings.robot.ip}:{settings.robot.port}"
)))
setup.append(IncludeLaunchDescription( setup.append(IncludeLaunchDescription(
PythonLaunchDescriptionSource( PythonLaunchDescriptionSource(
PathJoinSubstitution([ PathJoinSubstitution([
@@ -1,10 +1,13 @@
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 +22,9 @@ 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")
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 +32,9 @@ 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)
robot_description = converter.load_robot_description( robot_description = converter.load_robot_description(
model_path=description, model_path=description,
@@ -75,6 +80,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 +95,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 +111,22 @@ 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"),
ros2_control_node, ros2_control_node,
jsb, jsb,
jtc_after_jsb, jtc_after_jsb,
@@ -138,7 +136,7 @@ def _setup_controllers(context, *args, **kwargs):
def generate_launch_description(): 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("robot_ip", default_value="192.170.10.2"),
DeclareLaunchArgument("controller", default_value="jtc"), DeclareLaunchArgument("fri_port", default_value="30200"),
OpaqueFunction(function=_setup_controllers), OpaqueFunction(function=_setup_controllers),
]) ])
+1 -1
View File
@@ -26,7 +26,7 @@
<exec_depend>rviz2</exec_depend> <exec_depend>rviz2</exec_depend>
<!-- ros2_control --> <!-- ros2_control -->
<exec_depend>controller_manager</exec_depend> <exec_depend version_gte="4.48.0">controller_manager</exec_depend>
<exec_depend>joint_state_broadcaster</exec_depend> <exec_depend>joint_state_broadcaster</exec_depend>
<exec_depend>joint_trajectory_controller</exec_depend> <exec_depend>joint_trajectory_controller</exec_depend>
<exec_depend>forward_command_controller</exec_depend> <exec_depend>forward_command_controller</exec_depend>
@@ -8,8 +8,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 +56,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 -4
View File
@@ -1,11 +1,8 @@
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_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
+14 -1
View File
@@ -33,7 +33,7 @@ file(GLOB_RECURSE FRI_SOURCES
add_library(fri_client_sdk STATIC ${FRI_SOURCES}) add_library(fri_client_sdk STATIC ${FRI_SOURCES})
target_include_directories(fri_client_sdk PUBLIC target_include_directories(fri_client_sdk SYSTEM PUBLIC
${FRI_SDK_DIR}/include ${FRI_SDK_DIR}/include
${FRI_SDK_DIR}/src/nanopb-0.2.8 ${FRI_SDK_DIR}/src/nanopb-0.2.8
${FRI_SDK_DIR}/src/protobuf ${FRI_SDK_DIR}/src/protobuf
@@ -93,4 +93,17 @@ ament_export_targets(export_${PROJECT_NAME})
ament_export_dependencies( ament_export_dependencies(
controller_interface hardware_interface pluginlib rclcpp rclcpp_lifecycle realtime_tools std_msgs) controller_interface hardware_interface pluginlib rclcpp rclcpp_lifecycle realtime_tools std_msgs)
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() 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,67 @@
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 [с] 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 — постоянная времени экспоненциального фильтра позиций [с]. FRIClient();
// Аналог 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 requested_pos_{};
// Инициализируется IPO-позицией в waitForCommand(), чтобы не было скачка при старте. Joints sent_pos_{};
std::array<double, N_JOINTS> filtered_pos_{}; Joints lower_, upper_, max_velocity_;
IIWAStateSnapshot snapshot_{}; bool initialized_{false};
bool requested_{false};
// Обновить snapshot_ без поля ipo_pos (в Monitor-режиме getIpoJointPosition() недоступна) double expected_sample_time_{0.0};
void captureMonitoringData(); std::chrono::steady_clock::time_point requested_at_{};
// Обновить snapshot_ вместе с ipo_pos (в Commanding-режиме) std::chrono::steady_clock::time_point last_command_at_{};
void captureCommandingData(); 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,22 +60,26 @@ private:
// Параметры из секции <hardware><param> в URDF // Параметры из секции <hardware><param> в URDF
std::string robot_ip_; std::string robot_ip_;
int fri_port_{30200}; int fri_port_{30200};
int fri_cycle_ms_{5};
bool simulate_{false}; bool simulate_{false};
double joint_position_tau_{0.04};
// EMA-фильтр скорости: сглаживает одиночные выбросы конечных разностей.
// joint_velocity_tau = 0 отключает фильтр (raw finite difference).
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_;
@@ -83,19 +89,15 @@ private:
std::array<hardware_interface::CommandInterface::SharedPtr, N_JOINTS> h_cmd_pos_; std::array<hardware_interface::CommandInterface::SharedPtr, N_JOINTS> h_cmd_pos_;
// Вычисление скорости: конечные разности + EMA-фильтр // Вычисление скорости по измеренной позиции и меткам времени FRI.
std::array<double, N_JOINTS> prev_pos_{}; std::array<double, N_JOINTS> prev_pos_{};
std::array<double, N_JOINTS> velocity_{}; // отфильтрованная скорость, публикуется в JTC std::array<double, N_JOINTS> velocity_{}; // измеренная скорость [рад/с]
std::array<double, N_JOINTS> velocity_raw_{}; // сырая скорость до фильтра
unsigned int last_ts_sec_{0}; unsigned int last_ts_sec_{0};
unsigned int last_ts_nsec_{0}; unsigned int last_ts_nsec_{0};
bool velocity_initialized_{false}; bool velocity_initialized_{false};
void compute_velocity_(const IIWAStateSnapshot & snap); void compute_velocity_(const IIWAStateSnapshot & snap);
// Отслеживание сессии FRI для обнаружения потери управления
KUKA::FRI::ESessionState previous_session_state_{KUKA::FRI::IDLE};
rclcpp::Clock throttle_clock_{RCL_STEADY_TIME};
}; };
+1
View File
@@ -17,6 +17,7 @@
<depend>realtime_tools</depend> <depend>realtime_tools</depend>
<depend>std_msgs</depend> <depend>std_msgs</depend>
<test_depend>ament_cmake_gtest</test_depend>
<test_depend>ament_lint_auto</test_depend> <test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend> <test_depend>ament_lint_common</test_depend>
+164 -112
View File
@@ -1,155 +1,207 @@
#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()
{ {
switch (s) { lower_.fill(-std::numeric_limits<double>::infinity());
case KUKA::FRI::IDLE: return "IDLE"; upper_.fill(std::numeric_limits<double>::infinity());
case KUKA::FRI::MONITORING_WAIT: return "MONITORING_WAIT"; max_velocity_.fill(std::numeric_limits<double>::infinity());
case KUKA::FRI::MONITORING_READY: return "MONITORING_READY"; }
case KUKA::FRI::COMMANDING_WAIT: return "COMMANDING_WAIT";
case KUKA::FRI::COMMANDING_ACTIVE: return "COMMANDING_ACTIVE"; void FRIClient::setLimits(const Joints & lower, const Joints & upper, const Joints & velocity)
default: return "UNKNOWN"; {
lower_ = lower;
upper_ = upper;
max_velocity_ = velocity;
}
void FRIClient::captureData(bool commanding)
{
const auto & state = robotState();
std::copy_n(state.getMeasuredJointPosition(), N_JOINTS, current_.measured_pos.begin());
std::copy_n(state.getMeasuredTorque(), N_JOINTS, current_.measured_tau.begin());
std::copy_n(state.getExternalTorque(), N_JOINTS, current_.external_tau.begin());
current_.sample_time = state.getSampleTime();
current_.session = state.getSessionState();
current_.quality = state.getConnectionQuality();
current_.safety = state.getSafetyState();
current_.drives = state.getDriveState();
current_.operation = state.getOperationMode();
current_.control = state.getControlMode();
current_.command_mode = commanding ? state.getClientCommandMode() : NO_COMMAND_MODE;
current_.overlay = commanding ? state.getOverlayType() : NO_OVERLAY;
current_.tracking_performance = commanding ? state.getTrackingPerformance() : 0.0;
current_.ipo_valid = commanding;
if (commanding) {
std::copy_n(state.getIpoJointPosition(), N_JOINTS, current_.ipo_pos.begin());
}
const auto sec = state.getTimestampSec();
const auto nsec = state.getTimestampNanoSec();
if (current_.valid && (sec < current_.time_stamp_sec ||
(sec == current_.time_stamp_sec && nsec <= current_.time_stamp_nano_sec)))
{
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");
}
if (expected_sample_time_ > 0.0 &&
std::abs(current_.sample_time - expected_sample_time_) > 1e-9)
{
throw std::runtime_error(
"Sunrise FRI period is " + std::to_string(current_.sample_time * 1000.0) +
" 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");
}
}
for (size_t i = 0; i < N_JOINTS; ++i) {
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");
} }
} }
FRIClient::FRIClient(double joint_position_tau)
: joint_position_tau_(joint_position_tau)
{
target_pos_.fill(0.0);
filtered_pos_.fill(0.0);
}
// Вызывается только в Monitor-состояниях.
// В Monitor-режиме getIpoJointPosition() бросает FRIException, поэтому здесь не зовём.
void FRIClient::captureMonitoringData()
{
std::memcpy(
snapshot_.measured_pos.data(),
robotState().getMeasuredJointPosition(), N_JOINTS * sizeof(double));
std::memcpy(
snapshot_.measured_tau.data(),
robotState().getMeasuredTorque(), N_JOINTS * sizeof(double));
std::memcpy(
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() void FRIClient::monitor()
{ {
std::lock_guard<std::mutex> lock(data_mutex_); captureData(false);
captureMonitoringData(); LBRClient::monitor();
initialized_ = false;
publishState();
} }
// Вызывается в COMMANDING_WAIT.
// По документации FRI (п. 6.2.2) клиент должен отправлять команды в каждом цикле.
// Переход в COMMANDING_ACTIVE происходит только когда разница между commanded_position
// и IPO_position меньше 0.001 рад для всех суставов.
// Важно эхировать именно IPO-позицию, не measured. Если взять measured,
// статическое отклонение не даст выполниться этому условию.
void FRIClient::waitForCommand() void FRIClient::waitForCommand()
{ {
std::lock_guard<std::mutex> lock(data_mutex_); captureData(true);
captureCommandingData(); validateCommanding();
LBRClient::waitForCommand(); // SDK 1.16 mirrors IPO, not measured position.
// Инициализируем цель и фильтр IPO-позицией. sent_pos_ = target_pos_ = current_.ipo_pos;
// Фильтр стартует с IPO — это гарантирует нулевой скачок при переходе в COMMANDING_ACTIVE. initialized_ = true;
std::memcpy(target_pos_.data(), snapshot_.ipo_pos.data(), N_JOINTS * sizeof(double)); last_command_at_ = current_.received_at;
std::memcpy(filtered_pos_.data(), snapshot_.ipo_pos.data(), N_JOINTS * sizeof(double)); publishState();
robotCommand().setJointPosition(filtered_pos_.data());
} }
// Вызывается в COMMANDING_ACTIVE, основной цикл управления
void FRIClient::command() void FRIClient::command()
{ {
std::lock_guard<std::mutex> lock(data_mutex_); captureData(true);
validateCommanding();
// EMA-фильтр применяется ДО захвата снимка — тогда snapshot_.measured_pos = filtered_pos_ if (!initialized_) {
// будет содержать то, что реально отправлено роботу в этом цикле (не прошлом). sent_pos_ = target_pos_ = current_.ipo_pos;
// Это соответствует lbr_fri_ros2_stack: снимок захватывается post-EMA. last_command_at_ = current_.received_at;
const double dt = robotState().getSampleTime(); initialized_ = true;
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];
} }
robotCommand().setJointPosition(filtered_pos_.data());
// Захватываем снимок ПОСЛЕ EMA: measured_pos = filtered_pos_ = что робот только что получил.
captureCommandingData();
}
void FRIClient::onStateChange(
KUKA::FRI::ESessionState oldState, KUKA::FRI::ESessionState newState)
{
session_state_.store(newState, std::memory_order_relaxed);
RCLCPP_INFO(
rclcpp::get_logger("FRIClient"),
"FRI смена состояния: %s, теперь %s", friStateName(oldState), friStateName(newState));
if (newState == KUKA::FRI::IDLE ||
newState == KUKA::FRI::MONITORING_WAIT ||
newState == KUKA::FRI::MONITORING_READY)
{ {
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;
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");
}
const double step = target_pos_[i] - sent_pos_[i];
sent_pos_[i] += std::clamp(step, -max_velocity_[i] * dt, max_velocity_[i] * dt);
}
robotCommand().setJointPosition(sent_pos_.data());
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 std::array<double, N_JOINTS> & q) void FRIClient::setTargetJointPositions(const Joints & q)
{ {
// До первой команды контроллера интерфейс содержит NaN. for (size_t i = 0; i < N_JOINTS; ++i) {
// Если отправить NaN роботу в COMMANDING_ACTIVE, получим CK_COMPOUND_RETURN_ERROR. if (!std::isfinite(q[i]) || q[i] < lower_[i] || q[i] > upper_[i]) {
for (const auto & v : q) { throw std::invalid_argument("Invalid FRI position command");
if (!std::isfinite(v)) {
return;
} }
} }
std::lock_guard<std::mutex> lock(data_mutex_); std::lock_guard<std::mutex> lock(command_mutex_);
target_pos_ = q; 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
+248 -149
View File
@@ -1,5 +1,10 @@
#include "iiwa_controller/IIWAHardwareInterface.hpp" #include "iiwa_controller/IIWAHardwareInterface.hpp"
#include <algorithm>
#include <arpa/inet.h>
#include <limits>
#include <stdexcept>
#include "friException.h"
#include <chrono> #include <chrono>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
@@ -21,6 +26,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 +74,32 @@ CallbackReturn IIWAHardwareInterface::on_init(
const auto & info = params.hardware_info; const auto & info = params.hardware_info;
try {
robot_ip_ = getParam(info, "robot_ip", "192.170.10.2"); robot_ip_ = getParam(info, "robot_ip", "192.170.10.2");
fri_port_ = std::stoi(getParam(info, "fri_port", "30200")); size_t used = 0;
simulate_ = (getParam(info, "simulate", "false") == "true"); const auto port = getParam(info, "fri_port", "30200");
joint_position_tau_ = std::stod(getParam(info, "joint_position_tau", "0.04")); fri_port_ = std::stoi(port, &used);
joint_velocity_tau_ = std::stod(getParam(info, "joint_velocity_tau", "0.01")); if (used != port.size() || fri_port_ < 1 || fri_port_ > 65535) {
throw std::invalid_argument("Invalid fri_port");
RCLCPP_INFO(rclcpp::get_logger(LOG), }
"on_init: ip=%s port=%d simulate=%s pos_tau=%.3f vel_tau=%.3f", const auto cycle = getParam(info, "fri_cycle_ms", "5");
robot_ip_.c_str(), fri_port_, fri_cycle_ms_ = std::stoi(cycle, &used);
simulate_ ? "true" : "false", if (used != cycle.size() || fri_cycle_ms_ < 1 || fri_cycle_ms_ > 100) {
joint_position_tau_, throw std::invalid_argument("Invalid fri_cycle_ms (expected 1..100)");
joint_velocity_tau_); }
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 +107,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;
} }
@@ -88,7 +165,7 @@ IIWAHardwareInterface::export_unlisted_state_interface_descriptions()
} }
// ── 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 +174,160 @@ CallbackReturn IIWAHardwareInterface::on_configure(const rclcpp_lifecycle::State
return CallbackReturn::SUCCESS; return CallbackReturn::SUCCESS;
} }
fri_client_ = std::make_unique<FRIClient>(joint_position_tau_); releaseFRI();
// 100 мс таймаут recvfrom — поток корректно завершится после disconnect(). fri_client_ = std::make_unique<FRIClient>();
connection_ = std::make_unique<KUKA::FRI::UdpConnection>(100); fri_client_->setLimits(lower_, upper_, max_velocity_);
fri_client_->setExpectedSampleTime(fri_cycle_ms_ * 0.001);
connection_ = std::make_unique<StartupConnection>(fri_running_);
app_ = std::make_unique<KUKA::FRI::ClientApplication>(*connection_, *fri_client_); app_ = std::make_unique<KUKA::FRI::ClientApplication>(*connection_, *fri_client_);
if (!app_->connect(fri_port_, robot_ip_.c_str())) {
RCLCPP_FATAL(rclcpp::get_logger(LOG),
"Не удалось открыть UDP-сокет на порту %d (робот: %s)", fri_port_, robot_ip_.c_str());
return CallbackReturn::ERROR;
}
RCLCPP_INFO(rclcpp::get_logger(LOG),
"UDP-порт %d открыт. Запустите ServerFriRos2 на роботе (%s)...",
fri_port_, robot_ip_.c_str());
return CallbackReturn::SUCCESS; return CallbackReturn::SUCCESS;
} }
// ── on_activate ──────────────────────────────────────────────────────────────── CallbackReturn IIWAHardwareInterface::on_activate(const rclcpp_lifecycle::State & state)
// Получает хэндлы интерфейсов, запускает FRI-поток и ждёт установки сессии.
CallbackReturn IIWAHardwareInterface::on_activate(const rclcpp_lifecycle::State &)
{ {
RCLCPP_INFO(rclcpp::get_logger(LOG), "Активация..."); stopFRI();
velocity_initialized_ = false;
last_snapshot_ = {};
try {
for (size_t i = 0; i < N_JOINTS; ++i) { for (size_t i = 0; i < N_JOINTS; ++i) {
const std::string & jn = info_.joints[i].name; const auto & name = info_.joints[i].name;
h_pos_[i] = get_state_interface_handle(name + "/position");
h_pos_[i] = get_state_interface_handle(jn + "/" + hardware_interface::HW_IF_POSITION); h_vel_[i] = get_state_interface_handle(name + "/velocity");
h_vel_[i] = get_state_interface_handle(jn + "/" + hardware_interface::HW_IF_VELOCITY); h_eff_[i] = get_state_interface_handle(name + "/effort");
h_eff_[i] = get_state_interface_handle(jn + "/" + hardware_interface::HW_IF_EFFORT); h_ext_[i] = get_state_interface_handle(name + "/external_torque");
h_ext_[i] = get_state_interface_handle(jn + "/external_torque"); h_cmd_pos_[i] = get_command_interface_handle(name + "/position");
h_cmd_pos_[i] = get_command_interface_handle(jn + "/" + hardware_interface::HW_IF_POSITION); }
if (!simulate_) {
if (!h_pos_[i] || !h_vel_[i] || !h_eff_[i] || !h_ext_[i] || !h_cmd_pos_[i]) // Recreate SDK sequence counters and buffers for every new session.
if (on_configure(state) != CallbackReturn::SUCCESS ||
!app_->connect(fri_port_, robot_ip_.c_str()))
{ {
RCLCPP_FATAL(rclcpp::get_logger(LOG), releaseFRI();
"Не удалось получить хэндл интерфейса для сустава '%s'. " return CallbackReturn::ERROR;
"Проверьте объявление <state_interface>/<command_interface> в URDF.", }
jn.c_str()); 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; return CallbackReturn::ERROR;
} }
} }
for (size_t i = 0; i < N_JOINTS; ++i) {
double position = 0.0;
if (simulate_) { if (simulate_) {
return CallbackReturn::SUCCESS; get_state(h_pos_[i], position, true);
} if (!std::isfinite(position)) {position = 0.0;}
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 { } else {
RCLCPP_INFO(rclcpp::get_logger(LOG), "FRI сессия установлена!"); position = last_snapshot_.measured_pos[i];
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;
} }
set_state(h_pos_[i], position, true);
previous_session_state_ = fri_client_->getSessionState(); 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_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;
}
active_ = true;
return CallbackReturn::SUCCESS; return CallbackReturn::SUCCESS;
} }
// ── on_deactivate ────────────────────────────────────────────────────────────── void IIWAHardwareInterface::stopFRI()
// Останавливает FRI-поток. FRI-объекты остаются — их очищает on_cleanup(). {
active_ = false;
fri_running_.store(false);
cycle_gate_.stop();
// Receive has a finite timeout. Never close a socket while step() uses it.
if (fri_thread_.joinable()) {fri_thread_.join();}
if (app_) {app_->disconnect();}
}
void IIWAHardwareInterface::releaseFRI()
{
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 {
bool synchronized = false;
while (fri_running_.load(std::memory_order_relaxed)) { const auto startup_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(15);
while (fri_running_.load()) {
if (!app_->step()) { if (!app_->step()) {
RCLCPP_WARN_THROTTLE(rclcpp::get_logger(LOG), throttle_clock_, 2000, throw std::runtime_error("FRI step failed");
"FRI: step() вернул false, возможно потеряли соединение"); }
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) {
RCLCPP_INFO(rclcpp::get_logger(LOG), "FRI поток завершён"); 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);
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 +336,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 +351,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,10 +368,13 @@ 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);
@@ -300,19 +383,23 @@ hardware_interface::return_type IIWAHardwareInterface::read(
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) {
@@ -330,22 +417,34 @@ hardware_interface::return_type IIWAHardwareInterface::read(
hardware_interface::return_type IIWAHardwareInterface::write( hardware_interface::return_type IIWAHardwareInterface::write(
const rclcpp::Time &, const rclcpp::Duration &) const rclcpp::Time &, const rclcpp::Duration &)
{ {
if (simulate_) { if (!active_ || simulate_) {return hardware_interface::return_type::OK;}
return hardware_interface::return_type::OK; if (!fri_client_ || fri_fault_.load() || !fri_running_.load()) {
return hardware_interface::return_type::ERROR;
} }
if (!fri_client_->isCommandingActive()) {
if (fri_client_->getSessionState() != KUKA::FRI::COMMANDING_ACTIVE) { return cycle_gate_.complete() ? hardware_interface::return_type::OK :
return hardware_interface::return_type::OK; hardware_interface::return_type::ERROR;
} }
std::array<double, N_JOINTS> pos_cmd{}; std::array<double, N_JOINTS> pos_cmd{};
for (size_t i = 0; i < N_JOINTS; ++i) { for (size_t i = 0; i < N_JOINTS; ++i) {
get_command(h_cmd_pos_[i], pos_cmd[i], false); if (!get_command(h_cmd_pos_[i], pos_cmd[i], false)) {
cycle_gate_.stop();
return hardware_interface::return_type::ERROR;
} }
if (!std::isfinite(pos_cmd[i]) || pos_cmd[i] < lower_[i] || pos_cmd[i] > upper_[i]) {
fri_fault_.store(true);
fri_running_.store(false);
return hardware_interface::return_type::ERROR;
}
}
try {
fri_client_->setTargetJointPositions(pos_cmd); fri_client_->setTargetJointPositions(pos_cmd);
} catch (const std::exception &) {
return hardware_interface::return_type::OK; cycle_gate_.stop();
return hardware_interface::return_type::ERROR;
}
return cycle_gate_.complete() ? hardware_interface::return_type::OK :
hardware_interface::return_type::ERROR;
} }
} // namespace iiwa_controller } // namespace iiwa_controller
@@ -0,0 +1,112 @@
// Offline protocol peer, not a dynamics or Sunrise safety simulator.
// Both endpoints are hardcoded loopback addresses. Never accepts a robot IP.
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <array>
#include <chrono>
#include <cmath>
#include <fstream>
#include <iostream>
#include <thread>
#include "FRIMessages.pb.h"
#include "pb_encode.h"
#include "pb_decode.h"
#include "pb_frimessages_callbacks.h"
int main(int argc, char ** argv)
{
if (argc != 2) {return 2;}
std::ofstream csv(argv[1]);
csv << "sequence,session,receive_seconds,position,delta\n";
csv.flush();
int fd = socket(AF_INET, SOCK_DGRAM, 0);
sockaddr_in local{}, remote{};
local.sin_family = remote.sin_family = AF_INET;
local.sin_port = remote.sin_port = htons(30283);
inet_pton(AF_INET, "127.0.0.2", &local.sin_addr);
inet_pton(AF_INET, "127.0.0.1", &remote.sin_addr);
if (fd < 0 || bind(fd, reinterpret_cast<sockaddr *>(&local), sizeof(local))) {
perror("fake FRI loopback bind"); return 2;
}
timeval timeout{0, 20000};
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
std::array<double, 7> q{}, zero{}, received{};
std::array<int64_t, 7> drives{};
drives.fill(DriveState_ACTIVE);
tRepeatedDoubleArguments positions{7, 7, q.data()}, zeros{7, 7, zero.data()};
tRepeatedIntArguments drive_values{7, 7, drives.data()};
FRIMonitoringMessage m{};
m.header.messageIdentifier = 0x245142; // Bundled SDK LBRMONITORMESSAGEID.
m.has_robotInfo = m.has_monitorData = m.has_connectionInfo = true;
m.robotInfo.has_numberOfJoints = m.robotInfo.has_safetyState = true;
m.robotInfo.has_operationMode = m.robotInfo.has_controlMode = true;
m.robotInfo.numberOfJoints = 7;
m.robotInfo.safetyState = SafetyState_NORMAL_OPERATION;
m.robotInfo.operationMode = OperationMode_AUTOMATIC_MODE;
m.robotInfo.controlMode = ControlMode_POSITION_CONTROLMODE;
m.robotInfo.driveState.funcs.encode = encode_repeatedInt;
m.robotInfo.driveState.arg = &drive_values;
m.connectionInfo.quality = FRIConnectionQuality_GOOD;
m.connectionInfo.has_sendPeriod = m.connectionInfo.has_receiveMultiplier = true;
m.connectionInfo.sendPeriod = 10;
m.connectionInfo.receiveMultiplier = 1;
auto & md = m.monitorData;
md.has_timestamp = md.has_measuredJointPosition = md.has_commandedJointPosition = true;
md.has_measuredTorque = md.has_commandedTorque = md.has_externalTorque = true;
for (auto * values : {&md.measuredJointPosition, &md.commandedJointPosition, &m.ipoData.jointPosition}) {
values->value.funcs.encode = encode_repeatedDouble;
values->value.arg = &positions;
}
for (auto * values : {&md.measuredTorque, &md.commandedTorque, &md.externalTorque}) {
values->value.funcs.encode = encode_repeatedDouble;
values->value.arg = &zeros;
}
m.ipoData.has_jointPosition = m.ipoData.has_clientCommandMode = true;
m.ipoData.has_overlayType = m.ipoData.has_trackingPerformance = true;
m.ipoData.clientCommandMode = ClientCommandMode_POSITION;
m.ipoData.overlayType = OverlayType_JOINT;
m.ipoData.trackingPerformance = 1.0;
using Clock = std::chrono::steady_clock;
const auto start = Clock::now();
auto next = start;
unsigned replies = 0;
for (unsigned seq = 0; Clock::now() - start < std::chrono::seconds(45); ++seq) {
m.header.sequenceCounter = seq;
m.connectionInfo.sessionState = replies < 20 ? FRISessionState_MONITORING_READY :
replies < 40 ? FRISessionState_COMMANDING_WAIT : FRISessionState_COMMANDING_ACTIVE;
m.has_ipoData = replies >= 20;
md.timestamp.sec = 1 + seq / 100;
md.timestamp.nanosec = (seq % 100) * 10000000;
uint8_t buffer[2048];
auto output = pb_ostream_from_buffer(buffer, sizeof(buffer));
if (!pb_encode(&output, FRIMonitoringMessage_fields, &m)) {return 3;}
sendto(fd, buffer, output.bytes_written, 0, reinterpret_cast<sockaddr *>(&remote), sizeof(remote));
auto count = recv(fd, buffer, sizeof(buffer), 0);
if (count > 0) {
FRICommandMessage command{};
tRepeatedDoubleArguments target{0, 7, received.data()};
command.commandData.jointPosition.value.funcs.decode = decode_repeatedDouble;
command.commandData.jointPosition.value.arg = &target;
auto input = pb_istream_from_buffer(buffer, count);
if (!pb_decode(&input, FRICommandMessage_fields, &command) ||
!command.has_commandData || !command.commandData.has_jointPosition) {return 4;}
for (double value : received) {if (!std::isfinite(value)) {return 5;}}
const double delta = received[0] - q[0];
csv << seq << ',' << m.connectionInfo.sessionState << ',' <<
std::chrono::duration<double>(Clock::now() - start).count() << ',' << received[0] << ',' << delta << '\n';
csv.flush();
q = received;
m.header.reflectedSequenceCounter = command.header.sequenceCounter;
++replies;
} else if (replies) {
std::cerr << "Reply timeout after " << replies << " frames\n";
close(fd); return 6;
}
// Before connection, avoid catch-up bursts after receive timeouts.
next = replies ? next + std::chrono::milliseconds(10) : Clock::now();
std::this_thread::sleep_until(next);
}
close(fd);
return 0;
}
@@ -0,0 +1,20 @@
// Test-only interposer: allow the unmodified SDK and fake robot to share one host.
// The SDK otherwise binds all local addresses at the robot's source port.
#include <arpa/inet.h>
#include <dlfcn.h>
#include <sys/socket.h>
extern "C" int bind(int fd, const sockaddr * address, socklen_t length)
{
using Bind = int (*)(int, const sockaddr *, socklen_t);
static auto real_bind = reinterpret_cast<Bind>(dlsym(RTLD_NEXT, "bind"));
if (address->sa_family == AF_INET && length == sizeof(sockaddr_in)) {
auto local = *reinterpret_cast<const sockaddr_in *>(address);
// Dedicated smoke-test port only; never alter DDS or other application binds.
if (local.sin_port == htons(30283) && local.sin_addr.s_addr == htonl(INADDR_ANY)) {
inet_pton(AF_INET, "127.0.0.1", &local.sin_addr);
return real_bind(fd, reinterpret_cast<const sockaddr *>(&local), sizeof(local));
}
}
return real_bind(fd, address, length);
}
+83
View File
@@ -0,0 +1,83 @@
"""Manual bounded real-SDK/JTC integration check over loopback only.
Run after building/sourcing iiwa_controller; never connects physical hardware.
"""
import csv
import sys
import time
import os
import signal
import subprocess
import tempfile
from pathlib import Path
import xacro
import yaml
root = Path(__file__).resolve().parents[3]
work = Path(tempfile.mkdtemp(prefix='iiwa-fri-smoke-'))
env = dict(os.environ, ROS_DOMAIN_ID='83', ROS_LOCALHOST_ONLY='1', ROS_LOG_DIR=str(work / 'logs'), ROS_HOME=str(work / 'ros'))
env['CYCLONEDDS_URI'] = '<CycloneDDS><Domain><General><Interfaces><NetworkInterface name="lo"/></Interfaces><AllowMulticast>false</AllowMulticast></General><Discovery><Peers><Peer Address="127.0.0.1"/></Peers></Discovery></Domain></CycloneDDS>'
xml = xacro.process_file(str(root / 'src/iiwa_description/urdf/iiwa7.urdf.xacro'), mappings={'robot_ip': '127.0.0.2', 'fri_port': '30283', 'fri_cycle_ms': '10'}).toxml()
assert '<param name="simulate">false</param>' in xml
assert '<param name="robot_ip">127.0.0.2</param>' in xml
assert '<param name="fri_port">30283</param>' in xml
build = Path(sys.argv[1]) if len(sys.argv) > 1 else root / 'build/iiwa_controller'
server = build / 'fake_fri_server'
shim = build / 'libfri_loopback_bind.so'
assert server.is_file() and shim.is_file(), 'Build the test harness first'
trace = work / 'commands.csv'
params = work / 'robot.yaml'
params.write_text(yaml.safe_dump({'/**': {'ros__parameters': {'robot_description': xml}}}))
processes = []
handles = []
try:
server_log = (work / 'fake_fri_server.log').open('w')
handles.append(server_log)
processes.append(subprocess.Popen([str(server), str(trace)], stdout=server_log, stderr=subprocess.STDOUT, start_new_session=True))
time.sleep(0.1)
assert processes[0].poll() is None, 'Fake peer failed to bind'
for package, executable, extra in [
('robot_state_publisher', 'robot_state_publisher', []),
('controller_manager', 'ros2_control_node', ['--params-file', str(root / 'src/iiwa_config/config/moveit/iiwa_controller.yaml'), '-p', 'update_rate:=100', '-p', 'hardware_synchronization.expect_blocking_read_write:=true']),
]:
log = (work / (executable + '.log')).open('w')
handles.append(log)
processes.append(subprocess.Popen(['ros2', 'run', package, executable, '--ros-args', '--params-file', str(params)] + extra,
env=dict(env, **({'LD_PRELOAD': str(shim)} if executable == 'ros2_control_node' else {})), stdout=log, stderr=subprocess.STDOUT, start_new_session=True))
for name in ['joint_state_broadcaster', 'iiwa_arm_controller']:
result = subprocess.run(['ros2', 'run', 'controller_manager', 'spawner', name, '--controller-manager-timeout', '15'],
env=env, text=True, capture_output=True, timeout=25)
print(result.stdout, result.stderr, flush=True)
assert result.returncode == 0, name
result = subprocess.run(['ros2', 'control', 'list_controllers'], env=env, text=True, capture_output=True, timeout=10)
print(result.stdout, result.stderr, flush=True)
assert result.returncode == 0 and 'iiwa_arm_controller' in result.stdout and 'active' in result.stdout
initial = yaml.safe_load((root / 'src/iiwa_config/config/moveit/initial_positions.yaml').read_text())['initial_positions']
positions = [float(initial[f'joint{i}']) for i in range(1, 8)]
positions[0] += 0.02
goal = {'trajectory': {'joint_names': [f'joint{i}' for i in range(1, 8)],
'points': [{'positions': positions, 'time_from_start': {'sec': 2, 'nanosec': 0}}]}}
result = subprocess.run(['ros2', 'action', 'send_goal', '/iiwa_arm_controller/follow_joint_trajectory',
'control_msgs/action/FollowJointTrajectory', yaml.safe_dump(goal)],
env=env, text=True, capture_output=True, timeout=15)
print(result.stdout, result.stderr, flush=True)
assert result.returncode == 0 and 'SUCCEEDED' in result.stdout
rows = list(csv.DictReader(trace.open()))
assert len(rows) > 100 and processes[0].poll() is None, 'FRI stream stopped'
active = [r for r in rows if int(r['session']) == 4]
assert active and abs(float(active[-1]['position']) - 0.02) < 0.001
deltas = [abs(float(r['delta'])) for r in active]
intervals = [float(b['receive_seconds']) - float(a['receive_seconds']) for a, b in zip(active, active[1:])]
assert max(deltas) <= 1.71 * 0.01 + 1e-6, 'Command exceeds per-frame velocity limit'
print(f'SMOKE PASS: real SDK loopback + actual CM/JTC action succeeded; {len(active)} active frames, max delta={max(deltas):.8f} rad; receive interval min/max={min(intervals):.6f}/{max(intervals):.6f} s.', flush=True)
finally:
for process in processes:
if process.poll() is None:
os.killpg(process.pid, signal.SIGINT)
for process in processes:
try: process.wait(timeout=5)
except subprocess.TimeoutExpired: os.killpg(process.pid, signal.SIGKILL)
for handle in handles: handle.close()
for log in work.glob('*.log'):
print('\n' + log.name + '\n' + log.read_text()[-4500:])
print('Logs:', work)
+59
View File
@@ -0,0 +1,59 @@
"""Manual local JTC integration check; never opens a FRI connection."""
import os
import signal
import subprocess
import tempfile
from pathlib import Path
import xacro
import yaml
root = Path(__file__).resolve().parents[3]
work = Path(tempfile.mkdtemp(prefix='iiwa-controller-smoke-'))
env = dict(os.environ, ROS_DOMAIN_ID='83', ROS_LOCALHOST_ONLY='1', ROS_LOG_DIR=str(work / 'logs'), ROS_HOME=str(work / 'ros'))
env['CYCLONEDDS_URI'] = '<CycloneDDS><Domain><General><Interfaces><NetworkInterface name="lo"/></Interfaces><AllowMulticast>false</AllowMulticast></General><Discovery><Peers><Peer Address="127.0.0.1"/></Peers></Discovery></Domain></CycloneDDS>'
xml = xacro.process_file(str(root / 'src/iiwa_description/urdf/iiwa7.urdf.xacro')).toxml()
assert '<param name="simulate">false</param>' in xml
# Exercise our plugin's explicit local simulation path, with no FRI socket.
xml = xml.replace('<param name="simulate">false</param>', '<param name="simulate">true</param>')
params = work / 'robot.yaml'
params.write_text(yaml.safe_dump({'/**': {'ros__parameters': {'robot_description': xml}}}))
processes = []
handles = []
try:
for package, executable, extra in [
('robot_state_publisher', 'robot_state_publisher', []),
('controller_manager', 'ros2_control_node', ['--params-file', str(root / 'src/iiwa_config/config/moveit/iiwa_controller.yaml')]),
]:
log = (work / (executable + '.log')).open('w')
handles.append(log)
processes.append(subprocess.Popen(['ros2', 'run', package, executable, '--ros-args', '--params-file', str(params)] + extra,
env=env, stdout=log, stderr=subprocess.STDOUT, start_new_session=True))
for name in ['joint_state_broadcaster', 'iiwa_arm_controller']:
result = subprocess.run(['ros2', 'run', 'controller_manager', 'spawner', name, '--controller-manager-timeout', '15'],
env=env, text=True, capture_output=True, timeout=25)
print(result.stdout, result.stderr, flush=True)
assert result.returncode == 0, name
result = subprocess.run(['ros2', 'control', 'list_controllers'], env=env, text=True, capture_output=True, timeout=10)
print(result.stdout, result.stderr, flush=True)
assert result.returncode == 0 and 'iiwa_arm_controller' in result.stdout and 'active' in result.stdout
initial = yaml.safe_load((root / 'src/iiwa_config/config/moveit/initial_positions.yaml').read_text())['initial_positions']
positions = [float(initial[f'joint{i}']) for i in range(1, 8)]
positions[0] += 0.02
goal = {'trajectory': {'joint_names': [f'joint{i}' for i in range(1, 8)],
'points': [{'positions': positions, 'time_from_start': {'sec': 2, 'nanosec': 0}}]}}
result = subprocess.run(['ros2', 'action', 'send_goal', '/iiwa_arm_controller/follow_joint_trajectory',
'control_msgs/action/FollowJointTrajectory', yaml.safe_dump(goal)],
env=env, text=True, capture_output=True, timeout=15)
print(result.stdout, result.stderr, flush=True)
assert result.returncode == 0 and 'SUCCEEDED' in result.stdout
print('SMOKE PASS: actual URDF, iiwa hardware plugin, broadcaster and JTC active; no FRI connection.', flush=True)
finally:
for process in processes:
os.killpg(process.pid, signal.SIGINT)
for process in processes:
try: process.wait(timeout=5)
except subprocess.TimeoutExpired: os.killpg(process.pid, signal.SIGKILL)
for handle in handles: handle.close()
for log in work.glob('*.log'):
print('\n' + log.name + '\n' + log.read_text()[-4500:])
print('Logs:', work)
@@ -0,0 +1,262 @@
#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:
ClientFixture() : FRIClient(), 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, PositionCommandsHaveNoEmaDelay)
{
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);
EXPECT_DOUBLE_EQ(client.sentPosition(), 0.41);
EXPECT_DOUBLE_EQ(client.getStateSnapshot().measured_pos[0], 0.2);
}
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.401);
}
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);
for (size_t frame = 1; frame < sent.size(); ++frame) {
EXPECT_NEAR(sent[frame] - sent[frame - 1], 0.001, 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,93 @@
#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, 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);
}
+2 -4
View File
@@ -6,8 +6,7 @@
<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="joint_position_tau" default="0.04"/> <xacro:arg name="fri_cycle_ms" default="5"/>
<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 +144,8 @@
<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="fri_cycle_ms">$(arg fri_cycle_ms)</param>
<param name="simulate">false</param> <param name="simulate">false</param>
<param name="joint_position_tau">$(arg joint_position_tau)</param>
<param name="joint_velocity_tau">$(arg joint_velocity_tau)</param>
</hardware> </hardware>
<joint name="joint1"> <joint name="joint1">
@@ -15,8 +15,6 @@ class RobotCfg:
port: int port: int
description: str description: str
fri_cycle_ms: int fri_cycle_ms: int
joint_position_tau: float
active_controller: str # "jtc" | "forward"
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -290,8 +288,6 @@ def build_settings(settings_path: str, check_files: bool = True) -> Settings:
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)),
active_controller=str(robot_raw.get("active_controller", "jtc")),
) )
# digital_twin # digital_twin