更新transfer_resource_to_another参数,支持spot入参

This commit is contained in:
Xuwznln
2025-10-11 02:41:37 +08:00
parent df33e1a214
commit 0c42d60cf2
9 changed files with 598 additions and 519 deletions

View File

@@ -45,7 +45,7 @@ def convert_argv_dashes_to_underscores(args: argparse.ArgumentParser):
for i, arg in enumerate(sys.argv): for i, arg in enumerate(sys.argv):
for option_string in option_strings: for option_string in option_strings:
if arg.startswith(option_string): if arg.startswith(option_string):
new_arg = arg[:2] + arg[2 : len(option_string)].replace("-", "_") + arg[len(option_string) :] new_arg = arg[:2] + arg[2:len(option_string)].replace("-", "_") + arg[len(option_string):]
sys.argv[i] = new_arg sys.argv[i] = new_arg
break break
@@ -335,12 +335,11 @@ def main():
resource_edge_info.pop(edge_info - ind - 1) resource_edge_info.pop(edge_info - ind - 1)
continue continue
# 如果从远端获取了物料信息,则与本地物料进行同步 tree_set = ResourceTreeSet.from_raw_list(request_startup_json["nodes"])
if request_startup_json and "nodes" in request_startup_json: for root_node in tree_set.root_nodes: 希望和本地的resources_config进行同步根节点device
print_status("开始同步远端物料到本地...", "info") id能对上的且是resource的则自动添加进来根节点上不是device的也包含进来
remote_tree_set = ResourceTreeSet.from_raw_list(request_startup_json["nodes"]) device_id = root_node.
resource_tree_set.merge_remote_resources(remote_tree_set) # tree_set.all_nodes
print_status("远端物料同步完成", "info")
# 使用 ResourceTreeSet 代替 list # 使用 ResourceTreeSet 代替 list
args_dict["resources_config"] = resource_tree_set args_dict["resources_config"] = resource_tree_set

View File

@@ -155,11 +155,12 @@ class BioyondWorkstation(WorkstationBase):
"resources": [self.deck] "resources": [self.deck]
}) })
def transfer_resource_to_another(self, resource: ResourceSlot, mount_device_id: DeviceSlot, mount_resource: ResourceSlot): def transfer_resource_to_another(self, resource: List[ResourceSlot], mount_resource: List[ResourceSlot], sites: List[str], mount_device_id: DeviceSlot):
ROS2DeviceNode.run_async_func(self._ros_node.transfer_resource_to_another, True, **{ ROS2DeviceNode.run_async_func(self._ros_node.transfer_resource_to_another, True, **{
"plr_resources": [resource], "plr_resources": resource,
"target_device_id": mount_device_id, "target_device_id": mount_device_id,
"target_resource_uuid": getattr(mount_resource, "unilabos_uuid", None), "target_resources": mount_resource,
"sites": sites,
}) })
def _configure_station_type(self, station_config: Optional[Dict[str, Any]] = None) -> None: def _configure_station_type(self, station_config: Optional[Dict[str, Any]] = None) -> None:

View File

