Compare commits

..

11 Commits

Author SHA1 Message Date
Xuwznln
6ae77e0408 temp disable initialize resource 2025-06-08 17:07:48 +08:00
Xuwznln
bab4b1d67a biomek switch back to non-test 2025-06-08 17:05:48 +08:00
Guangxin Zhang
12c17ec26e 同步了Biomek.py 现在应可用 2025-06-08 16:58:19 +08:00
Guangxin Zhang
6577fe12eb 0608 DONE 2025-06-08 16:49:11 +08:00
qxw138
f1fee5fad9 Merge branch '37-biomek-i5i7' of https://github.com/dptech-corp/Uni-Lab-OS into 37-biomek-i5i7 2025-06-08 15:52:31 +08:00
qxw138
9b3377aedb Update biomek_test.py 2025-06-08 15:52:20 +08:00
Xuwznln
526327727d 取消raiseValueError提示 2025-06-08 15:34:56 +08:00
Xuwznln
aaa86314e3 同步执行状态信息 2025-06-08 15:34:16 +08:00
Xuwznln
6a14104e6b 正确发送return_info结果 2025-06-08 15:06:38 +08:00
Xuwznln
ab0c4b708b 修正物料上传时间
改用biomek_test
增加ResultInfoEncoder
支持返回结果上传
2025-06-08 14:43:07 +08:00
Xuwznln
c0b7f2decd host node新增resource add时间统计
create_resource新增handle
bump version to 0.9.2
2025-06-08 13:23:55 +08:00
61 changed files with 5073 additions and 2531 deletions

View File

@@ -45,7 +45,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n environment_name
# Currently, you need to install the `unilabos_msgs` package # Currently, you need to install the `unilabos_msgs` package
# You can download the system-specific package from the Release page # You can download the system-specific package from the Release page
conda install ros-humble-unilabos-msgs-0.9.1-xxxxx.tar.bz2 conda install ros-humble-unilabos-msgs-0.9.2-xxxxx.tar.bz2
# Install PyLabRobot and other prerequisites # Install PyLabRobot and other prerequisites
git clone https://github.com/PyLabRobot/pylabrobot plr_repo git clone https://github.com/PyLabRobot/pylabrobot plr_repo

View File

@@ -45,7 +45,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n 环境名
# 现阶段,需要安装 `unilabos_msgs` 包 # 现阶段,需要安装 `unilabos_msgs` 包
# 可以前往 Release 页面下载系统对应的包进行安装 # 可以前往 Release 页面下载系统对应的包进行安装
conda install ros-humble-unilabos-msgs-0.9.1-xxxxx.tar.bz2 conda install ros-humble-unilabos-msgs-0.9.2-xxxxx.tar.bz2
# 安装PyLabRobot等前置 # 安装PyLabRobot等前置
git clone https://github.com/PyLabRobot/pylabrobot plr_repo git clone https://github.com/PyLabRobot/pylabrobot plr_repo

View File

@@ -1,6 +1,6 @@
package: package:
name: ros-humble-unilabos-msgs name: ros-humble-unilabos-msgs
version: 0.9.1 version: 0.9.2
source: source:
path: ../../unilabos_msgs path: ../../unilabos_msgs
folder: ros-humble-unilabos-msgs/src/work folder: ros-humble-unilabos-msgs/src/work

View File

@@ -1,6 +1,6 @@
package: package:
name: unilabos name: unilabos
version: "0.9.1" version: "0.9.2"
source: source:
path: ../.. path: ../..

View File

@@ -4,7 +4,7 @@ package_name = 'unilabos'
setup( setup(
name=package_name, name=package_name,
version='0.9.1', version='0.9.2',
packages=find_packages(), packages=find_packages(),
include_package_data=True, include_package_data=True,
install_requires=['setuptools'], install_requires=['setuptools'],

View File

@@ -0,0 +1,22 @@
{
"nodes": [
{
"id": "BIOMEK",
"name": "BIOMEK",
"parent": null,
"type": "device",
"class": "liquid_handler.biomek",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
},
"data": {},
"children": [
]
}
],
"links": []
}

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,8 @@ from copy import deepcopy
import yaml import yaml
from unilabos.resources.graphio import tree_to_list
# 首先添加项目根目录到路径 # 首先添加项目根目录到路径
current_dir = os.path.dirname(os.path.abspath(__file__)) current_dir = os.path.dirname(os.path.abspath(__file__))
unilabos_dir = os.path.dirname(os.path.dirname(current_dir)) unilabos_dir = os.path.dirname(os.path.dirname(current_dir))
@@ -144,19 +146,19 @@ def main():
else read_graphml(args_dict["graph"]) else read_graphml(args_dict["graph"])
) )
devices_and_resources = dict_from_graph(graph_res.physical_setup_graph) devices_and_resources = dict_from_graph(graph_res.physical_setup_graph)
args_dict["resources_config"] = initialize_resources(list(deepcopy(devices_and_resources).values())) # args_dict["resources_config"] = initialize_resources(list(deepcopy(devices_and_resources).values()))
args_dict["resources_config"] = list(devices_and_resources.values())
args_dict["devices_config"] = dict_to_nested_dict(deepcopy(devices_and_resources), devices_only=False) args_dict["devices_config"] = dict_to_nested_dict(deepcopy(devices_and_resources), devices_only=False)
# args_dict["resources_config"] = dict_to_tree(devices_and_resources, devices_only=False)
args_dict["graph"] = graph_res.physical_setup_graph args_dict["graph"] = graph_res.physical_setup_graph
else: else:
if args_dict["devices"] is None or args_dict["resources"] is None: if args_dict["devices"] is None or args_dict["resources"] is None:
print_status("Either graph or devices and resources must be provided.", "error") print_status("Either graph or devices and resources must be provided.", "error")
sys.exit(1) sys.exit(1)
args_dict["devices_config"] = json.load(open(args_dict["devices"], encoding="utf-8")) args_dict["devices_config"] = json.load(open(args_dict["devices"], encoding="utf-8"))
args_dict["resources_config"] = initialize_resources( # args_dict["resources_config"] = initialize_resources(
list(json.load(open(args_dict["resources"], encoding="utf-8")).values()) # list(json.load(open(args_dict["resources"], encoding="utf-8")).values())
) # )
args_dict["resources_config"] = list(json.load(open(args_dict["resources"], encoding="utf-8")).values())
print_status(f"{len(args_dict['resources_config'])} Resources loaded:", "info") print_status(f"{len(args_dict['resources_config'])} Resources loaded:", "info")
for i in args_dict["resources_config"]: for i in args_dict["resources_config"]:

View File

@@ -1,6 +1,7 @@
import json import json
import time import time
import traceback import traceback
from typing import Optional
import uuid import uuid
import paho.mqtt.client as mqtt import paho.mqtt.client as mqtt
@@ -163,10 +164,12 @@ class MQTTClient:
self.client.publish(address, json.dumps(status), qos=2) self.client.publish(address, json.dumps(status), qos=2)
logger.critical(f"Device status published: address: {address}, {status}") logger.critical(f"Device status published: address: {address}, {status}")
def publish_job_status(self, feedback_data: dict, job_id: str, status: str): def publish_job_status(self, feedback_data: dict, job_id: str, status: str, return_info: Optional[str] = None):
if self.mqtt_disable: if self.mqtt_disable:
return return
jobdata = {"job_id": job_id, "data": feedback_data, "status": status} if return_info is None:
return_info = "{}"
jobdata = {"job_id": job_id, "data": feedback_data, "status": status, "return_info": return_info}
self.client.publish(f"labs/{MQConfig.lab_id}/job/list/", json.dumps(jobdata), qos=2) self.client.publish(f"labs/{MQConfig.lab_id}/job/list/", json.dumps(jobdata), qos=2)
def publish_registry(self, device_id: str, device_info: dict): def publish_registry(self, device_id: str, device_info: dict):

