mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-17 13:01:12 +00:00
Squash merge from dev
Update recipe.yaml fix: figure_resource use call_async in all service to avoid deadlock fix: prcxi import error 临时兼容错误的driver写法 fix protocol node fix filter protocol bugfixes on organic protocols fix and remove redundant info feat: 新增use_remote_resource参数 fix all protocol_compilers and remove deprecated devices feat: 优化protocol node节点运行日志 fix pumps and liquid_handler handle feat: workstation example add: prcxi res fix: startup slow fix: prcxi_res fix: discard_tips fix: discard_tips error fix: drop_tips not using auto resource select feat: 添加ChinWe设备控制类,支持串口通信和电机控制功能 (#79) feat: add trace log level modify default discovery_interval to 15s fix: working dir error when input config path feat: report publish topic when error fix: workstation handlers and vessel_id parsing Cleanup registry to be easy-understanding (#76) * delete deprecated mock devices * rename categories * combine chromatographic devices * rename rviz simulation nodes * organic virtual devices * parse vessel_id * run registry completion before merge --------- Co-authored-by: Xuwznln <18435084+Xuwznln@users.noreply.github.com>
This commit is contained in:
@@ -52,7 +52,7 @@ def main(
|
||||
visual: str = "disable",
|
||||
resources_mesh_config: dict = {},
|
||||
rclpy_init_args: List[str] = ["--log-level", "debug"],
|
||||
discovery_interval: float = 5.0,
|
||||
discovery_interval: float = 15.0,
|
||||
) -> None:
|
||||
"""主函数"""
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ from unilabos.resources.graphio import (
|
||||
resource_plr_to_ulab,
|
||||
tree_to_list,
|
||||
)
|
||||
from unilabos.resources.plr_additional_res_reg import register
|
||||
from unilabos.ros.msgs.message_converter import (
|
||||
convert_to_ros_msg,
|
||||
convert_from_ros_msg,
|
||||
@@ -51,7 +52,7 @@ from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
|
||||
from unilabos.ros.x.rclpyx import get_event_loop
|
||||
from unilabos.ros.utils.driver_creator import ProtocolNodeCreator, PyLabRobotCreator, DeviceClassCreator
|
||||
from unilabos.utils.async_util import run_async_func
|
||||
from unilabos.utils.log import info, debug, warning, error, critical, logger
|
||||
from unilabos.utils.log import info, debug, warning, error, critical, logger, trace
|
||||
from unilabos.utils.type_check import get_type_class, TypeEncoder, serialize_result_info
|
||||
|
||||
T = TypeVar("T")
|
||||
@@ -82,6 +83,7 @@ class ROSLoggerAdapter:
|
||||
self.level_2_logger_func = {
|
||||
"info": info,
|
||||
"debug": debug,
|
||||
"trace": trace,
|
||||
"warning": warning,
|
||||
"error": error,
|
||||
"critical": critical,
|
||||
@@ -96,6 +98,10 @@ class ROSLoggerAdapter:
|
||||
ros_log_func(msg)
|
||||
self.level_2_logger_func[level](msg, *args, stack_level=1, **kwargs)
|
||||
|
||||
def trace(self, msg, *args, **kwargs):
|
||||
"""记录TRACE级别日志"""
|
||||
self._log("trace", msg, *args, **kwargs)
|
||||
|
||||
def debug(self, msg, *args, **kwargs):
|
||||
"""记录DEBUG级别日志"""
|
||||
self._log("debug", msg, *args, **kwargs)
|
||||
@@ -175,47 +181,46 @@ class PropertyPublisher:
|
||||
self.timer = node.create_timer(self.timer_period, self.publish_property)
|
||||
self.__loop = get_event_loop()
|
||||
str_msg_type = str(msg_type)[8:-2]
|
||||
self.node.lab_logger().debug(f"发布属性: {name}, 类型: {str_msg_type}, 周期: {initial_period}秒")
|
||||
self.node.lab_logger().trace(f"发布属性: {name}, 类型: {str_msg_type}, 周期: {initial_period}秒")
|
||||
|
||||
def get_property(self):
|
||||
if asyncio.iscoroutinefunction(self.get_method):
|
||||
# 如果是异步函数,运行事件循环并等待结果
|
||||
self.node.get_logger().debug(f"【PropertyPublisher.get_property】获取异步属性: {self.name}")
|
||||
self.node.lab_logger().trace(f"【PropertyPublisher.get_property】获取异步属性: {self.name}")
|
||||
loop = self.__loop
|
||||
if loop:
|
||||
future = asyncio.run_coroutine_threadsafe(self.get_method(), loop)
|
||||
self._value = future.result()
|
||||
return self._value
|
||||
else:
|
||||
self.node.get_logger().error(f"【PropertyPublisher.get_property】事件循环未初始化")
|
||||
self.node.lab_logger().error(f"【PropertyPublisher.get_property】事件循环未初始化")
|
||||
return None
|
||||
else:
|
||||
# 如果是同步函数,直接调用并返回结果
|
||||
self.node.get_logger().debug(f"【PropertyPublisher.get_property】获取同步属性: {self.name}")
|
||||
self.node.lab_logger().trace(f"【PropertyPublisher.get_property】获取同步属性: {self.name}")
|
||||
self._value = self.get_method()
|
||||
return self._value
|
||||
|
||||
async def get_property_async(self):
|
||||
try:
|
||||
# 获取异步属性值
|
||||
self.node.get_logger().debug(f"【PropertyPublisher.get_property_async】异步获取属性: {self.name}")
|
||||
self.node.lab_logger().trace(f"【PropertyPublisher.get_property_async】异步获取属性: {self.name}")
|
||||
self._value = await self.get_method()
|
||||
except Exception as e:
|
||||
self.node.get_logger().error(f"【PropertyPublisher.get_property_async】获取异步属性出错: {str(e)}")
|
||||
self.node.lab_logger().error(f"【PropertyPublisher.get_property_async】获取异步属性出错: {str(e)}")
|
||||
|
||||
def publish_property(self):
|
||||
try:
|
||||
self.node.get_logger().debug(f"【PropertyPublisher.publish_property】开始发布属性: {self.name}")
|
||||
self.node.lab_logger().trace(f"【PropertyPublisher.publish_property】开始发布属性: {self.name}")
|
||||
value = self.get_property()
|
||||
if self.print_publish:
|
||||
self.node.get_logger().info(f"【PropertyPublisher.publish_property】发布 {self.msg_type}: {value}")
|
||||
self.node.lab_logger().trace(f"【PropertyPublisher.publish_property】发布 {self.msg_type}: {value}")
|
||||
if value is not None:
|
||||
msg = convert_to_ros_msg(self.msg_type, value)
|
||||
self.publisher_.publish(msg)
|
||||
self.node.get_logger().debug(f"【PropertyPublisher.publish_property】属性 {self.name} 发布成功")
|
||||
self.node.lab_logger().trace(f"【PropertyPublisher.publish_property】属性 {self.name} 发布成功")
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
self.node.get_logger().error(f"【PropertyPublisher.publish_property】发布属性出错: {str(e)}")
|
||||
self.node.lab_logger().error(f"【PropertyPublisher.publish_property】发布属性 {self.publisher_.topic} 出错: {str(e)}\n{traceback.format_exc()}")
|
||||
|
||||
def change_frequency(self, period):
|
||||
# 动态改变定时器频率
|
||||
@@ -331,7 +336,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
res.response = ""
|
||||
return res
|
||||
|
||||
def append_resource(req: SerialCommand_Request, res: SerialCommand_Response):
|
||||
async def append_resource(req: SerialCommand_Request, res: SerialCommand_Response):
|
||||
# 物料传输到对应的node节点
|
||||
rclient = self.create_client(ResourceAdd, "/resources/add")
|
||||
rclient.wait_for_service()
|
||||
@@ -390,10 +395,11 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
if "data" not in resource:
|
||||
resource["data"] = {}
|
||||
resource["data"].update(json.loads(container_instance.data))
|
||||
request.resources[0].name = resource["name"]
|
||||
logger.info(f"更新物料{container_query_dict['name']}的数据{resource['data']} dict")
|
||||
else:
|
||||
logger.info(f"更新物料{container_query_dict['name']}出现不支持的数据类型{type(resource)} {resource}")
|
||||
response = rclient.call(request)
|
||||
response = await rclient.call_async(request)
|
||||
# 应该先add_resource了
|
||||
res.response = "OK"
|
||||
# 如果driver自己就有assign的方法,那就使用driver自己的assign方法
|
||||
@@ -607,7 +613,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
callback_group=ReentrantCallbackGroup(),
|
||||
)
|
||||
|
||||
self.lab_logger().debug(f"发布动作: {action_name}, 类型: {str_action_type}")
|
||||
self.lab_logger().trace(f"发布动作: {action_name}, 类型: {str_action_type}")
|
||||
|
||||
def get_real_function(self, instance, attr_name):
|
||||
if hasattr(instance.__class__, attr_name):
|
||||
@@ -649,7 +655,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_kwargs}")
|
||||
self.lab_logger().debug(f"任务 {ACTION.__name__} 接收到原始目标: {action_kwargs}")
|
||||
error_skip = False
|
||||
# 向Host查询物料当前状态,如果是host本身的增加物料的请求,则直接跳过
|
||||
if action_name not in ["create_resource_detailed", "create_resource"]:
|
||||
for k, v in goal.get_fields_and_field_types().items():
|
||||
@@ -659,7 +666,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
# TODO: resource后面需要分组
|
||||
only_one_resource = False
|
||||
try:
|
||||
if len(action_kwargs[k]) > 1:
|
||||
if isinstance(action_kwargs[k], list) and len(action_kwargs[k]) > 1:
|
||||
for i in action_kwargs[k]:
|
||||
r = ResourceGet.Request()
|
||||
r.id = i["id"] # splash optional
|
||||
@@ -689,17 +696,43 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
final_resource = convert_resources_to_type(resources_list, final_type)
|
||||
else:
|
||||
final_resource = [convert_resources_to_type([i], final_type)[0] for i in resources_list]
|
||||
action_kwargs[k] = self.resource_tracker.figure_resource(final_resource)
|
||||
try:
|
||||
action_kwargs[k] = self.resource_tracker.figure_resource(final_resource, try_mode=True)
|
||||
except Exception as e:
|
||||
self.lab_logger().error(f"物料实例获取失败: {e}\n{traceback.format_exc()}")
|
||||
error_skip = True
|
||||
execution_error = traceback.format_exc()
|
||||
break
|
||||
|
||||
##### self.lab_logger().info(f"准备执行: {action_kwargs}, 函数: {ACTION.__name__}")
|
||||
time_start = time.time()
|
||||
time_overall = 100
|
||||
future = None
|
||||
if not error_skip:
|
||||
# 将阻塞操作放入线程池执行
|
||||
if asyncio.iscoroutinefunction(ACTION):
|
||||
try:
|
||||
##### self.lab_logger().info(f"异步执行动作 {ACTION}")
|
||||
future = ROS2DeviceNode.run_async_func(ACTION, trace_error=False, **action_kwargs)
|
||||
|
||||
# 将阻塞操作放入线程池执行
|
||||
if asyncio.iscoroutinefunction(ACTION):
|
||||
try:
|
||||
##### self.lab_logger().info(f"异步执行动作 {ACTION}")
|
||||
future = ROS2DeviceNode.run_async_func(ACTION, trace_error=False, **action_kwargs)
|
||||
def _handle_future_exception(fut):
|
||||
nonlocal execution_error, execution_success, action_return_value
|
||||
try:
|
||||
action_return_value = fut.result()
|
||||
execution_success = True
|
||||
except Exception as e:
|
||||
execution_error = traceback.format_exc()
|
||||
error(f"异步任务 {ACTION.__name__} 报错了\n{traceback.format_exc()}\n原始输入:{action_kwargs}")
|
||||
error(traceback.format_exc())
|
||||
|
||||
future.add_done_callback(_handle_future_exception)
|
||||
except Exception as e:
|
||||
execution_error = traceback.format_exc()
|
||||
execution_success = False
|
||||
self.lab_logger().error(f"创建异步任务失败: {traceback.format_exc()}")
|
||||
else:
|
||||
##### self.lab_logger().info(f"同步执行动作 {ACTION}")
|
||||
future = self._executor.submit(ACTION, **action_kwargs)
|
||||
|
||||
def _handle_future_exception(fut):
|
||||
nonlocal execution_error, execution_success, action_return_value
|
||||
@@ -707,35 +740,15 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
action_return_value = fut.result()
|
||||
execution_success = True
|
||||
except Exception as e:
|
||||
execution_error = traceback.format_exc()
|
||||
##### error(f"异步任务 {ACTION.__name__} 报错了")
|
||||
error(traceback.format_exc())
|
||||
error(f"同步任务 {ACTION.__name__} 报错了\n{traceback.format_exc()}\n原始输入:{action_kwargs}")
|
||||
|
||||
future.add_done_callback(_handle_future_exception)
|
||||
except Exception as e:
|
||||
self.lab_logger().error(f"创建异步任务失败: {traceback.format_exc()}")
|
||||
raise e
|
||||
else:
|
||||
##### self.lab_logger().info(f"同步执行动作 {ACTION}")
|
||||
future = self._executor.submit(ACTION, **action_kwargs)
|
||||
|
||||
def _handle_future_exception(fut):
|
||||
nonlocal execution_error, execution_success, action_return_value
|
||||
try:
|
||||
action_return_value = fut.result()
|
||||
execution_success = True
|
||||
except Exception as e:
|
||||
execution_error = traceback.format_exc()
|
||||
error(f"同步任务 {ACTION.__name__} 报错了")
|
||||
error(traceback.format_exc())
|
||||
|
||||
future.add_done_callback(_handle_future_exception)
|
||||
|
||||
action_type = action_value_mapping["type"]
|
||||
feedback_msg_types = action_type.Feedback.get_fields_and_field_types()
|
||||
result_msg_types = action_type.Result.get_fields_and_field_types()
|
||||
|
||||
while not future.done():
|
||||
while future is not None and not future.done():
|
||||
if goal_handle.is_cancel_requested:
|
||||
self.lab_logger().info(f"取消动作: {action_name}")
|
||||
future.cancel() # 尝试取消线程池中的任务
|
||||
@@ -767,7 +780,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
goal_handle.publish_feedback(feedback_msg)
|
||||
time.sleep(0.5)
|
||||
|
||||
if future.cancelled():
|
||||
if future is not None and future.cancelled():
|
||||
self.lab_logger().info(f"动作 {action_name} 已取消")
|
||||
return action_type.Result()
|
||||
|
||||
@@ -937,17 +950,14 @@ class ROS2DeviceNode:
|
||||
if use_pylabrobot_creator:
|
||||
# 先对pylabrobot的子资源进行加载,不然subclass无法认出
|
||||
# 在下方对于加载Deck等Resource要手动import
|
||||
# noinspection PyUnresolvedReferences
|
||||
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300Deck
|
||||
# noinspection PyUnresolvedReferences
|
||||
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300Container
|
||||
register()
|
||||
self._driver_creator = PyLabRobotCreator(
|
||||
driver_class, children=children, resource_tracker=self.resource_tracker
|
||||
)
|
||||
else:
|
||||
from unilabos.ros.nodes.presets.protocol_node import ROS2ProtocolNode
|
||||
|
||||
if self._driver_class is ROS2ProtocolNode:
|
||||
if issubclass(self._driver_class, ROS2ProtocolNode): # 是ProtocolNode的子节点,就要调用ProtocolNodeCreator
|
||||
self._driver_creator = ProtocolNodeCreator(driver_class, children=children, resource_tracker=self.resource_tracker)
|
||||
else:
|
||||
self._driver_creator = DeviceClassCreator(driver_class, children=children, resource_tracker=self.resource_tracker)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import collections
|
||||
import copy
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
import uuid
|
||||
from typing import Optional, Dict, Any, List, ClassVar, Set
|
||||
from typing import Optional, Dict, Any, List, ClassVar, Set, Union
|
||||
|
||||
from action_msgs.msg import GoalStatus
|
||||
from geometry_msgs.msg import Point
|
||||
@@ -38,6 +39,7 @@ from unilabos.ros.msgs.message_converter import (
|
||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, ROS2DeviceNode, DeviceNodeResourceTracker
|
||||
from unilabos.ros.nodes.presets.controller_node import ControllerNode
|
||||
from unilabos.utils.exception import DeviceClassInvalid
|
||||
from unilabos.utils.type_check import serialize_result_info
|
||||
|
||||
|
||||
class HostNode(BaseROS2DeviceNode):
|
||||
@@ -254,7 +256,7 @@ class HostNode(BaseROS2DeviceNode):
|
||||
检测ROS2网络中的所有设备节点,并为它们创建ActionClient
|
||||
同时检测设备离线情况
|
||||
"""
|
||||
self.lab_logger().debug("[Host Node] Discovering devices in the network...")
|
||||
self.lab_logger().trace("[Host Node] Discovering devices in the network...")
|
||||
|
||||
# 获取当前所有设备
|
||||
nodes_and_names = self.get_node_names_and_namespaces()
|
||||
@@ -303,7 +305,7 @@ class HostNode(BaseROS2DeviceNode):
|
||||
|
||||
# 更新在线设备列表
|
||||
self._online_devices = current_devices
|
||||
self.lab_logger().debug(f"[Host Node] Total online devices: {len(self._online_devices)}")
|
||||
self.lab_logger().trace(f"[Host Node] Total online devices: {len(self._online_devices)}")
|
||||
|
||||
def _discovery_devices_callback(self) -> None:
|
||||
"""
|
||||
@@ -335,7 +337,7 @@ class HostNode(BaseROS2DeviceNode):
|
||||
self._action_clients[action_id] = ActionClient(
|
||||
self, action_type, action_id, callback_group=self.callback_group
|
||||
)
|
||||
self.lab_logger().debug(f"[Host Node] Created ActionClient (Discovery): {action_id}")
|
||||
self.lab_logger().trace(f"[Host Node] Created ActionClient (Discovery): {action_id}")
|
||||
action_name = action_id[len(namespace) + 1 :]
|
||||
edge_device_id = namespace[9:]
|
||||
# from unilabos.app.mq import mqtt_client
|
||||
@@ -349,9 +351,9 @@ class HostNode(BaseROS2DeviceNode):
|
||||
except Exception as e:
|
||||
self.lab_logger().error(f"[Host Node] Failed to create ActionClient for {action_id}: {str(e)}")
|
||||
|
||||
def create_resource_detailed(
|
||||
async def create_resource_detailed(
|
||||
self,
|
||||
resources: list["Resource"],
|
||||
resources: list[Union[list["Resource"], "Resource"]],
|
||||
device_ids: list[str],
|
||||
bind_parent_ids: list[str],
|
||||
bind_locations: list[Point],
|
||||
@@ -391,26 +393,28 @@ class HostNode(BaseROS2DeviceNode):
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
response = sclient.call(request)
|
||||
response = await sclient.call_async(request)
|
||||
responses.append(response)
|
||||
return responses
|
||||
|
||||
def create_resource(
|
||||
async def create_resource(
|
||||
self,
|
||||
device_id: str,
|
||||
res_id: str,
|
||||
class_name: str,
|
||||
parent: str,
|
||||
bind_locations: Point,
|
||||
liquid_input_slot: list[int],
|
||||
liquid_type: list[str],
|
||||
liquid_volume: list[int],
|
||||
slot_on_deck: str,
|
||||
liquid_input_slot: list[int] = [],
|
||||
liquid_type: list[str] = [],
|
||||
liquid_volume: list[int] = [],
|
||||
slot_on_deck: str = "",
|
||||
):
|
||||
# 暂不支持多对同名父子同时存在
|
||||
res_creation_input = {
|
||||
"name": res_id,
|
||||
"id": res_id.split("/")[-1],
|
||||
"name": res_id.split("/")[-1],
|
||||
"class": class_name,
|
||||
"parent": parent,
|
||||
"parent": parent.split("/")[-1],
|
||||
"position": {
|
||||
"x": bind_locations.x,
|
||||
"y": bind_locations.y,
|
||||
@@ -418,16 +422,22 @@ class HostNode(BaseROS2DeviceNode):
|
||||
},
|
||||
}
|
||||
if len(liquid_input_slot) and liquid_input_slot[0] == -1: # 目前container只逐个创建
|
||||
res_creation_input.update({
|
||||
"data": {
|
||||
"liquid_type": liquid_type[0] if liquid_type else None,
|
||||
"liquid_volume": liquid_volume[0] if liquid_volume else None,
|
||||
res_creation_input.update(
|
||||
{
|
||||
"data": {
|
||||
"liquids": [{
|
||||
"liquid_type": liquid_type[0] if liquid_type else None,
|
||||
"liquid_volume": liquid_volume[0] if liquid_volume else None,
|
||||
}]
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
init_new_res = initialize_resource(res_creation_input) # flatten的格式
|
||||
resources = init_new_res # initialize_resource已经返回list[dict]
|
||||
if len(init_new_res) > 1: # 一个物料,多个子节点
|
||||
init_new_res = [init_new_res]
|
||||
resources: List[Resource] | List[List[Resource]] = init_new_res # initialize_resource已经返回list[dict]
|
||||
device_ids = [device_id]
|
||||
bind_parent_id = [parent]
|
||||
bind_parent_id = [res_creation_input["parent"]]
|
||||
bind_location = [bind_locations]
|
||||
other_calling_param = [
|
||||
json.dumps(
|
||||
@@ -441,7 +451,9 @@ class HostNode(BaseROS2DeviceNode):
|
||||
)
|
||||
]
|
||||
|
||||
return self.create_resource_detailed(resources, device_ids, bind_parent_id, bind_location, other_calling_param)
|
||||
response = await self.create_resource_detailed(resources, device_ids, bind_parent_id, bind_location, other_calling_param)
|
||||
|
||||
return response
|
||||
|
||||
def initialize_device(self, device_id: str, device_config: Dict[str, Any]) -> None:
|
||||
"""
|
||||
@@ -476,7 +488,7 @@ class HostNode(BaseROS2DeviceNode):
|
||||
if action_id not in self._action_clients:
|
||||
action_type = action_value_mapping["type"]
|
||||
self._action_clients[action_id] = ActionClient(self, action_type, action_id)
|
||||
self.lab_logger().debug(
|
||||
self.lab_logger().trace(
|
||||
f"[Host Node] Created ActionClient (Local): {action_id}"
|
||||
) # 子设备再创建用的是Discover发现的
|
||||
# from unilabos.app.mq import mqtt_client
|
||||
@@ -521,7 +533,7 @@ class HostNode(BaseROS2DeviceNode):
|
||||
self.device_status_timestamps[device_id] = {}
|
||||
|
||||
# 默认初始化属性值为 None
|
||||
self.device_status[device_id][property_name] = None
|
||||
self.device_status[device_id] = collections.defaultdict()
|
||||
self.device_status_timestamps[device_id][property_name] = 0 # 初始化时间戳
|
||||
|
||||
# 动态创建订阅
|
||||
@@ -539,7 +551,7 @@ class HostNode(BaseROS2DeviceNode):
|
||||
)
|
||||
# 标记为已订阅
|
||||
self._subscribed_topics.add(topic)
|
||||
self.lab_logger().debug(f"[Host Node] Subscribed to new topic: {topic}")
|
||||
self.lab_logger().trace(f"[Host Node] Subscribed to new topic: {topic}")
|
||||
except (NameError, SyntaxError) as e:
|
||||
self.lab_logger().error(f"[Host Node] Failed to create subscription for topic {topic}: {e}")
|
||||
|
||||
@@ -557,10 +569,15 @@ class HostNode(BaseROS2DeviceNode):
|
||||
# 更新设备状态字典
|
||||
if hasattr(msg, "data"):
|
||||
bChange = False
|
||||
bCreate = False
|
||||
if isinstance(msg.data, (float, int, str)):
|
||||
if self.device_status[device_id][property_name] != msg.data:
|
||||
if property_name not in self.device_status[device_id]:
|
||||
bCreate = True
|
||||
bChange = True
|
||||
self.device_status[device_id][property_name] = msg.data
|
||||
self.device_status[device_id][property_name] = msg.data
|
||||
elif self.device_status[device_id][property_name] != msg.data:
|
||||
bChange = True
|
||||
self.device_status[device_id][property_name] = msg.data
|
||||
# 更新时间戳
|
||||
self.device_status_timestamps[device_id][property_name] = time.time()
|
||||
else:
|
||||
@@ -573,9 +590,14 @@ class HostNode(BaseROS2DeviceNode):
|
||||
for bridge in self.bridges:
|
||||
if hasattr(bridge, "publish_device_status"):
|
||||
bridge.publish_device_status(self.device_status, device_id, property_name)
|
||||
self.lab_logger().debug(
|
||||
f"[Host Node] Status updated: {device_id}.{property_name} = {msg.data}"
|
||||
)
|
||||
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}"
|
||||
)
|
||||
|
||||
def send_goal(
|
||||
self,
|
||||
@@ -667,20 +689,34 @@ class HostNode(BaseROS2DeviceNode):
|
||||
result_msg = future.result().result
|
||||
result_data = convert_from_ros_msg(result_msg)
|
||||
status = "success"
|
||||
try:
|
||||
ret = json.loads(result_data.get("return_info", "{}")) # 确保返回信息是有效的JSON
|
||||
suc = ret.get("suc", False)
|
||||
if not suc:
|
||||
return_info_str = result_data.get("return_info")
|
||||
|
||||
if return_info_str is not None:
|
||||
try:
|
||||
ret = json.loads(return_info_str)
|
||||
suc = ret.get("suc", False)
|
||||
if not suc:
|
||||
status = "failed"
|
||||
except json.JSONDecodeError:
|
||||
status = "failed"
|
||||
except json.JSONDecodeError:
|
||||
status = "failed"
|
||||
self.lab_logger().info(f"[Host Node] Result for {action_id} ({uuid_str}): success")
|
||||
else:
|
||||
# 无 return_info 字段时,回退到 success 字段(若存在)
|
||||
suc_field = result_data.get("success")
|
||||
if isinstance(suc_field, bool):
|
||||
status = "success" if suc_field else "failed"
|
||||
return_info_str = serialize_result_info("", suc_field, result_data)
|
||||
else:
|
||||
# 最保守的回退:标记失败并返回空JSON
|
||||
status = "failed"
|
||||
return_info_str = serialize_result_info("缺少return_info", False, result_data)
|
||||
|
||||
self.lab_logger().info(f"[Host Node] Result for {action_id} ({uuid_str}): {status}")
|
||||
self.lab_logger().debug(f"[Host Node] Result data: {result_data}")
|
||||
|
||||
if uuid_str:
|
||||
for bridge in self.bridges:
|
||||
if hasattr(bridge, "publish_job_status"):
|
||||
bridge.publish_job_status(result_data, uuid_str, status, result_data.get("return_info", "{}"))
|
||||
bridge.publish_job_status(result_data, uuid_str, status, return_info_str)
|
||||
|
||||
def cancel_goal(self, goal_uuid: str) -> None:
|
||||
"""取消目标"""
|
||||
@@ -809,10 +845,19 @@ class HostNode(BaseROS2DeviceNode):
|
||||
success = bool(r)
|
||||
|
||||
response.success = success
|
||||
|
||||
if success:
|
||||
from unilabos.resources.graphio import physical_setup_graph
|
||||
for resource in resources:
|
||||
if resource.get("id") not in physical_setup_graph.nodes:
|
||||
physical_setup_graph.add_node(resource["id"], **resource)
|
||||
else:
|
||||
physical_setup_graph.nodes[resource["id"]]["data"].update(resource["data"])
|
||||
|
||||
self.lab_logger().info(f"[Host Node-Resource] Add request completed, success: {success}")
|
||||
return response
|
||||
|
||||
def _resource_get_callback(self, request, response):
|
||||
def _resource_get_callback(self, request: ResourceGet.Request, response: ResourceGet.Response):
|
||||
"""
|
||||
获取资源回调
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import json
|
||||
import time
|
||||
import traceback
|
||||
from pprint import pprint, saferepr, pformat
|
||||
from typing import Union
|
||||
|
||||
import rclpy
|
||||
from rosidl_runtime_py import message_to_ordereddict
|
||||
|
||||
from unilabos.messages import * # type: ignore # protocol names
|
||||
from rclpy.action import ActionServer, ActionClient
|
||||
from rclpy.action.server import ServerGoalHandle
|
||||
@@ -88,6 +92,9 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
|
||||
if device_config.get("type", "device") != "device":
|
||||
continue
|
||||
# 设置硬件接口代理
|
||||
if device_id not in self.sub_devices:
|
||||
self.lab_logger().error(f"[Protocol Node] {device_id} 还没有正确初始化,跳过...")
|
||||
continue
|
||||
d = self.sub_devices[device_id]
|
||||
if d:
|
||||
hardware_interface = d.ros_node_instance._hardware_interface
|
||||
@@ -139,6 +146,7 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
|
||||
# 为子设备的每个动作创建动作客户端
|
||||
if d is not None and hasattr(d, "ros_node_instance"):
|
||||
node = d.ros_node_instance
|
||||
node.resource_tracker = self.resource_tracker # 站内应当共享资源跟踪器
|
||||
for action_name, action_mapping in node._action_value_mappings.items():
|
||||
if action_name.startswith("auto-") or str(action_mapping.get("type", "")).startswith("UniLabJsonCommand"):
|
||||
continue
|
||||
@@ -151,7 +159,7 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
|
||||
except Exception as ex:
|
||||
self.lab_logger().error(f"创建动作客户端失败: {action_id}, 错误: {ex}")
|
||||
continue
|
||||
self.lab_logger().debug(f"为子设备 {device_id} 创建动作客户端: {action_name}")
|
||||
self.lab_logger().trace(f"为子设备 {device_id} 创建动作客户端: {action_name}")
|
||||
return d
|
||||
|
||||
def create_ros_action_server(self, action_name, action_value_mapping):
|
||||
@@ -171,7 +179,7 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
|
||||
callback_group=ReentrantCallbackGroup(),
|
||||
)
|
||||
|
||||
self.lab_logger().debug(f"发布动作: {action_name}, 类型: {str_action_type}")
|
||||
self.lab_logger().trace(f"发布动作: {action_name}, 类型: {str_action_type}")
|
||||
|
||||
def _create_protocol_execute_callback(self, protocol_name, protocol_steps_generator):
|
||||
async def execute_protocol(goal_handle: ServerGoalHandle):
|
||||
@@ -182,6 +190,7 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
|
||||
protocol_return_value = None
|
||||
self.get_logger().info(f"Executing {protocol_name} action...")
|
||||
action_value_mapping = self._action_value_mappings[protocol_name]
|
||||
step_results = []
|
||||
try:
|
||||
print("+" * 30)
|
||||
print(protocol_steps_generator)
|
||||
@@ -209,22 +218,26 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
|
||||
[convert_from_ros_msg(rs) for rs in response.resources]
|
||||
)
|
||||
|
||||
self.lab_logger().info(f"🔍 最终传递给协议的 protocol_kwargs: {protocol_kwargs}")
|
||||
self.lab_logger().info(f"🔍 最终的 vessel: {protocol_kwargs.get('vessel', 'NOT_FOUND')}")
|
||||
|
||||
from unilabos.resources.graphio import physical_setup_graph
|
||||
|
||||
self.lab_logger().info(f"Working on physical setup: {physical_setup_graph}")
|
||||
protocol_steps = protocol_steps_generator(G=physical_setup_graph, **protocol_kwargs)
|
||||
|
||||
self.lab_logger().info(f"Goal received: {protocol_kwargs}, running steps: \n{protocol_steps}")
|
||||
logs = []
|
||||
for step in protocol_steps:
|
||||
if isinstance(step, dict) and "log_message" in step.get("action_kwargs", {}):
|
||||
logs.append(step)
|
||||
elif isinstance(step, list):
|
||||
logs.append(step)
|
||||
self.lab_logger().info(f"Goal received: {protocol_kwargs}, running steps: "
|
||||
f"{json.dumps(logs, indent=4, ensure_ascii=False)}")
|
||||
|
||||
time_start = time.time()
|
||||
time_overall = 100
|
||||
self._busy = True
|
||||
|
||||
# 逐步执行工作流
|
||||
step_results = []
|
||||
for i, action in enumerate(protocol_steps):
|
||||
# self.get_logger().info(f"Running step {i + 1}: {action}")
|
||||
if isinstance(action, dict):
|
||||
@@ -235,6 +248,9 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
|
||||
else:
|
||||
result = await self.execute_single_action(**action)
|
||||
step_results.append({"step": i + 1, "action": action["action_name"], "result": result})
|
||||
ret_info = json.loads(getattr(result, "return_info", "{}"))
|
||||
if not ret_info.get("suc", False):
|
||||
raise RuntimeError(f"Step {i + 1} failed.")
|
||||
elif isinstance(action, list):
|
||||
# 如果是并行动作,同时执行
|
||||
actions = action
|
||||
@@ -272,11 +288,10 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
|
||||
|
||||
except Exception as e:
|
||||
# 捕获并记录错误信息
|
||||
execution_error = traceback.format_exc()
|
||||
str_step_results = [{k: dict(message_to_ordereddict(v)) if k == "result" and hasattr(v, "SLOT_TYPES") else v for k, v in i.items()} for i in step_results]
|
||||
execution_error = f"{traceback.format_exc()}\n\nStep Result: {pformat(str_step_results)}"
|
||||
execution_success = False
|
||||
error(f"协议 {protocol_name} 执行失败")
|
||||
error(traceback.format_exc())
|
||||
self.lab_logger().error(f"协议执行出错: {str(e)}")
|
||||
self.lab_logger().error(f"协议 {protocol_name} 执行出错: {str(e)} \n{traceback.format_exc()}")
|
||||
|
||||
# 设置动作失败
|
||||
goal_handle.abort()
|
||||
@@ -302,7 +317,7 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
|
||||
serialize_result_info(execution_error, execution_success, protocol_return_value),
|
||||
)
|
||||
|
||||
self.lab_logger().info(f"🤩🤩🤩🤩🤩🤩协议 {protocol_name} 完成并返回结果😎😎😎😎😎😎")
|
||||
self.lab_logger().info(f"协议 {protocol_name} 完成并返回结果")
|
||||
return result
|
||||
|
||||
return execute_protocol
|
||||
|
||||
86
unilabos/ros/nodes/presets/workstation.py
Normal file
86
unilabos/ros/nodes/presets/workstation.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import collections
|
||||
from typing import Union, Dict, Any, Optional
|
||||
|
||||
from unilabos_msgs.msg import Resource
|
||||
from pylabrobot.resources import Resource as PLRResource, Plate, TipRack, Coordinate
|
||||
from unilabos.ros.nodes.presets.protocol_node import ROS2ProtocolNode
|
||||
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
|
||||
|
||||
|
||||
class WorkStationContainer(Plate, TipRack):
|
||||
"""
|
||||
WorkStation 专用 Container 类,继承自 Plate和TipRack
|
||||
注意这个物料必须通过plr_additional_res_reg.py注册到edge,才能正常序列化
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, size_x: float, size_y: float, size_z: float, category: str, ordering: collections.OrderedDict, model: Optional[str] = None,):
|
||||
"""
|
||||
这里的初始化入参要和plr的保持一致
|
||||
"""
|
||||
super().__init__(name, size_x, size_y, size_z, category=category, ordering=ordering, model=model)
|
||||
self._unilabos_state = {} # 必须有此行,自己的类描述的是物料的
|
||||
|
||||
def load_state(self, state: Dict[str, Any]) -> None:
|
||||
"""从给定的状态加载工作台信息。"""
|
||||
super().load_state(state)
|
||||
self._unilabos_state = state
|
||||
|
||||
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
||||
data = super().serialize_state()
|
||||
data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等)
|
||||
return data
|
||||
|
||||
|
||||
def get_workstation_plate_resource(name: str) -> PLRResource: # 要给定一个返回plr的方法
|
||||
"""
|
||||
用于获取一些模板,例如返回一个带有特定信息/子物料的 Plate,这里需要到注册表注册,例如unilabos/registry/resources/organic/workstation.yaml
|
||||
可以直接运行该函数或者利用注册表补全机制,来检查是否资源出错
|
||||
:param name: 资源名称
|
||||
:return: Resource对象
|
||||
"""
|
||||
plate = WorkStationContainer(name, size_x=50, size_y=50, size_z=10, category="plate", ordering=collections.OrderedDict())
|
||||
tip_rack = WorkStationContainer("tip_rack_inside_plate", size_x=50, size_y=50, size_z=10, category="tip_rack", ordering=collections.OrderedDict())
|
||||
plate.assign_child_resource(tip_rack, Coordinate.zero())
|
||||
return plate
|
||||
|
||||
|
||||
class WorkStationExample(ROS2ProtocolNode):
|
||||
def __init__(self,
|
||||
# 你可以在这里增加任意的参数,对应启动json填写相应的参数内容
|
||||
device_id: str,
|
||||
children: dict,
|
||||
protocol_type: Union[str, list[str]],
|
||||
resource_tracker: DeviceNodeResourceTracker
|
||||
):
|
||||
super().__init__(device_id, children, protocol_type, resource_tracker)
|
||||
|
||||
def create_resource(
|
||||
self,
|
||||
resource_tracker: DeviceNodeResourceTracker,
|
||||
resources: list[Resource],
|
||||
bind_parent_id: str,
|
||||
bind_location: dict[str, float],
|
||||
liquid_input_slot: list[int],
|
||||
liquid_type: list[str],
|
||||
liquid_volume: list[int],
|
||||
slot_on_deck: int,
|
||||
) -> Dict[str, Any]:
|
||||
return { # edge侧返回给前端的创建物料的结果。云端调用初始化瓶子等。执行该函数时,物料已经上报给云端,一般不需要继承使用
|
||||
|
||||
}
|
||||
|
||||
def transfer_bottle(self, tip_rack: PLRResource, base_plate: PLRResource): # 使用自定义物料的举例
|
||||
"""
|
||||
将tip_rack assign给base_plate,两个入参都得是PLRResource,unilabos会代替当前物料操作,自动刷新他们的父子关系等状态
|
||||
"""
|
||||
pass
|
||||
|
||||
def trigger_resource_update(self, from_plate: PLRResource, to_base_plate: PLRResource):
|
||||
"""
|
||||
有些时候物料发生了子设备的迁移,一般对该设备的最大一级的物料进行操作,例如要将A物料搬移到B物料上,他们不共同拥有一个物料
|
||||
该步骤操作结束后,会主动刷新from_plate的父物料,与to_base_plate的父物料(如没有则刷新自身)
|
||||
|
||||
"""
|
||||
to_base_plate.assign_child_resource(from_plate, Coordinate.zero())
|
||||
pass
|
||||
|
||||
@@ -36,7 +36,9 @@ class DeviceNodeResourceTracker(object):
|
||||
|
||||
def figure_resource(self, query_resource, try_mode=False):
|
||||
if isinstance(query_resource, list):
|
||||
return [self.figure_resource(r) for r in query_resource]
|
||||
return [self.figure_resource(r, try_mode) for r in query_resource]
|
||||
elif isinstance(query_resource, dict) and "id" not in query_resource and "name" not in query_resource: # 临时处理,要删除的,driver有太多类型错误标注
|
||||
return [self.figure_resource(r, try_mode) for r in query_resource.values()]
|
||||
res_id = query_resource.id if hasattr(query_resource, "id") else (query_resource.get("id") if isinstance(query_resource, dict) else None)
|
||||
res_name = query_resource.name if hasattr(query_resource, "name") else (query_resource.get("name") if isinstance(query_resource, dict) else None)
|
||||
res_identifier = res_id if res_id else res_name
|
||||
|
||||
@@ -51,7 +51,7 @@ class DeviceClassCreator(Generic[T]):
|
||||
"""
|
||||
if self.device_instance is not None:
|
||||
for c in self.children.values():
|
||||
if c["type"] == "container":
|
||||
if c["type"] != "device":
|
||||
self.resource_tracker.add_resource(c)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user