mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-26 20:43:15 +00:00
Compare commits
9 Commits
feat/lab_r
...
prcix9320
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d13d3f7dfe | ||
|
|
71d35d31af | ||
|
|
7f4b57f589 | ||
|
|
0c667e68e6 | ||
|
|
9430be51a4 | ||
|
|
a187a57430 | ||
|
|
68029217de | ||
|
|
792504e08c | ||
|
|
ca985f92ab |
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
package:
|
package:
|
||||||
name: unilabos-env
|
name: unilabos-env
|
||||||
version: 0.10.19
|
version: 0.10.17
|
||||||
|
|
||||||
build:
|
build:
|
||||||
noarch: generic
|
noarch: generic
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -253,4 +253,8 @@ test_config.py
|
|||||||
|
|
||||||
|
|
||||||
/.claude
|
/.claude
|
||||||
|
/.conda
|
||||||
/.cursor
|
/.cursor
|
||||||
|
/.github
|
||||||
|
/.conda/base
|
||||||
|
.conda/base/recipe.yaml
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -201,42 +201,17 @@ class ResourceVisualization:
|
|||||||
self.moveit_controllers_yaml['moveit_simple_controller_manager'][f"{name}_{controller_name}"] = moveit_dict['moveit_simple_controller_manager'][controller_name]
|
self.moveit_controllers_yaml['moveit_simple_controller_manager'][f"{name}_{controller_name}"] = moveit_dict['moveit_simple_controller_manager'][controller_name]
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _ensure_ros2_env() -> dict:
|
|
||||||
"""确保 ROS2 环境变量正确设置,返回可用于子进程的 env dict"""
|
|
||||||
import sys
|
|
||||||
env = dict(os.environ)
|
|
||||||
conda_prefix = os.path.dirname(os.path.dirname(sys.executable))
|
|
||||||
|
|
||||||
if "AMENT_PREFIX_PATH" not in env or not env["AMENT_PREFIX_PATH"].strip():
|
|
||||||
candidate = os.pathsep.join([conda_prefix, os.path.join(conda_prefix, "Library")])
|
|
||||||
env["AMENT_PREFIX_PATH"] = candidate
|
|
||||||
os.environ["AMENT_PREFIX_PATH"] = candidate
|
|
||||||
|
|
||||||
extra_bin_dirs = [
|
|
||||||
os.path.join(conda_prefix, "Library", "bin"),
|
|
||||||
os.path.join(conda_prefix, "Library", "lib"),
|
|
||||||
os.path.join(conda_prefix, "Scripts"),
|
|
||||||
conda_prefix,
|
|
||||||
]
|
|
||||||
current_path = env.get("PATH", "")
|
|
||||||
for d in extra_bin_dirs:
|
|
||||||
if d not in current_path:
|
|
||||||
current_path = d + os.pathsep + current_path
|
|
||||||
env["PATH"] = current_path
|
|
||||||
os.environ["PATH"] = current_path
|
|
||||||
|
|
||||||
return env
|
|
||||||
|
|
||||||
def create_launch_description(self) -> LaunchDescription:
|
def create_launch_description(self) -> LaunchDescription:
|
||||||
"""
|
"""
|
||||||
创建launch描述,包含robot_state_publisher和move_group节点
|
创建launch描述,包含robot_state_publisher和move_group节点
|
||||||
|
|
||||||
|
Args:
|
||||||
|
urdf_str: URDF文本
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
LaunchDescription: launch描述对象
|
LaunchDescription: launch描述对象
|
||||||
"""
|
"""
|
||||||
launch_env = self._ensure_ros2_env()
|
# 检查ROS 2环境变量
|
||||||
|
|
||||||
if "AMENT_PREFIX_PATH" not in os.environ:
|
if "AMENT_PREFIX_PATH" not in os.environ:
|
||||||
raise OSError(
|
raise OSError(
|
||||||
"ROS 2环境未正确设置。需要设置 AMENT_PREFIX_PATH 环境变量。\n"
|
"ROS 2环境未正确设置。需要设置 AMENT_PREFIX_PATH 环境变量。\n"
|
||||||
@@ -315,7 +290,7 @@ class ResourceVisualization:
|
|||||||
{"robot_description": robot_description},
|
{"robot_description": robot_description},
|
||||||
ros2_controllers,
|
ros2_controllers,
|
||||||
],
|
],
|
||||||
env=launch_env,
|
env=dict(os.environ)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
for controller in self.moveit_controllers_yaml['moveit_simple_controller_manager']['controller_names']:
|
for controller in self.moveit_controllers_yaml['moveit_simple_controller_manager']['controller_names']:
|
||||||
@@ -325,7 +300,7 @@ class ResourceVisualization:
|
|||||||
executable="spawner",
|
executable="spawner",
|
||||||
arguments=[f"{controller}", "--controller-manager", f"controller_manager"],
|
arguments=[f"{controller}", "--controller-manager", f"controller_manager"],
|
||||||
output="screen",
|
output="screen",
|
||||||
env=launch_env,
|
env=dict(os.environ)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
controllers.append(
|
controllers.append(
|
||||||
@@ -334,7 +309,7 @@ class ResourceVisualization:
|
|||||||
executable="spawner",
|
executable="spawner",
|
||||||
arguments=["joint_state_broadcaster", "--controller-manager", f"controller_manager"],
|
arguments=["joint_state_broadcaster", "--controller-manager", f"controller_manager"],
|
||||||
output="screen",
|
output="screen",
|
||||||
env=launch_env,
|
env=dict(os.environ)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
for i in controllers:
|
for i in controllers:
|
||||||
@@ -342,6 +317,7 @@ class ResourceVisualization:
|
|||||||
else:
|
else:
|
||||||
ros2_controllers = None
|
ros2_controllers = None
|
||||||
|
|
||||||
|
# 创建robot_state_publisher节点
|
||||||
robot_state_publisher = nd(
|
robot_state_publisher = nd(
|
||||||
package='robot_state_publisher',
|
package='robot_state_publisher',
|
||||||
executable='robot_state_publisher',
|
executable='robot_state_publisher',
|
||||||
@@ -351,8 +327,9 @@ class ResourceVisualization:
|
|||||||
'robot_description': robot_description,
|
'robot_description': robot_description,
|
||||||
'use_sim_time': False
|
'use_sim_time': False
|
||||||
},
|
},
|
||||||
|
# kinematics_dict
|
||||||
],
|
],
|
||||||
env=launch_env,
|
env=dict(os.environ)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -384,7 +361,7 @@ class ResourceVisualization:
|
|||||||
executable='move_group',
|
executable='move_group',
|
||||||
output='screen',
|
output='screen',
|
||||||
parameters=moveit_params,
|
parameters=moveit_params,
|
||||||
env=launch_env,
|
env=dict(os.environ)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -402,11 +379,13 @@ class ResourceVisualization:
|
|||||||
arguments=['-d', f"{str(self.mesh_path)}/view_robot.rviz"],
|
arguments=['-d', f"{str(self.mesh_path)}/view_robot.rviz"],
|
||||||
output='screen',
|
output='screen',
|
||||||
parameters=[
|
parameters=[
|
||||||
{'robot_description_kinematics': kinematics_dict},
|
{'robot_description_kinematics': kinematics_dict,
|
||||||
|
},
|
||||||
robot_description_planning,
|
robot_description_planning,
|
||||||
planning_pipelines,
|
planning_pipelines,
|
||||||
|
|
||||||
],
|
],
|
||||||
env=launch_env,
|
env=dict(os.environ)
|
||||||
)
|
)
|
||||||
self.launch_description.add_action(rviz_node)
|
self.launch_description.add_action(rviz_node)
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
150
unilabos/devices/liquid_handling/prcxi/prcxi_modules.py
Normal file
150
unilabos/devices/liquid_handling/prcxi/prcxi_modules.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from .prcxi import PRCXI9300ModuleSite
|
||||||
|
|
||||||
|
|
||||||
|
class PRCXI9300FunctionalModule(PRCXI9300ModuleSite):
|
||||||
|
"""
|
||||||
|
PRCXI 9300 功能模块基类(加热/冷却/震荡/加热震荡/磁吸等)。
|
||||||
|
|
||||||
|
设计目标:
|
||||||
|
- 作为一个可以在工作台上拖拽摆放的实体资源(继承自 PRCXI9300ModuleSite -> ItemizedCarrier)。
|
||||||
|
- 顶面存在一个站点(site),可吸附标准板类资源(plate / tip_rack / tube_rack 等)。
|
||||||
|
- 支持注入 `material_info` (UUID 等),并且在 serialize_state 时做安全过滤。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
size_x: float,
|
||||||
|
size_y: float,
|
||||||
|
size_z: float,
|
||||||
|
module_type: Optional[str] = None,
|
||||||
|
category: str = "module",
|
||||||
|
model: Optional[str] = None,
|
||||||
|
material_info: Optional[Dict[str, Any]] = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
size_x=size_x,
|
||||||
|
size_y=size_y,
|
||||||
|
size_z=size_z,
|
||||||
|
material_info=material_info,
|
||||||
|
model=model,
|
||||||
|
category=category,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 记录模块类型(加热 / 冷却 / 震荡 / 加热震荡 / 磁吸)
|
||||||
|
self.module_type = module_type or "generic"
|
||||||
|
|
||||||
|
# 与 PRCXI9300PlateAdapter 一致,使用 _unilabos_state 保存扩展信息
|
||||||
|
if not hasattr(self, "_unilabos_state") or self._unilabos_state is None:
|
||||||
|
self._unilabos_state = {}
|
||||||
|
|
||||||
|
# super().__init__ 已经在有 material_info 时写入 "Material",这里仅确保存在
|
||||||
|
if material_info is not None and "Material" not in self._unilabos_state:
|
||||||
|
self._unilabos_state["Material"] = material_info
|
||||||
|
|
||||||
|
# 额外标记 category 和模块类型,便于前端或上层逻辑区分
|
||||||
|
self._unilabos_state.setdefault("category", category)
|
||||||
|
self._unilabos_state["module_type"] = module_type
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 具体功能模块定义
|
||||||
|
# 这里的尺寸和 material_info 目前为占位参数,后续可根据实际测量/JSON 配置进行更新。
|
||||||
|
# 顶面站点尺寸与模块外形一致,保证可以吸附标准 96 板/储液槽等。
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def PRCXI_Heating_Module(name: str) -> PRCXI9300FunctionalModule:
|
||||||
|
"""加热模块(顶面可吸附标准板)。"""
|
||||||
|
return PRCXI9300FunctionalModule(
|
||||||
|
name=name,
|
||||||
|
size_x=127.76,
|
||||||
|
size_y=85.48,
|
||||||
|
size_z=40.0,
|
||||||
|
module_type="heating",
|
||||||
|
model="PRCXI_Heating_Module",
|
||||||
|
material_info={
|
||||||
|
"uuid": "TODO-HEATING-MODULE-UUID",
|
||||||
|
"Code": "HEAT-MOD",
|
||||||
|
"Name": "PRCXI 加热模块",
|
||||||
|
"SupplyType": 3,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def PRCXI_MetalCooling_Module(name: str) -> PRCXI9300FunctionalModule:
|
||||||
|
"""金属冷却模块(顶面可吸附标准板)。"""
|
||||||
|
return PRCXI9300FunctionalModule(
|
||||||
|
name=name,
|
||||||
|
size_x=127.76,
|
||||||
|
size_y=85.48,
|
||||||
|
size_z=40.0,
|
||||||
|
module_type="metal_cooling",
|
||||||
|
model="PRCXI_MetalCooling_Module",
|
||||||
|
material_info={
|
||||||
|
"uuid": "TODO-METAL-COOLING-MODULE-UUID",
|
||||||
|
"Code": "METAL-COOL-MOD",
|
||||||
|
"Name": "PRCXI 金属冷却模块",
|
||||||
|
"SupplyType": 3,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def PRCXI_Shaking_Module(name: str) -> PRCXI9300FunctionalModule:
|
||||||
|
"""震荡模块(顶面可吸附标准板)。"""
|
||||||
|
return PRCXI9300FunctionalModule(
|
||||||
|
name=name,
|
||||||
|
size_x=127.76,
|
||||||
|
size_y=85.48,
|
||||||
|
size_z=50.0,
|
||||||
|
module_type="shaking",
|
||||||
|
model="PRCXI_Shaking_Module",
|
||||||
|
material_info={
|
||||||
|
"uuid": "TODO-SHAKING-MODULE-UUID",
|
||||||
|
"Code": "SHAKE-MOD",
|
||||||
|
"Name": "PRCXI 震荡模块",
|
||||||
|
"SupplyType": 3,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def PRCXI_Heating_Shaking_Module(name: str) -> PRCXI9300FunctionalModule:
|
||||||
|
"""加热震荡模块(顶面可吸附标准板)。"""
|
||||||
|
return PRCXI9300FunctionalModule(
|
||||||
|
name=name,
|
||||||
|
size_x=127.76,
|
||||||
|
size_y=85.48,
|
||||||
|
size_z=55.0,
|
||||||
|
module_type="heating_shaking",
|
||||||
|
model="PRCXI_Heating_Shaking_Module",
|
||||||
|
material_info={
|
||||||
|
"uuid": "TODO-HEATING-SHAKING-MODULE-UUID",
|
||||||
|
"Code": "HEAT-SHAKE-MOD",
|
||||||
|
"Name": "PRCXI 加热震荡模块",
|
||||||
|
"SupplyType": 3,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def PRCXI_Magnetic_Module(name: str) -> PRCXI9300FunctionalModule:
|
||||||
|
"""磁吸模块(顶面可吸附标准板)。"""
|
||||||
|
return PRCXI9300FunctionalModule(
|
||||||
|
name=name,
|
||||||
|
size_x=127.76,
|
||||||
|
size_y=85.48,
|
||||||
|
size_z=30.0,
|
||||||
|
module_type="magnetic",
|
||||||
|
model="PRCXI_Magnetic_Module",
|
||||||
|
material_info={
|
||||||
|
"uuid": "TODO-MAGNETIC-MODULE-UUID",
|
||||||
|
"Code": "MAG-MOD",
|
||||||
|
"Name": "PRCXI 磁吸模块",
|
||||||
|
"SupplyType": 3,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
@@ -59,7 +59,6 @@ class UniLiquidHandlerRvizBackend(LiquidHandlerBackend):
|
|||||||
self.total_height = total_height
|
self.total_height = total_height
|
||||||
self.joint_config = kwargs.get("joint_config", None)
|
self.joint_config = kwargs.get("joint_config", None)
|
||||||
self.lh_device_id = kwargs.get("lh_device_id", "lh_joint_publisher")
|
self.lh_device_id = kwargs.get("lh_device_id", "lh_joint_publisher")
|
||||||
self.simulate_rviz = kwargs.get("simulate_rviz", False)
|
|
||||||
if not rclpy.ok():
|
if not rclpy.ok():
|
||||||
rclpy.init()
|
rclpy.init()
|
||||||
self.joint_state_publisher = None
|
self.joint_state_publisher = None
|
||||||
@@ -70,7 +69,7 @@ class UniLiquidHandlerRvizBackend(LiquidHandlerBackend):
|
|||||||
self.joint_state_publisher = LiquidHandlerJointPublisher(
|
self.joint_state_publisher = LiquidHandlerJointPublisher(
|
||||||
joint_config=self.joint_config,
|
joint_config=self.joint_config,
|
||||||
lh_device_id=self.lh_device_id,
|
lh_device_id=self.lh_device_id,
|
||||||
simulate_rviz=self.simulate_rviz)
|
simulate_rviz=True)
|
||||||
|
|
||||||
# 启动ROS executor
|
# 启动ROS executor
|
||||||
self.executor = rclpy.executors.MultiThreadedExecutor()
|
self.executor = rclpy.executors.MultiThreadedExecutor()
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ class LiquidHandlerJointPublisher(Node):
|
|||||||
while self.resource_action is None:
|
while self.resource_action is None:
|
||||||
self.resource_action = self.check_tf_update_actions()
|
self.resource_action = self.check_tf_update_actions()
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
self.get_logger().info(f'Waiting for TfUpdate server: {self.resource_action}')
|
|
||||||
|
|
||||||
self.resource_action_client = ActionClient(self, SendCmd, self.resource_action)
|
self.resource_action_client = ActionClient(self, SendCmd, self.resource_action)
|
||||||
while not self.resource_action_client.wait_for_server(timeout_sec=1.0):
|
while not self.resource_action_client.wait_for_server(timeout_sec=1.0):
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
70
unilabos/registry/resources/prcxi/modules.yaml
Normal file
70
unilabos/registry/resources/prcxi/modules.yaml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
PRCXI_Heating_Module:
|
||||||
|
category:
|
||||||
|
- prcxi
|
||||||
|
- modules
|
||||||
|
class:
|
||||||
|
module: unilabos.devices.liquid_handling.prcxi.prcxi_modules:PRCXI_Heating_Module
|
||||||
|
type: pylabrobot
|
||||||
|
description: '加热模块 (Code: HEAT-MOD)'
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
registry_type: resource
|
||||||
|
version: 1.0.0
|
||||||
|
|
||||||
|
PRCXI_MetalCooling_Module:
|
||||||
|
category:
|
||||||
|
- prcxi
|
||||||
|
- modules
|
||||||
|
class:
|
||||||
|
module: unilabos.devices.liquid_handling.prcxi.prcxi_modules:PRCXI_MetalCooling_Module
|
||||||
|
type: pylabrobot
|
||||||
|
description: '金属冷却模块 (Code: METAL-COOL-MOD)'
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
registry_type: resource
|
||||||
|
version: 1.0.0
|
||||||
|
|
||||||
|
PRCXI_Shaking_Module:
|
||||||
|
category:
|
||||||
|
- prcxi
|
||||||
|
- modules
|
||||||
|
class:
|
||||||
|
module: unilabos.devices.liquid_handling.prcxi.prcxi_modules:PRCXI_Shaking_Module
|
||||||
|
type: pylabrobot
|
||||||
|
description: '震荡模块 (Code: SHAKE-MOD)'
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
registry_type: resource
|
||||||
|
version: 1.0.0
|
||||||
|
|
||||||
|
PRCXI_Heating_Shaking_Module:
|
||||||
|
category:
|
||||||
|
- prcxi
|
||||||
|
- modules
|
||||||
|
class:
|
||||||
|
module: unilabos.devices.liquid_handling.prcxi.prcxi_modules:PRCXI_Heating_Shaking_Module
|
||||||
|
type: pylabrobot
|
||||||
|
description: '加热震荡模块 (Code: HEAT-SHAKE-MOD)'
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
registry_type: resource
|
||||||
|
version: 1.0.0
|
||||||
|
|
||||||
|
PRCXI_Magnetic_Module:
|
||||||
|
category:
|
||||||
|
- prcxi
|
||||||
|
- modules
|
||||||
|
class:
|
||||||
|
module: unilabos.devices.liquid_handling.prcxi.prcxi_modules:PRCXI_Magnetic_Module
|
||||||
|
type: pylabrobot
|
||||||
|
description: '磁吸模块 (Code: MAG-MOD)'
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
registry_type: resource
|
||||||
|
version: 1.0.0
|
||||||
|
|
||||||
@@ -9,6 +9,9 @@ def register():
|
|||||||
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300TipRack
|
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300TipRack
|
||||||
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300Trash
|
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300Trash
|
||||||
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300TubeRack
|
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300TubeRack
|
||||||
|
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300ModuleSite
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
from unilabos.devices.liquid_handling.prcxi.prcxi_modules import PRCXI9300FunctionalModule
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
from unilabos.devices.workstation.workstation_base import WorkStationContainer
|
from unilabos.devices.workstation.workstation_base import WorkStationContainer
|
||||||
|
|
||||||
|
|||||||
@@ -459,6 +459,8 @@ class ResourceTreeSet(object):
|
|||||||
"reagent_bottle": "reagent_bottle",
|
"reagent_bottle": "reagent_bottle",
|
||||||
"flask": "flask",
|
"flask": "flask",
|
||||||
"beaker": "beaker",
|
"beaker": "beaker",
|
||||||
|
"module": "module",
|
||||||
|
"carrier": "carrier",
|
||||||
}
|
}
|
||||||
if source in replace_info:
|
if source in replace_info:
|
||||||
return replace_info[source]
|
return replace_info[source]
|
||||||
@@ -596,6 +598,8 @@ class ResourceTreeSet(object):
|
|||||||
"deck": "Deck",
|
"deck": "Deck",
|
||||||
"container": "RegularContainer",
|
"container": "RegularContainer",
|
||||||
"tip_spot": "TipSpot",
|
"tip_spot": "TipSpot",
|
||||||
|
"module": "PRCXI9300ModuleSite",
|
||||||
|
"carrier": "ItemizedCarrier",
|
||||||
}
|
}
|
||||||
|
|
||||||
def collect_node_data(node: ResourceDictInstance, name_to_uuid: dict, all_states: dict, name_to_extra: dict):
|
def collect_node_data(node: ResourceDictInstance, name_to_uuid: dict, all_states: dict, name_to_extra: dict):
|
||||||
@@ -735,7 +739,7 @@ class ResourceTreeSet(object):
|
|||||||
plr_resources.append(plr_resource)
|
plr_resources.append(plr_resource)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"转换 PLR 资源失败: {e}")
|
logger.error(f"转换 PLR 资源失败: {e} {str(plr_dict)[:1000]}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
logger.error(f"堆栈: {traceback.format_exc()}")
|
logger.error(f"堆栈: {traceback.format_exc()}")
|
||||||
@@ -743,38 +747,15 @@ class ResourceTreeSet(object):
|
|||||||
|
|
||||||
if requested_uuids:
|
if requested_uuids:
|
||||||
# 按请求的 UUID 顺序返回对应资源(从整棵树中按 uuid 提取)
|
# 按请求的 UUID 顺序返回对应资源(从整棵树中按 uuid 提取)
|
||||||
# 优先使用 tracker.uuid_to_resources;若映射缺失,再递归遍历 PLR 树兜底搜索。
|
|
||||||
def _find_plr_by_uuid(roots: List["PLRResource"], uid: str) -> Optional["PLRResource"]:
|
|
||||||
stack = list(roots)
|
|
||||||
while stack:
|
|
||||||
node = stack.pop()
|
|
||||||
node_uid = getattr(node, "unilabos_uuid", None)
|
|
||||||
if node_uid == uid:
|
|
||||||
return node
|
|
||||||
children = getattr(node, "children", None) or []
|
|
||||||
stack.extend(children)
|
|
||||||
return None
|
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
missing_uuids = []
|
|
||||||
for uid in requested_uuids:
|
for uid in requested_uuids:
|
||||||
found = tracker.uuid_to_resources.get(uid)
|
if uid in tracker.uuid_to_resources:
|
||||||
if found is None:
|
result.append(tracker.uuid_to_resources[uid])
|
||||||
found = _find_plr_by_uuid(plr_resources, uid)
|
|
||||||
if found is not None:
|
|
||||||
# 回填缓存,后续相同 uuid 可直接命中
|
|
||||||
tracker.uuid_to_resources[uid] = found
|
|
||||||
if found is None:
|
|
||||||
missing_uuids.append(uid)
|
|
||||||
else:
|
else:
|
||||||
result.append(found)
|
raise ValueError(
|
||||||
|
f"请求的 UUID {uid} 在资源树中未找到。"
|
||||||
if missing_uuids:
|
f"可用 UUID 数量: {len(tracker.uuid_to_resources)}"
|
||||||
raise ValueError(
|
)
|
||||||
f"请求的 UUID 未在资源树中找到: {missing_uuids}。"
|
|
||||||
f"可用 UUID 数量: {len(tracker.uuid_to_resources)},"
|
|
||||||
f"资源树数量: {len(self.trees)}"
|
|
||||||
)
|
|
||||||
return result
|
return result
|
||||||
return plr_resources
|
return plr_resources
|
||||||
|
|
||||||
@@ -793,13 +774,7 @@ class ResourceTreeSet(object):
|
|||||||
ValueError: 当建立关系时发现不一致
|
ValueError: 当建立关系时发现不一致
|
||||||
"""
|
"""
|
||||||
# 第一步:将字典列表转换为 ResourceDictInstance 列表
|
# 第一步:将字典列表转换为 ResourceDictInstance 列表
|
||||||
parsed_list = []
|
instances = [ResourceDictInstance.get_resource_instance_from_dict(node_dict) for node_dict in raw_list]
|
||||||
for node_dict in raw_list:
|
|
||||||
if isinstance(node_dict, str):
|
|
||||||
import json
|
|
||||||
node_dict = json.loads(node_dict)
|
|
||||||
parsed_list.append(node_dict)
|
|
||||||
instances = [ResourceDictInstance.get_resource_instance_from_dict(node_dict) for node_dict in parsed_list]
|
|
||||||
|
|
||||||
# 第二步:建立映射关系
|
# 第二步:建立映射关系
|
||||||
uuid_to_instance: Dict[str, ResourceDictInstance] = {}
|
uuid_to_instance: Dict[str, ResourceDictInstance] = {}
|
||||||
@@ -987,6 +962,17 @@ class ResourceTreeSet(object):
|
|||||||
f"从远端同步了 {added_count} 个物料子树"
|
f"从远端同步了 {added_count} 个物料子树"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
# 二级是物料
|
||||||
|
if remote_child_name not in local_children_map:
|
||||||
|
# 本地不存在该物料,直接引入
|
||||||
|
remote_child.res_content.parent = local_device.res_content
|
||||||
|
local_device.children.append(remote_child)
|
||||||
|
local_children_map[remote_child_name] = remote_child
|
||||||
|
logger.info(
|
||||||
|
f"物料 '{remote_root_id}/{remote_child_name}': "
|
||||||
|
f"从远端同步了整个子树"
|
||||||
|
)
|
||||||
|
continue
|
||||||
# 二级物料已存在,比较三级子节点是否缺失
|
# 二级物料已存在,比较三级子节点是否缺失
|
||||||
local_material = local_children_map[remote_child_name]
|
local_material = local_children_map[remote_child_name]
|
||||||
local_material_children_map = {child.res_content.name: child for child in
|
local_material_children_map = {child.res_content.name: child for child in
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ def main(
|
|||||||
bridges: List[Any] = [],
|
bridges: List[Any] = [],
|
||||||
visual: str = "disable",
|
visual: str = "disable",
|
||||||
resources_mesh_config: dict = {},
|
resources_mesh_config: dict = {},
|
||||||
resources_mesh_resource_list: list = [],
|
|
||||||
rclpy_init_args: List[str] = ["--log-level", "debug"],
|
rclpy_init_args: List[str] = ["--log-level", "debug"],
|
||||||
discovery_interval: float = 15.0,
|
discovery_interval: float = 15.0,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -78,12 +77,12 @@ def main(
|
|||||||
if visual != "disable":
|
if visual != "disable":
|
||||||
from unilabos.ros.nodes.presets.joint_republisher import JointRepublisher
|
from unilabos.ros.nodes.presets.joint_republisher import JointRepublisher
|
||||||
|
|
||||||
# 优先使用从 main.py 传入的完整资源列表(包含所有子资源)
|
# 将 ResourceTreeSet 转换为 list 用于 visual 组件
|
||||||
if resources_mesh_resource_list:
|
resources_list = (
|
||||||
resources_list = resources_mesh_resource_list
|
[node.res_content.model_dump(by_alias=True) for node in resources_config.all_nodes]
|
||||||
else:
|
if resources_config
|
||||||
# fallback: 从 ResourceTreeSet 获取
|
else []
|
||||||
resources_list = [node.res_content.model_dump(by_alias=True) for node in resources_config.all_nodes]
|
)
|
||||||
resource_mesh_manager = ResourceMeshManager(
|
resource_mesh_manager = ResourceMeshManager(
|
||||||
resources_mesh_config,
|
resources_mesh_config,
|
||||||
resources_list,
|
resources_list,
|
||||||
@@ -91,7 +90,7 @@ def main(
|
|||||||
device_id="resource_mesh_manager",
|
device_id="resource_mesh_manager",
|
||||||
device_uuid=str(uuid.uuid4()),
|
device_uuid=str(uuid.uuid4()),
|
||||||
)
|
)
|
||||||
joint_republisher = JointRepublisher("joint_republisher","joint_republisher", host_node.resource_tracker)
|
joint_republisher = JointRepublisher("joint_republisher", host_node.resource_tracker)
|
||||||
# lh_joint_pub = LiquidHandlerJointPublisher(
|
# lh_joint_pub = LiquidHandlerJointPublisher(
|
||||||
# resources_config=resources_list, resource_tracker=host_node.resource_tracker
|
# resources_config=resources_list, resource_tracker=host_node.resource_tracker
|
||||||
# )
|
# )
|
||||||
@@ -115,7 +114,6 @@ def slave(
|
|||||||
bridges: List[Any] = [],
|
bridges: List[Any] = [],
|
||||||
visual: str = "disable",
|
visual: str = "disable",
|
||||||
resources_mesh_config: dict = {},
|
resources_mesh_config: dict = {},
|
||||||
resources_mesh_resource_list: list = [],
|
|
||||||
rclpy_init_args: List[str] = ["--log-level", "debug"],
|
rclpy_init_args: List[str] = ["--log-level", "debug"],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""从节点函数"""
|
"""从节点函数"""
|
||||||
@@ -210,12 +208,12 @@ def slave(
|
|||||||
if visual != "disable":
|
if visual != "disable":
|
||||||
from unilabos.ros.nodes.presets.joint_republisher import JointRepublisher
|
from unilabos.ros.nodes.presets.joint_republisher import JointRepublisher
|
||||||
|
|
||||||
# 优先使用从 main.py 传入的完整资源列表(包含所有子资源)
|
# 将 ResourceTreeSet 转换为 list 用于 visual 组件
|
||||||
if resources_mesh_resource_list:
|
resources_list = (
|
||||||
resources_list = resources_mesh_resource_list
|
[node.res_content.model_dump(by_alias=True) for node in resources_config.all_nodes]
|
||||||
else:
|
if resources_config
|
||||||
# fallback: 从 ResourceTreeSet 获取
|
else []
|
||||||
resources_list = [node.res_content.model_dump(by_alias=True) for node in resources_config.all_nodes]
|
)
|
||||||
resource_mesh_manager = ResourceMeshManager(
|
resource_mesh_manager = ResourceMeshManager(
|
||||||
resources_mesh_config,
|
resources_mesh_config,
|
||||||
resources_list,
|
resources_list,
|
||||||
|
|||||||
@@ -23,32 +23,17 @@ from unilabos_msgs.action import SendCmd
|
|||||||
from rclpy.action.server import ServerGoalHandle
|
from rclpy.action.server import ServerGoalHandle
|
||||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode,DeviceNodeResourceTracker
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode,DeviceNodeResourceTracker
|
||||||
from unilabos.resources.graphio import initialize_resources
|
from unilabos.resources.graphio import initialize_resources
|
||||||
from unilabos.resources.resource_tracker import EXTRA_CLASS
|
|
||||||
from unilabos.registry.registry import lab_registry
|
from unilabos.registry.registry import lab_registry
|
||||||
|
|
||||||
class ResourceMeshManager(BaseROS2DeviceNode):
|
class ResourceMeshManager(BaseROS2DeviceNode):
|
||||||
def __init__(
|
def __init__(self, resource_model: dict, resource_config: list,resource_tracker, device_id: str = "resource_mesh_manager", registry_name: str = "", rate=50, **kwargs):
|
||||||
self,
|
|
||||||
resource_model: Optional[dict] = None,
|
|
||||||
resource_config: Optional[list] = None,
|
|
||||||
resource_tracker=None,
|
|
||||||
device_id: str = "resource_mesh_manager",
|
|
||||||
registry_name: str = "",
|
|
||||||
rate=50,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
"""初始化资源网格管理器节点
|
"""初始化资源网格管理器节点
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
resource_model: 资源模型字典(可选,为 None 时自动从 registry 构建)
|
resource_model (dict): 资源模型字典,包含资源的3D模型信息
|
||||||
resource_config: 资源配置列表(可选,为 None 时启动后通过 ActionServer 或 load_from_resource_tree 加载)
|
resource_config (dict): 资源配置字典,包含资源的配置信息
|
||||||
resource_tracker: 资源追踪器
|
device_id (str): 节点名称
|
||||||
device_id: 节点名称
|
|
||||||
rate: TF 发布频率
|
|
||||||
"""
|
"""
|
||||||
if resource_tracker is None:
|
|
||||||
resource_tracker = DeviceNodeResourceTracker()
|
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
driver_instance=self,
|
driver_instance=self,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
@@ -61,10 +46,8 @@ class ResourceMeshManager(BaseROS2DeviceNode):
|
|||||||
device_uuid=kwargs.get("uuid", str(uuid.uuid4())),
|
device_uuid=kwargs.get("uuid", str(uuid.uuid4())),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.resource_model = resource_model if resource_model is not None else {}
|
self.resource_model = resource_model
|
||||||
self.resource_config_dict = (
|
self.resource_config_dict = {item['uuid']: item for item in resource_config}
|
||||||
{item['uuid']: item for item in resource_config} if resource_config else {}
|
|
||||||
)
|
|
||||||
self.move_group_ready = False
|
self.move_group_ready = False
|
||||||
self.resource_tf_dict = {}
|
self.resource_tf_dict = {}
|
||||||
self.tf_broadcaster = TransformBroadcaster(self)
|
self.tf_broadcaster = TransformBroadcaster(self)
|
||||||
@@ -94,6 +77,7 @@ class ResourceMeshManager(BaseROS2DeviceNode):
|
|||||||
callback_group=callback_group,
|
callback_group=callback_group,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Create a service for applying the planning scene
|
||||||
self._apply_planning_scene_service = self.create_client(
|
self._apply_planning_scene_service = self.create_client(
|
||||||
srv_type=ApplyPlanningScene,
|
srv_type=ApplyPlanningScene,
|
||||||
srv_name="/apply_planning_scene",
|
srv_name="/apply_planning_scene",
|
||||||
@@ -119,36 +103,27 @@ class ResourceMeshManager(BaseROS2DeviceNode):
|
|||||||
AttachedCollisionObject, "/attached_collision_object", 0
|
AttachedCollisionObject, "/attached_collision_object", 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 创建一个Action Server用于修改resource_tf_dict
|
||||||
self._action_server = ActionServer(
|
self._action_server = ActionServer(
|
||||||
self,
|
self,
|
||||||
SendCmd,
|
SendCmd,
|
||||||
f"tf_update",
|
f"tf_update",
|
||||||
self.tf_update,
|
self.tf_update,
|
||||||
callback_group=callback_group,
|
callback_group=callback_group
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 创建一个Action Server用于添加新的资源模型与resource_tf_dict
|
||||||
self._add_resource_mesh_action_server = ActionServer(
|
self._add_resource_mesh_action_server = ActionServer(
|
||||||
self,
|
self,
|
||||||
SendCmd,
|
SendCmd,
|
||||||
f"add_resource_mesh",
|
f"add_resource_mesh",
|
||||||
self.add_resource_mesh_callback,
|
self.add_resource_mesh_callback,
|
||||||
callback_group=callback_group,
|
callback_group=callback_group
|
||||||
)
|
)
|
||||||
|
|
||||||
self._reload_resource_mesh_action_server = ActionServer(
|
self.resource_tf_dict = self.resource_mesh_setup(self.resource_config_dict)
|
||||||
self,
|
self.create_timer(1/self.rate, self.publish_resource_tf)
|
||||||
SendCmd,
|
self.create_timer(1/self.rate, self.check_resource_pose_changes)
|
||||||
f"reload_resource_mesh",
|
|
||||||
self._reload_resource_mesh_callback,
|
|
||||||
callback_group=callback_group,
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.resource_config_dict:
|
|
||||||
self.resource_tf_dict = self.resource_mesh_setup(self.resource_config_dict)
|
|
||||||
else:
|
|
||||||
self.get_logger().info("未提供 resource_config,将通过 ActionServer 或 load_from_resource_tree 加载")
|
|
||||||
self.create_timer(1 / self.rate, self.publish_resource_tf)
|
|
||||||
self.create_timer(1 / self.rate, self.check_resource_pose_changes)
|
|
||||||
|
|
||||||
def check_move_group_ready(self):
|
def check_move_group_ready(self):
|
||||||
"""检查move_group节点是否已初始化完成"""
|
"""检查move_group节点是否已初始化完成"""
|
||||||
@@ -165,107 +140,10 @@ class ResourceMeshManager(BaseROS2DeviceNode):
|
|||||||
self.add_resource_collision_meshes(self.resource_tf_dict)
|
self.add_resource_collision_meshes(self.resource_tf_dict)
|
||||||
|
|
||||||
|
|
||||||
def _build_resource_model_for_config(self, resource_config_dict: dict):
|
def add_resource_mesh_callback(self, goal_handle : ServerGoalHandle):
|
||||||
"""从 registry 中为给定的资源配置自动构建 resource_model(mesh 信息)"""
|
|
||||||
registry = lab_registry
|
|
||||||
for _uuid, res_cfg in resource_config_dict.items():
|
|
||||||
resource_id = res_cfg.get('id', '')
|
|
||||||
resource_class = res_cfg.get('class', '')
|
|
||||||
if not resource_class:
|
|
||||||
continue
|
|
||||||
if resource_class not in registry.resource_type_registry:
|
|
||||||
continue
|
|
||||||
reg_entry = registry.resource_type_registry[resource_class]
|
|
||||||
if 'model' not in reg_entry:
|
|
||||||
continue
|
|
||||||
model_config = reg_entry['model']
|
|
||||||
if model_config.get('type') != 'resource':
|
|
||||||
continue
|
|
||||||
if resource_id in self.resource_model:
|
|
||||||
continue
|
|
||||||
self.resource_model[resource_id] = {
|
|
||||||
'mesh': f"{str(self.mesh_path)}/device_mesh/resources/{model_config['mesh']}",
|
|
||||||
'mesh_tf': model_config['mesh_tf'],
|
|
||||||
}
|
|
||||||
if model_config.get('children_mesh') is not None:
|
|
||||||
self.resource_model[f"{resource_id}_"] = {
|
|
||||||
'mesh': f"{str(self.mesh_path)}/device_mesh/resources/{model_config['children_mesh']}",
|
|
||||||
'mesh_tf': model_config['children_mesh_tf'],
|
|
||||||
}
|
|
||||||
|
|
||||||
def load_from_resource_tree(self):
|
|
||||||
"""从 resource_tracker 中读取资源树,自动构建 resource_config_dict / resource_model 并刷新 TF"""
|
|
||||||
new_config_dict: dict = {}
|
|
||||||
|
|
||||||
def _collect_plr_resource(res, parent_uuid: Optional[str] = None):
|
|
||||||
res_uuid = getattr(res, 'unilabos_uuid', None)
|
|
||||||
if not res_uuid:
|
|
||||||
res_uuid = str(uuid.uuid4())
|
|
||||||
extra = getattr(res, 'unilabos_extra', {}) or {}
|
|
||||||
resource_class = extra.get(EXTRA_CLASS, '')
|
|
||||||
|
|
||||||
location = getattr(res, 'location', None)
|
|
||||||
pos_x = float(location.x) if location else 0.0
|
|
||||||
pos_y = float(location.y) if location else 0.0
|
|
||||||
pos_z = float(location.z) if location else 0.0
|
|
||||||
|
|
||||||
rotation = extra.get('rotation', {'x': 0, 'y': 0, 'z': 0})
|
|
||||||
|
|
||||||
new_config_dict[res_uuid] = {
|
|
||||||
'id': res.name,
|
|
||||||
'uuid': res_uuid,
|
|
||||||
'class': resource_class,
|
|
||||||
'parent_uuid': parent_uuid,
|
|
||||||
'pose': {
|
|
||||||
'position': {'x': pos_x, 'y': pos_y, 'z': pos_z},
|
|
||||||
'rotation': rotation,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for child in getattr(res, 'children', []) or []:
|
|
||||||
_collect_plr_resource(child, res_uuid)
|
|
||||||
|
|
||||||
for resource in self.resource_tracker.resources:
|
|
||||||
root_parent_uuid = None
|
|
||||||
plr_parent = getattr(resource, 'parent', None)
|
|
||||||
if plr_parent is not None:
|
|
||||||
root_parent_uuid = getattr(plr_parent, 'unilabos_uuid', None)
|
|
||||||
_collect_plr_resource(resource, root_parent_uuid)
|
|
||||||
|
|
||||||
if not new_config_dict:
|
|
||||||
self.get_logger().warning("resource_tracker 中没有找到任何资源")
|
|
||||||
return
|
|
||||||
|
|
||||||
self.resource_config_dict = {**self.resource_config_dict, **new_config_dict}
|
|
||||||
self._build_resource_model_for_config(new_config_dict)
|
|
||||||
|
|
||||||
tf_dict = self.resource_mesh_setup(new_config_dict)
|
|
||||||
self.resource_tf_dict = {**self.resource_tf_dict, **tf_dict}
|
|
||||||
self.publish_resource_tf()
|
|
||||||
if self.move_group_ready:
|
|
||||||
self.add_resource_collision_meshes(tf_dict)
|
|
||||||
self.get_logger().info(f"从资源树加载了 {len(new_config_dict)} 个资源")
|
|
||||||
|
|
||||||
def _reload_resource_mesh_callback(self, goal_handle: ServerGoalHandle):
|
|
||||||
"""ActionServer 回调:重新从资源树加载所有 mesh"""
|
|
||||||
try:
|
|
||||||
self.load_from_resource_tree()
|
|
||||||
except Exception as e:
|
|
||||||
self.get_logger().error(f"重新加载资源失败: {e}")
|
|
||||||
goal_handle.abort()
|
|
||||||
return SendCmd.Result(success=False)
|
|
||||||
goal_handle.succeed()
|
|
||||||
return SendCmd.Result(success=True)
|
|
||||||
|
|
||||||
def add_resource_mesh_callback(self, goal_handle: ServerGoalHandle):
|
|
||||||
tf_update_msg = goal_handle.request
|
tf_update_msg = goal_handle.request
|
||||||
try:
|
try:
|
||||||
parsed = json.loads(tf_update_msg.command.replace("'", '"'))
|
self.add_resource_mesh(tf_update_msg.command)
|
||||||
if 'resources' in parsed:
|
|
||||||
for res_config in parsed['resources']:
|
|
||||||
self.add_resource_mesh(json.dumps(res_config))
|
|
||||||
else:
|
|
||||||
self.add_resource_mesh(tf_update_msg.command)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.get_logger().error(f"添加资源失败: {e}")
|
self.get_logger().error(f"添加资源失败: {e}")
|
||||||
goal_handle.abort()
|
goal_handle.abort()
|
||||||
@@ -273,48 +151,45 @@ class ResourceMeshManager(BaseROS2DeviceNode):
|
|||||||
goal_handle.succeed()
|
goal_handle.succeed()
|
||||||
return SendCmd.Result(success=True)
|
return SendCmd.Result(success=True)
|
||||||
|
|
||||||
def add_resource_mesh(self, resource_config_str: str):
|
def add_resource_mesh(self,resource_config_str:str):
|
||||||
"""添加单个资源的 mesh 配置"""
|
"""刷新资源配置"""
|
||||||
|
|
||||||
registry = lab_registry
|
registry = lab_registry
|
||||||
resource_config = json.loads(resource_config_str.replace("'", '"'))
|
resource_config = json.loads(resource_config_str.replace("'",'"'))
|
||||||
|
|
||||||
if resource_config['id'] in self.resource_config_dict:
|
if resource_config['id'] in self.resource_config_dict:
|
||||||
self.get_logger().info(f'资源 {resource_config["id"]} 已存在')
|
self.get_logger().info(f'资源 {resource_config["id"]} 已存在')
|
||||||
return
|
return
|
||||||
resource_class = resource_config.get('class', '')
|
if resource_config['class'] in registry.resource_type_registry.keys():
|
||||||
if resource_class and resource_class in registry.resource_type_registry:
|
model_config = registry.resource_type_registry[resource_config['class']]['model']
|
||||||
reg_entry = registry.resource_type_registry[resource_class]
|
if model_config['type'] == 'resource':
|
||||||
if 'model' in reg_entry:
|
self.resource_model[resource_config['id']] = {
|
||||||
model_config = reg_entry['model']
|
'mesh': f"{str(self.mesh_path)}/device_mesh/resources/{model_config['mesh']}",
|
||||||
if model_config.get('type') == 'resource':
|
'mesh_tf': model_config['mesh_tf']}
|
||||||
self.resource_model[resource_config['id']] = {
|
if 'children_mesh' in model_config.keys():
|
||||||
'mesh': f"{str(self.mesh_path)}/device_mesh/resources/{model_config['mesh']}",
|
self.resource_model[f"{resource_config['id']}_"] = {
|
||||||
'mesh_tf': model_config['mesh_tf'],
|
'mesh': f"{str(self.mesh_path)}/device_mesh/resources/{model_config['children_mesh']}",
|
||||||
|
'mesh_tf': model_config['children_mesh_tf']
|
||||||
}
|
}
|
||||||
if model_config.get('children_mesh') is not None:
|
|
||||||
self.resource_model[f"{resource_config['id']}_"] = {
|
|
||||||
'mesh': f"{str(self.mesh_path)}/device_mesh/resources/{model_config['children_mesh']}",
|
|
||||||
'mesh_tf': model_config['children_mesh_tf'],
|
|
||||||
}
|
|
||||||
resources = initialize_resources([resource_config])
|
resources = initialize_resources([resource_config])
|
||||||
resource_dict = {item['id']: item for item in resources}
|
resource_dict = {item['id']: item for item in resources}
|
||||||
self.resource_config_dict = {**self.resource_config_dict, **resource_dict}
|
self.resource_config_dict = {**self.resource_config_dict,**resource_dict}
|
||||||
tf_dict = self.resource_mesh_setup(resource_dict)
|
tf_dict = self.resource_mesh_setup(resource_dict)
|
||||||
self.resource_tf_dict = {**self.resource_tf_dict, **tf_dict}
|
self.resource_tf_dict = {**self.resource_tf_dict,**tf_dict}
|
||||||
self.publish_resource_tf()
|
self.publish_resource_tf()
|
||||||
self.add_resource_collision_meshes(tf_dict)
|
self.add_resource_collision_meshes(tf_dict)
|
||||||
|
|
||||||
def resource_mesh_setup(self, resource_config_dict: dict):
|
|
||||||
"""根据资源配置字典设置 TF 关系"""
|
def resource_mesh_setup(self, resource_config_dict:dict):
|
||||||
|
"""move_group初始化完成后的设置"""
|
||||||
self.get_logger().info('开始设置资源网格管理器')
|
self.get_logger().info('开始设置资源网格管理器')
|
||||||
|
#遍历resource_config中的资源配置,判断panent是否在resource_model中,
|
||||||
resource_tf_dict = {}
|
resource_tf_dict = {}
|
||||||
for resource_uuid, resource_config in resource_config_dict.items():
|
for resource_uuid, resource_config in resource_config_dict.items():
|
||||||
parent = None
|
parent = None
|
||||||
resource_id = resource_config['id']
|
resource_id = resource_config['id']
|
||||||
parent_uuid = resource_config.get('parent_uuid')
|
if resource_config['parent_uuid'] is not None and resource_config['parent_uuid'] != "":
|
||||||
if parent_uuid is not None and parent_uuid != "":
|
parent = resource_config_dict[resource_config['parent_uuid']]['id']
|
||||||
parent_entry = resource_config_dict.get(parent_uuid) or self.resource_config_dict.get(parent_uuid)
|
|
||||||
parent = parent_entry['id'] if parent_entry else None
|
|
||||||
|
|
||||||
parent_link = 'world'
|
parent_link = 'world'
|
||||||
if parent in self.resource_model:
|
if parent in self.resource_model:
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
"occupied_by": null,
|
"occupied_by": null,
|
||||||
"position": {
|
"position": {
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 288,
|
||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
@@ -89,7 +89,9 @@
|
|||||||
"plates",
|
"plates",
|
||||||
"tip_racks",
|
"tip_racks",
|
||||||
"tube_rack",
|
"tube_rack",
|
||||||
"adaptor"
|
"adaptor",
|
||||||
|
"plateadapter",
|
||||||
|
"module"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -98,7 +100,7 @@
|
|||||||
"occupied_by": null,
|
"occupied_by": null,
|
||||||
"position": {
|
"position": {
|
||||||
"x": 138,
|
"x": 138,
|
||||||
"y": 0,
|
"y": 288,
|
||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
@@ -112,7 +114,9 @@
|
|||||||
"plates",
|
"plates",
|
||||||
"tip_racks",
|
"tip_racks",
|
||||||
"tube_rack",
|
"tube_rack",
|
||||||
"adaptor"
|
"adaptor",
|
||||||
|
"plateadapter",
|
||||||
|
"module"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -121,7 +125,7 @@
|
|||||||
"occupied_by": null,
|
"occupied_by": null,
|
||||||
"position": {
|
"position": {
|
||||||
"x": 276,
|
"x": 276,
|
||||||
"y": 0,
|
"y": 288,
|
||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
@@ -135,7 +139,9 @@
|
|||||||
"plates",
|
"plates",
|
||||||
"tip_racks",
|
"tip_racks",
|
||||||
"tube_rack",
|
"tube_rack",
|
||||||
"adaptor"
|
"adaptor",
|
||||||
|
"plateadapter",
|
||||||
|
"module"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -144,6 +150,231 @@
|
|||||||
"occupied_by": null,
|
"occupied_by": null,
|
||||||
"position": {
|
"position": {
|
||||||
"x": 414,
|
"x": 414,
|
||||||
|
"y": 288,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"width": 128.0,
|
||||||
|
"height": 86,
|
||||||
|
"depth": 0
|
||||||
|
},
|
||||||
|
"content_type": [
|
||||||
|
"plate",
|
||||||
|
"tip_rack",
|
||||||
|
"plates",
|
||||||
|
"tip_racks",
|
||||||
|
"tube_rack",
|
||||||
|
"adaptor",
|
||||||
|
"plateadapter",
|
||||||
|
"module"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "T5",
|
||||||
|
"visible": true,
|
||||||
|
"occupied_by": null,
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 192,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"width": 128.0,
|
||||||
|
"height": 86,
|
||||||
|
"depth": 0
|
||||||
|
},
|
||||||
|
"content_type": [
|
||||||
|
"plate",
|
||||||
|
"tip_rack",
|
||||||
|
"plates",
|
||||||
|
"tip_racks",
|
||||||
|
"tube_rack",
|
||||||
|
"adaptor",
|
||||||
|
"plateadapter",
|
||||||
|
"module"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "T6",
|
||||||
|
"visible": true,
|
||||||
|
"occupied_by": null,
|
||||||
|
"position": {
|
||||||
|
"x": 138,
|
||||||
|
"y": 192,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"width": 128.0,
|
||||||
|
"height": 86,
|
||||||
|
"depth": 0
|
||||||
|
},
|
||||||
|
"content_type": [
|
||||||
|
"plate",
|
||||||
|
"tip_rack",
|
||||||
|
"plates",
|
||||||
|
"tip_racks",
|
||||||
|
"tube_rack",
|
||||||
|
"adaptor",
|
||||||
|
"plateadapter",
|
||||||
|
"module"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "T7",
|
||||||
|
"visible": true,
|
||||||
|
"occupied_by": null,
|
||||||
|
"position": {
|
||||||
|
"x": 276,
|
||||||
|
"y": 192,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"width": 128.0,
|
||||||
|
"height": 86,
|
||||||
|
"depth": 0
|
||||||
|
},
|
||||||
|
"content_type": [
|
||||||
|
"plate",
|
||||||
|
"tip_rack",
|
||||||
|
"plates",
|
||||||
|
"tip_racks",
|
||||||
|
"tube_rack",
|
||||||
|
"adaptor",
|
||||||
|
"plateadapter",
|
||||||
|
"module"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "T8",
|
||||||
|
"visible": true,
|
||||||
|
"occupied_by": null,
|
||||||
|
"position": {
|
||||||
|
"x": 414,
|
||||||
|
"y": 192,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"width": 128.0,
|
||||||
|
"height": 86,
|
||||||
|
"depth": 0
|
||||||
|
},
|
||||||
|
"content_type": [
|
||||||
|
"plate",
|
||||||
|
"tip_rack",
|
||||||
|
"plates",
|
||||||
|
"tip_racks",
|
||||||
|
"tube_rack",
|
||||||
|
"adaptor",
|
||||||
|
"plateadapter",
|
||||||
|
"module"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "T9",
|
||||||
|
"visible": true,
|
||||||
|
"occupied_by": null,
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 96,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"width": 128.0,
|
||||||
|
"height": 86,
|
||||||
|
"depth": 0
|
||||||
|
},
|
||||||
|
"content_type": [
|
||||||
|
"plate",
|
||||||
|
"tip_rack",
|
||||||
|
"plates",
|
||||||
|
"tip_racks",
|
||||||
|
"tube_rack",
|
||||||
|
"adaptor",
|
||||||
|
"plateadapter",
|
||||||
|
"module"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "T10",
|
||||||
|
"visible": true,
|
||||||
|
"occupied_by": null,
|
||||||
|
"position": {
|
||||||
|
"x": 138,
|
||||||
|
"y": 96,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"width": 128.0,
|
||||||
|
"height": 86,
|
||||||
|
"depth": 0
|
||||||
|
},
|
||||||
|
"content_type": [
|
||||||
|
"plate",
|
||||||
|
"tip_rack",
|
||||||
|
"plates",
|
||||||
|
"tip_racks",
|
||||||
|
"tube_rack",
|
||||||
|
"adaptor",
|
||||||
|
"plateadapter",
|
||||||
|
"module"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "T11",
|
||||||
|
"visible": true,
|
||||||
|
"occupied_by": null,
|
||||||
|
"position": {
|
||||||
|
"x": 276,
|
||||||
|
"y": 96,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"width": 128.0,
|
||||||
|
"height": 86,
|
||||||
|
"depth": 0
|
||||||
|
},
|
||||||
|
"content_type": [
|
||||||
|
"plate",
|
||||||
|
"tip_rack",
|
||||||
|
"plates",
|
||||||
|
"tip_racks",
|
||||||
|
"tube_rack",
|
||||||
|
"adaptor",
|
||||||
|
"plateadapter",
|
||||||
|
"module"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "T12",
|
||||||
|
"visible": true,
|
||||||
|
"occupied_by": null,
|
||||||
|
"position": {
|
||||||
|
"x": 414,
|
||||||
|
"y": 96,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"width": 128.0,
|
||||||
|
"height": 86,
|
||||||
|
"depth": 0
|
||||||
|
},
|
||||||
|
"content_type": [
|
||||||
|
"plate",
|
||||||
|
"tip_rack",
|
||||||
|
"plates",
|
||||||
|
"tip_racks",
|
||||||
|
"tube_rack",
|
||||||
|
"adaptor",
|
||||||
|
"plateadapter",
|
||||||
|
"module"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "T13",
|
||||||
|
"visible": true,
|
||||||
|
"occupied_by": null,
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
@@ -158,214 +389,9 @@
|
|||||||
"plates",
|
"plates",
|
||||||
"tip_racks",
|
"tip_racks",
|
||||||
"tube_rack",
|
"tube_rack",
|
||||||
"adaptor"
|
"adaptor",
|
||||||
]
|
"plateadapter",
|
||||||
},
|
"module"
|
||||||
{
|
|
||||||
"label": "T5",
|
|
||||||
"visible": true,
|
|
||||||
"occupied_by": null,
|
|
||||||
"position": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 96,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"width": 128.0,
|
|
||||||
"height": 86,
|
|
||||||
"depth": 0
|
|
||||||
},
|
|
||||||
"content_type": [
|
|
||||||
"plate",
|
|
||||||
"tip_rack",
|
|
||||||
"plates",
|
|
||||||
"tip_racks",
|
|
||||||
"tube_rack",
|
|
||||||
"adaptor"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "T6",
|
|
||||||
"visible": true,
|
|
||||||
"occupied_by": null,
|
|
||||||
"position": {
|
|
||||||
"x": 138,
|
|
||||||
"y": 96,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"width": 128.0,
|
|
||||||
"height": 86,
|
|
||||||
"depth": 0
|
|
||||||
},
|
|
||||||
"content_type": [
|
|
||||||
"plate",
|
|
||||||
"tip_rack",
|
|
||||||
"plates",
|
|
||||||
"tip_racks",
|
|
||||||
"tube_rack",
|
|
||||||
"adaptor"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "T7",
|
|
||||||
"visible": true,
|
|
||||||
"occupied_by": null,
|
|
||||||
"position": {
|
|
||||||
"x": 276,
|
|
||||||
"y": 96,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"width": 128.0,
|
|
||||||
"height": 86,
|
|
||||||
"depth": 0
|
|
||||||
},
|
|
||||||
"content_type": [
|
|
||||||
"plate",
|
|
||||||
"tip_rack",
|
|
||||||
"plates",
|
|
||||||
"tip_racks",
|
|
||||||
"tube_rack",
|
|
||||||
"adaptor"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "T8",
|
|
||||||
"visible": true,
|
|
||||||
"occupied_by": null,
|
|
||||||
"position": {
|
|
||||||
"x": 414,
|
|
||||||
"y": 96,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"width": 128.0,
|
|
||||||
"height": 86,
|
|
||||||
"depth": 0
|
|
||||||
},
|
|
||||||
"content_type": [
|
|
||||||
"plate",
|
|
||||||
"tip_rack",
|
|
||||||
"plates",
|
|
||||||
"tip_racks",
|
|
||||||
"tube_rack",
|
|
||||||
"adaptor"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "T9",
|
|
||||||
"visible": true,
|
|
||||||
"occupied_by": null,
|
|
||||||
"position": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 192,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"width": 128.0,
|
|
||||||
"height": 86,
|
|
||||||
"depth": 0
|
|
||||||
},
|
|
||||||
"content_type": [
|
|
||||||
"plate",
|
|
||||||
"tip_rack",
|
|
||||||
"plates",
|
|
||||||
"tip_racks",
|
|
||||||
"tube_rack",
|
|
||||||
"adaptor"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "T10",
|
|
||||||
"visible": true,
|
|
||||||
"occupied_by": null,
|
|
||||||
"position": {
|
|
||||||
"x": 138,
|
|
||||||
"y": 192,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"width": 128.0,
|
|
||||||
"height": 86,
|
|
||||||
"depth": 0
|
|
||||||
},
|
|
||||||
"content_type": [
|
|
||||||
"plate",
|
|
||||||
"tip_rack",
|
|
||||||
"plates",
|
|
||||||
"tip_racks",
|
|
||||||
"tube_rack",
|
|
||||||
"adaptor"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "T11",
|
|
||||||
"visible": true,
|
|
||||||
"occupied_by": null,
|
|
||||||
"position": {
|
|
||||||
"x": 276,
|
|
||||||
"y": 192,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"width": 128.0,
|
|
||||||
"height": 86,
|
|
||||||
"depth": 0
|
|
||||||
},
|
|
||||||
"content_type": [
|
|
||||||
"plate",
|
|
||||||
"tip_rack",
|
|
||||||
"plates",
|
|
||||||
"tip_racks",
|
|
||||||
"tube_rack",
|
|
||||||
"adaptor"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "T12",
|
|
||||||
"visible": true,
|
|
||||||
"occupied_by": null,
|
|
||||||
"position": {
|
|
||||||
"x": 414,
|
|
||||||
"y": 192,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"width": 128.0,
|
|
||||||
"height": 86,
|
|
||||||
"depth": 0
|
|
||||||
},
|
|
||||||
"content_type": [
|
|
||||||
"plate",
|
|
||||||
"tip_rack",
|
|
||||||
"plates",
|
|
||||||
"tip_racks",
|
|
||||||
"tube_rack",
|
|
||||||
"adaptor"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "T13",
|
|
||||||
"visible": true,
|
|
||||||
"occupied_by": null,
|
|
||||||
"position": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 288,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"width": 128.0,
|
|
||||||
"height": 86,
|
|
||||||
"depth": 0
|
|
||||||
},
|
|
||||||
"content_type": [
|
|
||||||
"plate",
|
|
||||||
"tip_rack",
|
|
||||||
"plates",
|
|
||||||
"tip_racks",
|
|
||||||
"tube_rack",
|
|
||||||
"adaptor"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -374,7 +400,7 @@
|
|||||||
"occupied_by": null,
|
"occupied_by": null,
|
||||||
"position": {
|
"position": {
|
||||||
"x": 138,
|
"x": 138,
|
||||||
"y": 288,
|
"y": 0,
|
||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
@@ -388,7 +414,9 @@
|
|||||||
"plates",
|
"plates",
|
||||||
"tip_racks",
|
"tip_racks",
|
||||||
"tube_rack",
|
"tube_rack",
|
||||||
"adaptor"
|
"adaptor",
|
||||||
|
"plateadapter",
|
||||||
|
"module"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -397,7 +425,7 @@
|
|||||||
"occupied_by": null,
|
"occupied_by": null,
|
||||||
"position": {
|
"position": {
|
||||||
"x": 276,
|
"x": 276,
|
||||||
"y": 288,
|
"y": 0,
|
||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
@@ -411,7 +439,9 @@
|
|||||||
"plates",
|
"plates",
|
||||||
"tip_racks",
|
"tip_racks",
|
||||||
"tube_rack",
|
"tube_rack",
|
||||||
"adaptor"
|
"adaptor",
|
||||||
|
"plateadapter",
|
||||||
|
"module"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -420,7 +450,7 @@
|
|||||||
"occupied_by": null,
|
"occupied_by": null,
|
||||||
"position": {
|
"position": {
|
||||||
"x": 414,
|
"x": 414,
|
||||||
"y": 288,
|
"y": 0,
|
||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
@@ -434,7 +464,9 @@
|
|||||||
"plates",
|
"plates",
|
||||||
"tip_racks",
|
"tip_racks",
|
||||||
"tube_rack",
|
"tube_rack",
|
||||||
"adaptor"
|
"adaptor",
|
||||||
|
"plateadapter",
|
||||||
|
"module"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -108,7 +108,8 @@
|
|||||||
"tip_rack",
|
"tip_rack",
|
||||||
"plates",
|
"plates",
|
||||||
"tip_racks",
|
"tip_racks",
|
||||||
"tube_rack"
|
"tube_rack",
|
||||||
|
"plateadapter"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -153,7 +154,8 @@
|
|||||||
"tip_rack",
|
"tip_rack",
|
||||||
"plates",
|
"plates",
|
||||||
"tip_racks",
|
"tip_racks",
|
||||||
"tube_rack"
|
"tube_rack",
|
||||||
|
"plateadapter"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -198,7 +200,8 @@
|
|||||||
"tip_rack",
|
"tip_rack",
|
||||||
"plates",
|
"plates",
|
||||||
"tip_racks",
|
"tip_racks",
|
||||||
"tube_rack"
|
"tube_rack",
|
||||||
|
"plateadapter"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -243,7 +246,8 @@
|
|||||||
"tip_rack",
|
"tip_rack",
|
||||||
"plates",
|
"plates",
|
||||||
"tip_racks",
|
"tip_racks",
|
||||||
"tube_rack"
|
"tube_rack",
|
||||||
|
"plateadapter"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -288,7 +292,8 @@
|
|||||||
"tip_rack",
|
"tip_rack",
|
||||||
"plates",
|
"plates",
|
||||||
"tip_racks",
|
"tip_racks",
|
||||||
"tube_rack"
|
"tube_rack",
|
||||||
|
"plateadapter"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -333,7 +338,8 @@
|
|||||||
"tip_rack",
|
"tip_rack",
|
||||||
"plates",
|
"plates",
|
||||||
"tip_racks",
|
"tip_racks",
|
||||||
"tube_rack"
|
"tube_rack",
|
||||||
|
"plateadapter"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -378,7 +384,8 @@
|
|||||||
"tip_rack",
|
"tip_rack",
|
||||||
"plates",
|
"plates",
|
||||||
"tip_racks",
|
"tip_racks",
|
||||||
"tube_rack"
|
"tube_rack",
|
||||||
|
"plateadapter"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -423,7 +430,8 @@
|
|||||||
"tip_rack",
|
"tip_rack",
|
||||||
"plates",
|
"plates",
|
||||||
"tip_racks",
|
"tip_racks",
|
||||||
"tube_rack"
|
"tube_rack",
|
||||||
|
"plateadapter"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -468,7 +476,8 @@
|
|||||||
"tip_rack",
|
"tip_rack",
|
||||||
"plates",
|
"plates",
|
||||||
"tip_racks",
|
"tip_racks",
|
||||||
"tube_rack"
|
"tube_rack",
|
||||||
|
"plateadapter"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -513,7 +522,8 @@
|
|||||||
"tip_rack",
|
"tip_rack",
|
||||||
"plates",
|
"plates",
|
||||||
"tip_racks",
|
"tip_racks",
|
||||||
"tube_rack"
|
"tube_rack",
|
||||||
|
"plateadapter"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -558,7 +568,8 @@
|
|||||||
"tip_rack",
|
"tip_rack",
|
||||||
"plates",
|
"plates",
|
||||||
"tip_racks",
|
"tip_racks",
|
||||||
"tube_rack"
|
"tube_rack",
|
||||||
|
"plateadapter"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -603,7 +614,8 @@
|
|||||||
"tip_rack",
|
"tip_rack",
|
||||||
"plates",
|
"plates",
|
||||||
"tip_racks",
|
"tip_racks",
|
||||||
"tube_rack"
|
"tube_rack",
|
||||||
|
"plateadapter"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -648,7 +660,8 @@
|
|||||||
"tip_rack",
|
"tip_rack",
|
||||||
"plates",
|
"plates",
|
||||||
"tip_racks",
|
"tip_racks",
|
||||||
"tube_rack"
|
"tube_rack",
|
||||||
|
"plateadapter"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -693,7 +706,8 @@
|
|||||||
"tip_rack",
|
"tip_rack",
|
||||||
"plates",
|
"plates",
|
||||||
"tip_racks",
|
"tip_racks",
|
||||||
"tube_rack"
|
"tube_rack",
|
||||||
|
"plateadapter"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -738,7 +752,8 @@
|
|||||||
"tip_rack",
|
"tip_rack",
|
||||||
"plates",
|
"plates",
|
||||||
"tip_racks",
|
"tip_racks",
|
||||||
"tube_rack"
|
"tube_rack",
|
||||||
|
"plateadapter"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -783,7 +798,8 @@
|
|||||||
"tip_rack",
|
"tip_rack",
|
||||||
"plates",
|
"plates",
|
||||||
"tip_racks",
|
"tip_racks",
|
||||||
"tube_rack"
|
"tube_rack",
|
||||||
|
"plateadapter"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -51,7 +51,6 @@
|
|||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
- 遍历 workflow 数组,为每个动作创建步骤节点
|
- 遍历 workflow 数组,为每个动作创建步骤节点
|
||||||
- 参数重命名: asp_vol -> asp_vols, dis_vol -> dis_vols, asp_flow_rate -> asp_flow_rates, dis_flow_rate -> dis_flow_rates
|
- 参数重命名: asp_vol -> asp_vols, dis_vol -> dis_vols, asp_flow_rate -> asp_flow_rates, dis_flow_rate -> dis_flow_rates
|
||||||
- 参数输入转换: liquid_height(按 wells 扩展);mix_stage/mix_times/mix_vol/mix_rate/mix_liquid_height 保持标量
|
|
||||||
- 参数扩展: 根据 targets 的 wells 数量,将单值扩展为数组
|
- 参数扩展: 根据 targets 的 wells 数量,将单值扩展为数组
|
||||||
例: asp_vol=100.0, targets 有 3 个 wells -> asp_vols=[100.0, 100.0, 100.0]
|
例: asp_vol=100.0, targets 有 3 个 wells -> asp_vols=[100.0, 100.0, 100.0]
|
||||||
- 连接处理: 如果 sources/targets 已通过 set_liquid_from_plate 连接,参数值改为 []
|
- 连接处理: 如果 sources/targets 已通过 set_liquid_from_plate 连接,参数值改为 []
|
||||||
@@ -120,14 +119,11 @@ DEVICE_NAME_DEFAULT = "PRCXI" # transfer_liquid, set_liquid_from_plate 等动
|
|||||||
# 节点类型
|
# 节点类型
|
||||||
NODE_TYPE_DEFAULT = "ILab" # 所有节点的默认类型
|
NODE_TYPE_DEFAULT = "ILab" # 所有节点的默认类型
|
||||||
|
|
||||||
CLASS_NAMES_MAPPING = {
|
|
||||||
"plate": "PRCXI_BioER_96_wellplate",
|
|
||||||
"tip_rack": "PRCXI_300ul_Tips",
|
|
||||||
}
|
|
||||||
# create_resource 节点默认参数
|
# create_resource 节点默认参数
|
||||||
CREATE_RESOURCE_DEFAULTS = {
|
CREATE_RESOURCE_DEFAULTS = {
|
||||||
"device_id": "/PRCXI",
|
"device_id": "/PRCXI",
|
||||||
"parent_template": "/PRCXI/PRCXI_Deck",
|
"parent_template": "/PRCXI/PRCXI_Deck",
|
||||||
|
"class_name": "PRCXI_BioER_96_wellplate",
|
||||||
}
|
}
|
||||||
|
|
||||||
# 默认液体体积 (uL)
|
# 默认液体体积 (uL)
|
||||||
@@ -142,16 +138,6 @@ PARAM_RENAME_MAPPING = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _map_deck_slot(raw_slot: str, object_type: str = "") -> str:
|
|
||||||
"""协议槽位 -> 实际 deck:4→13,8→14,12+trash→16,其余不变。"""
|
|
||||||
s = "" if raw_slot is None else str(raw_slot).strip()
|
|
||||||
if not s:
|
|
||||||
return ""
|
|
||||||
if s == "12" and (object_type or "").strip().lower() == "trash":
|
|
||||||
return "16"
|
|
||||||
return {"4": "13", "8": "14"}.get(s, s)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------- Graph ----------------
|
# ---------------- Graph ----------------
|
||||||
|
|
||||||
|
|
||||||
@@ -381,10 +367,11 @@ def build_protocol_graph(
|
|||||||
"""统一的协议图构建函数,根据设备类型自动选择构建逻辑
|
"""统一的协议图构建函数,根据设备类型自动选择构建逻辑
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
labware_info: labware 信息字典,格式为 {name: {slot, well, labware, ...}, ...}
|
labware_info: reagent 信息字典,格式为 {name: {slot, well}, ...},用于 set_liquid 和 well 查找
|
||||||
protocol_steps: 协议步骤列表
|
protocol_steps: 协议步骤列表
|
||||||
workstation_name: 工作站名称
|
workstation_name: 工作站名称
|
||||||
action_resource_mapping: action 到 resource_name 的映射字典,可选
|
action_resource_mapping: action 到 resource_name 的映射字典,可选
|
||||||
|
labware_defs: labware 定义列表,格式为 [{"name": "...", "slot": "1", "type": "lab_xxx"}, ...]
|
||||||
"""
|
"""
|
||||||
G = WorkflowGraph()
|
G = WorkflowGraph()
|
||||||
resource_last_writer = {} # reagent_name -> "node_id:port"
|
resource_last_writer = {} # reagent_name -> "node_id:port"
|
||||||
@@ -392,22 +379,7 @@ def build_protocol_graph(
|
|||||||
|
|
||||||
protocol_steps = refactor_data(protocol_steps, action_resource_mapping)
|
protocol_steps = refactor_data(protocol_steps, action_resource_mapping)
|
||||||
|
|
||||||
# ==================== 第一步:按 slot 去重创建 create_resource 节点 ====================
|
# ==================== 第一步:按 slot 创建 create_resource 节点 ====================
|
||||||
# 收集所有唯一的 slot
|
|
||||||
slots_info = {} # slot -> {labware, res_id}
|
|
||||||
for labware_id, item in labware_info.items():
|
|
||||||
object_type = item.get("object", "") or ""
|
|
||||||
slot = _map_deck_slot(str(item.get("slot", "")), object_type)
|
|
||||||
labware = item.get("labware", "")
|
|
||||||
if slot and slot not in slots_info:
|
|
||||||
res_id = f"{labware}_slot_{slot}"
|
|
||||||
slots_info[slot] = {
|
|
||||||
"labware": labware,
|
|
||||||
"res_id": res_id,
|
|
||||||
"labware_id": labware_id,
|
|
||||||
"object": object_type,
|
|
||||||
}
|
|
||||||
|
|
||||||
# 创建 Group 节点,包含所有 create_resource 节点
|
# 创建 Group 节点,包含所有 create_resource 节点
|
||||||
group_node_id = str(uuid.uuid4())
|
group_node_id = str(uuid.uuid4())
|
||||||
G.add_node(
|
G.add_node(
|
||||||
@@ -423,44 +395,41 @@ def build_protocol_graph(
|
|||||||
param=None,
|
param=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
trash_create_node_id = None # 记录 trash 的 create_resource 节点
|
# 直接使用 JSON 中的 labware 定义,每个 slot 一条记录,type 即 class_name
|
||||||
|
res_index = 0
|
||||||
|
for lw in (labware_defs or []):
|
||||||
|
slot = str(lw.get("slot", ""))
|
||||||
|
if not slot or slot in slot_to_create_resource:
|
||||||
|
continue # 跳过空 slot 或已处理的 slot
|
||||||
|
|
||||||
# 为每个唯一的 slot 创建 create_resource 节点
|
lw_name = lw.get("name", f"slot {slot}")
|
||||||
for slot, info in slots_info.items():
|
lw_type = lw.get("type", CREATE_RESOURCE_DEFAULTS["class_name"])
|
||||||
|
res_id = f"plate_slot_{slot}"
|
||||||
|
|
||||||
|
res_index += 1
|
||||||
node_id = str(uuid.uuid4())
|
node_id = str(uuid.uuid4())
|
||||||
res_id = info["res_id"]
|
|
||||||
res_type_name = info["labware"].lower().replace(".", "point")
|
|
||||||
object_type = info.get("object", "")
|
|
||||||
res_type_name = f"lab_{res_type_name}"
|
|
||||||
if object_type == "trash":
|
|
||||||
res_type_name = "PRCXI_trash"
|
|
||||||
G.add_node(
|
G.add_node(
|
||||||
node_id,
|
node_id,
|
||||||
template_name="create_resource",
|
template_name="create_resource",
|
||||||
resource_name="host_node",
|
resource_name="host_node",
|
||||||
name=f"{res_type_name}_slot{slot}",
|
name=lw_name,
|
||||||
description=f"Create plate on slot {slot}",
|
description=f"Create {lw_name}",
|
||||||
lab_node_type="Labware",
|
lab_node_type="Labware",
|
||||||
footer="create_resource-host_node",
|
footer="create_resource-host_node",
|
||||||
device_name=DEVICE_NAME_HOST,
|
device_name=DEVICE_NAME_HOST,
|
||||||
type=NODE_TYPE_DEFAULT,
|
type=NODE_TYPE_DEFAULT,
|
||||||
parent_uuid=group_node_id, # 指向 Group 节点
|
parent_uuid=group_node_id,
|
||||||
minimized=True, # 折叠显示
|
minimized=True,
|
||||||
param={
|
param={
|
||||||
"res_id": res_id,
|
"res_id": res_id,
|
||||||
"device_id": CREATE_RESOURCE_DEFAULTS["device_id"],
|
"device_id": CREATE_RESOURCE_DEFAULTS["device_id"],
|
||||||
"class_name": res_type_name,
|
"class_name": lw_type,
|
||||||
"parent": CREATE_RESOURCE_DEFAULTS["parent_template"].format(slot=slot),
|
"parent": CREATE_RESOURCE_DEFAULTS["parent_template"],
|
||||||
"bind_locations": {"x": 0.0, "y": 0.0, "z": 0.0},
|
"bind_locations": {"x": 0.0, "y": 0.0, "z": 0.0},
|
||||||
"slot_on_deck": slot,
|
"slot_on_deck": slot,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
slot_to_create_resource[slot] = node_id
|
slot_to_create_resource[slot] = node_id
|
||||||
if object_type == "tiprack":
|
|
||||||
resource_last_writer[info["labware_id"]] = f"{node_id}:labware"
|
|
||||||
if object_type == "trash":
|
|
||||||
trash_create_node_id = node_id
|
|
||||||
# create_resource 之间不需要 ready 连接
|
|
||||||
|
|
||||||
# ==================== 第二步:为每个 reagent 创建 set_liquid_from_plate 节点 ====================
|
# ==================== 第二步:为每个 reagent 创建 set_liquid_from_plate 节点 ====================
|
||||||
# 创建 Group 节点,包含所有 set_liquid_from_plate 节点
|
# 创建 Group 节点,包含所有 set_liquid_from_plate 节点
|
||||||
@@ -487,8 +456,7 @@ def build_protocol_graph(
|
|||||||
if item.get("type") == "hardware":
|
if item.get("type") == "hardware":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
object_type = item.get("object", "") or ""
|
slot = str(item.get("slot", ""))
|
||||||
slot = _map_deck_slot(str(item.get("slot", "")), object_type)
|
|
||||||
wells = item.get("well", [])
|
wells = item.get("well", [])
|
||||||
if not wells or not slot:
|
if not wells or not slot:
|
||||||
continue
|
continue
|
||||||
@@ -496,7 +464,6 @@ def build_protocol_graph(
|
|||||||
# res_id 不能有空格
|
# res_id 不能有空格
|
||||||
res_id = str(labware_id).replace(" ", "_")
|
res_id = str(labware_id).replace(" ", "_")
|
||||||
well_count = len(wells)
|
well_count = len(wells)
|
||||||
liquid_volume = DEFAULT_LIQUID_VOLUME if object_type == "source" else 0
|
|
||||||
|
|
||||||
node_id = str(uuid.uuid4())
|
node_id = str(uuid.uuid4())
|
||||||
set_liquid_index += 1
|
set_liquid_index += 1
|
||||||
@@ -517,7 +484,7 @@ def build_protocol_graph(
|
|||||||
"plate": [], # 通过连接传递
|
"plate": [], # 通过连接传递
|
||||||
"well_names": wells, # 孔位名数组,如 ["A1", "A3", "A5"]
|
"well_names": wells, # 孔位名数组,如 ["A1", "A3", "A5"]
|
||||||
"liquid_names": [res_id] * well_count,
|
"liquid_names": [res_id] * well_count,
|
||||||
"volumes": [liquid_volume] * well_count,
|
"volumes": [DEFAULT_LIQUID_VOLUME] * well_count,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -531,12 +498,8 @@ def build_protocol_graph(
|
|||||||
# set_liquid_from_plate 的输出 output_wells 用于连接 transfer_liquid
|
# set_liquid_from_plate 的输出 output_wells 用于连接 transfer_liquid
|
||||||
resource_last_writer[labware_id] = f"{node_id}:output_wells"
|
resource_last_writer[labware_id] = f"{node_id}:output_wells"
|
||||||
|
|
||||||
# 收集所有 create_resource 节点 ID,用于让第一个 transfer_liquid 等待所有资源创建完成
|
# transfer_liquid 之间通过 ready 串联,从 None 开始
|
||||||
all_create_resource_node_ids = list(slot_to_create_resource.values())
|
last_control_node_id = None
|
||||||
|
|
||||||
# transfer_liquid 之间通过 ready 串联;第一个 transfer_liquid 需要等待所有 create_resource 完成
|
|
||||||
last_control_node_id = trash_create_node_id
|
|
||||||
is_first_action_node = True
|
|
||||||
|
|
||||||
# 端口名称映射:JSON 字段名 -> 实际 handle key
|
# 端口名称映射:JSON 字段名 -> 实际 handle key
|
||||||
INPUT_PORT_MAPPING = {
|
INPUT_PORT_MAPPING = {
|
||||||
@@ -548,7 +511,6 @@ def build_protocol_graph(
|
|||||||
"reagent": "reagent",
|
"reagent": "reagent",
|
||||||
"solvent": "solvent",
|
"solvent": "solvent",
|
||||||
"compound": "compound",
|
"compound": "compound",
|
||||||
"tip_racks": "tip_rack_identifier",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
OUTPUT_PORT_MAPPING = {
|
OUTPUT_PORT_MAPPING = {
|
||||||
@@ -563,17 +525,8 @@ def build_protocol_graph(
|
|||||||
"compound": "compound",
|
"compound": "compound",
|
||||||
}
|
}
|
||||||
|
|
||||||
# 需要根据 wells 数量扩展的参数列表:
|
# 需要根据 wells 数量扩展的参数列表(复数形式)
|
||||||
# - 复数参数(asp_vols 等)支持单值自动扩展
|
EXPAND_BY_WELLS_PARAMS = ["asp_vols", "dis_vols", "asp_flow_rates", "dis_flow_rates"]
|
||||||
# - liquid_height 按 wells 扩展为数组
|
|
||||||
# - mix_* 参数保持标量,避免被转换为 list
|
|
||||||
EXPAND_BY_WELLS_PARAMS = [
|
|
||||||
"asp_vols",
|
|
||||||
"dis_vols",
|
|
||||||
"asp_flow_rates",
|
|
||||||
"dis_flow_rates",
|
|
||||||
"liquid_height",
|
|
||||||
]
|
|
||||||
|
|
||||||
# 处理协议步骤
|
# 处理协议步骤
|
||||||
for step in protocol_steps:
|
for step in protocol_steps:
|
||||||
@@ -587,57 +540,6 @@ def build_protocol_graph(
|
|||||||
if old_name in params:
|
if old_name in params:
|
||||||
params[new_name] = params.pop(old_name)
|
params[new_name] = params.pop(old_name)
|
||||||
|
|
||||||
# touch_tip 输入归一化:
|
|
||||||
# - 支持 bool / 0/1 / "true"/"false" / 单元素 list
|
|
||||||
# - 最终统一为 bool 标量,避免被下游误当作序列处理
|
|
||||||
if "touch_tip" in params:
|
|
||||||
touch_tip_value = params.get("touch_tip")
|
|
||||||
if isinstance(touch_tip_value, list):
|
|
||||||
if len(touch_tip_value) == 1:
|
|
||||||
touch_tip_value = touch_tip_value[0]
|
|
||||||
elif len(touch_tip_value) == 0:
|
|
||||||
touch_tip_value = False
|
|
||||||
else:
|
|
||||||
warnings.append(f"touch_tip 期望标量,但收到长度为 {len(touch_tip_value)} 的列表,使用首个值")
|
|
||||||
touch_tip_value = touch_tip_value[0]
|
|
||||||
if isinstance(touch_tip_value, str):
|
|
||||||
norm = touch_tip_value.strip().lower()
|
|
||||||
if norm in {"true", "1", "yes", "y", "on"}:
|
|
||||||
touch_tip_value = True
|
|
||||||
elif norm in {"false", "0", "no", "n", "off", ""}:
|
|
||||||
touch_tip_value = False
|
|
||||||
else:
|
|
||||||
warnings.append(f"touch_tip 字符串值无法识别: {touch_tip_value},按 True 处理")
|
|
||||||
touch_tip_value = True
|
|
||||||
elif isinstance(touch_tip_value, (int, float)):
|
|
||||||
touch_tip_value = bool(touch_tip_value)
|
|
||||||
elif touch_tip_value is None:
|
|
||||||
touch_tip_value = False
|
|
||||||
else:
|
|
||||||
touch_tip_value = bool(touch_tip_value)
|
|
||||||
params["touch_tip"] = touch_tip_value
|
|
||||||
|
|
||||||
# delays 输入归一化:
|
|
||||||
# - 支持标量(int/float/字符串数字)与 list
|
|
||||||
# - 最终统一为数字列表,供下游按 delays[0]/delays[1] 使用
|
|
||||||
if "delays" in params:
|
|
||||||
delays_value = params.get("delays")
|
|
||||||
if delays_value is None or delays_value == "":
|
|
||||||
params["delays"] = []
|
|
||||||
else:
|
|
||||||
raw_list = delays_value if isinstance(delays_value, list) else [delays_value]
|
|
||||||
normalized_delays = []
|
|
||||||
for delay_item in raw_list:
|
|
||||||
if isinstance(delay_item, str):
|
|
||||||
delay_item = delay_item.strip()
|
|
||||||
if delay_item == "":
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
normalized_delays.append(float(delay_item))
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
warnings.append(f"delays 包含无法转换为数字的值: {delay_item},已忽略")
|
|
||||||
params["delays"] = normalized_delays
|
|
||||||
|
|
||||||
# 处理输入连接
|
# 处理输入连接
|
||||||
for param_key, target_port in INPUT_PORT_MAPPING.items():
|
for param_key, target_port in INPUT_PORT_MAPPING.items():
|
||||||
resource_name = params.get(param_key)
|
resource_name = params.get(param_key)
|
||||||
@@ -704,12 +606,7 @@ def build_protocol_graph(
|
|||||||
G.add_node(node_id, **step_copy)
|
G.add_node(node_id, **step_copy)
|
||||||
|
|
||||||
# 控制流
|
# 控制流
|
||||||
if is_first_action_node:
|
if last_control_node_id is not None:
|
||||||
# 第一个 transfer_liquid 需要等待所有 create_resource 完成
|
|
||||||
for cr_node_id in all_create_resource_node_ids:
|
|
||||||
G.add_edge(cr_node_id, node_id, source_port="ready", target_port="ready")
|
|
||||||
is_first_action_node = False
|
|
||||||
elif last_control_node_id is not None:
|
|
||||||
G.add_edge(last_control_node_id, node_id, source_port="ready", target_port="ready")
|
G.add_edge(last_control_node_id, node_id, source_port="ready", target_port="ready")
|
||||||
last_control_node_id = node_id
|
last_control_node_id = node_id
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user