View File

@@ -30,18 +30,18 @@ class HTTPClient:
self.auth = MQConfig.lab_id self.auth = MQConfig.lab_id
info(f"HTTPClient 初始化完成: remote_addr={self.remote_addr}") info(f"HTTPClient 初始化完成: remote_addr={self.remote_addr}")
def resource_add(self, resources: List[Dict[str, Any]]) -> requests.Response: def resource_add(self, resources: List[Dict[str, Any]], database_process_later:bool) -> requests.Response:
""" """
添加资源 添加资源
Args: Args:
resources: 要添加的资源列表 resources: 要添加的资源列表
database_process_later: 后台处理资源
Returns: Returns:
Response: API响应对象 Response: API响应对象
""" """
response = requests.post( response = requests.post(
f"{self.remote_addr}/lab/resource/", f"{self.remote_addr}/lab/resource/?database_process_later={1 if database_process_later else 0}",
json=resources, json=resources,
headers={"Authorization": f"lab {self.auth}"}, headers={"Authorization": f"lab {self.auth}"},
timeout=5, timeout=5,

File diff suppressed because it is too large Load Diff

View File

@@ -276,14 +276,14 @@ class LiquidHandlerBiomek:
"Class": f"LabwareClasses\\{class_name}", "Class": f"LabwareClasses\\{class_name}",
"DataSets": {"Volume": {}}, "DataSets": {"Volume": {}},
"RuntimeDataSets": {"Volume": {}}, "RuntimeDataSets": {"Volume": {}},
"EvalAmounts": (float(liquid_volume[0]),) if liquid_volume else (0.0,), "EvalAmounts": (float(liquid_volume[0]),) if liquid_volume else (0,),
"Nominal": False, "Nominal": False,
"EvalLiquids": (liquid_type[0],) if liquid_type else ("Water",) "EvalLiquids": (liquid_type[0],) if liquid_type else ("Water",)
} }
elif instrument_type == "plate_96": elif instrument_type == "plate_96":
# 96孔板类型配置 # 96孔板类型配置
volume_per_well = float(liquid_volume[0]) if liquid_volume else 500.0 volume_per_well = float(liquid_volume[0]) if liquid_volume else 0
liquid_per_well = liquid_type[0] if liquid_type else "Water" liquid_per_well = liquid_type[0] if liquid_type else "Water"
config = { config = {
@@ -468,455 +468,455 @@ class LiquidHandlerBiomek:
if __name__ == "__main__": if __name__ == "__main__":
print("=== Biomek完整流程测试 ===") print("=== Biomek完整流程测试 ===")
print("包含: 仪器设置 + 完整实验步骤") print("包含: 仪器设置 + 完整实验步骤")
# 完整的步骤信息从biomek.py复制 # 完整的步骤信息从biomek.py复制
steps_info = ''' steps_info = '''
{ {
"steps": [ "steps": [
{ {
"step_number": 1, "step_number": 1,
"operation": "transfer", "operation": "transfer",
"description": "转移PCR产物或酶促反应液至0.05ml 96孔板中", "description": "转移PCR产物或酶促反应液至0.5ml 96孔板中",
"parameters": { "parameters": {
"source": "P1", "source": "P1",
"target": "P11", "target": "P11",
"tip_rack": "BC230", "tip_rack": "BC230",
"volume": 50 "volume": 50
} }
}, },
{ {
"step_number": 2, "step_number": 2,
"operation": "transfer", "operation": "transfer",
"description": "加入2倍体积Bind Beads BC至产物中", "description": "加入2倍体积Bind Beads BC至产物中",
"parameters": { "parameters": {
"source": "P2", "source": "P2",
"target": "P11", "target": "P11",
"tip_rack": "BC230", "tip_rack": "BC230",
"volume": 100 "volume": 100
} }
}, },
{ {
"step_number": 3, "step_number": 3,
"operation": "move_labware", "operation": "oscillation",
"description": "移动P11至Orbital1用于振荡混匀", "description": "振荡混匀300秒",
"parameters": { "parameters": {
"source": "P11", "rpm": 800,
"target": "Orbital1" "time": 300
} }
}, },
{ {
"step_number": 4, "step_number": 4,
"operation": "oscillation", "operation": "move_labware",
"description": "在Orbital1上振荡混匀Bind Beads BC与PCR产物700-900rpm300秒", "description": "转移至96孔磁力架上吸附3分钟",
"parameters": { "parameters": {
"rpm": 800, "source": "P11",
"time": 300 "target": "P12"
} }
}, },
{ {
"step_number": 5, "step_number": 5,
"operation": "move_labware", "operation": "incubation",
"description": "移动混匀后的板回P11", "description": "吸附3分钟",
"parameters": { "parameters": {
"source": "Orbital1", "time": 180
"target": "P11" }
} },
}, {
{ "step_number": 6,
"step_number": 6, "operation": "transfer",
"operation": "move_labware", "description": "吸弃或倒除上清液",
"description": "将P11移动到磁力架P12吸附3分钟", "parameters": {
"parameters": { "source": "P12",
"source": "P11", "target": "P22",
"target": "P12" "tip_rack": "BC230",
} "volume": 150
}, }
{ },
"step_number": 7, {
"operation": "incubation", "step_number": 7,
"description": "磁力架上室温静置3分钟完成吸附", "operation": "transfer",
"parameters": { "description": "加入300-500μl 75%乙醇",
"time": 180 "parameters": {
} "source": "P3",
}, "target": "P12",
{ "tip_rack": "BC230",
"step_number": 8, "volume": 400
"operation": "transfer", }
"description": "去除上清液至废液槽", },
"parameters": { {
"source": "P12", "step_number": 8,
"target": "P22", "operation": "move_labware",
"tip_rack": "BC230", "description": "移动至振荡器进行振荡混匀",
"volume": 150 "parameters": {
} "source": "P12",
}, "target": "Orbital1"
{ }
"step_number": 9, },
"operation": "transfer", {
"description": "加入300-500μl 75%乙醇清洗", "step_number": 9,
"parameters": { "operation": "oscillation",
"source": "P3", "description": "振荡混匀60秒",
"target": "P12", "parameters": {
"tip_rack": "BC230", "rpm": 800,
"volume": 400 "time": 60
} }
}, },
{ {
"step_number": 10, "step_number": 10,
"operation": "move_labware", "operation": "move_labware",
"description": "移动清洗板到Orbital1进行振荡", "description": "转移至96孔磁力架上吸附3分钟",
"parameters": { "parameters": {
"source": "P12", "source": "Orbital1",
"target": "Orbital1" "target": "P12"
} }
}, },
{ {
"step_number": 11, "step_number": 11,
"operation": "oscillation", "operation": "incubation",
"description": "乙醇清洗液振荡混匀700-900rpm, 45秒", "description": "吸附3分钟",
"parameters": { "parameters": {
"rpm": 800, "time": 180
"time": 45 }
} },
}, {
{ "step_number": 12,
"step_number": 12, "operation": "transfer",
"operation": "move_labware", "description": "吸弃或倒弃废液",
"description": "振荡后将板移回磁力架P12吸附", "parameters": {
"parameters": { "source": "P12",
"source": "Orbital1", "target": "P22",
"target": "P12" "tip_rack": "BC230",
} "volume": 400
}, }
{ },
"step_number": 13, {
"operation": "incubation", "step_number": 13,
"description": "吸附3分钟", "operation": "transfer",
"parameters": { "description": "重复加入75%乙醇",
"time": 180 "parameters": {
} "source": "P3",
}, "target": "P12",
{ "tip_rack": "BC230",
"step_number": 14, "volume": 400
"operation": "transfer", }
"description": "去除乙醇上清液至废液槽", },
"parameters": { {
"source": "P12", "step_number": 14,
"target": "P22", "operation": "move_labware",
"tip_rack": "BC230", "description": "移动至振荡器进行振荡混匀",
"volume": 400 "parameters": {
} "source": "P12",
}, "target": "Orbital1"
{ }
"step_number": 15, },
"operation": "transfer", {
"description": "第二次加入300-500μl 75%乙醇清洗", "step_number": 15,
"parameters": { "operation": "oscillation",
"source": "P3", "description": "振荡混匀60秒",
"target": "P12", "parameters": {
"tip_rack": "BC230", "rpm": 800,
"volume": 400 "time": 60
} }
}, },
{ {
"step_number": 16, "step_number": 16,
"operation": "move_labware", "operation": "move_labware",
"description": "再次移动清洗板到Orbital1振荡", "description": "转移至96孔磁力架上吸附3分钟",
"parameters": { "parameters": {
"source": "P12", "source": "Orbital1",
"target": "Orbital1" "target": "P12"
} }
}, },
{ {
"step_number": 17, "step_number": 17,
"operation": "oscillation", "operation": "incubation",
"description": "再次乙醇清洗液振荡混匀700-900rpm, 45秒", "description": "吸附3分钟",
"parameters": { "parameters": {
"rpm": 800, "time": 180
"time": 45 }
} },
}, {
{ "step_number": 18,
"step_number": 18, "operation": "transfer",
"operation": "move_labware", "description": "吸弃或倒弃废液",
"description": "振荡后板送回磁力架P12吸附", "parameters": {
"parameters": { "source": "P12",
"source": "Orbital1", "target": "P22",
"target": "P12" "tip_rack": "BC230",
} "volume": 400
}, }
{ },
"step_number": 19, {
"operation": "incubation", "step_number": 19,
"description": "再次吸附3分钟", "operation": "move_labware",
"parameters": { "description": "正放96孔板空气干燥15分钟",
"time": 180 "parameters": {
} "source": "P12",
}, "target": "P13"
{ }
"step_number": 20, },
"operation": "transfer", {
"description": "去除乙醇上清液至废液槽", "step_number": 20,
"parameters": { "operation": "incubation",
"source": "P12", "description": "空气干燥15分钟",
"target": "P22", "parameters": {
"tip_rack": "BC230", "time": 900
"volume": 400 }
} },
}, {
{ "step_number": 21,
"step_number": 21, "operation": "transfer",
"operation": "incubation", "description": "加入30-50μl Elution Buffer",
"description": "空气干燥15分钟", "parameters": {
"parameters": { "source": "P4",
"time": 900 "target": "P13",
} "tip_rack": "BC230",
}, "volume": 40
{ }
"step_number": 22, },
"operation": "transfer", {
"description": "加30-50μl Elution Buffer洗脱", "step_number": 22,
"parameters": { "operation": "move_labware",
"source": "P4", "description": "移动至振荡器进行振荡混匀",
"target": "P12", "parameters": {
"tip_rack": "BC230", "source": "P13",
"volume": 40 "target": "Orbital1"
} }
}, },
{ {
"step_number": 23, "step_number": 23,
"operation": "move_labware", "operation": "oscillation",
"description": "移动到Orbital1振荡混匀60秒", "description": "振荡混匀60秒",
"parameters": { "parameters": {
"source": "P12", "rpm": 800,
"target": "Orbital1" "time": 60
} }
}, },
{ {
"step_number": 24, "step_number": 24,
"operation": "oscillation", "operation": "move_labware",
"description": "Elution Buffer振荡混匀700-900rpm, 60秒", "description": "室温静置3分钟",
"parameters": { "parameters": {
"rpm": 800, "source": "Orbital1",
"time": 60 "target": "P13"
} }
}, },
{ {
"step_number": 25, "step_number": 25,
"operation": "move_labware", "operation": "incubation",
"description": "振荡后送回磁力架P12", "description": "室温静置3分钟",
"parameters": { "parameters": {
"source": "Orbital1", "time": 180
"target": "P12" }
} },
}, {
{ "step_number": 26,
"step_number": 26, "operation": "move_labware",
"operation": "incubation", "description": "转移至96孔磁力架上吸附2分钟",
"description": "室温静置3分钟洗脱反应", "parameters": {
"parameters": { "source": "P13",
"time": 180 "target": "P12"
} }
}, },
{ {
"step_number": 27, "step_number": 27,
"operation": "transfer", "operation": "incubation",
"description": "将上清液DNA转移到新板P13", "description": "吸附2分钟",
"parameters": { "parameters": {
"source": "P12", "time": 120
"target": "P13", }
"tip_rack": "BC230", },
"volume": 40 {
"step_number": 28,
"operation": "transfer",
"description": "将DNA转移至新的板中",
"parameters": {
"source": "P12",
"target": "P14",
"tip_rack": "BC230",
"volume": 40
} }
} }
] ]
} }
''' '''
# 完整的labware配置信息
# 完整的labware配置信息从biomek.py复制
labware_with_liquid = ''' labware_with_liquid = '''
[ [
{ {
"id": "Tip Rack BC230 on TL1", "id": "Tip Rack BC230 TL1",
"parent": "deck", "parent": "deck",
"slot_on_deck": "TL1", "slot_on_deck": "TL1",
"class_name": "BC230", "class_name": "BC230",
"liquid_type": [], "liquid_type": [],
"liquid_volume": [], "liquid_volume": [],
"liquid_input_wells": [] "liquid_input_wells": []
}, },
{ {
"id": "Tip Rack BC230 on TL2", "id": "Tip Rack BC230 TL2",
"parent": "deck", "parent": "deck",
"slot_on_deck": "TL2", "slot_on_deck": "TL2",
"class_name": "BC230", "class_name": "BC230",
"liquid_type": [], "liquid_type": [],
"liquid_volume": [], "liquid_volume": [],
"liquid_input_wells": [] "liquid_input_wells": []
}, },
{ {
"id": "Tip Rack BC230 on TL3", "id": "Tip Rack BC230 TL3",
"parent": "deck", "parent": "deck",
"slot_on_deck": "TL3", "slot_on_deck": "TL3",
"class_name": "BC230", "class_name": "BC230",
"liquid_type": [], "liquid_type": [],
"liquid_volume": [], "liquid_volume": [],
"liquid_input_wells": [] "liquid_input_wells": []
}, },
{ {
"id": "Tip Rack BC230 on TL4", "id": "Tip Rack BC230 TL4",
"parent": "deck", "parent": "deck",
"slot_on_deck": "TL4", "slot_on_deck": "TL4",
"class_name": "BC230", "class_name": "BC230",
"liquid_type": [], "liquid_type": [],
"liquid_volume": [], "liquid_volume": [],
"liquid_input_wells": [] "liquid_input_wells": []
}, },
{ {
"id": "Tip Rack BC230 on TL5", "id": "Tip Rack BC230 TL5",
"parent": "deck", "parent": "deck",
"slot_on_deck": "TL5", "slot_on_deck": "TL5",
"class_name": "BC230", "class_name": "BC230",
"liquid_type": [], "liquid_type": [],
"liquid_volume": [], "liquid_volume": [],
"liquid_input_wells": [] "liquid_input_wells": []
}, },
{ {
"id": "Tip Rack BC230 on P5", "id": "Tip Rack BC230 P5",
"parent": "deck", "parent": "deck",
"slot_on_deck": "P5", "slot_on_deck": "P5",
"class_name": "BC230", "class_name": "BC230",
"liquid_type": [], "liquid_type": [],
"liquid_volume": [], "liquid_volume": [],
"liquid_input_wells": [] "liquid_input_wells": []
}, },
{ {
"id": "Tip Rack BC230 on P6", "id": "Tip Rack BC230 P6",
"parent": "deck", "parent": "deck",
"slot_on_deck": "P6", "slot_on_deck": "P6",
"class_name": "BC230", "class_name": "BC230",
"liquid_type": [], "liquid_type": [],
"liquid_volume": [], "liquid_volume": [],
"liquid_input_wells": [] "liquid_input_wells": []
}, },
{ {
"id": "Tip Rack BC230 on P15", "id": "Tip Rack BC230 P15",
"parent": "deck", "parent": "deck",
"slot_on_deck": "P15", "slot_on_deck": "P15",
"class_name": "BC230", "class_name": "BC230",
"liquid_type": [], "liquid_type": [],
"liquid_volume": [], "liquid_volume": [],
"liquid_input_wells": [] "liquid_input_wells": []
}, },
{ {
"id": "Tip Rack BC230 on P16", "id": "Tip Rack BC230 P16",
"parent": "deck", "parent": "deck",
"slot_on_deck": "P16", "slot_on_deck": "P16",
"class_name": "BC230", "class_name": "BC230",
"liquid_type": [], "liquid_type": [],
"liquid_volume": [], "liquid_volume": [],
"liquid_input_wells": [] "liquid_input_wells": []
}, },
{ {
"id": "stock plate on P1", "id": "stock plate on P1",
"parent": "deck", "parent": "deck",
"slot_on_deck": "P1", "slot_on_deck": "P1",
"class_name": "nest_12_reservoir_15ml", "class_name": "AgilentReservoir",
"liquid_type": [ "liquid_type": ["PCR product"],
"master_mix" "liquid_volume": [5000],
], "liquid_input_wells": ["A1"]
"liquid_volume": [10000], },
"liquid_input_wells": [ {
"A1" "id": "stock plate on P2",
] "parent": "deck",
}, "slot_on_deck": "P2",
{ "class_name": "AgilentReservoir",
"id": "stock plate on P2", "liquid_type": ["bind beads"],
"parent": "deck", "liquid_volume": [100000],
"slot_on_deck": "P2", "liquid_input_wells": ["A1"]
"class_name": "nest_12_reservoir_15ml", },
"liquid_type": [ {
"bind beads" "id": "stock plate on P3",
], "parent": "deck",
"liquid_volume": [10000], "slot_on_deck": "P3",
"liquid_input_wells": [ "class_name": "AgilentReservoir",
"A1" "liquid_type": ["75% ethanol"],
] "liquid_volume": [100000],
}, "liquid_input_wells": ["A1"]
{ },
"id": "stock plate on P3", {
"parent": "deck", "id": "stock plate on P4",
"slot_on_deck": "P3", "parent": "deck",
"class_name": "nest_12_reservoir_15ml", "slot_on_deck": "P4",
"liquid_type": [ "class_name": "AgilentReservoir",
"ethyl alcohol" "liquid_type": ["Elution Buffer"],
], "liquid_volume": [5000],
"liquid_volume": [10000], "liquid_input_wells": ["A1"]
"liquid_input_wells": [ },
"A1" {
] "id": "working plate on P11",
}, "parent": "deck",
{ "slot_on_deck": "P11",
"id": "elution buffer on P4", "class_name": "BCDeep96Round",
"parent": "deck", "liquid_type": [],
"slot_on_deck": "P4", "liquid_volume": [],
"class_name": "nest_12_reservoir_15ml", "liquid_input_wells": []
"liquid_type": [ },
"elution buffer" {
], "id": "working plate on P12",
"liquid_volume": [5000], "parent": "deck",
"liquid_input_wells": [ "slot_on_deck": "P12",
"A1" "class_name": "BCDeep96Round",
] "liquid_type": [],
}, "liquid_volume": [],
{ "liquid_input_wells": []
"id": "oscillation", },
"parent": "deck", {
"slot_on_deck": "Orbital1", "id": "working plate on P13",
"class_name": "Orbital", "parent": "deck",
"liquid_type": [], "slot_on_deck": "P13",
"liquid_volume": [], "class_name": "BCDeep96Round",
"liquid_input_wells": [] "liquid_type": [],
}, "liquid_volume": [],
{ "liquid_input_wells": []
"id": "working plate on P11", },
"parent": "deck", {
"slot_on_deck": "P11", "id": "working plate on P14",
"class_name": "NEST 2ml Deep Well Plate", "parent": "deck",
"liquid_type": [], "slot_on_deck": "P14",
"liquid_volume": [], "class_name": "BCDeep96Round",
"liquid_input_wells": [] "liquid_type": [],
}, "liquid_volume": [],
{ "liquid_input_wells": []
"id": "magnetics module on P12", },
"parent": "deck", {
"slot_on_deck": "P12", "id": "waste on P22",
"class_name": "magnetics module", "parent": "deck",
"liquid_type": [], "slot_on_deck": "P22",
"liquid_volume": [], "class_name": "AgilentReservoir",
"liquid_input_wells": [] "liquid_type": [],
}, "liquid_volume": [],
{ "liquid_input_wells": []
"id": "working plate on P13", },
"parent": "deck", {
"slot_on_deck": "P13", "id": "oscillation",
"class_name": "NEST 2ml Deep Well Plate", "parent": "deck",
"liquid_type": [], "slot_on_deck": "Orbital1",
"liquid_volume": [], "class_name": "Orbital",
"liquid_input_wells": [] "liquid_type": [],
}, "liquid_volume": [],
{ "liquid_input_wells": []
"id": "waste on P22", }
"parent": "deck",
"slot_on_deck": "P22",
"class_name": "nest_1_reservoir_195ml",
"liquid_type": [],
"liquid_volume": [],
"liquid_input_wells": []
}
] ]
''' '''
@@ -1000,7 +1000,7 @@ if __name__ == "__main__":
script_dir = pathlib.Path(__file__).parent script_dir = pathlib.Path(__file__).parent
# 保存完整协议 # 保存完整协议
complete_output_path = script_dir / "complete_biomek_protocol_0607.json" complete_output_path = script_dir / "complete_biomek_protocol_0608.json"
with open(complete_output_path, 'w', encoding='utf-8') as f: with open(complete_output_path, 'w', encoding='utf-8') as f:
json.dump(handler.temp_protocol, f, indent=4, ensure_ascii=False) json.dump(handler.temp_protocol, f, indent=4, ensure_ascii=False)

View File

@@ -85,7 +85,15 @@ class Registry:
"goal_default": yaml.safe_load( "goal_default": yaml.safe_load(
io.StringIO(get_yaml_from_goal_type(self.ResourceCreateFromOuterEasy.Goal)) io.StringIO(get_yaml_from_goal_type(self.ResourceCreateFromOuterEasy.Goal))
), ),
"handles": {}, "handles": {
"output": [{
"handler_key": "Labware",
"label": "Labware",
"data_type": "resource",
"data_source": "handle",
"data_key": "liquid"
}]
},
}, },
"test_latency": { "test_latency": {
"type": self.EmptyIn, "type": self.EmptyIn,

View File

@@ -1,5 +1,4 @@
import copy import copy
import functools
import json import json
import threading import threading
import time import time
@@ -20,16 +19,29 @@ from rclpy.service import Service
from unilabos_msgs.action import SendCmd from unilabos_msgs.action import SendCmd
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
from unilabos.resources.graphio import convert_resources_to_type, convert_resources_from_type, resource_ulab_to_plr, \ from unilabos.resources.graphio import (
initialize_resources, list_to_nested_dict, dict_to_tree, resource_plr_to_ulab, tree_to_list convert_resources_to_type,
convert_resources_from_type,
resource_ulab_to_plr,
initialize_resources,
dict_to_tree,
resource_plr_to_ulab,
tree_to_list,
)
from unilabos.ros.msgs.message_converter import ( from unilabos.ros.msgs.message_converter import (
convert_to_ros_msg, convert_to_ros_msg,
convert_from_ros_msg, convert_from_ros_msg,
convert_from_ros_msg_with_mapping, convert_from_ros_msg_with_mapping,
convert_to_ros_msg_with_mapping, ros_action_to_json_schema, convert_to_ros_msg_with_mapping,
) )
from unilabos_msgs.srv import ResourceAdd, ResourceGet, ResourceDelete, ResourceUpdate, ResourceList, \ from unilabos_msgs.srv import (
SerialCommand # type: ignore ResourceAdd,
ResourceGet,
ResourceDelete,
ResourceUpdate,
ResourceList,
SerialCommand,
) # type: ignore
from unilabos_msgs.msg import Resource # type: ignore from unilabos_msgs.msg import Resource # type: ignore
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
@@ -37,7 +49,7 @@ from unilabos.ros.x.rclpyx import get_event_loop
from unilabos.ros.utils.driver_creator import ProtocolNodeCreator, PyLabRobotCreator, DeviceClassCreator from unilabos.ros.utils.driver_creator import ProtocolNodeCreator, PyLabRobotCreator, DeviceClassCreator
from unilabos.utils.async_util import run_async_func 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
from unilabos.utils.type_check import get_type_class, TypeEncoder from unilabos.utils.type_check import get_type_class, TypeEncoder, serialize_result_info
T = TypeVar("T") T = TypeVar("T")
@@ -292,7 +304,9 @@ class BaseROS2DeviceNode(Node, Generic[T]):
self.create_ros_action_server(action_name, action_value_mapping) self.create_ros_action_server(action_name, action_value_mapping)
# 创建线程池执行器 # 创建线程池执行器
self._executor = ThreadPoolExecutor(max_workers=max(len(action_value_mappings), 1), thread_name_prefix=f"ROSDevice{self.device_id}") self._executor = ThreadPoolExecutor(
max_workers=max(len(action_value_mappings), 1), thread_name_prefix=f"ROSDevice{self.device_id}"
)
# 创建资源管理客户端 # 创建资源管理客户端
self._resource_clients: Dict[str, Client] = { self._resource_clients: Dict[str, Client] = {
@@ -334,7 +348,9 @@ class BaseROS2DeviceNode(Node, Generic[T]):
other_calling_param["slot"] = slot other_calling_param["slot"] = slot
# 本地拿到这个物料,可能需要先做初始化? # 本地拿到这个物料,可能需要先做初始化?
if isinstance(resources, list): if isinstance(resources, list):
if len(resources) == 1 and isinstance(resources[0], list) and not initialize_full: # 取消,不存在的情况 if (
len(resources) == 1 and isinstance(resources[0], list) and not initialize_full
): # 取消,不存在的情况
# 预先initialize过以整组的形式传入 # 预先initialize过以整组的形式传入
request.resources = [convert_to_ros_msg(Resource, resource_) for resource_ in resources[0]] request.resources = [convert_to_ros_msg(Resource, resource_) for resource_ in resources[0]]
elif initialize_full: elif initialize_full:
@@ -373,6 +389,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
from pylabrobot.resources import Coordinate from pylabrobot.resources import Coordinate
from pylabrobot.resources import OTDeck from pylabrobot.resources import OTDeck
from pylabrobot.resources import Plate from pylabrobot.resources import Plate
contain_model = not isinstance(resource, Deck) contain_model = not isinstance(resource, Deck)
if isinstance(resource, ResourcePLR): if isinstance(resource, ResourcePLR):
# resources.list() # resources.list()
@@ -380,25 +397,38 @@ class BaseROS2DeviceNode(Node, Generic[T]):
plr_instance = resource_ulab_to_plr(resources_tree[0], contain_model) plr_instance = resource_ulab_to_plr(resources_tree[0], contain_model)
if isinstance(plr_instance, Plate): if isinstance(plr_instance, Plate):
empty_liquid_info_in = [(None, 0)] * plr_instance.num_items empty_liquid_info_in = [(None, 0)] * plr_instance.num_items
for liquid_type, liquid_volume, liquid_input_slot in zip(ADD_LIQUID_TYPE, LIQUID_VOLUME, LIQUID_INPUT_SLOT): for liquid_type, liquid_volume, liquid_input_slot in zip(
ADD_LIQUID_TYPE, LIQUID_VOLUME, LIQUID_INPUT_SLOT
):
empty_liquid_info_in[liquid_input_slot] = (liquid_type, liquid_volume) empty_liquid_info_in[liquid_input_slot] = (liquid_type, liquid_volume)
plr_instance.set_well_liquids(empty_liquid_info_in) plr_instance.set_well_liquids(empty_liquid_info_in)
if isinstance(resource, OTDeck) and "slot" in other_calling_param: if isinstance(resource, OTDeck) and "slot" in other_calling_param:
resource.assign_child_at_slot(plr_instance, **other_calling_param) resource.assign_child_at_slot(plr_instance, **other_calling_param)
else: else:
_discard_slot = other_calling_param.pop("slot", -1) _discard_slot = other_calling_param.pop("slot", -1)
resource.assign_child_resource(plr_instance, Coordinate(location["x"], location["y"], location["z"]), **other_calling_param) resource.assign_child_resource(
request2.resources = [convert_to_ros_msg(Resource, r) for r in tree_to_list([resource_plr_to_ulab(resource)])] plr_instance,
Coordinate(location["x"], location["y"], location["z"]),
**other_calling_param,
)
request2.resources = [
convert_to_ros_msg(Resource, r) for r in tree_to_list([resource_plr_to_ulab(resource)])
]
rclient2.call(request2) rclient2.call(request2)
# 发送给ResourceMeshManager # 发送给ResourceMeshManager
action_client = ActionClient( action_client = ActionClient(
self, SendCmd, "/devices/resource_mesh_manager/add_resource_mesh", callback_group=self.callback_group self,
SendCmd,
"/devices/resource_mesh_manager/add_resource_mesh",
callback_group=self.callback_group,
) )
goal = SendCmd.Goal() goal = SendCmd.Goal()
goal.command = json.dumps({ goal.command = json.dumps(
"resources": resources, {
"bind_parent_id": bind_parent_id, "resources": resources,
}) "bind_parent_id": bind_parent_id,
}
)
future = action_client.send_goal_async(goal, goal_uuid=uuid.uuid4()) future = action_client.send_goal_async(goal, goal_uuid=uuid.uuid4())
def done_cb(*args): def done_cb(*args):
@@ -415,10 +445,16 @@ class BaseROS2DeviceNode(Node, Generic[T]):
# noinspection PyTypeChecker # noinspection PyTypeChecker
self._service_server: Dict[str, Service] = { self._service_server: Dict[str, Service] = {
"query_host_name": self.create_service( "query_host_name": self.create_service(
SerialCommand, f"/srv{self.namespace}/query_host_name", query_host_name_cb, callback_group=self.callback_group SerialCommand,
f"/srv{self.namespace}/query_host_name",
query_host_name_cb,
callback_group=self.callback_group,
), ),
"append_resource": self.create_service( "append_resource": self.create_service(
SerialCommand, f"/srv{self.namespace}/append_resource", append_resource, callback_group=self.callback_group SerialCommand,
f"/srv{self.namespace}/append_resource",
append_resource,
callback_group=self.callback_group,
), ),
} }
@@ -447,6 +483,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
registered_devices[self.device_id] = device_info registered_devices[self.device_id] = device_info
from unilabos.config.config import BasicConfig from unilabos.config.config import BasicConfig
from unilabos.ros.nodes.presets.host_node import HostNode from unilabos.ros.nodes.presets.host_node import HostNode
if not BasicConfig.is_host_mode: if not BasicConfig.is_host_mode:
sclient = self.create_client(SerialCommand, "/node_info_update") sclient = self.create_client(SerialCommand, "/node_info_update")
# 启动线程执行发送任务 # 启动线程执行发送任务
@@ -454,7 +491,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
target=self.send_slave_node_info, target=self.send_slave_node_info,
args=(sclient,), args=(sclient,),
daemon=True, daemon=True,
name=f"ROSDevice{self.device_id}_send_slave_node_info" name=f"ROSDevice{self.device_id}_send_slave_node_info",
).start() ).start()
else: else:
host_node = HostNode.get_instance(0) host_node = HostNode.get_instance(0)
@@ -465,12 +502,18 @@ class BaseROS2DeviceNode(Node, Generic[T]):
sclient.wait_for_service() sclient.wait_for_service()
request = SerialCommand.Request() request = SerialCommand.Request()
from unilabos.config.config import BasicConfig from unilabos.config.config import BasicConfig
request.command = json.dumps({
"SYNC_SLAVE_NODE_INFO": { request.command = json.dumps(
"machine_name": BasicConfig.machine_name, {
"type": "slave", "SYNC_SLAVE_NODE_INFO": {
"edge_device_id": self.device_id "machine_name": BasicConfig.machine_name,
}}, ensure_ascii=False, cls=TypeEncoder) "type": "slave",
"edge_device_id": self.device_id,
}
},
ensure_ascii=False,
cls=TypeEncoder,
)
# 发送异步请求并等待结果 # 发送异步请求并等待结果
future = sclient.call_async(request) future = sclient.call_async(request)
@@ -543,6 +586,11 @@ class BaseROS2DeviceNode(Node, Generic[T]):
"""创建动作执行回调函数""" """创建动作执行回调函数"""
async def execute_callback(goal_handle: ServerGoalHandle): async def execute_callback(goal_handle: ServerGoalHandle):
# 初始化结果信息变量
execution_error = ""
execution_success = False
action_return_value = None
self.lab_logger().info(f"执行动作: {action_name}") self.lab_logger().info(f"执行动作: {action_name}")
goal = goal_handle.request goal = goal_handle.request
@@ -582,7 +630,11 @@ class BaseROS2DeviceNode(Node, Generic[T]):
current_resources.extend(response.resources) current_resources.extend(response.resources)
else: else:
r = ResourceGet.Request() r = ResourceGet.Request()
r.id = action_kwargs[k]["id"] if v == "unilabos_msgs/Resource" else action_kwargs[k][0]["id"] r.id = (
action_kwargs[k]["id"]
if v == "unilabos_msgs/Resource"
else action_kwargs[k][0]["id"]
)
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.extend(response.resources)
@@ -605,7 +657,19 @@ class BaseROS2DeviceNode(Node, Generic[T]):
if asyncio.iscoroutinefunction(ACTION): if asyncio.iscoroutinefunction(ACTION):
try: try:
self.lab_logger().info(f"异步执行动作 {ACTION}") self.lab_logger().info(f"异步执行动作 {ACTION}")
future = ROS2DeviceNode.run_async_func(ACTION, **action_kwargs) 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__} 报错了")
error(traceback.format_exc())
future.add_done_callback(_handle_future_exception)
except Exception as e: except Exception as e:
self.lab_logger().error(f"创建异步任务失败: {traceback.format_exc()}") self.lab_logger().error(f"创建异步任务失败: {traceback.format_exc()}")
raise e raise e
@@ -614,9 +678,12 @@ class BaseROS2DeviceNode(Node, Generic[T]):
future = self._executor.submit(ACTION, **action_kwargs) future = self._executor.submit(ACTION, **action_kwargs)
def _handle_future_exception(fut): def _handle_future_exception(fut):
nonlocal execution_error, execution_success, action_return_value
try: try:
fut.result() action_return_value = fut.result()
execution_success = True
except Exception as e: except Exception as e:
execution_error = traceback.format_exc()
error(f"同步任务 {ACTION.__name__} 报错了") error(f"同步任务 {ACTION.__name__} 报错了")
error(traceback.format_exc()) error(traceback.format_exc())
@@ -707,6 +774,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
for attr_name in result_msg_types.keys(): for attr_name in result_msg_types.keys():
if attr_name in ["success", "reached_goal"]: if attr_name in ["success", "reached_goal"]:
setattr(result_msg, attr_name, True) setattr(result_msg, attr_name, True)
elif attr_name == "return_info":
setattr(result_msg, attr_name, serialize_result_info(execution_error, execution_success, action_return_value))
self.lab_logger().info(f"动作 {action_name} 完成并返回结果") self.lab_logger().info(f"动作 {action_name} 完成并返回结果")
return result_msg return result_msg
@@ -752,8 +821,8 @@ class ROS2DeviceNode:
return cls._loop return cls._loop
@classmethod @classmethod
def run_async_func(cls, func, **kwargs): def run_async_func(cls, func, trace_error=True, **kwargs):
return run_async_func(func, loop=cls._loop, **kwargs) return run_async_func(func, loop=cls._loop, trace_error=trace_error, **kwargs)
@property @property
def driver_instance(self): def driver_instance(self):
@@ -805,9 +874,11 @@ class ROS2DeviceNode:
self.resource_tracker = DeviceNodeResourceTracker() self.resource_tracker = DeviceNodeResourceTracker()
# use_pylabrobot_creator 使用 cls的包路径检测 # use_pylabrobot_creator 使用 cls的包路径检测
use_pylabrobot_creator = (driver_class.__module__.startswith("pylabrobot") use_pylabrobot_creator = (
or driver_class.__name__ == "LiquidHandlerAbstract" driver_class.__module__.startswith("pylabrobot")
or driver_class.__name__ == "LiquidHandlerBiomek") or driver_class.__name__ == "LiquidHandlerAbstract"
or driver_class.__name__ == "LiquidHandlerBiomek"
)
# TODO: 要在创建之前预先请求服务器是否有当前id的物料放到resource_tracker中让pylabrobot进行创建 # TODO: 要在创建之前预先请求服务器是否有当前id的物料放到resource_tracker中让pylabrobot进行创建
# 创建设备类实例 # 创建设备类实例

