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',