feat: update version and dependencies in setup.py; add privilege management and process handling modules
- Updated version from 2026.05.31 to 2026.06.11 in setup.py - Replaced 'textual' with 'rich' in install_requires - Added privilege.py for managing sudo privileges with a keep-alive mechanism - Introduced process.py for handling subprocesses with enhanced control and output streaming - Created ui.py for unified console interactions and user prompts
This commit is contained in:
+159
-242
@@ -2,17 +2,13 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, List, Optional, Tuple
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from ruamel.yaml import YAML
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Footer, Static
|
||||
|
||||
from cobot.tui import SCREEN_CSS, InputScreen, PickScreen
|
||||
from cobot import ui
|
||||
|
||||
_PROJECT_DIR = Path(__file__).parent.parent.parent
|
||||
_CONFIG_PATH = _PROJECT_DIR / "cobot-setting.yaml"
|
||||
@@ -23,34 +19,28 @@ _TOOL_ACTIVE_XACRO = (
|
||||
)
|
||||
_SRDF_PATH = _PROJECT_DIR / "src" / "iiwa_config" / "config" / "moveit" / "iiwa7.srdf"
|
||||
|
||||
# Use ruamel.yaml instead of PyYAML so comments and formatting in the config file are preserved.
|
||||
# Use ruamel.yaml instead of PyYAML so comments and formatting in the config are preserved.
|
||||
# Используем ruamel.yaml вместо PyYAML, чтобы комментарии и форматирование в конфиге сохранялись.
|
||||
_yaml = YAML()
|
||||
_yaml.preserve_quotes = True
|
||||
|
||||
|
||||
# One question inside a configuration block.
|
||||
# A field can either show a pick list (options) or a free-text input (no options).
|
||||
# Один вопрос внутри блока конфигурации.
|
||||
# Поле может показывать список вариантов (options) или поле для ввода текста (без options).
|
||||
@dataclass
|
||||
class _Field:
|
||||
key: str # dot-separated path within the block, e.g. "webots.world"
|
||||
question: str
|
||||
default: Any
|
||||
note: str = ""
|
||||
options: Optional[List[str]] = None # if set - PickScreen, else - InputScreen
|
||||
options: Optional[List[str]] = None # if set - select(), else - text()
|
||||
|
||||
def label(self) -> str:
|
||||
return self.key.split(".")[-1]
|
||||
|
||||
|
||||
# A group of related fields shown together under one "Configure X?" question.
|
||||
# Группа связанных полей, показываемая вместе под одним вопросом "Настроить X?".
|
||||
@dataclass
|
||||
class _Block:
|
||||
yaml_key: str # top-level key in cobot-setting.yaml
|
||||
title: str # shown in "Configure <title>?" prompt
|
||||
title: str # shown in "Настроить <title>?" prompt
|
||||
fields: List[_Field]
|
||||
|
||||
|
||||
@@ -76,15 +66,10 @@ def _build_tool_block() -> Optional[_Block]:
|
||||
)
|
||||
return _Block(
|
||||
yaml_key="tool",
|
||||
title="Tool / End-effector",
|
||||
title="Инструмент / Захват",
|
||||
fields=[
|
||||
_Field(
|
||||
"active",
|
||||
"Выберите активный инструмент:",
|
||||
options[0],
|
||||
note=labels,
|
||||
options=options,
|
||||
),
|
||||
_Field("active", "Выберите активный инструмент:", options[0],
|
||||
note=labels, options=options),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -96,76 +81,83 @@ _BLOCKS: List[_Block] = [
|
||||
yaml_key="foxglove",
|
||||
title="Foxglove bridge",
|
||||
fields=[
|
||||
_Field("enabled", "Enable Foxglove bridge?", "true",
|
||||
note="Start foxglove_bridge alongside the robot node",
|
||||
_Field("enabled", "Включить Foxglove bridge?", "true",
|
||||
note="Запускать foxglove_bridge вместе с узлом робота",
|
||||
options=["true", "false"]),
|
||||
_Field("port", "WebSocket port:", "8765",
|
||||
note="Port Foxglove Studio connects to (default 8765)"),
|
||||
_Field("address", "Listen address:", "0.0.0.0",
|
||||
note="0.0.0.0 = all interfaces, 127.0.0.1 = localhost only",
|
||||
_Field("port", "Порт WebSocket:", "8765",
|
||||
note="Порт, к которому подключается Foxglove Studio (по умолчанию 8765)"),
|
||||
_Field("address", "Адрес прослушивания:", "0.0.0.0",
|
||||
note="0.0.0.0 = все интерфейсы, 127.0.0.1 = только localhost",
|
||||
options=["0.0.0.0", "127.0.0.1"]),
|
||||
_Field("use_sim_time", "Use simulation time (/clock)?", "false",
|
||||
note="Subscribe to /clock instead of using wall time",
|
||||
_Field("use_sim_time", "Использовать симуляционное время (/clock)?", "false",
|
||||
note="Подписываться на /clock вместо системного времени",
|
||||
options=["false", "true"]),
|
||||
_Field("debug", "Enable verbose bridge logging?", "false",
|
||||
_Field("debug", "Подробное логирование bridge?", "false",
|
||||
options=["false", "true"]),
|
||||
_Field("num_threads", "Executor threads (0 = auto):", "0"),
|
||||
_Field("num_threads", "Потоки executor (0 = авто):", "0"),
|
||||
],
|
||||
),
|
||||
_Block(
|
||||
yaml_key="web",
|
||||
title="Веб-сервер (FastAPI)",
|
||||
fields=[
|
||||
_Field("enabled", "Включить веб-сервер?", "true",
|
||||
note="Запускать FastAPI-сервер для HTTP/WebSocket-управления",
|
||||
options=["true", "false"]),
|
||||
_Field("host", "Адрес прослушивания:", "0.0.0.0",
|
||||
note="0.0.0.0 = все интерфейсы, 127.0.0.1 = только localhost",
|
||||
options=["0.0.0.0", "127.0.0.1"]),
|
||||
_Field("port", "Порт HTTP:", "8007",
|
||||
note="Порт FastAPI-сервера (по умолчанию 8007)"),
|
||||
],
|
||||
),
|
||||
_Block(
|
||||
yaml_key="planning",
|
||||
title="MoveIt planning",
|
||||
title="MoveIt планирование",
|
||||
fields=[
|
||||
_Field("planning_group", "Planning group:", "iiwa_arm",
|
||||
note="MoveIt planning group as defined in the SRDF"),
|
||||
_Field("default_frame", "Default reference frame:", "base_link"),
|
||||
_Field("default_planner", "Default planner:", "ompl",
|
||||
_Field("planning_group", "Группа планирования:", "iiwa_arm",
|
||||
note="Группа планирования MoveIt из SRDF"),
|
||||
_Field("default_frame", "Система отсчёта по умолчанию:", "base_link"),
|
||||
_Field("default_planner", "Планировщик по умолчанию:", "ompl",
|
||||
options=["ompl", "pilz_industrial_motion_planner", "chomp"]),
|
||||
_Field("planning_attempts", "Planning attempts:", "3"),
|
||||
_Field("planning_attempts", "Попыток планирования:", "3"),
|
||||
],
|
||||
),
|
||||
_Block(
|
||||
yaml_key="digital_twin",
|
||||
title="Digital twin (Webots / RViz)",
|
||||
title="Цифровой двойник (Webots / RViz)",
|
||||
fields=[
|
||||
_Field("webots.transform", "Robot transform in Webots scene (x y z, metres):", "-0.25 0 0.79"),
|
||||
_Field("webots.rotation", "Robot rotation in Webots scene (ax ay az angle):", "0 0 1 0"),
|
||||
_Field("webots.controller_timer", "Webots controller step timer (ms):", "50"),
|
||||
_Field("webots.transform", "Трансформ робота в сцене Webots (x y z, метры):", "-0.25 0 0.79"),
|
||||
_Field("webots.rotation", "Поворот робота в сцене Webots (ax ay az угол):", "0 0 1 0"),
|
||||
_Field("webots.controller_timer", "Шаг таймера контроллера Webots (мс):", "50"),
|
||||
],
|
||||
),
|
||||
_Block(
|
||||
yaml_key="robot",
|
||||
title="Robot connection",
|
||||
title="Подключение робота",
|
||||
fields=[
|
||||
_Field("name", "Robot model name:", "iiwa7"),
|
||||
_Field("ip", "Robot IP address:", "192.170.10.2",
|
||||
note="IP of the KUKA controller on the FRI network interface"),
|
||||
_Field("port", "FRI port:", "30200"),
|
||||
_Field("command_mode", "Command mode:", "position",
|
||||
note="position = joint position control, torque = joint torque control",
|
||||
options=["position", "torque"]),
|
||||
_Field("fri_cycle_ms", "FRI cycle time (ms):", "10",
|
||||
note="5 ms = 200 Hz, 10 ms = 100 Hz",
|
||||
_Field("name", "Имя модели робота:", "iiwa7"),
|
||||
_Field("ip", "IP-адрес робота:", "192.170.10.2",
|
||||
note="IP контроллера KUKA на сетевом интерфейсе FRI"),
|
||||
_Field("port", "Порт FRI:", "30200"),
|
||||
_Field("fri_cycle_ms", "Цикл FRI (мс):", "10",
|
||||
note="5 мс = 200 Гц, 10 мс = 100 Гц",
|
||||
options=["10", "5"]),
|
||||
_Field("active_controller", "Active ROS controller:", "jtc",
|
||||
_Field("active_controller", "Активный ROS-контроллер:", "jtc",
|
||||
note="jtc = JointTrajectoryController (MoveIt), forward = ForwardCommandController",
|
||||
options=["jtc", "forward"]),
|
||||
_Field("joint_position_tau", "Position EMA filter tau (s):", "0.04",
|
||||
note="Smooths position commands before sending to FRI"),
|
||||
_Field("joint_velocity_tau", "Velocity EMA filter tau (s):", "0.01",
|
||||
note="Removes spikes from finite-difference velocity estimation"),
|
||||
_Field("joint_position_tau", "EMA tau фильтра положения (с):", "0.04",
|
||||
note="Сглаживает команды положения перед отправкой в FRI"),
|
||||
_Field("joint_velocity_tau", "EMA tau фильтра скорости (с):", "0.01",
|
||||
note="Убирает выбросы из оценки скорости конечной разностью"),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
# Try to keep the original YAML type (bool, int, float) when saving a value back.
|
||||
# Trying to preserve type prevents "true" from becoming a plain string in the YAML file.
|
||||
# Пытаемся сохранить исходный тип YAML (bool, int, float) при записи значения обратно.
|
||||
# Сохранение типа предотвращает превращение "true" в обычную строку в YAML-файле.
|
||||
def _coerce(value: str, original: Any) -> Any:
|
||||
"""Convert a string value to match the type of the original YAML value (bool, int, float, str).
|
||||
Преобразует строковое значение к типу исходного значения YAML (bool, int, float, str).
|
||||
"""Convert a string value to match the type of the original YAML value.
|
||||
Преобразует строковое значение к типу исходного значения YAML.
|
||||
"""
|
||||
if isinstance(original, bool):
|
||||
return value.lower() == "true"
|
||||
@@ -182,11 +174,9 @@ def _coerce(value: str, original: Any) -> Any:
|
||||
return value
|
||||
|
||||
|
||||
# Read a value from a nested YAML mapping using a dot-separated key like "webots.transform".
|
||||
# Читаем значение из вложенного YAML-словаря по ключу с точками, например "webots.transform".
|
||||
def _get_nested(mapping: Any, path: str) -> Any:
|
||||
"""Return the value at a dot-separated path inside a nested YAML mapping, or None if missing.
|
||||
Возвращает значение по пути с точками внутри вложенного YAML-словаря, или None если отсутствует.
|
||||
"""Return the value at a dot-separated path inside a nested YAML mapping, or None.
|
||||
Возвращает значение по пути с точками внутри вложенного YAML-словаря, или None.
|
||||
"""
|
||||
keys = path.split(".")
|
||||
cur = mapping
|
||||
@@ -197,172 +187,108 @@ def _get_nested(mapping: Any, path: str) -> Any:
|
||||
return cur
|
||||
|
||||
|
||||
# Write a value into a nested YAML mapping using a dot-separated key.
|
||||
# Записываем значение в вложенный YAML-словарь по ключу с точками.
|
||||
def _infer(value: str) -> Any:
|
||||
"""Infer a bool / int / float / str from a raw string when there is no original
|
||||
value to match the type against (i.e. the key is new in the config).
|
||||
Выводит bool / int / float / str из строки, когда нет исходного значения для
|
||||
сопоставления типа (т.е. ключ новый в конфиге).
|
||||
"""
|
||||
low = value.strip().lower()
|
||||
if low in ("true", "false"):
|
||||
return low == "true"
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
pass
|
||||
return value
|
||||
|
||||
|
||||
def _set_nested(mapping: Any, path: str, value: Any) -> None:
|
||||
"""Set the value at a dot-separated path inside a nested YAML mapping, coercing type to match.
|
||||
Устанавливает значение по пути с точками во вложенном YAML-словаре, приводя тип к исходному.
|
||||
"""Set the value at a dot-separated path, creating missing intermediate maps.
|
||||
|
||||
If the leaf key already exists its type is preserved via _coerce; otherwise the
|
||||
type is inferred from the string with _infer. This makes the wizard tolerant of
|
||||
configs that do not yet contain every field (e.g. an older cobot-setting.yaml).
|
||||
|
||||
Устанавливает значение по пути с точками, создавая отсутствующие промежуточные
|
||||
словари. Если конечный ключ уже есть — тип сохраняется через _coerce; иначе тип
|
||||
выводится из строки через _infer. Это делает мастер устойчивым к конфигам, где
|
||||
ещё нет всех полей (например, более старый cobot-setting.yaml).
|
||||
"""
|
||||
keys = path.split(".")
|
||||
cur = mapping
|
||||
for k in keys[:-1]:
|
||||
if k not in cur or cur[k] is None:
|
||||
cur[k] = {}
|
||||
cur = cur[k]
|
||||
original = cur[keys[-1]]
|
||||
cur[keys[-1]] = _coerce(value, original)
|
||||
leaf = keys[-1]
|
||||
if leaf in cur and cur[leaf] is not None:
|
||||
cur[leaf] = _coerce(value, cur[leaf])
|
||||
else:
|
||||
cur[leaf] = _infer(value)
|
||||
|
||||
|
||||
# Shown after all blocks have been configured to confirm the file was saved.
|
||||
# Показывается после настройки всех блоков для подтверждения сохранения файла.
|
||||
class _SavedScreen(Screen[None]):
|
||||
"""Confirmation screen shown after all configuration blocks are saved. Press Enter to close.
|
||||
Экран подтверждения, показываемый после сохранения всех блоков конфигурации. Enter для закрытия.
|
||||
"""
|
||||
BINDINGS = [Binding("enter,escape", "close", "Close")]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Static("Done", id="step")
|
||||
yield Static(f"Configuration saved to {_CONFIG_PATH.name}", id="question")
|
||||
yield Static("Press Enter to close.", id="note")
|
||||
yield Footer()
|
||||
|
||||
def action_close(self) -> None:
|
||||
self.dismiss(None)
|
||||
|
||||
|
||||
# The main configuration wizard. Goes through each block in order.
|
||||
# For each block it first asks "Configure X?" then steps through all its fields.
|
||||
# Главный мастер конфигурации. Проходит по каждому блоку по порядку.
|
||||
# Для каждого блока сначала спрашивает "Настроить X?" а затем проходит по всем его полям.
|
||||
class _Wizard(App[None]):
|
||||
"""Configuration wizard that iterates over all _BLOCKS. For each block it asks
|
||||
"Configure X?" and if confirmed steps through every field with PickScreen or InputScreen.
|
||||
Saves to cobot-setting.yaml when all blocks are done and shows _SavedScreen.
|
||||
Мастер конфигурации, проходящий по всем _BLOCKS. Для каждого блока спрашивает
|
||||
"Настроить X?" и при подтверждении проходит по всем полям через PickScreen или InputScreen.
|
||||
Сохраняет в cobot-setting.yaml по завершении и показывает _SavedScreen.
|
||||
"""
|
||||
CSS = SCREEN_CSS
|
||||
|
||||
def __init__(self, data: Any, extra_blocks: Optional[List[_Block]] = None):
|
||||
super().__init__()
|
||||
self._data = data
|
||||
self._blocks = list(extra_blocks or []) + list(_BLOCKS)
|
||||
self._block_idx = 0
|
||||
self._field_idx = 0
|
||||
self._current_block: Optional[_Block] = None
|
||||
self._pending_fields: List[_Field] = []
|
||||
self.did_save = False
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self._next_block()
|
||||
|
||||
def _next_block(self) -> None:
|
||||
if self._block_idx >= len(self._blocks):
|
||||
# All blocks done - save and show the confirmation screen.
|
||||
# Все блоки пройдены - сохраняем и показываем экран подтверждения.
|
||||
_save_config(self._data)
|
||||
self.did_save = True
|
||||
self.push_screen(_SavedScreen(), lambda _: self.exit())
|
||||
return
|
||||
block = self._blocks[self._block_idx]
|
||||
total = len(self._blocks)
|
||||
step = f"Block {self._block_idx + 1} of {total}"
|
||||
self.push_screen(
|
||||
PickScreen(
|
||||
step,
|
||||
f"Configure {block.title}?",
|
||||
["Yes", "No"],
|
||||
"Yes",
|
||||
),
|
||||
lambda v: self._got_block_choice(v, block),
|
||||
)
|
||||
|
||||
def _got_block_choice(self, v: Optional[str], block: _Block) -> None:
|
||||
if v is None:
|
||||
self.exit()
|
||||
return
|
||||
self._block_idx += 1
|
||||
if v == "Yes":
|
||||
self._current_block = block
|
||||
self._pending_fields = list(block.fields)
|
||||
self._field_idx = 0
|
||||
self._next_field()
|
||||
else:
|
||||
# Skip all fields in this block and jump to the next block.
|
||||
# Пропускаем все поля этого блока и переходим к следующему.
|
||||
self._next_block()
|
||||
|
||||
def _next_field(self) -> None:
|
||||
if not self._pending_fields:
|
||||
self._next_block()
|
||||
return
|
||||
|
||||
f = self._pending_fields[0]
|
||||
block = self._current_block
|
||||
total_blocks = len(self._blocks)
|
||||
block_num = self._block_idx # already incremented
|
||||
self._field_idx += 1
|
||||
field_num = self._field_idx
|
||||
total_fields = len(block.fields)
|
||||
|
||||
step = f"Block {block_num} of {total_blocks} - Field {field_num} of {total_fields}"
|
||||
|
||||
# Resolve current value from loaded YAML as the pre-filled default
|
||||
yaml_val = _get_nested(self._data[block.yaml_key], f.key)
|
||||
current = str(yaml_val) if yaml_val is not None else f.default
|
||||
|
||||
if f.options:
|
||||
# Make the current value the default selection
|
||||
default_opt = current if current in f.options else f.options[0]
|
||||
screen = PickScreen(step, f.question, f.options, default_opt, note=f.note)
|
||||
else:
|
||||
screen = InputScreen(step, f.question, current, note=f.note)
|
||||
|
||||
self.push_screen(screen, lambda v, _f=f: self._got_field(v, _f))
|
||||
|
||||
def _got_field(self, v: Optional[str], f: _Field) -> None:
|
||||
if v is None:
|
||||
self.exit()
|
||||
return
|
||||
block = self._current_block
|
||||
_set_nested(self._data[block.yaml_key], f.key, v)
|
||||
# Remove the field we just handled and move on to the next one.
|
||||
# Удаляем только что обработанное поле и переходим к следующему.
|
||||
self._pending_fields.pop(0)
|
||||
self._next_field()
|
||||
|
||||
|
||||
# Load the config file preserving all comments and key order.
|
||||
# Загружаем конфиг-файл, сохраняя все комментарии и порядок ключей.
|
||||
def _load_config() -> Any:
|
||||
"""Load cobot-setting.yaml with ruamel.yaml, preserving comments and key order.
|
||||
Загружает cobot-setting.yaml с помощью ruamel.yaml, сохраняя комментарии и порядок ключей.
|
||||
"""Load cobot-setting.yaml preserving comments and key order.
|
||||
Загружает cobot-setting.yaml, сохраняя комментарии и порядок ключей.
|
||||
"""
|
||||
with open(_CONFIG_PATH, "r", encoding="utf-8") as fh:
|
||||
return _yaml.load(fh)
|
||||
|
||||
|
||||
# Write the modified config back to disk preserving comments and formatting.
|
||||
# Записываем изменённый конфиг обратно на диск, сохраняя комментарии и форматирование.
|
||||
def _save_config(data: Any) -> None:
|
||||
"""Write the modified YAML data back to cobot-setting.yaml, preserving comments.
|
||||
Записывает изменённые данные YAML обратно в cobot-setting.yaml, сохраняя комментарии.
|
||||
"""Write the modified YAML back to cobot-setting.yaml, preserving comments.
|
||||
Записывает изменённый YAML обратно в cobot-setting.yaml, сохраняя комментарии.
|
||||
"""
|
||||
with open(_CONFIG_PATH, "w", encoding="utf-8") as fh:
|
||||
_yaml.dump(data, fh)
|
||||
|
||||
|
||||
def _run_wizard(data: Any, blocks: List[_Block]) -> bool:
|
||||
"""Walk every block: ask "Configure X?", and if yes step through its fields.
|
||||
Returns True if the user completed the wizard (config was saved), False on cancel.
|
||||
Проходит по каждому блоку: спрашивает "Настроить X?", и если да — проходит по полям.
|
||||
Возвращает True если мастер завершён (конфиг сохранён), False при отмене.
|
||||
"""
|
||||
total = len(blocks)
|
||||
for bi, block in enumerate(blocks):
|
||||
ui.header(f"Блок {bi + 1}/{total}", block.title)
|
||||
if not ui.confirm(f"Настроить {block.title}?", default=True):
|
||||
continue
|
||||
for fi, f in enumerate(block.fields):
|
||||
yaml_val = _get_nested(data[block.yaml_key], f.key)
|
||||
current = str(yaml_val) if yaml_val is not None else str(f.default)
|
||||
step_note = f"Поле {fi + 1}/{len(block.fields)}"
|
||||
note = f"{step_note}\n{f.note}" if f.note else step_note
|
||||
if f.options:
|
||||
default_opt = current if current in f.options else f.options[0]
|
||||
value = ui.select(f.question, f.options, default_opt, note=note)
|
||||
else:
|
||||
value = ui.text(f.question, current, note=note)
|
||||
if value is None:
|
||||
ui.info("[yellow]Отменено.[/yellow]")
|
||||
return False
|
||||
_set_nested(data[block.yaml_key], f.key, value)
|
||||
|
||||
_save_config(data)
|
||||
ui.done(True, f"Конфигурация сохранена в {_CONFIG_PATH.name}")
|
||||
return True
|
||||
|
||||
|
||||
def register(subparsers: argparse._SubParsersAction) -> None:
|
||||
p = subparsers.add_parser("robot-setup", help="Configure cobot-setting.yaml interactively")
|
||||
p.set_defaults(func=run)
|
||||
|
||||
|
||||
def run(args: argparse.Namespace) -> None:
|
||||
from rich.console import Console
|
||||
console = Console()
|
||||
ui.header("Настройка робота", "cobot-setting.yaml")
|
||||
|
||||
if not _CONFIG_PATH.exists():
|
||||
console.print(f"[red]Config not found:[/red] {_CONFIG_PATH}")
|
||||
ui.error(f"Конфиг не найден: {_CONFIG_PATH}")
|
||||
sys.exit(1)
|
||||
|
||||
data = _load_config()
|
||||
@@ -372,46 +298,37 @@ def run(args: argparse.Namespace) -> None:
|
||||
data["tool"] = {"active": "patron"}
|
||||
|
||||
tool_block = _build_tool_block()
|
||||
extra = [tool_block] if tool_block else []
|
||||
blocks = ([tool_block] if tool_block else []) + list(_BLOCKS)
|
||||
|
||||
wizard = _Wizard(data, extra_blocks=extra)
|
||||
wizard.run()
|
||||
|
||||
if not wizard.did_save:
|
||||
if not _run_wizard(data, blocks):
|
||||
return
|
||||
|
||||
# Применяем выбранный инструмент: перезаписываем tool_active.xacro и iiwa7.srdf
|
||||
active_tool = str(data["tool"].get("active", "patron"))
|
||||
if _TOOLS_YAML.exists():
|
||||
try:
|
||||
registry = _load_tools_registry()
|
||||
if active_tool not in registry:
|
||||
raise ValueError(
|
||||
f"Unknown tool '{active_tool}'. Available: {', '.join(registry)}"
|
||||
)
|
||||
tool_cfg = dict(registry[active_tool])
|
||||
if not _TOOLS_YAML.exists():
|
||||
ui.info(f"[yellow]tools.yaml не найден ({_TOOLS_YAML}) — пропуск применения инструмента[/yellow]")
|
||||
return
|
||||
|
||||
import sys as _sys
|
||||
_sys.path.insert(0, str(_PROJECT_DIR / "src" / "iiwa_utils"))
|
||||
from iiwa_utils.tool_manager import apply_tool
|
||||
apply_tool(
|
||||
tool_cfg=tool_cfg,
|
||||
xacro_out_path=_TOOL_ACTIVE_XACRO,
|
||||
srdf_path=_SRDF_PATH,
|
||||
)
|
||||
try:
|
||||
registry = _load_tools_registry()
|
||||
if active_tool not in registry:
|
||||
raise ValueError(f"Неизвестный инструмент '{active_tool}'. Доступны: {', '.join(registry)}")
|
||||
tool_cfg = dict(registry[active_tool])
|
||||
|
||||
# Синхронизируем planning.pose_link с tcp_link выбранного инструмента
|
||||
tcp_link = tool_cfg.get("tcp_link", "link_ee")
|
||||
if "planning" in data:
|
||||
data["planning"]["pose_link"] = tcp_link
|
||||
_save_config(data)
|
||||
sys.path.insert(0, str(_PROJECT_DIR / "src" / "iiwa_utils"))
|
||||
from iiwa_utils.tool_manager import apply_tool
|
||||
apply_tool(tool_cfg=tool_cfg, xacro_out_path=_TOOL_ACTIVE_XACRO, srdf_path=_SRDF_PATH)
|
||||
|
||||
console.print(
|
||||
f"[green]✓[/green] Tool [bold]{active_tool}[/bold] applied: "
|
||||
f"tool_active.xacro, iiwa7.srdf updated, "
|
||||
f"planning.pose_link → [bold]{tcp_link}[/bold]."
|
||||
)
|
||||
except Exception as exc:
|
||||
console.print(f"[red]Tool apply failed:[/red] {exc}")
|
||||
else:
|
||||
console.print(f"[yellow]tools.yaml not found at {_TOOLS_YAML} — skipping tool apply[/yellow]")
|
||||
# Синхронизируем planning.pose_link с tcp_link выбранного инструмента
|
||||
tcp_link = tool_cfg.get("tcp_link", "link_ee")
|
||||
if "planning" in data:
|
||||
data["planning"]["pose_link"] = tcp_link
|
||||
_save_config(data)
|
||||
|
||||
ui.info(
|
||||
f"[green]✓[/green] Инструмент [bold]{active_tool}[/bold] применён: "
|
||||
f"tool_active.xacro, iiwa7.srdf обновлены, "
|
||||
f"planning.pose_link → [bold]{tcp_link}[/bold]."
|
||||
)
|
||||
except Exception as exc:
|
||||
ui.error(f"Не удалось применить инструмент: {exc}")
|
||||
|
||||
Reference in New Issue
Block a user