View File

@@ -151,7 +151,7 @@ class HostNode(BaseROS2DeviceNode):
mqtt_client.publish_registry(device_info["id"], device_info) mqtt_client.publish_registry(device_info["id"], device_info)
for resource_info in lab_registry.obtain_registry_resource_info(): for resource_info in lab_registry.obtain_registry_resource_info():
mqtt_client.publish_registry(resource_info["id"], resource_info) mqtt_client.publish_registry(resource_info["id"], resource_info)
time.sleep(1) # 等待MQTT连接稳定
# 首次发现网络中的设备 # 首次发现网络中的设备
self._discover_devices() self._discover_devices()
@@ -203,8 +203,12 @@ class HostNode(BaseROS2DeviceNode):
try: try:
for bridge in self.bridges: for bridge in self.bridges:
if hasattr(bridge, "resource_add"): if hasattr(bridge, "resource_add"):
self.lab_logger().info("[Host Node-Resource] Adding resources to bridge.") resource_start_time = time.time()
resource_add_res = bridge.resource_add(add_schema(resource_with_parent_name)) resource_add_res = bridge.resource_add(add_schema(resource_with_parent_name), True)
resource_end_time = time.time()
self.lab_logger().info(
f"[Host Node-Resource] 物料上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms"
)
except Exception as ex: except Exception as ex:
self.lab_logger().error("[Host Node-Resource] 添加物料出错!") self.lab_logger().error("[Host Node-Resource] 添加物料出错!")
self.lab_logger().error(traceback.format_exc()) self.lab_logger().error(traceback.format_exc())
@@ -610,13 +614,21 @@ class HostNode(BaseROS2DeviceNode):
"""获取结果回调""" """获取结果回调"""
result_msg = future.result().result result_msg = future.result().result
result_data = convert_from_ros_msg(result_msg) 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:
status = "failed"
except json.JSONDecodeError:
status = "failed"
self.lab_logger().info(f"[Host Node] Result for {action_id} ({uuid_str}): success") self.lab_logger().info(f"[Host Node] Result for {action_id} ({uuid_str}): success")
self.lab_logger().debug(f"[Host Node] Result data: {result_data}") self.lab_logger().debug(f"[Host Node] Result data: {result_data}")
if uuid_str: if uuid_str:
for bridge in self.bridges: for bridge in self.bridges:
if hasattr(bridge, "publish_job_status"): if hasattr(bridge, "publish_job_status"):
bridge.publish_job_status(result_data, uuid_str, "success") bridge.publish_job_status(result_data, uuid_str, status, result_data.get("return_info", "{}"))
def cancel_goal(self, goal_uuid: str) -> None: def cancel_goal(self, goal_uuid: str) -> None:
"""取消目标""" """取消目标"""
@@ -856,7 +868,6 @@ class HostNode(BaseROS2DeviceNode):
测试网络延迟的action实现 测试网络延迟的action实现
通过5次ping-pong机制校对时间误差并计算实际延迟 通过5次ping-pong机制校对时间误差并计算实际延迟
""" """
import time
import uuid as uuid_module import uuid as uuid_module
self.lab_logger().info("=" * 60) self.lab_logger().info("=" * 60)

