From f365cd491fcb099caf90e0d85a8e88d745b0f619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BD=D0=B8=D0=B8=D0=BB=20=D0=93=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=B0=D1=80=D1=8C?= Date: Tue, 26 May 2026 16:02:08 +0300 Subject: [PATCH] Update fields --- cobot-setting.yaml | 7 ++++ rosdep.yaml | 5 +++ src/iiwa_bringup/launch/iiwa.launch.py | 17 ++++++++ src/iiwa_config/config/setting.yaml | 7 ++++ src/iiwa_utils/iiwa_utils/setting_loader.py | 43 ++++++++++++++++++++ src/iiwa_web/iiwa_web/config_loader.py | 15 ++++--- src/iiwa_web/iiwa_web/dynamic_router.py | 9 +++-- src/iiwa_web/iiwa_web/main.py | 44 +++++++++++++-------- src/iiwa_web/iiwa_web/ros_node.py | 12 +++++- src/iiwa_web/package.xml | 1 + src/iiwa_web/setup.py | 3 +- 11 files changed, 136 insertions(+), 27 deletions(-) diff --git a/cobot-setting.yaml b/cobot-setting.yaml index 1b569f8..ec55b12 100644 --- a/cobot-setting.yaml +++ b/cobot-setting.yaml @@ -43,6 +43,13 @@ planning: default_planner: "ompl" # Планировщик по умолчанию planning_attempts: 3 # Число попыток планирования +web: + enabled: true + host: "0.0.0.0" + port: 8007 + endpoints: pkg://iiwa_config/config/api_endpoints.yaml + joint_limits: pkg://iiwa_config/config/moveit/joint_limits.yaml + foxglove: enabled: true # Запускать ли foxglove_bridge вместе с роботом port: 8765 # WebSocket-порт, к которому подключается Foxglove Studio (по умолчанию 8765) diff --git a/rosdep.yaml b/rosdep.yaml index cd02b7b..dc5ed4f 100644 --- a/rosdep.yaml +++ b/rosdep.yaml @@ -7,3 +7,8 @@ python3-uvicorn: ubuntu: pip: packages: ["uvicorn[standard]"] + +python3-multipart: + ubuntu: + pip: + packages: [python-multipart] diff --git a/src/iiwa_bringup/launch/iiwa.launch.py b/src/iiwa_bringup/launch/iiwa.launch.py index f806600..b5909a3 100644 --- a/src/iiwa_bringup/launch/iiwa.launch.py +++ b/src/iiwa_bringup/launch/iiwa.launch.py @@ -278,6 +278,23 @@ def _runtime_setup(context, *args, **kwargs): setup += [foxglove_bridge] + if settings.web.enabled: + web_server = Node( + package="iiwa_web", + executable="iiwa_web_server", + output="screen", + name="iiwa_web_server", + parameters=[{ + "host": settings.web.host, + "port": settings.web.port, + "endpoints_path": settings.web.endpoints, + "joint_limits_path": settings.web.joint_limits, + "use_sim_time": use_sim_time, + }], + ) + + setup += [web_server] + return setup def generate_launch_description(): diff --git a/src/iiwa_config/config/setting.yaml b/src/iiwa_config/config/setting.yaml index 1b569f8..ec55b12 100644 --- a/src/iiwa_config/config/setting.yaml +++ b/src/iiwa_config/config/setting.yaml @@ -43,6 +43,13 @@ planning: default_planner: "ompl" # Планировщик по умолчанию planning_attempts: 3 # Число попыток планирования +web: + enabled: true + host: "0.0.0.0" + port: 8007 + endpoints: pkg://iiwa_config/config/api_endpoints.yaml + joint_limits: pkg://iiwa_config/config/moveit/joint_limits.yaml + foxglove: enabled: true # Запускать ли foxglove_bridge вместе с роботом port: 8765 # WebSocket-порт, к которому подключается Foxglove Studio (по умолчанию 8765) diff --git a/src/iiwa_utils/iiwa_utils/setting_loader.py b/src/iiwa_utils/iiwa_utils/setting_loader.py index 3369b5d..5e6b58d 100644 --- a/src/iiwa_utils/iiwa_utils/setting_loader.py +++ b/src/iiwa_utils/iiwa_utils/setting_loader.py @@ -65,6 +65,15 @@ class PlanningCfg: planning_attempts: int +@dataclass(frozen=True) +class WebCfg: + enabled: bool + host: str + port: int + endpoints: str # resolved absolute path to api_endpoints.yaml + joint_limits: str # resolved absolute path to joint_limits.yaml + + @dataclass(frozen=True) class FoxgloveCfg: enabled: bool # Запускать ли foxglove_bridge @@ -100,6 +109,7 @@ class Settings: controller: ControllerCfg planning: PlanningCfg foxglove: FoxgloveCfg + web: WebCfg def to_dict(self) -> Dict[str, Any]: def _convert(obj: Any) -> Any: @@ -168,6 +178,32 @@ def assert_file(path: str, key: str) -> None: raise SettingsError(f"path for '{key}' is not a file: {path}") +# Web defaults +_WEB_DEFAULTS: Dict[str, Any] = { + "enabled": False, + "host": "0.0.0.0", + "port": 8007, + "endpoints": "pkg://iiwa_config/config/api_endpoints.yaml", + "joint_limits": "pkg://iiwa_config/config/moveit/joint_limits.yaml", +} + + +def _parse_web(raw: Optional[Dict[str, Any]], settings_dir: str) -> WebCfg: + if raw is None: + raw = {} + + def get(key: str) -> Any: + return raw.get(key, _WEB_DEFAULTS[key]) + + return WebCfg( + enabled=bool(get("enabled")), + host=str(get("host")), + port=int(get("port")), + endpoints=resolve_path(str(get("endpoints")), settings_dir), + joint_limits=resolve_path(str(get("joint_limits")), settings_dir), + ) + + # Foxglove defaults _FOXGLOVE_DEFAULTS: Dict[str, Any] = { "enabled": False, @@ -303,12 +339,16 @@ def build_settings(settings_path: str, check_files: bool = True) -> Settings: # foxglove foxglove = _parse_foxglove(raw.get("foxglove")) + # web + web = _parse_web(raw.get("web"), settings_dir) + s = Settings( robot=robot, digital_twin=digital_twin, controller=controller, planning=planning, foxglove=foxglove, + web=web, ) if check_files: @@ -326,5 +366,8 @@ def build_settings(settings_path: str, check_files: bool = True) -> Settings: assert_file(s.controller.moveit.initial_positions, "controller.moveit.initial_positions") assert_file(s.controller.moveit.moveit_controllers, "controller.moveit.moveit_controllers") assert_file(s.controller.moveit.moveit_cpp, "controller.moveit.moveit_cpp") + if s.web.enabled: + assert_file(s.web.endpoints, "web.endpoints") + assert_file(s.web.joint_limits, "web.joint_limits") return s diff --git a/src/iiwa_web/iiwa_web/config_loader.py b/src/iiwa_web/iiwa_web/config_loader.py index 8e92e4b..a496cf3 100644 --- a/src/iiwa_web/iiwa_web/config_loader.py +++ b/src/iiwa_web/iiwa_web/config_loader.py @@ -65,36 +65,39 @@ def _parse_joint_limits_data(data: dict) -> tuple[list[str], list[tuple[float, f def load_joint_names( + path: str | None = None, package: str = "iiwa_config", relative: str = "config/moveit/joint_limits.yaml", ) -> list[str]: """Возвращает упорядоченный список имён суставов из joint_limits.yaml.""" - path = _resolve_path(package, relative) - with open(path) as f: + resolved = Path(path) if path else _resolve_path(package, relative) + with open(resolved) as f: data = yaml.safe_load(f) names, _ = _parse_joint_limits_data(data) return names def load_joint_limits( + path: str | None = None, package: str = "iiwa_config", relative: str = "config/moveit/joint_limits.yaml", ) -> list[tuple[float, float]]: """Возвращает список (min, max) для каждого сустава по порядку joint1..jointN.""" - path = _resolve_path(package, relative) - with open(path) as f: + resolved = Path(path) if path else _resolve_path(package, relative) + with open(resolved) as f: data = yaml.safe_load(f) _, limits = _parse_joint_limits_data(data) return limits def load_api_config( + path: str | None = None, package: str = "iiwa_config", relative: str = "config/api_endpoints.yaml", ) -> list[EndpointDef]: """Загружает описания эндпоинтов из YAML и возвращает список EndpointDef.""" - path = _resolve_path(package, relative) - with open(path) as f: + resolved = Path(path) if path else _resolve_path(package, relative) + with open(resolved) as f: data = yaml.safe_load(f) endpoints: list[EndpointDef] = [] diff --git a/src/iiwa_web/iiwa_web/dynamic_router.py b/src/iiwa_web/iiwa_web/dynamic_router.py index de9bacd..cb15547 100644 --- a/src/iiwa_web/iiwa_web/dynamic_router.py +++ b/src/iiwa_web/iiwa_web/dynamic_router.py @@ -208,10 +208,13 @@ def _make_tf_handler(ep: EndpointDef): return handler -def build_dynamic_router() -> APIRouter: +def build_dynamic_router( + endpoints_path: str | None = None, + joint_limits_path: str | None = None, +) -> APIRouter: """Читает api_endpoints.yaml и joint_limits.yaml, возвращает готовый APIRouter.""" - endpoints = load_api_config() - joint_limits = load_joint_limits() + endpoints = load_api_config(path=endpoints_path) + joint_limits = load_joint_limits(path=joint_limits_path) router = APIRouter() diff --git a/src/iiwa_web/iiwa_web/main.py b/src/iiwa_web/iiwa_web/main.py index 67f4419..758ead2 100644 --- a/src/iiwa_web/iiwa_web/main.py +++ b/src/iiwa_web/iiwa_web/main.py @@ -1,27 +1,39 @@ -import uvicorn +import threading from contextlib import asynccontextmanager + +import rclpy +import uvicorn from fastapi import FastAPI from sensor_msgs.msg import JointState -from .ros_node import init_ros_node, get_bridge + from .dynamic_router import build_dynamic_router +from .ros_node import CobotWebNode, get_bridge, set_bridge from . import runner, trajectory -@asynccontextmanager -async def lifespan(_: FastAPI): - init_ros_node() - get_bridge().subscribe("/joint_states", JointState) - yield - - -app = FastAPI(lifespan=lifespan) -app.include_router(build_dynamic_router()) -app.include_router(runner.router) -app.include_router(trajectory.router) - - def main(): - uvicorn.run(app, host="localhost", port=8007) + rclpy.init() + node = CobotWebNode() + set_bridge(node) + threading.Thread(target=rclpy.spin, args=(node,), daemon=True).start() + + host = node.get_parameter('host').value + port = node.get_parameter('port').value + endpoints_path = node.get_parameter('endpoints_path').value or None + joint_limits_path = node.get_parameter('joint_limits_path').value or None + + @asynccontextmanager + async def lifespan(_: FastAPI): + get_bridge().subscribe("/joint_states", JointState) + yield + + app = FastAPI(lifespan=lifespan) + app.include_router(build_dynamic_router(endpoints_path, joint_limits_path)) + app.include_router(runner.router) + app.include_router(trajectory.router) + + uvicorn.run(app, host=host, port=port) + if __name__ == "__main__": main() diff --git a/src/iiwa_web/iiwa_web/ros_node.py b/src/iiwa_web/iiwa_web/ros_node.py index 3286df7..c4eb960 100644 --- a/src/iiwa_web/iiwa_web/ros_node.py +++ b/src/iiwa_web/iiwa_web/ros_node.py @@ -11,6 +11,11 @@ class CobotWebNode(Node): def __init__(self): super().__init__('cobot_web_node') + self.declare_parameter('host', '0.0.0.0') + self.declare_parameter('port', 8007) + self.declare_parameter('endpoints_path', '') + self.declare_parameter('joint_limits_path', '') + self._topic_cache: dict = {} self._pub_registry: dict = {} self._service_clients: dict = {} @@ -106,11 +111,16 @@ class CobotWebNode(Node): _bridge: CobotWebNode = None + +def set_bridge(node: CobotWebNode) -> None: + global _bridge + _bridge = node + + def init_ros_node() -> None: global _bridge rclpy.init() _bridge = CobotWebNode() - thread = threading.Thread(target=rclpy.spin, args=(_bridge,), daemon=True) thread.start() diff --git a/src/iiwa_web/package.xml b/src/iiwa_web/package.xml index ae46f6a..80df4bf 100644 --- a/src/iiwa_web/package.xml +++ b/src/iiwa_web/package.xml @@ -9,6 +9,7 @@ python3-fastapi python3-uvicorn + python3-multipart tf2_ros tf2_py diff --git a/src/iiwa_web/setup.py b/src/iiwa_web/setup.py index 1899902..6377dd5 100644 --- a/src/iiwa_web/setup.py +++ b/src/iiwa_web/setup.py @@ -14,7 +14,8 @@ setup( install_requires=[ 'setuptools', 'fastapi', - 'uvicorn[standard]' + 'uvicorn[standard]', + 'python-multipart', ], zip_safe=True, maintainer='daniel',