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
# 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
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` 包
# 可以前往 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等前置
git clone https://github.com/PyLabRobot/pylabrobot plr_repo

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ package_name = 'unilabos'
setup(
name=package_name,
version='0.9.1',
version='0.9.2',
packages=find_packages(),
include_package_data=True,
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
from unilabos.resources.graphio import tree_to_list
# 首先添加项目根目录到路径
current_dir = os.path.dirname(os.path.abspath(__file__))
unilabos_dir = os.path.dirname(os.path.dirname(current_dir))
@@ -144,19 +146,19 @@ def main():
else read_graphml(args_dict["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["resources_config"] = dict_to_tree(devices_and_resources, devices_only=False)
args_dict["graph"] = graph_res.physical_setup_graph
else:
if args_dict["devices"] is None or args_dict["resources"] is None:
print_status("Either graph or devices and resources must be provided.", "error")
sys.exit(1)
args_dict["devices_config"] = json.load(open(args_dict["devices"], encoding="utf-8"))
args_dict["resources_config"] = initialize_resources(
list(json.load(open(args_dict["resources"], encoding="utf-8")).values())
)
# args_dict["resources_config"] = initialize_resources(
# 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")
for i in args_dict["resources_config"]:

View File

@@ -1,6 +1,7 @@
import json
import time
import traceback
from typing import Optional
import uuid
import paho.mqtt.client as mqtt
@@ -163,10 +164,12 @@ class MQTTClient:
self.client.publish(address, json.dumps(status), qos=2)
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:
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)
def publish_registry(self, device_id: str, device_info: dict):

View File

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

View File

@@ -85,7 +85,15 @@ class Registry:
"goal_default": yaml.safe_load(
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": {
"type": self.EmptyIn,

View File

@@ -1,5 +1,4 @@
import copy
import functools
import json
import threading
import time
@@ -20,16 +19,29 @@ from rclpy.service import Service
from unilabos_msgs.action import SendCmd
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, \
initialize_resources, list_to_nested_dict, dict_to_tree, resource_plr_to_ulab, tree_to_list
from unilabos.resources.graphio import (
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 (
convert_to_ros_msg,
convert_from_ros_msg,
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, \
SerialCommand # type: ignore
from unilabos_msgs.srv import (
ResourceAdd,
ResourceGet,
ResourceDelete,
ResourceUpdate,
ResourceList,
SerialCommand,
) # type: ignore
from unilabos_msgs.msg import Resource # type: ignore
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.utils.async_util import run_async_func
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")
@@ -292,7 +304,9 @@ class BaseROS2DeviceNode(Node, Generic[T]):
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] = {
@@ -334,7 +348,9 @@ class BaseROS2DeviceNode(Node, Generic[T]):
other_calling_param["slot"] = slot
# 本地拿到这个物料,可能需要先做初始化?
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过以整组的形式传入
request.resources = [convert_to_ros_msg(Resource, resource_) for resource_ in resources[0]]
elif initialize_full:
@@ -373,6 +389,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
from pylabrobot.resources import Coordinate
from pylabrobot.resources import OTDeck
from pylabrobot.resources import Plate
contain_model = not isinstance(resource, Deck)
if isinstance(resource, ResourcePLR):
# resources.list()
@@ -380,25 +397,38 @@ class BaseROS2DeviceNode(Node, Generic[T]):
plr_instance = resource_ulab_to_plr(resources_tree[0], contain_model)
if isinstance(plr_instance, Plate):
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)
plr_instance.set_well_liquids(empty_liquid_info_in)
if isinstance(resource, OTDeck) and "slot" in other_calling_param:
resource.assign_child_at_slot(plr_instance, **other_calling_param)
else:
_discard_slot = other_calling_param.pop("slot", -1)
resource.assign_child_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)])]
resource.assign_child_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)
# 发送给ResourceMeshManager
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.command = json.dumps({
"resources": resources,
"bind_parent_id": bind_parent_id,
})
goal.command = json.dumps(
{
"resources": resources,
"bind_parent_id": bind_parent_id,
}
)
future = action_client.send_goal_async(goal, goal_uuid=uuid.uuid4())
def done_cb(*args):
@@ -415,10 +445,16 @@ class BaseROS2DeviceNode(Node, Generic[T]):
# noinspection PyTypeChecker
self._service_server: Dict[str, 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(
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
from unilabos.config.config import BasicConfig
from unilabos.ros.nodes.presets.host_node import HostNode
if not BasicConfig.is_host_mode:
sclient = self.create_client(SerialCommand, "/node_info_update")
# 启动线程执行发送任务
@@ -454,7 +491,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
target=self.send_slave_node_info,
args=(sclient,),
daemon=True,
name=f"ROSDevice{self.device_id}_send_slave_node_info"
name=f"ROSDevice{self.device_id}_send_slave_node_info",
).start()
else:
host_node = HostNode.get_instance(0)
@@ -465,12 +502,18 @@ class BaseROS2DeviceNode(Node, Generic[T]):
sclient.wait_for_service()
request = SerialCommand.Request()
from unilabos.config.config import BasicConfig
request.command = json.dumps({
"SYNC_SLAVE_NODE_INFO": {
"machine_name": BasicConfig.machine_name,
"type": "slave",
"edge_device_id": self.device_id
}}, ensure_ascii=False, cls=TypeEncoder)
request.command = json.dumps(
{
"SYNC_SLAVE_NODE_INFO": {
"machine_name": BasicConfig.machine_name,
"type": "slave",
"edge_device_id": self.device_id,
}
},
ensure_ascii=False,
cls=TypeEncoder,
)
# 发送异步请求并等待结果
future = sclient.call_async(request)
@@ -543,6 +586,11 @@ class BaseROS2DeviceNode(Node, Generic[T]):
"""创建动作执行回调函数"""
async def execute_callback(goal_handle: ServerGoalHandle):
# 初始化结果信息变量
execution_error = ""
execution_success = False
action_return_value = None
self.lab_logger().info(f"执行动作: {action_name}")
goal = goal_handle.request
@@ -582,7 +630,11 @@ class BaseROS2DeviceNode(Node, Generic[T]):
current_resources.extend(response.resources)
else:
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
response = await self._resource_clients["resource_get"].call_async(r)
current_resources.extend(response.resources)
@@ -605,7 +657,19 @@ class BaseROS2DeviceNode(Node, Generic[T]):
if asyncio.iscoroutinefunction(ACTION):
try:
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:
self.lab_logger().error(f"创建异步任务失败: {traceback.format_exc()}")
raise e
@@ -614,9 +678,12 @@ class BaseROS2DeviceNode(Node, Generic[T]):
future = self._executor.submit(ACTION, **action_kwargs)
def _handle_future_exception(fut):
nonlocal execution_error, execution_success, action_return_value
try:
fut.result()
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())
@@ -707,6 +774,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
for attr_name in result_msg_types.keys():
if attr_name in ["success", "reached_goal"]:
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} 完成并返回结果")
return result_msg
@@ -752,8 +821,8 @@ class ROS2DeviceNode:
return cls._loop
@classmethod
def run_async_func(cls, func, **kwargs):
return run_async_func(func, loop=cls._loop, **kwargs)
def run_async_func(cls, func, trace_error=True, **kwargs):
return run_async_func(func, loop=cls._loop, trace_error=trace_error, **kwargs)
@property
def driver_instance(self):
@@ -805,9 +874,11 @@ class ROS2DeviceNode:
self.resource_tracker = DeviceNodeResourceTracker()
# use_pylabrobot_creator 使用 cls的包路径检测
use_pylabrobot_creator = (driver_class.__module__.startswith("pylabrobot")
or driver_class.__name__ == "LiquidHandlerAbstract"
or driver_class.__name__ == "LiquidHandlerBiomek")
use_pylabrobot_creator = (
driver_class.__module__.startswith("pylabrobot")
or driver_class.__name__ == "LiquidHandlerAbstract"
or driver_class.__name__ == "LiquidHandlerBiomek"
)
# TODO: 要在创建之前预先请求服务器是否有当前id的物料放到resource_tracker中让pylabrobot进行创建
# 创建设备类实例

View File

@@ -151,7 +151,7 @@ class HostNode(BaseROS2DeviceNode):
mqtt_client.publish_registry(device_info["id"], device_info)
for resource_info in lab_registry.obtain_registry_resource_info():
mqtt_client.publish_registry(resource_info["id"], resource_info)
time.sleep(1) # 等待MQTT连接稳定
# 首次发现网络中的设备
self._discover_devices()
@@ -203,8 +203,12 @@ class HostNode(BaseROS2DeviceNode):
try:
for bridge in self.bridges:
if hasattr(bridge, "resource_add"):
self.lab_logger().info("[Host Node-Resource] Adding resources to bridge.")
resource_add_res = bridge.resource_add(add_schema(resource_with_parent_name))
resource_start_time = time.time()
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:
self.lab_logger().error("[Host Node-Resource] 添加物料出错!")
self.lab_logger().error(traceback.format_exc())
@@ -610,13 +614,21 @@ 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:
status = "failed"
except json.JSONDecodeError:
status = "failed"
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}")
if uuid_str:
for bridge in self.bridges:
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:
"""取消目标"""
@@ -856,7 +868,6 @@ class HostNode(BaseROS2DeviceNode):
测试网络延迟的action实现
通过5次ping-pong机制校对时间误差并计算实际延迟
"""
import time
import uuid as uuid_module
self.lab_logger().info("=" * 60)

View File

@@ -5,7 +5,7 @@ from asyncio import get_event_loop
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:
loop = get_event_loop()
@@ -17,5 +17,6 @@ def run_async_func(func, *, loop=None, **kwargs):
error(traceback.format_exc())
future = asyncio.run_coroutine_threadsafe(func(**kwargs), loop)
future.add_done_callback(_handle_future_exception)
return future
if trace_error:
future.add_done_callback(_handle_future_exception)
return future

View File

@@ -1,4 +1,4 @@
import collections
import collections.abc
import json
from typing import get_origin, get_args
@@ -21,3 +21,46 @@ class TypeEncoder(json.JSONEncoder):
return str(obj)[8:-2]
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
string to_repo_position
---
string return_info
bool success
---
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.
int32 repeats # Optional. Number of cleaning cycles to perform.
---
string return_info
bool success
---
string status

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
# Simple
string command
---
string return_info
bool success
---
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 settling_time # Optional. Time
---
string return_info
bool success
---
string status

View File

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

View File

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

View File

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

View File

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