View File

@@ -5,7 +5,7 @@ from asyncio import get_event_loop
from unilabos.utils.log import error from unilabos.utils.log import error
def run_async_func(func, *, loop=None, **kwargs): def run_async_func(func, *, loop=None, trace_error=True, **kwargs):
if loop is None: if loop is None:
loop = get_event_loop() loop = get_event_loop()
@@ -17,5 +17,6 @@ def run_async_func(func, *, loop=None, **kwargs):
error(traceback.format_exc()) error(traceback.format_exc())
future = asyncio.run_coroutine_threadsafe(func(**kwargs), loop) future = asyncio.run_coroutine_threadsafe(func(**kwargs), loop)
future.add_done_callback(_handle_future_exception) if trace_error:
return future future.add_done_callback(_handle_future_exception)
return future

View File

@@ -1,4 +1,4 @@
import collections import collections.abc
import json import json
from typing import get_origin, get_args from typing import get_origin, get_args
@@ -21,3 +21,46 @@ class TypeEncoder(json.JSONEncoder):
return str(obj)[8:-2] return str(obj)[8:-2]
return super().default(obj) return super().default(obj)
class ResultInfoEncoder(json.JSONEncoder):
"""专门用于处理任务执行结果信息的JSON编码器"""
def default(self, obj):
# 优先处理类型对象
if isinstance(obj, type):
return str(obj)[8:-2]
# 对于无法序列化的对象,统一转换为字符串
try:
# 尝试调用 __dict__ 或者其他序列化方法
if hasattr(obj, "__dict__"):
return obj.__dict__
elif hasattr(obj, "_asdict"): # namedtuple
return obj._asdict()
elif hasattr(obj, "to_dict"):
return obj.to_dict()
elif hasattr(obj, "dict"):
return obj.dict()
else:
# 如果都不行,转换为字符串
return str(obj)
except Exception:
# 如果转换失败,直接返回字符串表示
return str(obj)
def serialize_result_info(error: str, suc: bool, return_value=None) -> str:
"""
序列化任务执行结果信息
Args:
error: 错误信息字符串
suc: 是否成功的布尔值
return_value: 返回值,可以是任何类型
Returns:
JSON字符串格式的结果信息
"""
result_info = {"error": error, "suc": suc, "return_value": return_value}
return json.dumps(result_info, ensure_ascii=False, cls=ResultInfoEncoder)