@@ -889,6 +889,7 @@ dispensing_station.bioyond:
mount_device_id: null mount_device_id: null
mount_resource: null mount_resource: null
resource: null resource: null
sites: null
handles: {} handles: {}
placeholder_keys: placeholder_keys:
mount_device_id: unilabos_devices mount_device_id: unilabos_devices
@@ -904,155 +905,164 @@ dispensing_station.bioyond:
mount_device_id: mount_device_id:
type: object type: object
mount_resource: mount_resource:
properties: items:
category: properties:
type: string category:
children:
items:
type: string type: string
type: array children:
config: items:
type: string type: string
data: type: array
type: string config:
id: type: string
type: string data:
name: type: string
type: string id:
parent: type: string
type: string name:
pose: type: string
properties: parent:
orientation: type: string
properties: pose:
w: properties:
type: number orientation:
x: properties:
type: number w:
y: type: number
type: number x:
z: type: number
type: number y:
required: type: number
- x z:
- y type: number
- z required:
- w - x
title: orientation - y
type: object - z
position: - w
properties: title: orientation
x: type: object
type: number position:
y: properties:
type: number x:
z: type: number
type: number y:
required: type: number
- x z:
- y type: number
- z required:
title: position - x
type: object - y
required: - z
- position title: position
- orientation type: object
title: pose required:
type: object - position
sample_id: - orientation
type: string title: pose
type: type: object
type: string sample_id:
required: type: string
- id type:
- name type: string
- sample_id required:
- children - id
- parent - name
- type - sample_id
- category - children
- pose - parent
- config - type
- data - category
title: mount_resource - pose
type: object - config
- data
title: mount_resource
type: object
type: array
resource: resource:
properties: items:
category: properties:
type: string category:
children:
items:
type: string type: string
type: array children:
config: items:
type: string type: string
data: type: array
type: string config:
id: type: string
type: string data:
name: type: string
type: string id:
parent: type: string
type: string name:
pose: type: string
properties: parent:
orientation: type: string
properties: pose:
w: properties:
type: number orientation:
x: properties:
type: number w:
y: type: number
type: number x:
z: type: number
type: number y:
required: type: number
- x z:
- y type: number
- z required:
- w - x
title: orientation - y
type: object - z
position: - w
properties: title: orientation
x: type: object
type: number position:
y: properties:
type: number x:
z: type: number
type: number y:
required: type: number
- x z:
- y type: number
- z required:
title: position - x
type: object - y
required: - z
- position title: position
- orientation type: object
title: pose required:
type: object - position
sample_id: - orientation
type: string title: pose
type: type: object
type: string sample_id:
required: type: string
- id type:
- name type: string
- sample_id required:
- children - id
- parent - name
- type - sample_id
- category - children
- pose - parent
- config - type
- data - category
title: resource - pose
type: object - config
- data
title: resource
type: object
type: array
sites:
items:
type: string
type: array
required: required:
- resource - resource
- mount_device_id
- mount_resource - mount_resource
- sites
- mount_device_id
type: object type: object
result: {} result: {}
required: required:

View File

@@ -883,6 +883,7 @@ reaction_station.bioyond:
mount_device_id: null mount_device_id: null
mount_resource: null mount_resource: null
resource: null resource: null
sites: null
handles: {} handles: {}
placeholder_keys: placeholder_keys:
mount_device_id: unilabos_devices mount_device_id: unilabos_devices
@@ -898,155 +899,164 @@ reaction_station.bioyond:
mount_device_id: mount_device_id:
type: object type: object
mount_resource: mount_resource:
properties: items:
category: properties:
type: string category:
children:
items:
type: string type: string
type: array children:
config: items:
type: string type: string
data: type: array
type: string config:
id: type: string
type: string data:
name: type: string
type: string id:
parent: type: string
type: string name:
pose: type: string
properties: parent:
orientation: type: string
properties: pose:
w: properties:
type: number orientation:
x: properties:
type: number w:
y: type: number
type: number x:
z: type: number
type: number y:
required: type: number
- x z:
- y type: number
- z required:
- w - x
title: orientation - y
type: object - z
position: - w
properties: title: orientation
x: type: object
type: number position:
y: properties:
type: number x:
z: type: number
type: number y:
required: type: number
- x z:
- y type: number
- z required:
title: position - x
type: object - y
required: - z
- position title: position
- orientation type: object
title: pose required:
type: object - position
sample_id: - orientation
type: string title: pose
type: type: object
type: string sample_id:
required: type: string
- id type:
- name type: string
- sample_id required:
- children - id
- parent - name
- type - sample_id
- category - children
- pose - parent
- config - type
- data - category
title: mount_resource - pose
type: object - config
- data
title: mount_resource
type: object
type: array
resource: resource:
properties: items:
category: properties:
type: string category:
children:
items:
type: string type: string
type: array children:
config: items:
type: string type: string
data: type: array
type: string config:
id: type: string
type: string data:
name: type: string
type: string id:
parent: type: string
type: string name:
pose: type: string
properties: parent:
orientation: type: string
properties: pose:
w: properties:
type: number orientation:
x: properties:
type: number w:
y: type: number
type: number x:
z: type: number
type: number y:
required: type: number
- x z:
- y type: number
- z required:
- w - x
title: orientation - y
type: object - z
position: - w
properties: title: orientation
x: type: object
type: number position:
y: properties:
type: number x:
z: type: number
type: number y:
required: type: number
- x z:
- y type: number
- z required:
title: position - x
type: object - y
required: - z
- position title: position
- orientation type: object
title: pose required:
type: object - position
sample_id: - orientation
type: string title: pose
type: type: object
type: string sample_id:
required: type: string
- id type:
- name type: string
- sample_id required:
- children - id
- parent - name
- type - sample_id
- category - children
- pose - parent
- config - type
- data - category
title: resource - pose
type: object - config
- data
title: resource
type: object
type: array
sites:
items:
type: string
type: array
required: required:
- resource - resource
- mount_device_id
- mount_resource - mount_resource
- sites
- mount_device_id
type: object type: object
result: {} result: {}
required: required:

View File

@@ -7134,6 +7134,7 @@ workstation.bioyond:
mount_device_id: null mount_device_id: null
mount_resource: null mount_resource: null
resource: null resource: null
sites: null
handles: {} handles: {}
placeholder_keys: placeholder_keys:
mount_device_id: unilabos_devices mount_device_id: unilabos_devices
@@ -7149,155 +7150,164 @@ workstation.bioyond:
mount_device_id: mount_device_id:
type: object type: object
mount_resource: mount_resource:
properties: items:
category: properties:
type: string category:
children:
items:
type: string type: string
type: array children:
config: items:
type: string type: string
data: type: array
type: string config:
id: type: string
type: string data:
name: type: string
type: string id:
parent: type: string
type: string name:
pose: type: string
properties: parent:
orientation: type: string
properties: pose:
w: properties:
type: number orientation:
x: properties:
type: number w:
y: type: number
type: number x:
z: type: number
type: number y:
required: type: number
- x z:
- y type: number
- z required:
- w - x
title: orientation - y
type: object - z
position: - w
properties: title: orientation
x: type: object
type: number position:
y: properties:
type: number x:
z: type: number
type: number y:
required: type: number
- x z:
- y type: number
- z required:
title: position - x
type: object - y
required: - z
- position title: position
- orientation type: object
title: pose required:
type: object - position
sample_id: - orientation
type: string title: pose
type: type: object
type: string sample_id:
required: type: string
- id type:
- name type: string
- sample_id required:
- children - id
- parent - name
- type - sample_id
- category - children
- pose - parent
- config - type
- data - category
title: mount_resource - pose
type: object - config
- data
title: mount_resource
type: object
type: array
resource: resource:
properties: items:
category: properties:
type: string category:
children:
items:
type: string type: string
type: array children:
config: items:
type: string type: string
data: type: array
type: string config:
id: type: string
type: string data:
name: type: string
type: string id:
parent: type: string
type: string name:
pose: type: string
properties: parent:
orientation: type: string
properties: pose:
w: properties:
type: number orientation:
x: properties:
type: number w:
y: type: number
type: number x:
z: type: number
type: number y:
required: type: number
- x z:
- y type: number
- z required:
- w - x
title: orientation - y
type: object - z
position: - w
properties: title: orientation
x: type: object
type: number position:
y: properties:
type: number x:
z: type: number
type: number y:
required: type: number
- x z:
- y type: number
- z required:
title: position - x
type: object - y
required: - z
- position title: position
- orientation type: object
title: pose required:
type: object - position
sample_id: - orientation
type: string title: pose
type: type: object
type: string sample_id:
required: type: string
- id type:
- name type: string
- sample_id required:
- children - id
- parent - name
- type - sample_id
- category - children
- pose - parent
- config - type
- data - category
title: resource - pose
type: object - config
- data
title: resource
type: object
type: array
sites:
items:
type: string
type: array
required: required:
- resource - resource
- mount_device_id
- mount_resource - mount_resource
- sites
- mount_device_id
type: object type: object
result: {} result: {}
required: required:

View File

