Update fields

This commit is contained in:
Даниил Грабарь
2026-05-26 16:02:08 +03:00
parent d3912a5a12
commit f365cd491f
11 changed files with 136 additions and 27 deletions
+7
View File
@@ -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)
+5
View File
@@ -7,3 +7,8 @@ python3-uvicorn:
ubuntu: ubuntu:
pip: pip:
packages: ["uvicorn[standard]"] packages: ["uvicorn[standard]"]
python3-multipart:
ubuntu:
pip:
packages: [python-multipart]
+17
View File
@@ -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():
+7
View File
@@ -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
+9 -6
View File
@@ -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] = []
+6 -3
View File
@@ -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()
+28 -16
View File
@@ -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 -1
View File
@@ -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()
+1
View File
@@ -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>
+2 -1
View File
@@ -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',