View File

@@ -4,6 +4,7 @@ string from_repo_position
Resource to_repo Resource to_repo
string to_repo_position string to_repo_position
--- ---
string return_info
bool success bool success
--- ---
string status string status

View File

@@ -5,6 +5,7 @@ float64 volume # Optional. Volume of solvent to clean vessel with.
float64 temp # Optional. Temperature to heat vessel to while cleaning. float64 temp # Optional. Temperature to heat vessel to while cleaning.
int32 repeats # Optional. Number of cleaning cycles to perform. int32 repeats # Optional. Number of cleaning cycles to perform.
--- ---
string return_info
bool success bool success
--- ---
string status string status

View File

@@ -1,4 +1,4 @@
--- ---
string return_info
--- ---

View File

@@ -3,6 +3,7 @@ string vessel
string gas string gas
int32 repeats int32 repeats
--- ---
string return_info
bool success bool success
--- ---
string status string status

View File

@@ -5,6 +5,7 @@ float64 temp
float64 time float64 time
float64 stir_speed float64 stir_speed
--- ---
string return_info
bool success bool success
--- ---
string status string status

View File

@@ -1,4 +1,5 @@
float64 float_in float64 float_in
--- ---
string return_info
bool success bool success
--- ---

View File

@@ -6,6 +6,7 @@ bool stir
float64 stir_speed float64 stir_speed
string purpose string purpose
--- ---
string return_info
bool success bool success
--- ---
string status string status