@@ -9,6 +9,8 @@ from typing import Any, Dict, List, Union, Tuple
import msgcenterpy import msgcenterpy
import yaml import yaml
from rosidl_parser.definition import UnboundedSequence
from unilabos_msgs.action import LiquidHandlerTransfer
from unilabos_msgs.msg import Resource from unilabos_msgs.msg import Resource
from unilabos.config.config import BasicConfig from unilabos.config.config import BasicConfig
@@ -431,6 +433,11 @@ class Registry:
param_required = arg_info.get("required", True) param_required = arg_info.get("required", True)
if param_type == "unilabos.registry.placeholder_type:ResourceSlot": if param_type == "unilabos.registry.placeholder_type:ResourceSlot":
schema["properties"][param_name] = ros_message_to_json_schema(Resource, param_name) schema["properties"][param_name] = ros_message_to_json_schema(Resource, param_name)
elif param_type == ("list", "unilabos.registry.placeholder_type:ResourceSlot"):
schema["properties"][param_name] = {
"items": ros_message_to_json_schema(Resource, param_name),
"type": "array"
}
else: else:
schema["properties"][param_name] = self._generate_schema_from_info(param_name, param_type, param_default) schema["properties"][param_name] = self._generate_schema_from_info(param_name, param_type, param_default)
if param_required: if param_required:
@@ -543,9 +550,9 @@ class Registry:
"goal_default": {i["name"]: i["default"] for i in v["args"]}, "goal_default": {i["name"]: i["default"] for i in v["args"]},
"handles": [], "handles": [],
"placeholder_keys": { "placeholder_keys": {
i["name"]: "unilabos_resources" if i["type"] == "unilabos.registry.placeholder_type:ResourceSlot" else "unilabos_devices" i["name"]: "unilabos_resources" if i["type"] == "unilabos.registry.placeholder_type:ResourceSlot" or i["type"] == ("list", "unilabos.registry.placeholder_type:ResourceSlot") else "unilabos_devices"
for i in v["args"] for i in v["args"]
if i.get("type", "") in ["unilabos.registry.placeholder_type:ResourceSlot", "unilabos.registry.placeholder_type:DeviceSlot"] if i.get("type", "") in ["unilabos.registry.placeholder_type:ResourceSlot", "unilabos.registry.placeholder_type:DeviceSlot", ("list", "unilabos.registry.placeholder_type:ResourceSlot"), ("list", "unilabos.registry.placeholder_type:DeviceSlot")]
} }
} }
# 不生成已配置action的动作 # 不生成已配置action的动作

View File

