Update fields
This commit is contained in:
@@ -43,6 +43,13 @@ planning:
|
|||||||
default_planner: "ompl" # Планировщик по умолчанию
|
default_planner: "ompl" # Планировщик по умолчанию
|
||||||
planning_attempts: 3 # Число попыток планирования
|
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:
|
foxglove:
|
||||||
enabled: true # Запускать ли foxglove_bridge вместе с роботом
|
enabled: true # Запускать ли foxglove_bridge вместе с роботом
|
||||||
port: 8765 # WebSocket-порт, к которому подключается Foxglove Studio (по умолчанию 8765)
|
port: 8765 # WebSocket-порт, к которому подключается Foxglove Studio (по умолчанию 8765)
|
||||||
|
|||||||
@@ -7,3 +7,8 @@ python3-uvicorn:
|
|||||||
ubuntu:
|
ubuntu:
|
||||||
pip:
|
pip:
|
||||||
packages: ["uvicorn[standard]"]
|
packages: ["uvicorn[standard]"]
|
||||||
|
|
||||||
|
python3-multipart:
|
||||||
|
ubuntu:
|
||||||
|
pip:
|
||||||
|
packages: [python-multipart]
|
||||||
|
|||||||
@@ -278,6 +278,23 @@ def _runtime_setup(context, *args, **kwargs):
|
|||||||
|
|
||||||
setup += [foxglove_bridge]
|
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
|
return setup
|
||||||
|
|
||||||
def generate_launch_description():
|
def generate_launch_description():
|
||||||
|
|||||||
@@ -43,6 +43,13 @@ planning:
|
|||||||
default_planner: "ompl" # Планировщик по умолчанию
|
default_planner: "ompl" # Планировщик по умолчанию
|
||||||
planning_attempts: 3 # Число попыток планирования
|
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:
|
foxglove:
|
||||||
enabled: true # Запускать ли foxglove_bridge вместе с роботом
|
enabled: true # Запускать ли foxglove_bridge вместе с роботом
|
||||||
port: 8765 # WebSocket-порт, к которому подключается Foxglove Studio (по умолчанию 8765)
|
port: 8765 # WebSocket-порт, к которому подключается Foxglove Studio (по умолчанию 8765)
|
||||||
|
|||||||
@@ -65,6 +65,15 @@ class PlanningCfg:
|
|||||||
planning_attempts: int
|
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)
|
@dataclass(frozen=True)
|
||||||
class FoxgloveCfg:
|
class FoxgloveCfg:
|
||||||
enabled: bool # Запускать ли foxglove_bridge
|
enabled: bool # Запускать ли foxglove_bridge
|
||||||
@@ -100,6 +109,7 @@ class Settings:
|
|||||||
controller: ControllerCfg
|
controller: ControllerCfg
|
||||||
planning: PlanningCfg
|
planning: PlanningCfg
|
||||||
foxglove: FoxgloveCfg
|
foxglove: FoxgloveCfg
|
||||||
|
web: WebCfg
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
def _convert(obj: Any) -> 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}")
|
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
|
||||||
_FOXGLOVE_DEFAULTS: Dict[str, Any] = {
|
_FOXGLOVE_DEFAULTS: Dict[str, Any] = {
|
||||||
"enabled": False,
|
"enabled": False,
|
||||||
@@ -303,12 +339,16 @@ def build_settings(settings_path: str, check_files: bool = True) -> Settings:
|
|||||||
# foxglove
|
# foxglove
|
||||||
foxglove = _parse_foxglove(raw.get("foxglove"))
|
foxglove = _parse_foxglove(raw.get("foxglove"))
|
||||||
|
|
||||||
|
# web
|
||||||
|
web = _parse_web(raw.get("web"), settings_dir)
|
||||||
|
|
||||||
s = Settings(
|
s = Settings(
|
||||||
robot=robot,
|
robot=robot,
|
||||||
digital_twin=digital_twin,
|
digital_twin=digital_twin,
|
||||||
controller=controller,
|
controller=controller,
|
||||||
planning=planning,
|
planning=planning,
|
||||||
foxglove=foxglove,
|
foxglove=foxglove,
|
||||||
|
web=web,
|
||||||
)
|
)
|
||||||
|
|
||||||
if check_files:
|
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.initial_positions, "controller.moveit.initial_positions")
|
||||||
assert_file(s.controller.moveit.moveit_controllers, "controller.moveit.moveit_controllers")
|
assert_file(s.controller.moveit.moveit_controllers, "controller.moveit.moveit_controllers")
|
||||||
assert_file(s.controller.moveit.moveit_cpp, "controller.moveit.moveit_cpp")
|
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
|
return s
|
||||||
|
|||||||
@@ -65,36 +65,39 @@ def _parse_joint_limits_data(data: dict) -> tuple[list[str], list[tuple[float, f
|
|||||||
|
|
||||||
|
|
||||||
def load_joint_names(
|
def load_joint_names(
|
||||||
|
path: str | None = None,
|
||||||
package: str = "iiwa_config",
|
package: str = "iiwa_config",
|
||||||
relative: str = "config/moveit/joint_limits.yaml",
|
relative: str = "config/moveit/joint_limits.yaml",
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""Возвращает упорядоченный список имён суставов из joint_limits.yaml."""
|
"""Возвращает упорядоченный список имён суставов из joint_limits.yaml."""
|
||||||
path = _resolve_path(package, relative)
|
resolved = Path(path) if path else _resolve_path(package, relative)
|
||||||
with open(path) as f:
|
with open(resolved) as f:
|
||||||
data = yaml.safe_load(f)
|
data = yaml.safe_load(f)
|
||||||
names, _ = _parse_joint_limits_data(data)
|
names, _ = _parse_joint_limits_data(data)
|
||||||
return names
|
return names
|
||||||
|
|
||||||
|
|
||||||
def load_joint_limits(
|
def load_joint_limits(
|
||||||
|
path: str | None = None,
|
||||||
package: str = "iiwa_config",
|
package: str = "iiwa_config",
|
||||||
relative: str = "config/moveit/joint_limits.yaml",
|
relative: str = "config/moveit/joint_limits.yaml",
|
||||||
) -> list[tuple[float, float]]:
|
) -> list[tuple[float, float]]:
|
||||||
"""Возвращает список (min, max) для каждого сустава по порядку joint1..jointN."""
|
"""Возвращает список (min, max) для каждого сустава по порядку joint1..jointN."""
|
||||||
path = _resolve_path(package, relative)
|
resolved = Path(path) if path else _resolve_path(package, relative)
|
||||||
with open(path) as f:
|
with open(resolved) as f:
|
||||||
data = yaml.safe_load(f)
|
data = yaml.safe_load(f)
|
||||||
_, limits = _parse_joint_limits_data(data)
|
_, limits = _parse_joint_limits_data(data)
|
||||||
return limits
|
return limits
|
||||||
|
|
||||||
|
|
||||||
def load_api_config(
|
def load_api_config(
|
||||||
|
path: str | None = None,
|
||||||
package: str = "iiwa_config",
|
package: str = "iiwa_config",
|
||||||
relative: str = "config/api_endpoints.yaml",
|
relative: str = "config/api_endpoints.yaml",
|
||||||
) -> list[EndpointDef]:
|
) -> list[EndpointDef]:
|
||||||
"""Загружает описания эндпоинтов из YAML и возвращает список EndpointDef."""
|
"""Загружает описания эндпоинтов из YAML и возвращает список EndpointDef."""
|
||||||
path = _resolve_path(package, relative)
|
resolved = Path(path) if path else _resolve_path(package, relative)
|
||||||
with open(path) as f:
|
with open(resolved) as f:
|
||||||
data = yaml.safe_load(f)
|
data = yaml.safe_load(f)
|
||||||
|
|
||||||
endpoints: list[EndpointDef] = []
|
endpoints: list[EndpointDef] = []
|
||||||
|
|||||||
@@ -208,10 +208,13 @@ def _make_tf_handler(ep: EndpointDef):
|
|||||||
return handler
|
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."""
|
"""Читает api_endpoints.yaml и joint_limits.yaml, возвращает готовый APIRouter."""
|
||||||
endpoints = load_api_config()
|
endpoints = load_api_config(path=endpoints_path)
|
||||||
joint_limits = load_joint_limits()
|
joint_limits = load_joint_limits(path=joint_limits_path)
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,39 @@
|
|||||||
import uvicorn
|
import threading
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
import rclpy
|
||||||
|
import uvicorn
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from sensor_msgs.msg import JointState
|
from sensor_msgs.msg import JointState
|
||||||
from .ros_node import init_ros_node, get_bridge
|
|
||||||
from .dynamic_router import build_dynamic_router
|
from .dynamic_router import build_dynamic_router
|
||||||
|
from .ros_node import CobotWebNode, get_bridge, set_bridge
|
||||||
from . import runner, trajectory
|
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():
|
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__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ class CobotWebNode(Node):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__('cobot_web_node')
|
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._topic_cache: dict = {}
|
||||||
self._pub_registry: dict = {}
|
self._pub_registry: dict = {}
|
||||||
self._service_clients: dict = {}
|
self._service_clients: dict = {}
|
||||||
@@ -106,11 +111,16 @@ class CobotWebNode(Node):
|
|||||||
|
|
||||||
_bridge: CobotWebNode = None
|
_bridge: CobotWebNode = None
|
||||||
|
|
||||||
|
|
||||||
|
def set_bridge(node: CobotWebNode) -> None:
|
||||||
|
global _bridge
|
||||||
|
_bridge = node
|
||||||
|
|
||||||
|
|
||||||
def init_ros_node() -> None:
|
def init_ros_node() -> None:
|
||||||
global _bridge
|
global _bridge
|
||||||
rclpy.init()
|
rclpy.init()
|
||||||
_bridge = CobotWebNode()
|
_bridge = CobotWebNode()
|
||||||
|
|
||||||
thread = threading.Thread(target=rclpy.spin, args=(_bridge,), daemon=True)
|
thread = threading.Thread(target=rclpy.spin, args=(_bridge,), daemon=True)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
<exec_depend>python3-fastapi</exec_depend>
|
<exec_depend>python3-fastapi</exec_depend>
|
||||||
<exec_depend>python3-uvicorn</exec_depend>
|
<exec_depend>python3-uvicorn</exec_depend>
|
||||||
|
<exec_depend>python3-multipart</exec_depend>
|
||||||
<exec_depend>tf2_ros</exec_depend>
|
<exec_depend>tf2_ros</exec_depend>
|
||||||
<exec_depend>tf2_py</exec_depend>
|
<exec_depend>tf2_py</exec_depend>
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ setup(
|
|||||||
install_requires=[
|
install_requires=[
|
||||||
'setuptools',
|
'setuptools',
|
||||||
'fastapi',
|
'fastapi',
|
||||||
'uvicorn[standard]'
|
'uvicorn[standard]',
|
||||||
|
'python-multipart',
|
||||||
],
|
],
|
||||||
zip_safe=True,
|
zip_safe=True,
|
||||||
maintainer='daniel',
|
maintainer='daniel',
|
||||||
|
|||||||
Reference in New Issue
Block a user