View File

@@ -3,6 +3,7 @@ string vessel
float64 temp float64 temp
string purpose string purpose
--- ---
string return_info
bool success bool success
--- ---
string status string status

View File

@@ -1,6 +1,7 @@
# Organic # Organic
string vessel string vessel
--- ---
string return_info
bool success bool success
--- ---
string status string status

View File

@@ -1,4 +1,5 @@
int32 int_input int32 int_input
--- ---
string return_info
bool success bool success
--- ---

View File

@@ -15,6 +15,7 @@ int32 mix_rate
float64 mix_liquid_height float64 mix_liquid_height
string[] none_keys string[] none_keys
--- ---
string return_info
bool success bool success
--- ---
# 反馈 # 反馈

View File

@@ -7,5 +7,6 @@ float64[] liquid_height
float64[] blow_out_air_volume float64[] blow_out_air_volume
string spread string spread
--- ---
string return_info
bool success bool success
--- ---

View File

@@ -3,6 +3,7 @@
int32[] use_channels int32[] use_channels
--- ---
# 结果字段 # 结果字段
string return_info
bool success bool success
--- ---
# 反馈字段 # 反馈字段

View File

@@ -8,6 +8,7 @@ int32[] blow_out_air_volume
string spread string spread
--- ---
# 结果字段 # 结果字段
string return_info
bool success bool success
--- ---
# 反馈字段 # 反馈字段