@@ -9,7 +9,10 @@ from unilabos_msgs.msg import Resource
from unilabos.resources.container import RegularContainer from unilabos.resources.container import RegularContainer
from unilabos.ros.msgs.message_converter import convert_to_ros_msg from unilabos.ros.msgs.message_converter import convert_to_ros_msg
from unilabos.ros.nodes.resource_tracker import ResourceTreeSet from unilabos.ros.nodes.resource_tracker import (
ResourceDictInstance,
ResourceTreeSet,
)
from unilabos.utils.banner_print import print_status from unilabos.utils.banner_print import print_status
try: try:
@@ -51,20 +54,51 @@ def canonicalize_nodes_data(
if child in id2idx: if child in id2idx:
nodes[id2idx[child]]["parent"] = parent nodes[id2idx[child]]["parent"] = parent
# 第三步:打印节点信息(用于调试) # 第三步:使用 ResourceInstanceDictFlatten 标准化每个节点
standardized_instances = []
known_nodes: Dict[str, ResourceDictInstance] = {} # {node_id: ResourceDictInstance}
uuid_to_instance: Dict[str, ResourceDictInstance] = {} # {uuid: ResourceDictInstance}
for node in nodes: for node in nodes:
try: try:
print_status(f"DeviceId: {node['id']}, Class: {node['class']}", "info") print_status(f"DeviceId: {node['id']}, Class: {node['class']}", "info")
# 使用标准化方法
resource_instance = ResourceDictInstance.get_resource_instance_from_dict(node)
known_nodes[node["id"]] = resource_instance
uuid_to_instance[resource_instance.res_content.uuid] = resource_instance
standardized_instances.append(resource_instance)
except Exception as e: except Exception as e:
print_status(f"Failed to read node {node.get('id', 'unknown')}: {e}", "error") print_status(f"Failed to standardize node {node.get('id', 'unknown')}:\n{traceback.format_exc()}", "error")
continue
# 第四步:使用 from_raw_list 创建 ResourceTreeSet自动处理标准化、parent-children关系 # 第四步:建立 parentchildren 关系
try: for node in nodes:
resource_tree_set = ResourceTreeSet.from_raw_list(nodes) node_id = node["id"]
except Exception as e: if node_id not in known_nodes:
print_status(f"Failed to create ResourceTreeSet:\n{traceback.format_exc()}", "error") continue
raise
current_instance = known_nodes[node_id]
# 优先使用 parent_uuid 进行匹配,如果不存在则使用 parent
parent_uuid = node.get("parent_uuid")
parent_id = node.get("parent")
parent_instance = None
# 优先用 parent_uuid 匹配
if parent_uuid and parent_uuid in uuid_to_instance:
parent_instance = uuid_to_instance[parent_uuid]
# 否则用 parent_id 匹配
elif parent_id and parent_id in known_nodes:
parent_instance = known_nodes[parent_id]
# 设置 parent 引用
if parent_instance:
current_instance.res_content.parent = parent_instance.res_content
# 将当前节点添加到父节点的 children 列表
parent_instance.children.append(current_instance)
# 第五步:创建 ResourceTreeSet
resource_tree_set = ResourceTreeSet.from_nested_list(standardized_instances)
return resource_tree_set return resource_tree_set

View File

@@ -1,4 +1,5 @@
import copy import copy
import inspect
import io import io
import json import json
import threading import threading
@@ -332,7 +333,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
# 创建资源管理客户端 # 创建资源管理客户端
self._resource_clients: Dict[str, Client] = { self._resource_clients: Dict[str, Client] = {
"resource_add": self.create_client(ResourceAdd, "/resources/add"), "resource_add": self.create_client(ResourceAdd, "/resources/add"),
"resource_get": self.create_client(ResourceGet, "/resources/get"), "resource_get": self.create_client(SerialCommand, "/resources/get"),
"resource_delete": self.create_client(ResourceDelete, "/resources/delete"), "resource_delete": self.create_client(ResourceDelete, "/resources/delete"),
"resource_update": self.create_client(ResourceUpdate, "/resources/update"), "resource_update": self.create_client(ResourceUpdate, "/resources/update"),
"resource_list": self.create_client(ResourceList, "/resources/list"), "resource_list": self.create_client(ResourceList, "/resources/list"),
@@ -578,6 +579,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
for i in data: for i in data:
action = i.get("action") # remove, add, update action = i.get("action") # remove, add, update
resources_uuid: List[str] = i.get("data") # 资源数据 resources_uuid: List[str] = i.get("data") # 资源数据
additional_add_params = i.get("additional_add_params", {}) # 额外参数
self.lab_logger().info( self.lab_logger().info(
f"[Resource Tree Update] Processing {action} operation, " f"[Resource Tree Update] Processing {action} operation, "
f"resources count: {len(resources_uuid)}" f"resources count: {len(resources_uuid)}"
@@ -609,7 +611,13 @@ class BaseROS2DeviceNode(Node, Generic[T]):
f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_uuid}不存在") f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_uuid}不存在")
else: else:
try: try:
parent_resource.assign_child_resource(plr_resource, location=None) # 特殊兼容所有plr的物料的assign方法和create_resource append_resource后期同步
additional_params = {}
site = additional_add_params.get("site", None)
spec = inspect.signature(parent_resource.assign_child_resource)
if "spot" in spec.parameters:
additional_params["spot"] = site
parent_resource.assign_child_resource(plr_resource, location=None, **additional_params)
except Exception as e: except Exception as e:
self.lab_logger().warning( self.lab_logger().warning(
f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_resource}[{parent_uuid}]失败!\n{traceback.format_exc()}") f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_resource}[{parent_uuid}]失败!\n{traceback.format_exc()}")
@@ -666,14 +674,20 @@ class BaseROS2DeviceNode(Node, Generic[T]):
return res return res
async def transfer_resource_to_another(self, plr_resources: List["ResourcePLR"], target_device_id, target_resource_uuid: str): async def transfer_resource_to_another(self, plr_resources: List["ResourcePLR"], target_device_id: str, target_resources: List["ResourcePLR"], sites: List[str]):
# 准备工作 # 准备工作
uids = [] uids = []
target_uids = []
for plr_resource in plr_resources: for plr_resource in plr_resources:
uid = getattr(plr_resource, "unilabos_uuid", None) uid = getattr(plr_resource, "unilabos_uuid", None)
if uid is None: if uid is None:
raise ValueError(f"物料{plr_resource}没有unilabos_uuid属性无法转运") raise ValueError(f"来源物料{plr_resource}没有unilabos_uuid属性无法转运")
uids.append(uid) uids.append(uid)
for target_resource in target_resources:
uid = getattr(target_resource, "unilabos_uuid", None)
if uid is None:
raise ValueError(f"目标物料{target_resource}没有unilabos_uuid属性无法转运")
target_uids.append(uid)
srv_address = f"/srv{target_device_id}/s2c_resource_tree" srv_address = f"/srv{target_device_id}/s2c_resource_tree"
sclient = self.create_client(SerialCommand, srv_address) sclient = self.create_client(SerialCommand, srv_address)
# 等待服务可用(设置超时) # 等待服务可用(设置超时)
@@ -688,31 +702,33 @@ class BaseROS2DeviceNode(Node, Generic[T]):
}], ensure_ascii=False)), SerialCommand_Response()) }], ensure_ascii=False)), SerialCommand_Response())
# 通知云端转运资源 # 通知云端转运资源
tree_set = ResourceTreeSet.from_plr_resources(plr_resources) for plr_resource, target_uid, site in zip(plr_resources, target_uids, sites):
for root_node in tree_set.root_nodes: tree_set = ResourceTreeSet.from_plr_resources([plr_resource])
root_node.res_content.parent = None for root_node in tree_set.root_nodes:
root_node.res_content.parent_uuid = target_resource_uuid root_node.res_content.parent = None
r = SerialCommand.Request() root_node.res_content.parent_uuid = target_uid
r.command = json.dumps({"data": {"data": tree_set.dump()}, "action": "update"}) # 和Update Resource一致 r = SerialCommand.Request()
response: SerialCommand_Response = await self._resource_clients["c2s_update_resource_tree"].call_async(r) # type: ignore r.command = json.dumps({"data": {"data": tree_set.dump()}, "action": "update"}) # 和Update Resource一致
self.lab_logger().info(f"资源云端转运到{target_device_id}结果: {response.response}") response: SerialCommand_Response = await self._resource_clients["c2s_update_resource_tree"].call_async(r) # type: ignore
self.lab_logger().info(f"资源云端转运到{target_device_id}结果: {response.response}")
# 创建请求 # 创建请求
request = SerialCommand.Request() request = SerialCommand.Request()
request.command = json.dumps([{ request.command = json.dumps([{
"action": "add", "action": "add",
"data": tree_set.all_nodes_uuid # 只添加父节点,子节点会自动添加 "data": tree_set.all_nodes_uuid, # 只添加父节点,子节点会自动添加
}], ensure_ascii=False) "additional_add_params": {"site": site}
}], ensure_ascii=False)
future = sclient.call_async(request) future = sclient.call_async(request)
timeout = 30.0 timeout = 30.0
start_time = time.time() start_time = time.time()
while not future.done(): while not future.done():
if time.time() - start_time > timeout: if time.time() - start_time > timeout:
self.lab_logger().error(f"[{self.device_id} Node-Resource] Timeout waiting for response from {target_device_id}") self.lab_logger().error(f"[{self.device_id} Node-Resource] Timeout waiting for response from {target_device_id}")
return False return False
time.sleep(0.05) time.sleep(0.05)
self.lab_logger().info(f"资源本地增加到{target_device_id}结果: {response.response}") self.lab_logger().info(f"资源本地增加到{target_device_id}结果: {response.response}")
return None return None
def register_device(self): def register_device(self):
@@ -872,7 +888,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
for k, v in goal.get_fields_and_field_types().items(): for k, v in goal.get_fields_and_field_types().items():
if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]: if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
self.lab_logger().info(f"{action_name} 查询资源状态: Key: {k} Type: {v}") self.lab_logger().info(f"{action_name} 查询资源状态: Key: {k} Type: {v}")
current_resources: Union[List[Resource], List[List[Resource]]] = [] current_resources: List[List[Dict[str, Any]]] = []
# TODO: resource后面需要分组 # TODO: resource后面需要分组
only_one_resource = False only_one_resource = False
try: try:
@@ -881,8 +897,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
r = ResourceGet.Request() r = ResourceGet.Request()
r.id = i["id"] # splash optional r.id = i["id"] # splash optional
r.with_children = True r.with_children = True
response = await self._resource_clients["resource_get"].call_async(r) response: SerialCommand_Response = await self._resource_clients["resource_get"].call_async(r)
current_resources.append(response.resources) current_resources.append(json.loads(response.response))
else: else:
only_one_resource = True only_one_resource = True
r = ResourceGet.Request() r = ResourceGet.Request()
@@ -893,23 +909,21 @@ class BaseROS2DeviceNode(Node, Generic[T]):
) )
r.with_children = True r.with_children = True
response = await self._resource_clients["resource_get"].call_async(r) response = await self._resource_clients["resource_get"].call_async(r)
current_resources.extend(response.resources) current_resources.append(json.loads(response.response))
except Exception: except Exception:
logger.error(f"资源查询失败,默认使用本地资源") logger.error(f"资源查询失败,默认使用本地资源")
# 删除对response.resources的检查因为它总是存在 # 删除对response.resources的检查因为它总是存在
type_hint = action_paramtypes[k] type_hint = action_paramtypes[k]
final_type = get_type_class(type_hint) final_type = get_type_class(type_hint)
if only_one_resource: if only_one_resource:
resources_list: List[Dict[str, Any]] = [convert_from_ros_msg(rs) for rs in current_resources] # type: ignore tree_set = ResourceTreeSet.from_raw_list(current_resources[0])
self.lab_logger().debug(f"资源查询结果: {len(resources_list)} 个资源") self.lab_logger().debug(f"资源查询结果: {len(tree_set.all_nodes)} 个资源")
final_resource = convert_resources_to_type(resources_list, final_type) final_resource: List[ResourcePLR] | ResourcePLR = tree_set.to_plr_resources()[0]
# 判断 ACTION 是否需要特殊的物料类型如 pylabrobot.resources.Resource并做转换 # 判断 ACTION 是否需要特殊的物料类型如 pylabrobot.resources.Resource并做转换
else: else:
resources_list: List[List[Dict[str, Any]]] = [[convert_from_ros_msg(rs) for rs in sub_res_list] for sub_res_list in current_resources] # type: ignore final_resource: List[ResourcePLR] | ResourcePLR = []
final_resource = [ for entry in current_resources:
convert_resources_to_type(sub_res_list, final_type)[0] final_resource.append(ResourceTreeSet.from_raw_list(entry)[0]) # type: ignore
for sub_res_list in resources_list
]
try: try:
action_kwargs[k] = self.resource_tracker.figure_resource(final_resource, try_mode=False) action_kwargs[k] = self.resource_tracker.figure_resource(final_resource, try_mode=False)
except Exception as e: except Exception as e:

View File

@@ -29,9 +29,11 @@ from unilabos.utils.type_check import serialize_result_info, get_result_info_str
if TYPE_CHECKING: if TYPE_CHECKING:
from unilabos.devices.workstation.workstation_base import WorkstationBase from unilabos.devices.workstation.workstation_base import WorkstationBase
class ROS2WorkstationNodeTempError(Exception): class ROS2WorkstationNodeTempError(Exception):
pass pass
class ROS2WorkstationNode(BaseROS2DeviceNode): class ROS2WorkstationNode(BaseROS2DeviceNode):
""" """
ROS2WorkstationNode代表管理ROS2环境中设备通信和动作的协议节点。 ROS2WorkstationNode代表管理ROS2环境中设备通信和动作的协议节点。
@@ -63,10 +65,7 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
driver_instance=driver_instance, driver_instance=driver_instance,
device_id=device_id, device_id=device_id,
status_types=status_types, status_types=status_types,
action_value_mappings={ action_value_mappings={**action_value_mappings, **self.protocol_action_mappings},
**action_value_mappings,
**self.protocol_action_mappings
},
hardware_interface=hardware_interface, hardware_interface=hardware_interface,
print_publish=print_publish, print_publish=print_publish,
resource_tracker=resource_tracker, resource_tracker=resource_tracker,
@@ -89,7 +88,8 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
d = self.initialize_device(device_id, device_config) d = self.initialize_device(device_id, device_config)
except Exception as ex: except Exception as ex:
self.lab_logger().error( self.lab_logger().error(
f"[Protocol Node] Failed to initialize device {device_id}: {ex}\n{traceback.format_exc()}") f"[Protocol Node] Failed to initialize device {device_id}: {ex}\n{traceback.format_exc()}"
)
d = None d = None
if d is None: if d is None:
continue continue
@@ -109,10 +109,9 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
if d: if d:
hardware_interface = d.ros_node_instance._hardware_interface hardware_interface = d.ros_node_instance._hardware_interface
if ( if (
hasattr(d.driver_instance, hardware_interface["name"]) hasattr(d.driver_instance, hardware_interface["name"])
and hasattr(d.driver_instance, hardware_interface["write"]) and hasattr(d.driver_instance, hardware_interface["write"])
and ( and (hardware_interface["read"] is None or hasattr(d.driver_instance, hardware_interface["read"]))
hardware_interface["read"] is None or hasattr(d.driver_instance, hardware_interface["read"]))
): ):
name = getattr(d.driver_instance, hardware_interface["name"]) name = getattr(d.driver_instance, hardware_interface["name"])
@@ -160,7 +159,8 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
node.resource_tracker = self.resource_tracker # 站内应当共享资源跟踪器 node.resource_tracker = self.resource_tracker # 站内应当共享资源跟踪器
for action_name, action_mapping in node._action_value_mappings.items(): for action_name, action_mapping in node._action_value_mappings.items():
if action_name.startswith("auto-") or str(action_mapping.get("type", "")).startswith( if action_name.startswith("auto-") or str(action_mapping.get("type", "")).startswith(
"UniLabJsonCommand"): "UniLabJsonCommand"
):
continue continue
action_id = f"/devices/{device_id_abs}/{action_name}" action_id = f"/devices/{device_id_abs}/{action_name}"
if action_id not in self._action_clients: if action_id not in self._action_clients:
@@ -245,8 +245,10 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
logs.append(step) logs.append(step)
elif isinstance(step, list): elif isinstance(step, list):
logs.append(step) logs.append(step)
self.lab_logger().info(f"Goal received: {protocol_kwargs}, running steps: " self.lab_logger().info(
f"{json.dumps(logs, indent=4, ensure_ascii=False)}") f"Goal received: {protocol_kwargs}, running steps: "
f"{json.dumps(logs, indent=4, ensure_ascii=False)}"
)
time_start = time.time() time_start = time.time()
time_overall = 100 time_overall = 100
@@ -268,7 +270,9 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
if not ret_info.get("suc", False): if not ret_info.get("suc", False):
raise RuntimeError(f"Step {i + 1} failed.") raise RuntimeError(f"Step {i + 1} failed.")
except ROS2WorkstationNodeTempError as ex: except ROS2WorkstationNodeTempError as ex:
step_results.append({"step": i + 1, "action": action["action_name"], "result": ex.args[0]}) step_results.append(
{"step": i + 1, "action": action["action_name"], "result": ex.args[0]}
)
elif isinstance(action, list): elif isinstance(action, list):
# 如果是并行动作,同时执行 # 如果是并行动作,同时执行
actions = action actions = action
@@ -307,8 +311,12 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
except Exception as e: except Exception as e:
# 捕获并记录错误信息 # 捕获并记录错误信息
str_step_results = [ 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] 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_error = f"{traceback.format_exc()}\n\nStep Result: {pformat(str_step_results)}"
execution_success = False execution_success = False
self.lab_logger().error(f"协议 {protocol_name} 执行出错: {str(e)} \n{traceback.format_exc()}") self.lab_logger().error(f"协议 {protocol_name} 执行出错: {str(e)} \n{traceback.format_exc()}")
@@ -381,7 +389,7 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
"""还没有改过的部分""" """还没有改过的部分"""
def _setup_hardware_proxy( def _setup_hardware_proxy(
self, device: ROS2DeviceNode, communication_device: ROS2DeviceNode, read_method, write_method self, device: ROS2DeviceNode, communication_device: ROS2DeviceNode, read_method, write_method
): ):
"""为设备设置硬件接口代理""" """为设备设置硬件接口代理"""
# extra_info = [getattr(device.driver_instance, info) for info in communication_device.ros_node_instance._hardware_interface.get("extra_info", [])] # extra_info = [getattr(device.driver_instance, info) for info in communication_device.ros_node_instance._hardware_interface.get("extra_info", [])]
@@ -405,17 +413,3 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
if write_method: if write_method:
# bound_write = MethodType(_write, device.driver_instance) # bound_write = MethodType(_write, device.driver_instance)
setattr(device.driver_instance, write_method, _write) setattr(device.driver_instance, write_method, _write)
async def _update_resources(self, goal, protocol_kwargs):
"""更新资源状态"""
for k, v in goal.get_fields_and_field_types().items():
if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
if protocol_kwargs[k] is not None:
try:
r = ResourceUpdate.Request()
r.resources = [
convert_to_ros_msg(Resource, rs) for rs in nested_dict_to_list(protocol_kwargs[k])
]
await self._resource_clients["resource_update"].call_async(r)
except Exception as e:
self.lab_logger().error(f"更新资源失败: {e}")