mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2026-02-04 13:25:13 +00:00
Upgrade to py 3.11.14; ROS2 Humble 0.7; unilabos 0.10.16
Workbench example, adjust log level, and ci check (#220) * TestLatency Return Value Example & gitignore update * Adjust log level & Add workbench virtual example & Add not action decorator & Add check_mode & * Add CI Check Fix/workstation yb revision (#217) * Revert log change & update registry * Revert opcua client & move electrolyte node Workstation yb merge dev ready 260113 (#216) * feat(bioyond): 添加计算实验设计功能,支持化合物配比和滴定比例参数 * feat(bioyond): 添加测量小瓶功能,支持基本参数配置 * feat(bioyond): 添加测量小瓶配置,支持新设备参数 * feat(bioyond): 更新仓库布局和尺寸,支持竖向排列的测量小瓶和试剂存放堆栈 * feat(bioyond): 优化任务创建流程,确保无论成功与否都清理任务队列以避免重复累积 * feat(bioyond): 添加设置反应器温度功能,支持温度范围和异常处理 * feat(bioyond): 调整反应器位置配置,统一坐标格式 * feat(bioyond): 添加调度器启动功能,支持任务队列执行并处理异常 * feat(bioyond): 优化调度器启动功能,添加异常处理并更新相关配置 * feat(opcua): 增强节点ID解析兼容性和数据类型处理 改进节点ID解析逻辑以支持多种格式,包括字符串和数字标识符 添加数据类型转换处理,确保写入值时类型匹配 优化错误提示信息,便于调试节点连接问题 * feat(registry): 新增后处理站的设备配置文件 添加后处理站的YAML配置文件,包含动作映射、状态类型和设备描述 * 添加调度器启动功能,合并物料参数配置,优化物料参数处理逻辑 * 添加从 Bioyond 系统自动同步工作流序列的功能,并更新相关配置 * fix:兼容 BioyondReactionStation 中 workflow_sequence 被重写为 property * fix:同步工作流序列 * feat: remove commented workflow synchronization from `reaction_station.py`. * 添加时间约束功能及相关配置 * fix:自动更新物料缓存功能,添加物料时更新缓存并在删除时移除缓存项 * fix:在添加物料时处理字符串和字典返回值,确保正确更新缓存 * fix:更新奔曜错误处理报送为物料变更报送,调整日志记录和响应消息 * feat:添加实验报告简化功能,去除冗余信息并保留关键信息 * feat: 添加任务状态事件发布功能,监控并报告任务运行、超时、完成和错误状态 * fix: 修复添加物料时数据格式错误 * Refactor bioyond_dispensing_station and reaction_station_bioyond YAML configurations - Removed redundant action value mappings from bioyond_dispensing_station. - Updated goal properties in bioyond_dispensing_station to use enums for target_stack and other parameters. - Changed data types for end_point and start_point in reaction_station_bioyond to use string enums (Start, End). - Simplified descriptions and updated measurement units from μL to mL where applicable. - Removed unused commands from reaction_station_bioyond to streamline the configuration. * fix:Change the material unit from μL to mL * fix:refresh_material_cache * feat: 动态获取工作流步骤ID,优化工作流配置 * feat: 添加清空服务端所有非核心工作流功能 * fix:修复Bottle类的序列化和反序列化方法 * feat:增强材料缓存更新逻辑,支持处理返回数据中的详细信息 * Add debug log * feat(workstation): update bioyond config migration and coin cell material search logic - Migrate bioyond_cell config to JSON structure and remove global variable dependencies - Implement material search confirmation dialog auto-handling - Add documentation: 20260113_物料搜寻确认弹窗自动处理功能.md and 20260113_配置迁移修改总结.md * Refactor module paths for Bioyond devices in YAML configuration files - Updated the module path for BioyondDispensingStation in bioyond_dispensing_station.yaml to reflect the new directory structure. - Updated the module path for BioyondReactionStation and BioyondReactor in reaction_station_bioyond.yaml to align with the revised organization of the codebase. * fix: WareHouse 的不可哈希类型错误,优化父节点去重逻辑 * refactor: Move config from module to instance initialization * fix: 修正 reaction_station 目录名拼写错误 * feat: Integrate material search logic and cleanup deprecated files - Update coin_cell_assembly.py with material search dialog handling - Update YB_warehouses.py with latest warehouse configurations - Remove outdated documentation and test data files * Refactor: Use instance attributes for action names and workflow step IDs * refactor: Split tipbox storage into left and right warehouses * refactor: Merge tipbox storage left and right into single warehouse --------- Co-authored-by: ZiWei <131428629+ZiWei09@users.noreply.github.com> Co-authored-by: Andy6M <xieqiming1132@qq.com> fix: WareHouse 的不可哈希类型错误,优化父节点去重逻辑 fix parent_uuid fetch when bind_parent_id == node_name 物料更新也是用父节点进行报送 Add None conversion for tube rack etc. Add set_liquid example. Add create_resource and test_resource example. Add restart. Temp allow action message. Add no_update_feedback option. Create session_id by edge. bump version to 0.10.15 temp cancel update req
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
|
||||
# from nt import device_encoding
|
||||
import threading
|
||||
import time
|
||||
@@ -55,7 +56,11 @@ def main(
|
||||
) -> None:
|
||||
"""主函数"""
|
||||
|
||||
rclpy.init(args=rclpy_init_args)
|
||||
# Support restart - check if rclpy is already initialized
|
||||
if not rclpy.ok():
|
||||
rclpy.init(args=rclpy_init_args)
|
||||
else:
|
||||
logger.info("[ROS] rclpy already initialized, reusing context")
|
||||
executor = rclpy.__executor = MultiThreadedExecutor()
|
||||
# 创建主机节点
|
||||
host_node = HostNode(
|
||||
@@ -88,7 +93,7 @@ def main(
|
||||
joint_republisher = JointRepublisher("joint_republisher", host_node.resource_tracker)
|
||||
# lh_joint_pub = LiquidHandlerJointPublisher(
|
||||
# resources_config=resources_list, resource_tracker=host_node.resource_tracker
|
||||
# )
|
||||
# )
|
||||
executor.add_node(resource_mesh_manager)
|
||||
executor.add_node(joint_republisher)
|
||||
# executor.add_node(lh_joint_pub)
|
||||
|
||||
@@ -159,10 +159,14 @@ _msg_converter: Dict[Type, Any] = {
|
||||
else Pose()
|
||||
),
|
||||
config=json.dumps(x.get("config", {})),
|
||||
data=json.dumps(x.get("data", {})),
|
||||
data=json.dumps(obtain_data_with_uuid(x)),
|
||||
),
|
||||
}
|
||||
|
||||
def obtain_data_with_uuid(x: dict):
|
||||
data = x.get("data", {})
|
||||
data["unilabos_uuid"] = x.get("uuid", None)
|
||||
return data
|
||||
|
||||
def json_or_yaml_loads(data: str) -> Any:
|
||||
try:
|
||||
@@ -766,13 +770,16 @@ def ros_message_to_json_schema(msg_class: Any, field_name: str) -> Dict[str, Any
|
||||
return schema
|
||||
|
||||
|
||||
def ros_action_to_json_schema(action_class: Any, description="") -> Dict[str, Any]:
|
||||
def ros_action_to_json_schema(
|
||||
action_class: Any, description="", previous_schema: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
将 ROS Action 类转换为 JSON Schema
|
||||
|
||||
Args:
|
||||
action_class: ROS Action 类
|
||||
description: 描述
|
||||
previous_schema: 之前的 schema,用于保留 goal/feedback/result 下一级字段的 description
|
||||
|
||||
Returns:
|
||||
完整的 JSON Schema 定义
|
||||
@@ -806,9 +813,44 @@ def ros_action_to_json_schema(action_class: Any, description="") -> Dict[str, An
|
||||
"required": ["goal"],
|
||||
}
|
||||
|
||||
# 保留之前 schema 中 goal/feedback/result 下一级字段的 description
|
||||
if previous_schema:
|
||||
_preserve_field_descriptions(schema, previous_schema)
|
||||
|
||||
return schema
|
||||
|
||||
|
||||
def _preserve_field_descriptions(
|
||||
new_schema: Dict[str, Any], previous_schema: Dict[str, Any]
|
||||
) -> None:
|
||||
"""
|
||||
保留之前 schema 中 goal/feedback/result 下一级字段的 description 和 title
|
||||
|
||||
Args:
|
||||
new_schema: 新生成的 schema(会被修改)
|
||||
previous_schema: 之前的 schema
|
||||
"""
|
||||
for section in ["goal", "feedback", "result"]:
|
||||
new_section = new_schema.get("properties", {}).get(section, {})
|
||||
prev_section = previous_schema.get("properties", {}).get(section, {})
|
||||
|
||||
if not new_section or not prev_section:
|
||||
continue
|
||||
|
||||
new_props = new_section.get("properties", {})
|
||||
prev_props = prev_section.get("properties", {})
|
||||
|
||||
for field_name, field_schema in new_props.items():
|
||||
if field_name in prev_props:
|
||||
prev_field = prev_props[field_name]
|
||||
# 保留字段的 description
|
||||
if "description" in prev_field and prev_field["description"]:
|
||||
field_schema["description"] = prev_field["description"]
|
||||
# 保留字段的 title(用户自定义的中文名)
|
||||
if "title" in prev_field and prev_field["title"]:
|
||||
field_schema["title"] = prev_field["title"]
|
||||
|
||||
|
||||
def convert_ros_action_to_jsonschema(
|
||||
action_name_or_type: Union[str, Type], output_file: Optional[str] = None, format: str = "json"
|
||||
) -> Dict[str, Any]:
|
||||
|
||||
@@ -20,6 +20,8 @@ from rclpy.callback_groups import ReentrantCallbackGroup
|
||||
from rclpy.service import Service
|
||||
from unilabos_msgs.action import SendCmd
|
||||
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
|
||||
|
||||
from unilabos.config.config import BasicConfig
|
||||
from unilabos.utils.decorator import get_topic_config, get_all_subscriptions
|
||||
|
||||
from unilabos.resources.container import RegularContainer
|
||||
@@ -47,7 +49,6 @@ from unilabos.resources.resource_tracker import (
|
||||
ResourceTreeInstance,
|
||||
ResourceDictInstance,
|
||||
)
|
||||
from unilabos.ros.x.rclpyx import get_event_loop
|
||||
from unilabos.ros.utils.driver_creator import WorkstationNodeCreator, PyLabRobotCreator, DeviceClassCreator
|
||||
from rclpy.task import Task, Future
|
||||
from unilabos.utils.import_manager import default_manager
|
||||
@@ -183,7 +184,7 @@ class PropertyPublisher:
|
||||
f"创建发布者 {name} 失败,可能由于注册表有误,类型: {msg_type},错误: {ex}\n{traceback.format_exc()}"
|
||||
)
|
||||
self.timer = node.create_timer(self.timer_period, self.publish_property)
|
||||
self.__loop = get_event_loop()
|
||||
self.__loop = ROS2DeviceNode.get_asyncio_loop()
|
||||
str_msg_type = str(msg_type)[8:-2]
|
||||
self.node.lab_logger().trace(f"发布属性: {name}, 类型: {str_msg_type}, 周期: {initial_period}秒, QoS: {qos}")
|
||||
|
||||
@@ -390,9 +391,12 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
parent_resource = self.resource_tracker.figure_resource(
|
||||
{"name": bind_parent_id}
|
||||
)
|
||||
for r in rts.root_nodes:
|
||||
# noinspection PyUnresolvedReferences
|
||||
r.res_content.parent_uuid = parent_resource.unilabos_uuid
|
||||
for r in rts.root_nodes:
|
||||
# noinspection PyUnresolvedReferences
|
||||
r.res_content.parent_uuid = parent_resource.unilabos_uuid
|
||||
else:
|
||||
for r in rts.root_nodes:
|
||||
r.res_content.parent_uuid = self.uuid
|
||||
|
||||
if len(LIQUID_INPUT_SLOT) and LIQUID_INPUT_SLOT[0] == -1 and len(rts.root_nodes) == 1 and isinstance(rts.root_nodes[0], RegularContainer):
|
||||
# noinspection PyTypeChecker
|
||||
@@ -428,11 +432,14 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
})
|
||||
tree_response: SerialCommand.Response = await client.call_async(request)
|
||||
uuid_maps = json.loads(tree_response.response)
|
||||
self.resource_tracker.loop_update_uuid(input_resources, uuid_maps)
|
||||
plr_instances = rts.to_plr_resources()
|
||||
for plr_instance in plr_instances:
|
||||
self.resource_tracker.loop_update_uuid(plr_instance, uuid_maps)
|
||||
rts: ResourceTreeSet = ResourceTreeSet.from_plr_resources(plr_instances)
|
||||
self.lab_logger().info(f"Resource tree added. UUID mapping: {len(uuid_maps)} nodes")
|
||||
final_response = {
|
||||
"created_resources": rts.dump(),
|
||||
"liquid_input_resources": [],
|
||||
"created_resource_tree": rts.dump(),
|
||||
"liquid_input_resource_tree": [],
|
||||
}
|
||||
res.response = json.dumps(final_response)
|
||||
# 如果driver自己就有assign的方法,那就使用driver自己的assign方法
|
||||
@@ -458,7 +465,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
return res
|
||||
try:
|
||||
if len(rts.root_nodes) == 1 and parent_resource is not None:
|
||||
plr_instance = rts.to_plr_resources()[0]
|
||||
plr_instance = plr_instances[0]
|
||||
if isinstance(plr_instance, Plate):
|
||||
empty_liquid_info_in: List[Tuple[Optional[str], float]] = [(None, 0)] * plr_instance.num_items
|
||||
if len(ADD_LIQUID_TYPE) == 1 and len(LIQUID_VOLUME) == 1 and len(LIQUID_INPUT_SLOT) > 1:
|
||||
@@ -483,7 +490,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
input_wells = []
|
||||
for r in LIQUID_INPUT_SLOT:
|
||||
input_wells.append(plr_instance.children[r])
|
||||
final_response["liquid_input_resources"] = ResourceTreeSet.from_plr_resources(input_wells).dump()
|
||||
final_response["liquid_input_resource_tree"] = ResourceTreeSet.from_plr_resources(input_wells).dump()
|
||||
res.response = json.dumps(final_response)
|
||||
if issubclass(parent_resource.__class__, Deck) and hasattr(parent_resource, "assign_child_at_slot") and "slot" in other_calling_param:
|
||||
other_calling_param["slot"] = int(other_calling_param["slot"])
|
||||
@@ -617,7 +624,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
) # type: ignore
|
||||
raw_nodes = json.loads(response.response)
|
||||
tree_set = ResourceTreeSet.from_raw_dict_list(raw_nodes)
|
||||
self.lab_logger().debug(f"获取资源结果: {len(tree_set.trees)} 个资源树")
|
||||
self.lab_logger().trace(f"获取资源结果: {len(tree_set.trees)} 个资源树 {tree_set.root_nodes}")
|
||||
return tree_set
|
||||
|
||||
async def get_resource_with_dir(self, resource_id: str, with_children: bool = True) -> "ResourcePLR":
|
||||
@@ -651,61 +658,71 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
|
||||
def transfer_to_new_resource(
|
||||
self, plr_resource: "ResourcePLR", tree: ResourceTreeInstance, additional_add_params: Dict[str, Any]
|
||||
):
|
||||
) -> Optional["ResourcePLR"]:
|
||||
parent_uuid = tree.root_node.res_content.parent_uuid
|
||||
if parent_uuid:
|
||||
parent_resource: ResourcePLR = self.resource_tracker.uuid_to_resources.get(parent_uuid)
|
||||
if parent_resource is None:
|
||||
if not parent_uuid:
|
||||
self.lab_logger().warning(
|
||||
f"物料{plr_resource} parent未知,挂载到当前节点下,额外参数:{additional_add_params}"
|
||||
)
|
||||
return None
|
||||
if parent_uuid == self.uuid:
|
||||
self.lab_logger().warning(
|
||||
f"物料{plr_resource}请求挂载到{self.identifier},额外参数:{additional_add_params}"
|
||||
)
|
||||
return None
|
||||
parent_resource: ResourcePLR = self.resource_tracker.uuid_to_resources.get(parent_uuid)
|
||||
if parent_resource is None:
|
||||
self.lab_logger().warning(
|
||||
f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_uuid}不存在"
|
||||
)
|
||||
else:
|
||||
try:
|
||||
# 特殊兼容所有plr的物料的assign方法,和create_resource append_resource后期同步
|
||||
additional_params = {}
|
||||
extra = getattr(plr_resource, "unilabos_extra", {})
|
||||
if len(extra):
|
||||
self.lab_logger().info(f"发现物料{plr_resource}额外参数: " + str(extra))
|
||||
if "update_resource_site" in extra:
|
||||
additional_add_params["site"] = extra["update_resource_site"]
|
||||
site = additional_add_params.get("site", None)
|
||||
spec = inspect.signature(parent_resource.assign_child_resource)
|
||||
if "spot" in spec.parameters:
|
||||
ordering_dict: Dict[str, Any] = getattr(parent_resource, "_ordering")
|
||||
if ordering_dict:
|
||||
site = list(ordering_dict.keys()).index(site)
|
||||
additional_params["spot"] = site
|
||||
old_parent = plr_resource.parent
|
||||
if old_parent is not None:
|
||||
# plr并不支持同一个deck的加载和卸载
|
||||
self.lab_logger().warning(f"物料{plr_resource}请求从{old_parent}卸载")
|
||||
old_parent.unassign_child_resource(plr_resource)
|
||||
self.lab_logger().warning(
|
||||
f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_uuid}不存在"
|
||||
f"物料{plr_resource}请求挂载到{parent_resource},额外参数:{additional_params}"
|
||||
)
|
||||
else:
|
||||
try:
|
||||
# 特殊兼容所有plr的物料的assign方法,和create_resource append_resource后期同步
|
||||
additional_params = {}
|
||||
extra = getattr(plr_resource, "unilabos_extra", {})
|
||||
if len(extra):
|
||||
self.lab_logger().info(f"发现物料{plr_resource}额外参数: " + str(extra))
|
||||
if "update_resource_site" in extra:
|
||||
additional_add_params["site"] = extra["update_resource_site"]
|
||||
site = additional_add_params.get("site", None)
|
||||
spec = inspect.signature(parent_resource.assign_child_resource)
|
||||
if "spot" in spec.parameters:
|
||||
ordering_dict: Dict[str, Any] = getattr(parent_resource, "_ordering")
|
||||
if ordering_dict:
|
||||
site = list(ordering_dict.keys()).index(site)
|
||||
additional_params["spot"] = site
|
||||
old_parent = plr_resource.parent
|
||||
if old_parent is not None:
|
||||
# plr并不支持同一个deck的加载和卸载
|
||||
self.lab_logger().warning(f"物料{plr_resource}请求从{old_parent}卸载")
|
||||
old_parent.unassign_child_resource(plr_resource)
|
||||
self.lab_logger().warning(
|
||||
f"物料{plr_resource}请求挂载到{parent_resource},额外参数:{additional_params}"
|
||||
)
|
||||
|
||||
# ⭐ assign 之前,需要从 resources 列表中移除
|
||||
# 因为资源将不再是顶级资源,而是成为 parent_resource 的子资源
|
||||
# 如果不移除,figure_resource 会找到两次:一次在 resources,一次在 parent 的 children
|
||||
resource_id = id(plr_resource)
|
||||
for i, r in enumerate(self.resource_tracker.resources):
|
||||
if id(r) == resource_id:
|
||||
self.resource_tracker.resources.pop(i)
|
||||
self.lab_logger().debug(
|
||||
f"从顶级资源列表中移除 {plr_resource.name}(即将成为 {parent_resource.name} 的子资源)"
|
||||
)
|
||||
break
|
||||
# ⭐ assign 之前,需要从 resources 列表中移除
|
||||
# 因为资源将不再是顶级资源,而是成为 parent_resource 的子资源
|
||||
# 如果不移除,figure_resource 会找到两次:一次在 resources,一次在 parent 的 children
|
||||
resource_id = id(plr_resource)
|
||||
for i, r in enumerate(self.resource_tracker.resources):
|
||||
if id(r) == resource_id:
|
||||
self.resource_tracker.resources.pop(i)
|
||||
self.lab_logger().debug(
|
||||
f"从顶级资源列表中移除 {plr_resource.name}(即将成为 {parent_resource.name} 的子资源)"
|
||||
)
|
||||
break
|
||||
|
||||
parent_resource.assign_child_resource(plr_resource, location=None, **additional_params)
|
||||
parent_resource.assign_child_resource(plr_resource, location=None, **additional_params)
|
||||
|
||||
func = getattr(self.driver_instance, "resource_tree_transfer", None)
|
||||
if callable(func):
|
||||
# 分别是 物料的原来父节点,当前物料的状态,物料的新父节点(此时物料已经重新assign了)
|
||||
func(old_parent, plr_resource, parent_resource)
|
||||
except Exception as e:
|
||||
self.lab_logger().warning(
|
||||
f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_resource}[{parent_uuid}]失败!\n{traceback.format_exc()}"
|
||||
)
|
||||
func = getattr(self.driver_instance, "resource_tree_transfer", None)
|
||||
if callable(func):
|
||||
# 分别是 物料的原来父节点,当前物料的状态,物料的新父节点(此时物料已经重新assign了)
|
||||
func(old_parent, plr_resource, parent_resource)
|
||||
return parent_resource
|
||||
except Exception as e:
|
||||
self.lab_logger().warning(
|
||||
f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_resource}[{parent_uuid}]失败!\n{traceback.format_exc()}"
|
||||
)
|
||||
|
||||
async def s2c_resource_tree(self, req: SerialCommand_Request, res: SerialCommand_Response):
|
||||
"""
|
||||
@@ -720,7 +737,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
|
||||
def _handle_add(
|
||||
plr_resources: List[ResourcePLR], tree_set: ResourceTreeSet, additional_add_params: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
) -> Tuple[Dict[str, Any], List[ResourcePLR]]:
|
||||
"""
|
||||
处理资源添加操作的内部函数
|
||||
|
||||
@@ -732,15 +749,20 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
Returns:
|
||||
操作结果字典
|
||||
"""
|
||||
parents = [] # 放的是被变更的物料 / 被变更的物料父级
|
||||
for plr_resource, tree in zip(plr_resources, tree_set.trees):
|
||||
self.resource_tracker.add_resource(plr_resource)
|
||||
self.transfer_to_new_resource(plr_resource, tree, additional_add_params)
|
||||
parent = self.transfer_to_new_resource(plr_resource, tree, additional_add_params)
|
||||
if parent is not None:
|
||||
parents.append(parent)
|
||||
else:
|
||||
parents.append(plr_resource)
|
||||
|
||||
func = getattr(self.driver_instance, "resource_tree_add", None)
|
||||
if callable(func):
|
||||
func(plr_resources)
|
||||
|
||||
return {"success": True, "action": "add"}
|
||||
return {"success": True, "action": "add"}, parents
|
||||
|
||||
def _handle_remove(resources_uuid: List[str]) -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -775,11 +797,11 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
if plr_resource.parent is not None:
|
||||
plr_resource.parent.unassign_child_resource(plr_resource)
|
||||
self.resource_tracker.remove_resource(plr_resource)
|
||||
self.lab_logger().info(f"移除物料 {plr_resource} 及其子节点")
|
||||
self.lab_logger().info(f"[资源同步] 移除物料 {plr_resource} 及其子节点")
|
||||
|
||||
for other_plr_resource in other_plr_resources:
|
||||
self.resource_tracker.remove_resource(other_plr_resource)
|
||||
self.lab_logger().info(f"移除物料 {other_plr_resource} 及其子节点")
|
||||
self.lab_logger().info(f"[资源同步] 移除物料 {other_plr_resource} 及其子节点")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
@@ -790,7 +812,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
|
||||
def _handle_update(
|
||||
plr_resources: List[Union[ResourcePLR, ResourceDictInstance]], tree_set: ResourceTreeSet, additional_add_params: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
) -> Tuple[Dict[str, Any], List[ResourcePLR]]:
|
||||
"""
|
||||
处理资源更新操作的内部函数
|
||||
|
||||
@@ -802,6 +824,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
Returns:
|
||||
操作结果字典
|
||||
"""
|
||||
original_instances = []
|
||||
for plr_resource, tree in zip(plr_resources, tree_set.trees):
|
||||
if isinstance(plr_resource, ResourceDictInstance):
|
||||
self._lab_logger.info(f"跳过 非资源{plr_resource.res_content.name} 的更新")
|
||||
@@ -810,11 +833,16 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
original_instance: ResourcePLR = self.resource_tracker.figure_resource(
|
||||
{"uuid": tree.root_node.res_content.uuid}, try_mode=False
|
||||
)
|
||||
original_parent_resource = original_instance.parent
|
||||
original_parent_resource_uuid = getattr(original_parent_resource, "unilabos_uuid", None)
|
||||
target_parent_resource_uuid = tree.root_node.res_content.uuid_parent
|
||||
not_same_parent = original_parent_resource_uuid != target_parent_resource_uuid and original_parent_resource is not None
|
||||
old_name = original_instance.name
|
||||
new_name = plr_resource.name
|
||||
parent_appended = False
|
||||
|
||||
# Update操作中包含改名:需要先remove再add
|
||||
if original_instance.name != plr_resource.name:
|
||||
old_name = original_instance.name
|
||||
new_name = plr_resource.name
|
||||
# Update操作中包含改名:需要先remove再add,这里更新父节点即可
|
||||
if not not_same_parent and old_name != new_name:
|
||||
self.lab_logger().info(f"物料改名操作:{old_name} -> {new_name}")
|
||||
|
||||
# 收集所有相关的uuid(包括子节点)
|
||||
@@ -823,12 +851,10 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
_handle_add([original_instance], tree_set, additional_add_params)
|
||||
|
||||
self.lab_logger().info(f"物料改名完成:{old_name} -> {new_name}")
|
||||
original_instances.append(original_parent_resource)
|
||||
parent_appended = True
|
||||
|
||||
# 常规更新:不涉及改名
|
||||
original_parent_resource = original_instance.parent
|
||||
original_parent_resource_uuid = getattr(original_parent_resource, "unilabos_uuid", None)
|
||||
target_parent_resource_uuid = tree.root_node.res_content.uuid_parent
|
||||
|
||||
self.lab_logger().info(
|
||||
f"物料{original_instance} 原始父节点{original_parent_resource_uuid} "
|
||||
f"目标父节点{target_parent_resource_uuid} 更新"
|
||||
@@ -839,13 +865,12 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
original_instance.unilabos_extra = getattr(plr_resource, "unilabos_extra") # type: ignore # noqa: E501
|
||||
|
||||
# 如果父节点变化,需要重新挂载
|
||||
if (
|
||||
original_parent_resource_uuid != target_parent_resource_uuid
|
||||
and original_parent_resource is not None
|
||||
):
|
||||
self.transfer_to_new_resource(original_instance, tree, additional_add_params)
|
||||
if not_same_parent:
|
||||
parent = self.transfer_to_new_resource(original_instance, tree, additional_add_params)
|
||||
original_instances.append(parent)
|
||||
parent_appended = True
|
||||
else:
|
||||
# 判断是否变更了resource_site
|
||||
# 判断是否变更了resource_site,重新登记
|
||||
target_site = original_instance.unilabos_extra.get("update_resource_site")
|
||||
sites = original_instance.parent.sites if original_instance.parent is not None and hasattr(original_instance.parent, "sites") else None
|
||||
site_names = list(original_instance.parent._ordering.keys()) if original_instance.parent is not None and hasattr(original_instance.parent, "sites") else []
|
||||
@@ -853,7 +878,10 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
site_index = sites.index(original_instance)
|
||||
site_name = site_names[site_index]
|
||||
if site_name != target_site:
|
||||
self.transfer_to_new_resource(original_instance, tree, additional_add_params)
|
||||
parent = self.transfer_to_new_resource(original_instance, tree, additional_add_params)
|
||||
if parent is not None:
|
||||
original_instances.append(parent)
|
||||
parent_appended = True
|
||||
|
||||
# 加载状态
|
||||
original_instance.load_all_state(states)
|
||||
@@ -861,13 +889,15 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
self.lab_logger().info(
|
||||
f"更新了资源属性 {plr_resource}[{tree.root_node.res_content.uuid}] " f"及其子节点 {child_count} 个"
|
||||
)
|
||||
if not parent_appended:
|
||||
original_instances.append(original_instance)
|
||||
|
||||
# 调用driver的update回调
|
||||
func = getattr(self.driver_instance, "resource_tree_update", None)
|
||||
if callable(func):
|
||||
func(plr_resources)
|
||||
func(original_instances)
|
||||
|
||||
return {"success": True, "action": "update"}
|
||||
return {"success": True, "action": "update"}, original_instances
|
||||
|
||||
try:
|
||||
data = json.loads(req.command)
|
||||
@@ -877,8 +907,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
action = i.get("action") # remove, add, update
|
||||
resources_uuid: List[str] = i.get("data") # 资源数据
|
||||
additional_add_params = i.get("additional_add_params", {}) # 额外参数
|
||||
self.lab_logger().info(
|
||||
f"[Resource Tree Update] Processing {action} operation, " f"resources count: {len(resources_uuid)}"
|
||||
self.lab_logger().trace(
|
||||
f"[资源同步] 处理 {action}, " f"resources count: {len(resources_uuid)}"
|
||||
)
|
||||
tree_set = None
|
||||
if action in ["add", "update"]:
|
||||
@@ -890,8 +920,20 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
if tree_set is None:
|
||||
raise ValueError("tree_set不能为None")
|
||||
plr_resources = tree_set.to_plr_resources()
|
||||
result = _handle_add(plr_resources, tree_set, additional_add_params)
|
||||
new_tree_set = ResourceTreeSet.from_plr_resources(plr_resources)
|
||||
result, parents = _handle_add(plr_resources, tree_set, additional_add_params)
|
||||
parents: List[Optional["ResourcePLR"]] = [i for i in parents if i is not None]
|
||||
# de_dupe_parents = list(set(parents))
|
||||
# Fix unhashable type error for WareHouse
|
||||
de_dupe_parents = []
|
||||
_seen_ids = set()
|
||||
for p in parents:
|
||||
if id(p) not in _seen_ids:
|
||||
_seen_ids.add(id(p))
|
||||
de_dupe_parents.append(p)
|
||||
new_tree_set = ResourceTreeSet.from_plr_resources(de_dupe_parents) # 去重
|
||||
for tree in new_tree_set.trees:
|
||||
if tree.root_node.res_content.uuid_parent is None and self.node_name != "host_node":
|
||||
tree.root_node.res_content.parent_uuid = self.uuid
|
||||
r = SerialCommand.Request()
|
||||
r.command = json.dumps(
|
||||
{"data": {"data": new_tree_set.dump()}, "action": "update"}) # 和Update Resource一致
|
||||
@@ -908,14 +950,18 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
plr_resources.append(tree.root_node)
|
||||
else:
|
||||
plr_resources.append(ResourceTreeSet([tree]).to_plr_resources()[0])
|
||||
new_tree_set = ResourceTreeSet.from_plr_resources(plr_resources)
|
||||
result = _handle_update(plr_resources, tree_set, additional_add_params)
|
||||
r = SerialCommand.Request()
|
||||
r.command = json.dumps(
|
||||
{"data": {"data": new_tree_set.dump()}, "action": "update"}) # 和Update Resource一致
|
||||
response: SerialCommand_Response = await self._resource_clients[
|
||||
"c2s_update_resource_tree"].call_async(r) # type: ignore
|
||||
self.lab_logger().info(f"确认资源云端 Update 结果: {response.response}")
|
||||
result, original_instances = _handle_update(plr_resources, tree_set, additional_add_params)
|
||||
if not BasicConfig.no_update_feedback:
|
||||
new_tree_set = ResourceTreeSet.from_plr_resources(original_instances) # 去重
|
||||
for tree in new_tree_set.trees:
|
||||
if tree.root_node.res_content.uuid_parent is None and self.node_name != "host_node":
|
||||
tree.root_node.res_content.parent_uuid = self.uuid
|
||||
r = SerialCommand.Request()
|
||||
r.command = json.dumps(
|
||||
{"data": {"data": new_tree_set.dump()}, "action": "update"}) # 和Update Resource一致
|
||||
response: SerialCommand_Response = await self._resource_clients[
|
||||
"c2s_update_resource_tree"].call_async(r) # type: ignore
|
||||
self.lab_logger().info(f"确认资源云端 Update 结果: {response.response}")
|
||||
results.append(result)
|
||||
elif action == "remove":
|
||||
result = _handle_remove(resources_uuid)
|
||||
@@ -929,15 +975,15 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
# 返回处理结果
|
||||
result_json = {"results": results, "total": len(data)}
|
||||
res.response = json.dumps(result_json, ensure_ascii=False, cls=TypeEncoder)
|
||||
self.lab_logger().info(f"[Resource Tree Update] Completed processing {len(data)} operations")
|
||||
# self.lab_logger().info(f"[Resource Tree Update] Completed processing {len(data)} operations")
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
error_msg = f"Invalid JSON format: {str(e)}"
|
||||
self.lab_logger().error(f"[Resource Tree Update] {error_msg}")
|
||||
self.lab_logger().error(f"[资源同步] {error_msg}")
|
||||
res.response = json.dumps({"success": False, "error": error_msg}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
error_msg = f"Unexpected error: {str(e)}"
|
||||
self.lab_logger().error(f"[Resource Tree Update] {error_msg}")
|
||||
self.lab_logger().error(f"[资源同步] {error_msg}")
|
||||
self.lab_logger().error(traceback.format_exc())
|
||||
res.response = json.dumps({"success": False, "error": error_msg}, ensure_ascii=False)
|
||||
|
||||
@@ -1258,7 +1304,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
ACTION, action_paramtypes = self.get_real_function(self.driver_instance, action_name)
|
||||
|
||||
action_kwargs = convert_from_ros_msg_with_mapping(goal, action_value_mapping["goal"])
|
||||
self.lab_logger().debug(f"任务 {ACTION.__name__} 接收到原始目标: {action_kwargs}")
|
||||
self.lab_logger().debug(f"任务 {ACTION.__name__} 接收到原始目标: {str(action_kwargs)[:1000]}")
|
||||
self.lab_logger().trace(f"任务 {ACTION.__name__} 接收到原始目标: {action_kwargs}")
|
||||
error_skip = False
|
||||
# 向Host查询物料当前状态,如果是host本身的增加物料的请求,则直接跳过
|
||||
if action_name not in ["create_resource_detailed", "create_resource"]:
|
||||
@@ -1274,9 +1321,14 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
# 批量查询资源
|
||||
queried_resources = []
|
||||
for resource_data in resource_inputs:
|
||||
plr_resource = await self.get_resource_with_dir(
|
||||
resource_id=resource_data["id"], with_children=True
|
||||
)
|
||||
unilabos_uuid = resource_data.get("data", {}).get("unilabos_uuid")
|
||||
if unilabos_uuid is None:
|
||||
plr_resource = await self.get_resource_with_dir(
|
||||
resource_id=resource_data["id"], with_children=True
|
||||
)
|
||||
else:
|
||||
resource_tree = await self.get_resource([unilabos_uuid])
|
||||
plr_resource = resource_tree.to_plr_resources()[0]
|
||||
if "sample_id" in resource_data:
|
||||
plr_resource.unilabos_extra["sample_uuid"] = resource_data["sample_id"]
|
||||
queried_resources.append(plr_resource)
|
||||
@@ -1325,9 +1377,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
execution_success = True
|
||||
except Exception as _:
|
||||
execution_error = traceback.format_exc()
|
||||
error(
|
||||
f"异步任务 {ACTION.__name__} 报错了\n{traceback.format_exc()}\n原始输入:{action_kwargs}"
|
||||
)
|
||||
error(f"异步任务 {ACTION.__name__} 报错了\n{traceback.format_exc()}\n原始输入:{str(action_kwargs)[:1000]}")
|
||||
trace(f"异步任务 {ACTION.__name__} 报错了\n{traceback.format_exc()}\n原始输入:{action_kwargs}")
|
||||
|
||||
future = ROS2DeviceNode.run_async_func(ACTION, trace_error=False, **action_kwargs)
|
||||
future.add_done_callback(_handle_future_exception)
|
||||
@@ -1347,8 +1398,9 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
except Exception as _:
|
||||
execution_error = traceback.format_exc()
|
||||
error(
|
||||
f"同步任务 {ACTION.__name__} 报错了\n{traceback.format_exc()}\n原始输入:{action_kwargs}"
|
||||
)
|
||||
f"同步任务 {ACTION.__name__} 报错了\n{traceback.format_exc()}\n原始输入:{str(action_kwargs)[:1000]}")
|
||||
trace(
|
||||
f"同步任务 {ACTION.__name__} 报错了\n{traceback.format_exc()}\n原始输入:{action_kwargs}")
|
||||
|
||||
future.add_done_callback(_handle_future_exception)
|
||||
|
||||
@@ -1416,7 +1468,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
for r in rs:
|
||||
res = self.resource_tracker.parent_resource(r) # 获取 resource 对象
|
||||
else:
|
||||
res = self.resource_tracker.parent_resource(r)
|
||||
res = self.resource_tracker.parent_resource(rs)
|
||||
if id(res) not in seen:
|
||||
seen.add(id(res))
|
||||
unique_resources.append(res)
|
||||
@@ -1492,8 +1544,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
resource_data = function_args[arg_name]
|
||||
if isinstance(resource_data, dict) and "id" in resource_data:
|
||||
try:
|
||||
converted_resource = self._convert_resource_sync(resource_data)
|
||||
function_args[arg_name] = converted_resource
|
||||
function_args[arg_name] = self._convert_resources_sync(resource_data["uuid"])[0]
|
||||
except Exception as e:
|
||||
self.lab_logger().error(
|
||||
f"转换ResourceSlot参数 {arg_name} 失败: {e}\n{traceback.format_exc()}"
|
||||
@@ -1507,12 +1558,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
resource_list = function_args[arg_name]
|
||||
if isinstance(resource_list, list):
|
||||
try:
|
||||
converted_resources = []
|
||||
for resource_data in resource_list:
|
||||
if isinstance(resource_data, dict) and "id" in resource_data:
|
||||
converted_resource = self._convert_resource_sync(resource_data)
|
||||
converted_resources.append(converted_resource)
|
||||
function_args[arg_name] = converted_resources
|
||||
uuids = [r["uuid"] for r in resource_list if isinstance(r, dict) and "id" in r]
|
||||
function_args[arg_name] = self._convert_resources_sync(*uuids) if uuids else []
|
||||
except Exception as e:
|
||||
self.lab_logger().error(
|
||||
f"转换ResourceSlot列表参数 {arg_name} 失败: {e}\n{traceback.format_exc()}"
|
||||
@@ -1525,20 +1572,27 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
f"执行动作时JSON缺少function_name或function_args: {ex}\n原JSON: {string}\n{traceback.format_exc()}"
|
||||
)
|
||||
|
||||
def _convert_resource_sync(self, resource_data: Dict[str, Any]):
|
||||
"""同步转换资源数据为实例"""
|
||||
# 创建资源查询请求
|
||||
r = SerialCommand.Request()
|
||||
r.command = json.dumps(
|
||||
{
|
||||
"id": resource_data.get("id", None),
|
||||
"uuid": resource_data.get("uuid", None),
|
||||
"with_children": True,
|
||||
}
|
||||
)
|
||||
def _convert_resources_sync(self, *uuids: str) -> List["ResourcePLR"]:
|
||||
"""同步转换资源 UUID 为实例
|
||||
|
||||
# 同步调用资源查询服务
|
||||
future = self._resource_clients["resource_get"].call_async(r)
|
||||
Args:
|
||||
*uuids: 一个或多个资源 UUID
|
||||
|
||||
Returns:
|
||||
单个 UUID 时返回单个资源实例,多个 UUID 时返回资源实例列表
|
||||
"""
|
||||
if not uuids:
|
||||
raise ValueError("至少需要提供一个 UUID")
|
||||
|
||||
uuids_list = list(uuids)
|
||||
future = self._resource_clients["c2s_update_resource_tree"].call_async(SerialCommand.Request(
|
||||
command=json.dumps(
|
||||
{
|
||||
"data": {"data": uuids_list, "with_children": True},
|
||||
"action": "get",
|
||||
}
|
||||
)
|
||||
))
|
||||
|
||||
# 等待结果(使用while循环,每次sleep 0.05秒,最多等待30秒)
|
||||
timeout = 30.0
|
||||
@@ -1548,27 +1602,40 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
elapsed += 0.05
|
||||
|
||||
if not future.done():
|
||||
raise Exception(f"资源查询超时: {resource_data}")
|
||||
raise Exception(f"资源查询超时: {uuids_list}")
|
||||
|
||||
response = future.result()
|
||||
if response is None:
|
||||
raise Exception(f"资源查询返回空结果: {resource_data}")
|
||||
raise Exception(f"资源查询返回空结果: {uuids_list}")
|
||||
|
||||
raw_data = json.loads(response.response)
|
||||
|
||||
# 转换为 PLR 资源
|
||||
tree_set = ResourceTreeSet.from_raw_dict_list(raw_data)
|
||||
plr_resource = tree_set.to_plr_resources()[0]
|
||||
if not len(tree_set.trees):
|
||||
raise Exception(f"资源查询返回空树: {raw_data}")
|
||||
plr_resources = tree_set.to_plr_resources()
|
||||
|
||||
# 通过资源跟踪器获取本地实例
|
||||
res = self.resource_tracker.figure_resource(plr_resource, try_mode=True)
|
||||
if len(res) == 0:
|
||||
self.lab_logger().warning(f"资源转换未能索引到实例: {resource_data},返回新建实例")
|
||||
return plr_resource
|
||||
elif len(res) == 1:
|
||||
return res[0]
|
||||
else:
|
||||
raise ValueError(f"资源转换得到多个实例: {res}")
|
||||
figured_resources: List[ResourcePLR] = []
|
||||
for plr_resource, tree in zip(plr_resources, tree_set.trees):
|
||||
res = self.resource_tracker.figure_resource(plr_resource, try_mode=True)
|
||||
if len(res) == 0:
|
||||
self.lab_logger().warning(f"资源转换未能索引到实例: {tree.root_node.res_content},返回新建实例")
|
||||
figured_resources.append(plr_resource)
|
||||
elif len(res) == 1:
|
||||
figured_resources.append(res[0])
|
||||
else:
|
||||
raise ValueError(f"资源转换得到多个实例: {res}")
|
||||
|
||||
mapped_plr_resources = []
|
||||
for uuid in uuids_list:
|
||||
for plr_resource in figured_resources:
|
||||
r = self.resource_tracker.loop_find_with_uuid(plr_resource, uuid)
|
||||
mapped_plr_resources.append(r)
|
||||
break
|
||||
|
||||
return mapped_plr_resources
|
||||
|
||||
async def _execute_driver_command_async(self, string: str):
|
||||
try:
|
||||
@@ -1689,6 +1756,15 @@ class ROS2DeviceNode:
|
||||
它不继承设备类,而是通过代理模式访问设备类的属性和方法。
|
||||
"""
|
||||
|
||||
# 类变量,用于循环管理
|
||||
_asyncio_loop = None
|
||||
_asyncio_loop_running = False
|
||||
_asyncio_loop_thread = None
|
||||
|
||||
@classmethod
|
||||
def get_asyncio_loop(cls):
|
||||
return cls._asyncio_loop
|
||||
|
||||
@staticmethod
|
||||
async def safe_task_wrapper(trace_callback, func, **kwargs):
|
||||
try:
|
||||
@@ -1765,6 +1841,11 @@ class ROS2DeviceNode:
|
||||
print_publish: 是否打印发布信息
|
||||
driver_is_ros:
|
||||
"""
|
||||
# 在初始化时检查循环状态
|
||||
if ROS2DeviceNode._asyncio_loop_running and ROS2DeviceNode._asyncio_loop_thread is not None:
|
||||
pass
|
||||
elif ROS2DeviceNode._asyncio_loop_thread is None:
|
||||
self._start_loop()
|
||||
|
||||
# 保存设备类是否支持异步上下文
|
||||
self._has_async_context = hasattr(driver_class, "__aenter__") and hasattr(driver_class, "__aexit__")
|
||||
@@ -1856,6 +1937,17 @@ class ROS2DeviceNode:
|
||||
except Exception as e:
|
||||
self._ros_node.lab_logger().error(f"设备后初始化失败: {e}")
|
||||
|
||||
def _start_loop(self):
|
||||
def run_event_loop():
|
||||
loop = asyncio.new_event_loop()
|
||||
ROS2DeviceNode._asyncio_loop = loop
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_forever()
|
||||
|
||||
ROS2DeviceNode._asyncio_loop_thread = threading.Thread(target=run_event_loop, daemon=True, name="ROS2DeviceNode")
|
||||
ROS2DeviceNode._asyncio_loop_thread.start()
|
||||
logger.info(f"循环线程已启动")
|
||||
|
||||
|
||||
class DeviceInfoType(TypedDict):
|
||||
id: str
|
||||
|
||||
@@ -5,7 +5,8 @@ import threading
|
||||
import time
|
||||
import traceback
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING, Optional, Dict, Any, List, ClassVar, Set, TypedDict, Union
|
||||
from typing import TYPE_CHECKING, Optional, Dict, Any, List, ClassVar, Set, Union
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from action_msgs.msg import GoalStatus
|
||||
from geometry_msgs.msg import Point
|
||||
@@ -23,6 +24,7 @@ from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialComma
|
||||
from unique_identifier_msgs.msg import UUID
|
||||
|
||||
from unilabos.registry.registry import lab_registry
|
||||
from unilabos.resources.container import RegularContainer
|
||||
from unilabos.resources.graphio import initialize_resource
|
||||
from unilabos.resources.registry import add_schema
|
||||
from unilabos.ros.initialize_device import initialize_device_from_dict
|
||||
@@ -61,6 +63,18 @@ class TestResourceReturn(TypedDict):
|
||||
devices: List[DeviceSlot]
|
||||
|
||||
|
||||
class TestLatencyReturn(TypedDict):
|
||||
"""test_latency方法的返回值类型"""
|
||||
|
||||
avg_rtt_ms: float
|
||||
avg_time_diff_ms: float
|
||||
max_time_error_ms: float
|
||||
task_delay_ms: float
|
||||
raw_delay_ms: float
|
||||
test_count: int
|
||||
status: str
|
||||
|
||||
|
||||
class HostNode(BaseROS2DeviceNode):
|
||||
"""
|
||||
主机节点类,负责管理设备、资源和控制器
|
||||
@@ -70,6 +84,8 @@ class HostNode(BaseROS2DeviceNode):
|
||||
|
||||
_instance: ClassVar[Optional["HostNode"]] = None
|
||||
_ready_event: ClassVar[threading.Event] = threading.Event()
|
||||
_shutting_down: ClassVar[bool] = False # Flag to signal shutdown to background threads
|
||||
_background_threads: ClassVar[List[threading.Thread]] = [] # Track all background threads for cleanup
|
||||
_device_action_status: ClassVar[collections.defaultdict[str, DeviceActionStatus]] = collections.defaultdict(
|
||||
DeviceActionStatus
|
||||
)
|
||||
@@ -81,6 +97,48 @@ class HostNode(BaseROS2DeviceNode):
|
||||
return cls._instance
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def shutdown_background_threads(cls, timeout: float = 5.0) -> None:
|
||||
"""
|
||||
Gracefully shutdown all background threads for clean exit or restart.
|
||||
|
||||
This method:
|
||||
1. Sets shutdown flag to stop background operations
|
||||
2. Waits for background threads to finish with timeout
|
||||
3. Cleans up finished threads from tracking list
|
||||
|
||||
Args:
|
||||
timeout: Maximum time to wait for each thread (seconds)
|
||||
"""
|
||||
cls._shutting_down = True
|
||||
|
||||
# Wait for background threads to finish
|
||||
active_threads = []
|
||||
for t in cls._background_threads:
|
||||
if t.is_alive():
|
||||
t.join(timeout=timeout)
|
||||
if t.is_alive():
|
||||
active_threads.append(t.name)
|
||||
|
||||
if active_threads:
|
||||
logger.warning(f"[Host Node] Some background threads still running: {active_threads}")
|
||||
|
||||
# Clear the thread list
|
||||
cls._background_threads.clear()
|
||||
logger.info(f"[Host Node] Background threads shutdown complete")
|
||||
|
||||
@classmethod
|
||||
def reset_state(cls) -> None:
|
||||
"""
|
||||
Reset the HostNode singleton state for restart or clean exit.
|
||||
Call this after destroying the instance.
|
||||
"""
|
||||
cls._instance = None
|
||||
cls._ready_event.clear()
|
||||
cls._shutting_down = False
|
||||
cls._background_threads.clear()
|
||||
logger.info("[Host Node] State reset complete")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device_id: str,
|
||||
@@ -294,12 +352,36 @@ class HostNode(BaseROS2DeviceNode):
|
||||
bridge.publish_host_ready()
|
||||
self.lab_logger().debug(f"Host ready signal sent via {bridge.__class__.__name__}")
|
||||
|
||||
def _send_re_register(self, sclient):
|
||||
sclient.wait_for_service()
|
||||
request = SerialCommand.Request()
|
||||
request.command = ""
|
||||
future = sclient.call_async(request)
|
||||
response = future.result()
|
||||
def _send_re_register(self, sclient, device_namespace: str):
|
||||
"""
|
||||
Send re-register command to a device. This is a one-time operation.
|
||||
|
||||
Args:
|
||||
sclient: The service client
|
||||
device_namespace: The device namespace for logging
|
||||
"""
|
||||
try:
|
||||
# Use timeout to prevent indefinite blocking
|
||||
if not sclient.wait_for_service(timeout_sec=10.0):
|
||||
self.lab_logger().debug(f"[Host Node] Re-register timeout for {device_namespace}")
|
||||
return
|
||||
|
||||
# Check shutdown flag after wait
|
||||
if self._shutting_down:
|
||||
self.lab_logger().debug(f"[Host Node] Re-register aborted for {device_namespace} (shutdown)")
|
||||
return
|
||||
|
||||
request = SerialCommand.Request()
|
||||
request.command = ""
|
||||
future = sclient.call_async(request)
|
||||
# Use timeout for result as well
|
||||
future.result()
|
||||
except Exception as e:
|
||||
# Gracefully handle destruction during shutdown
|
||||
if "destruction was requested" in str(e) or self._shutting_down:
|
||||
self.lab_logger().debug(f"[Host Node] Re-register aborted for {device_namespace} (cleanup)")
|
||||
else:
|
||||
self.lab_logger().warning(f"[Host Node] Re-register failed for {device_namespace}: {e}")
|
||||
|
||||
def _discover_devices(self) -> None:
|
||||
"""
|
||||
@@ -331,23 +413,27 @@ class HostNode(BaseROS2DeviceNode):
|
||||
self._create_action_clients_for_device(device_id, namespace)
|
||||
self._online_devices.add(device_key)
|
||||
sclient = self.create_client(SerialCommand, f"/srv{namespace}/re_register_device")
|
||||
threading.Thread(
|
||||
t = threading.Thread(
|
||||
target=self._send_re_register,
|
||||
args=(sclient,),
|
||||
args=(sclient, namespace),
|
||||
daemon=True,
|
||||
name=f"ROSDevice{self.device_id}_re_register_device_{namespace}",
|
||||
).start()
|
||||
)
|
||||
self._background_threads.append(t)
|
||||
t.start()
|
||||
elif device_key not in self._online_devices:
|
||||
# 设备重新上线
|
||||
self.lab_logger().info(f"[Host Node] Device reconnected: {device_key}")
|
||||
self._online_devices.add(device_key)
|
||||
sclient = self.create_client(SerialCommand, f"/srv{namespace}/re_register_device")
|
||||
threading.Thread(
|
||||
t = threading.Thread(
|
||||
target=self._send_re_register,
|
||||
args=(sclient,),
|
||||
args=(sclient, namespace),
|
||||
daemon=True,
|
||||
name=f"ROSDevice{self.device_id}_re_register_device_{namespace}",
|
||||
).start()
|
||||
)
|
||||
self._background_threads.append(t)
|
||||
t.start()
|
||||
|
||||
# 检测离线设备
|
||||
offline_devices = self._online_devices - current_devices
|
||||
@@ -513,11 +599,10 @@ class HostNode(BaseROS2DeviceNode):
|
||||
)
|
||||
|
||||
try:
|
||||
new_li = []
|
||||
assert len(response) == 1, "Create Resource应当只返回一个结果"
|
||||
for i in response:
|
||||
res = json.loads(i)
|
||||
new_li.append(res)
|
||||
return {"resources": new_li, "liquid_input_resources": new_li}
|
||||
return res
|
||||
except Exception as ex:
|
||||
pass
|
||||
_n = "\n"
|
||||
@@ -663,7 +748,7 @@ class HostNode(BaseROS2DeviceNode):
|
||||
if bCreate:
|
||||
self.lab_logger().trace(f"Status created: {device_id}.{property_name} = {msg.data}")
|
||||
else:
|
||||
self.lab_logger().debug(f"Status updated: {device_id}.{property_name} = {msg.data}")
|
||||
self.lab_logger().trace(f"Status updated: {device_id}.{property_name} = {msg.data}")
|
||||
|
||||
def send_goal(
|
||||
self,
|
||||
@@ -705,13 +790,14 @@ class HostNode(BaseROS2DeviceNode):
|
||||
raise ValueError(f"ActionClient {action_id} not found.")
|
||||
|
||||
action_client: ActionClient = self._action_clients[action_id]
|
||||
|
||||
# 遍历action_kwargs下的所有子dict,将"sample_uuid"的值赋给"sample_id"
|
||||
def assign_sample_id(obj):
|
||||
if isinstance(obj, dict):
|
||||
if "sample_uuid" in obj:
|
||||
obj["sample_id"] = obj["sample_uuid"]
|
||||
obj.pop("sample_uuid")
|
||||
for k,v in obj.items():
|
||||
for k, v in obj.items():
|
||||
if k != "unilabos_extra":
|
||||
assign_sample_id(v)
|
||||
elif isinstance(obj, list):
|
||||
@@ -721,7 +807,8 @@ class HostNode(BaseROS2DeviceNode):
|
||||
assign_sample_id(action_kwargs)
|
||||
goal_msg = convert_to_ros_msg(action_client._action_type.Goal(), action_kwargs)
|
||||
|
||||
self.lab_logger().info(f"[Host Node] Sending goal for {action_id}: {goal_msg}")
|
||||
self.lab_logger().info(f"[Host Node] Sending goal for {action_id}: {str(goal_msg)[:1000]}")
|
||||
self.lab_logger().trace(f"[Host Node] Sending goal for {action_id}: {goal_msg}")
|
||||
action_client.wait_for_server()
|
||||
goal_uuid_obj = UUID(uuid=list(u.bytes))
|
||||
|
||||
@@ -742,9 +829,7 @@ class HostNode(BaseROS2DeviceNode):
|
||||
self.lab_logger().info(f"[Host Node] Goal {action_id} ({item.job_id}) accepted")
|
||||
self._goals[item.job_id] = goal_handle
|
||||
goal_future = goal_handle.get_result_async()
|
||||
goal_future.add_done_callback(
|
||||
lambda f: self.get_result_callback(item, action_id, f)
|
||||
)
|
||||
goal_future.add_done_callback(lambda f: self.get_result_callback(item, action_id, f))
|
||||
goal_future.result()
|
||||
|
||||
def feedback_callback(self, item: "QueueItem", action_id: str, feedback_msg) -> None:
|
||||
@@ -781,8 +866,13 @@ class HostNode(BaseROS2DeviceNode):
|
||||
# 适配后端的一些额外处理
|
||||
return_value = return_info.get("return_value")
|
||||
if isinstance(return_value, dict):
|
||||
unilabos_samples = return_info.get("unilabos_samples")
|
||||
if isinstance(unilabos_samples, list):
|
||||
unilabos_samples = return_value.pop("unilabos_samples", None)
|
||||
if isinstance(unilabos_samples, list) and unilabos_samples:
|
||||
self.lab_logger().info(
|
||||
f"[Host Node] Job {job_id[:8]} returned {len(unilabos_samples)} sample(s): "
|
||||
f"{[s.get('name', s.get('id', 'unknown')) if isinstance(s, dict) else str(s)[:20] for s in unilabos_samples[:5]]}"
|
||||
f"{'...' if len(unilabos_samples) > 5 else ''}"
|
||||
)
|
||||
return_info["unilabos_samples"] = unilabos_samples
|
||||
suc = return_info.get("suc", False)
|
||||
if not suc:
|
||||
@@ -809,7 +899,7 @@ class HostNode(BaseROS2DeviceNode):
|
||||
# 清理 _goals 中的记录
|
||||
if job_id in self._goals:
|
||||
del self._goals[job_id]
|
||||
self.lab_logger().debug(f"[Host Node] Removed goal {job_id[:8]} from _goals")
|
||||
self.lab_logger().trace(f"[Host Node] Removed goal {job_id[:8]} from _goals")
|
||||
|
||||
# 存储结果供 HTTP API 查询
|
||||
try:
|
||||
@@ -1061,11 +1151,11 @@ class HostNode(BaseROS2DeviceNode):
|
||||
|
||||
接收序列化的 ResourceTreeSet 数据并进行处理
|
||||
"""
|
||||
self.lab_logger().info(f"[Host Node-Resource] Resource tree add request received")
|
||||
try:
|
||||
# 解析请求数据
|
||||
data = json.loads(request.command)
|
||||
action = data["action"]
|
||||
self.lab_logger().info(f"[Host Node-Resource] Resource tree {action} request received")
|
||||
data = data["data"]
|
||||
if action == "add":
|
||||
await self._resource_tree_action_add_callback(data, response)
|
||||
@@ -1166,11 +1256,13 @@ class HostNode(BaseROS2DeviceNode):
|
||||
响应对象,包含查询到的资源
|
||||
"""
|
||||
try:
|
||||
from unilabos.app.web import http_client
|
||||
|
||||
data = json.loads(request.command)
|
||||
if "uuid" in data and data["uuid"] is not None:
|
||||
http_req = self.bridges[-1].resource_tree_get([data["uuid"]], data["with_children"])
|
||||
elif "id" in data and data["id"].startswith("/"):
|
||||
http_req = self.bridges[-1].resource_get(data["id"], data["with_children"])
|
||||
http_req = http_client.resource_tree_get([data["uuid"]], data["with_children"])
|
||||
elif "id" in data:
|
||||
http_req = http_client.resource_get(data["id"], data["with_children"])
|
||||
else:
|
||||
raise ValueError("没有使用正确的物料 id 或 uuid")
|
||||
response.response = json.dumps(http_req["data"])
|
||||
@@ -1252,10 +1344,20 @@ class HostNode(BaseROS2DeviceNode):
|
||||
self.lab_logger().debug(f"[Host Node-Resource] List parameters: {request}")
|
||||
return response
|
||||
|
||||
def test_latency(self):
|
||||
def test_latency(self) -> TestLatencyReturn:
|
||||
"""
|
||||
测试网络延迟的action实现
|
||||
通过5次ping-pong机制校对时间误差并计算实际延迟
|
||||
|
||||
Returns:
|
||||
TestLatencyReturn: 包含延迟测试结果的字典,包括:
|
||||
- avg_rtt_ms: 平均往返时间(毫秒)
|
||||
- avg_time_diff_ms: 平均时间差(毫秒)
|
||||
- max_time_error_ms: 最大时间误差(毫秒)
|
||||
- task_delay_ms: 实际任务延迟(毫秒),-1表示无法计算
|
||||
- raw_delay_ms: 原始时间差(毫秒),-1表示无法计算
|
||||
- test_count: 有效测试次数
|
||||
- status: 测试状态,"success"表示成功,"all_timeout"表示全部超时
|
||||
"""
|
||||
import uuid as uuid_module
|
||||
|
||||
@@ -1318,7 +1420,15 @@ class HostNode(BaseROS2DeviceNode):
|
||||
|
||||
if not ping_results:
|
||||
self.lab_logger().error("❌ 所有ping-pong测试都失败了")
|
||||
return {"status": "all_timeout"}
|
||||
return {
|
||||
"avg_rtt_ms": -1.0,
|
||||
"avg_time_diff_ms": -1.0,
|
||||
"max_time_error_ms": -1.0,
|
||||
"task_delay_ms": -1.0,
|
||||
"raw_delay_ms": -1.0,
|
||||
"test_count": 0,
|
||||
"status": "all_timeout",
|
||||
}
|
||||
|
||||
# 统计分析
|
||||
rtts = [r["rtt_ms"] for r in ping_results]
|
||||
@@ -1326,7 +1436,7 @@ class HostNode(BaseROS2DeviceNode):
|
||||
|
||||
avg_rtt_ms = sum(rtts) / len(rtts)
|
||||
avg_time_diff_ms = sum(time_diffs) / len(time_diffs)
|
||||
max_time_diff_error_ms = max(abs(min(time_diffs)), abs(max(time_diffs)))
|
||||
max_time_diff_error_ms: float = max(abs(min(time_diffs)), abs(max(time_diffs)))
|
||||
|
||||
self.lab_logger().info("-" * 50)
|
||||
self.lab_logger().info("[测试统计]")
|
||||
@@ -1366,7 +1476,7 @@ class HostNode(BaseROS2DeviceNode):
|
||||
|
||||
self.lab_logger().info("=" * 60)
|
||||
|
||||
return {
|
||||
res: TestLatencyReturn = {
|
||||
"avg_rtt_ms": avg_rtt_ms,
|
||||
"avg_time_diff_ms": avg_time_diff_ms,
|
||||
"max_time_error_ms": max_time_diff_error_ms,
|
||||
@@ -1377,12 +1487,23 @@ class HostNode(BaseROS2DeviceNode):
|
||||
"test_count": len(ping_results),
|
||||
"status": "success",
|
||||
}
|
||||
return res
|
||||
|
||||
def test_resource(
|
||||
self, resource: ResourceSlot, resources: List[ResourceSlot], device: DeviceSlot, devices: List[DeviceSlot]
|
||||
self,
|
||||
resource: ResourceSlot = None,
|
||||
resources: List[ResourceSlot] = None,
|
||||
device: DeviceSlot = None,
|
||||
devices: List[DeviceSlot] = None,
|
||||
) -> TestResourceReturn:
|
||||
if resources is None:
|
||||
resources = []
|
||||
if devices is None:
|
||||
devices = []
|
||||
if resource is None:
|
||||
resource = RegularContainer("test_resource传入None")
|
||||
return {
|
||||
"resources": ResourceTreeSet.from_plr_resources([resource, *resources]).dump(),
|
||||
"resources": ResourceTreeSet.from_plr_resources([resource, *resources], known_newly_created=True).dump(),
|
||||
"devices": [device, *devices],
|
||||
}
|
||||
|
||||
@@ -1434,7 +1555,9 @@ class HostNode(BaseROS2DeviceNode):
|
||||
|
||||
# 构建服务地址
|
||||
srv_address = f"/srv{namespace}/s2c_resource_tree"
|
||||
self.lab_logger().info(f"[Host Node-Resource] Notifying {device_id} for resource tree {action} operation")
|
||||
self.lab_logger().trace(
|
||||
f"[Host Node-Resource] Host -> {device_id} ResourceTree {action} operation started -------"
|
||||
)
|
||||
|
||||
# 创建服务客户端
|
||||
sclient = self.create_client(SerialCommand, srv_address)
|
||||
@@ -1469,8 +1592,8 @@ class HostNode(BaseROS2DeviceNode):
|
||||
time.sleep(0.05)
|
||||
|
||||
response = future.result()
|
||||
self.lab_logger().info(
|
||||
f"[Host Node-Resource] Resource tree {action} notification completed for {device_id}"
|
||||
self.lab_logger().trace(
|
||||
f"[Host Node-Resource] Host -> {device_id} ResourceTree {action} operation completed -------"
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
import asyncio
|
||||
from asyncio import events
|
||||
import threading
|
||||
|
||||
import rclpy
|
||||
from rclpy.impl.implementation_singleton import rclpy_implementation as _rclpy
|
||||
from rclpy.executors import await_or_execute, Executor
|
||||
from rclpy.action import ActionClient, ActionServer
|
||||
from rclpy.action.server import ServerGoalHandle, GoalResponse, GoalInfo, GoalStatus
|
||||
from std_msgs.msg import String
|
||||
from action_tutorials_interfaces.action import Fibonacci
|
||||
|
||||
|
||||
loop = None
|
||||
|
||||
def get_event_loop():
|
||||
global loop
|
||||
return loop
|
||||
|
||||
|
||||
async def default_handle_accepted_callback_async(goal_handle):
|
||||
"""Execute the goal."""
|
||||
await goal_handle.execute()
|
||||
|
||||
|
||||
class ServerGoalHandleX(ServerGoalHandle):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
async def execute(self, execute_callback=None):
|
||||
# It's possible that there has been a request to cancel the goal prior to executing.
|
||||
# In this case we want to avoid the illegal state transition to EXECUTING
|
||||
# but still call the users execute callback to let them handle canceling the goal.
|
||||
if not self.is_cancel_requested:
|
||||
self._update_state(_rclpy.GoalEvent.EXECUTE)
|
||||
await self._action_server.notify_execute_async(self, execute_callback)
|
||||
|
||||
|
||||
class ActionServerX(ActionServer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.register_handle_accepted_callback(default_handle_accepted_callback_async)
|
||||
|
||||
async def _execute_goal_request(self, request_header_and_message):
|
||||
request_header, goal_request = request_header_and_message
|
||||
goal_uuid = goal_request.goal_id
|
||||
goal_info = GoalInfo()
|
||||
goal_info.goal_id = goal_uuid
|
||||
|
||||
self._node.get_logger().debug('New goal request with ID: {0}'.format(goal_uuid.uuid))
|
||||
|
||||
# Check if goal ID is already being tracked by this action server
|
||||
with self._lock:
|
||||
goal_id_exists = self._handle.goal_exists(goal_info)
|
||||
|
||||
accepted = False
|
||||
if not goal_id_exists:
|
||||
# Call user goal callback
|
||||
response = await await_or_execute(self._goal_callback, goal_request.goal)
|
||||
if not isinstance(response, GoalResponse):
|
||||
self._node.get_logger().warning(
|
||||
'Goal request callback did not return a GoalResponse type. Rejecting goal.')
|
||||
else:
|
||||
accepted = GoalResponse.ACCEPT == response
|
||||
|
||||
if accepted:
|
||||
# Stamp time of acceptance
|
||||
goal_info.stamp = self._node.get_clock().now().to_msg()
|
||||
|
||||
# Create a goal handle
|
||||
try:
|
||||
with self._lock:
|
||||
goal_handle = ServerGoalHandleX(self, goal_info, goal_request.goal)
|
||||
except RuntimeError as e:
|
||||
self._node.get_logger().error(
|
||||
'Failed to accept new goal with ID {0}: {1}'.format(goal_uuid.uuid, e))
|
||||
accepted = False
|
||||
else:
|
||||
self._goal_handles[bytes(goal_uuid.uuid)] = goal_handle
|
||||
|
||||
# Send response
|
||||
response_msg = self._action_type.Impl.SendGoalService.Response()
|
||||
response_msg.accepted = accepted
|
||||
response_msg.stamp = goal_info.stamp
|
||||
self._handle.send_goal_response(request_header, response_msg)
|
||||
|
||||
if not accepted:
|
||||
self._node.get_logger().debug('New goal rejected: {0}'.format(goal_uuid.uuid))
|
||||
return
|
||||
|
||||
self._node.get_logger().debug('New goal accepted: {0}'.format(goal_uuid.uuid))
|
||||
|
||||
# Provide the user a reference to the goal handle
|
||||
# await await_or_execute(self._handle_accepted_callback, goal_handle)
|
||||
asyncio.create_task(self._handle_accepted_callback(goal_handle))
|
||||
|
||||
async def notify_execute_async(self, goal_handle, execute_callback):
|
||||
# Use provided callback, defaulting to a previously registered callback
|
||||
if execute_callback is None:
|
||||
if self._execute_callback is None:
|
||||
return
|
||||
execute_callback = self._execute_callback
|
||||
|
||||
# Schedule user callback for execution
|
||||
self._node.get_logger().info(f"{events.get_running_loop()}")
|
||||
asyncio.create_task(self._execute_goal(execute_callback, goal_handle))
|
||||
# loop = asyncio.new_event_loop()
|
||||
# asyncio.set_event_loop(loop)
|
||||
# task = loop.create_task(self._execute_goal(execute_callback, goal_handle))
|
||||
# await task
|
||||
|
||||
|
||||
class ActionClientX(ActionClient):
|
||||
feedback_queue = asyncio.Queue()
|
||||
|
||||
async def feedback_cb(self, msg):
|
||||
await self.feedback_queue.put(msg)
|
||||
|
||||
async def send_goal_async(self, goal_msg):
|
||||
goal_future = super().send_goal_async(
|
||||
goal_msg,
|
||||
feedback_callback=self.feedback_cb
|
||||
)
|
||||
client_goal_handle = await asyncio.ensure_future(goal_future)
|
||||
if not client_goal_handle.accepted:
|
||||
raise Exception("Goal rejected.")
|
||||
result_future = client_goal_handle.get_result_async()
|
||||
while True:
|
||||
feedback_future = asyncio.ensure_future(self.feedback_queue.get())
|
||||
tasks = [result_future, feedback_future]
|
||||
await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
|
||||
if result_future.done():
|
||||
result = result_future.result().result
|
||||
yield (None, result)
|
||||
break
|
||||
else:
|
||||
feedback = feedback_future.result().feedback
|
||||
yield (feedback, None)
|
||||
|
||||
|
||||
async def main(node):
|
||||
print('Node started.')
|
||||
action_client = ActionClientX(node, Fibonacci, 'fibonacci')
|
||||
goal_msg = Fibonacci.Goal()
|
||||
goal_msg.order = 10
|
||||
async for (feedback, result) in action_client.send_goal_async(goal_msg):
|
||||
if feedback:
|
||||
print(f'Feedback: {feedback}')
|
||||
else:
|
||||
print(f'Result: {result}')
|
||||
print('Finished.')
|
||||
|
||||
|
||||
async def ros_loop_node(node):
|
||||
while rclpy.ok():
|
||||
rclpy.spin_once(node, timeout_sec=0)
|
||||
await asyncio.sleep(1e-4)
|
||||
|
||||
|
||||
async def ros_loop(executor: Executor):
|
||||
while rclpy.ok():
|
||||
executor.spin_once(timeout_sec=0)
|
||||
await asyncio.sleep(1e-4)
|
||||
|
||||
|
||||
def run_event_loop():
|
||||
global loop
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_forever()
|
||||
|
||||
|
||||
def run_event_loop_in_thread():
|
||||
thread = threading.Thread(target=run_event_loop, args=())
|
||||
thread.start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
rclpy.init()
|
||||
node = rclpy.create_node('async_subscriber')
|
||||
future = asyncio.wait([ros_loop(node), main()])
|
||||
asyncio.get_event_loop().run_until_complete(future)
|
||||
Reference in New Issue
Block a user