View File

@@ -6,6 +6,7 @@ geometry_msgs/Point[] offsets
bool allow_nonzero_volume bool allow_nonzero_volume
--- ---
# 结果字段 # 结果字段
string return_info
bool success bool success
--- ---
# 反馈字段 # 反馈字段

View File

@@ -5,6 +5,7 @@ geometry_msgs/Point offset
bool allow_nonzero_volume bool allow_nonzero_volume
--- ---
# 结果字段 # 结果字段
string return_info
bool success bool success
--- ---
# 反馈字段 # 反馈字段

View File

@@ -1,5 +1,6 @@
int32 time int32 time
--- ---
string return_info
bool success bool success
--- ---

View File

@@ -6,6 +6,7 @@ geometry_msgs/Point[] offsets
float64 mix_rate float64 mix_rate
string[] none_keys string[] none_keys
--- ---
string return_info
bool success bool success
--- ---
# 反馈 # 反馈

View File

@@ -2,5 +2,6 @@ string source
string target string target
--- ---
string return_info
bool success bool success
--- ---

View File

@@ -12,6 +12,7 @@ string put_direction
float64 pickup_distance_from_top float64 pickup_distance_from_top
--- ---
# 结果字段 # 结果字段
string return_info
bool success bool success
--- ---
# 反馈字段 # 反馈字段

View File

@@ -13,6 +13,7 @@ string put_direction
float64 pickup_distance_from_top float64 pickup_distance_from_top
--- ---
# 结果字段 # 结果字段
string return_info
bool success bool success
--- ---
# 反馈字段 # 反馈字段

View File

@@ -12,6 +12,7 @@ string get_direction
string put_direction string put_direction
--- ---
# 结果字段 # 结果字段
string return_info
bool success bool success
--- ---
# 反馈字段 # 反馈字段

View File

@@ -2,6 +2,7 @@ Resource well
float64 dis_to_top float64 dis_to_top
int32 channel int32 channel
--- ---
string return_info
bool success bool success
--- ---
# 反馈 # 反馈

View File

@@ -2,5 +2,6 @@ int32 rpm
int32 time int32 time
--- ---
string return_info
bool success bool success
--- ---

View File

@@ -5,6 +5,7 @@ int32[] use_channels
geometry_msgs/Point[] offsets geometry_msgs/Point[] offsets
--- ---
# 结果字段 # 结果字段
string return_info
bool success bool success
--- ---
# 反馈字段 # 反馈字段

View File

@@ -4,6 +4,7 @@ Resource tip_rack
geometry_msgs/Point offset geometry_msgs/Point offset
--- ---
# 结果字段 # 结果字段
string return_info
bool success bool success
--- ---
# 反馈字段 # 反馈字段

View File

@@ -6,4 +6,5 @@ string protocol_date
string protocol_type string protocol_type
string[] none_keys string[] none_keys
--- ---
string return_info
--- ---

View File

@@ -12,6 +12,7 @@ bool is_96_well
float64[] top float64[] top
string[] none_keys string[] none_keys
--- ---
string return_info
bool success bool success
--- ---
# 反馈 # 反馈

View File

@@ -4,6 +4,7 @@ int32[] use_channels
bool allow_nonzero_volume bool allow_nonzero_volume
--- ---
# 结果字段 # 结果字段
string return_info
bool success bool success
--- ---
# 反馈字段 # 反馈字段

View File

@@ -3,6 +3,7 @@
bool allow_nonzero_volume bool allow_nonzero_volume
--- ---
# 结果字段 # 结果字段
string return_info
bool success bool success
--- ---
# 反馈字段 # 反馈字段

View File

@@ -7,6 +7,7 @@ float64 aspiration_flow_rate
float64 dispense_flow_rate float64 dispense_flow_rate
--- ---
# 结果字段 # 结果字段
string return_info
bool success bool success
--- ---
# 反馈字段 # 反馈字段

View File

@@ -20,6 +20,7 @@ float64 mix_liquid_height
int32[] delays int32[] delays
string[] none_keys string[] none_keys
--- ---
string return_info
bool success bool success
--- ---
# 反馈 # 反馈

View File

@@ -6,5 +6,6 @@ string aspirate_technique
string dispense_technique string dispense_technique
--- ---
string return_info
bool success bool success
--- ---

View File

@@ -2,5 +2,6 @@ float64 x
float64 y float64 y
float64 z float64 z
--- ---
string return_info
bool success bool success
--- ---

View File

@@ -10,6 +10,7 @@ float64 rinsing_volume
int32 rinsing_repeats int32 rinsing_repeats
bool solid bool solid
--- ---
string return_info
bool success bool success
--- ---
string status string status

View File

@@ -4,5 +4,6 @@ string[] bind_parent_ids
geometry_msgs/Point[] bind_locations geometry_msgs/Point[] bind_locations
string[] other_calling_params string[] other_calling_params
--- ---
string return_info
bool success bool success
--- ---

View File

@@ -8,5 +8,6 @@ string[] liquid_type
float32[] liquid_volume float32[] liquid_volume
int32 slot_on_deck int32 slot_on_deck
--- ---
string return_info
bool success bool success
--- ---

View File

@@ -1,6 +1,7 @@
# Simple # Simple
string command string command
--- ---
string return_info
bool success bool success
--- ---
string status string status

View File

@@ -13,6 +13,7 @@ float64 stir_time # Optional. Time stir for after adding solvent, before separat
float64 stir_speed # Optional. Speed to stir at after adding solvent, before separation of phases. float64 stir_speed # Optional. Speed to stir at after adding solvent, before separation of phases.
float64 settling_time # Optional. Time float64 settling_time # Optional. Time
--- ---
string return_info
bool success bool success
--- ---
string status string status

View File

@@ -2,6 +2,7 @@ int32 powder_tube_number
string target_tube_position string target_tube_position
float64 compound_mass float64 compound_mass
--- ---
string return_info
float64 actual_mass_mg float64 actual_mass_mg
bool success bool success
--- ---

View File

@@ -3,6 +3,7 @@ float64 stir_time
float64 stir_speed float64 stir_speed
float64 settling_time float64 settling_time
--- ---
string return_info
bool success bool success
--- ---
string status string status

View File

@@ -1,4 +1,5 @@
string string string string
--- ---
string return_info
bool success bool success
--- ---

View File

@@ -3,6 +3,7 @@ string wf_name
string params string params
Resource resource Resource resource
--- ---
string return_info
bool success bool success
--- ---
string status string status