mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-19 14:01:20 +00:00
Add battery resources, bioyond_cell device registry, and fix file path resolution
This commit is contained in:
101
new_cellconfig3c.json
Normal file
101
new_cellconfig3c.json
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
|
||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "bioyond_cell_workstation",
|
||||||
|
"name": "配液分液工站",
|
||||||
|
"parent": null,
|
||||||
|
"children": [
|
||||||
|
"YB_Bioyond_Deck"
|
||||||
|
],
|
||||||
|
"type": "device",
|
||||||
|
"class": "bioyond_cell",
|
||||||
|
"config": {
|
||||||
|
"deck": {
|
||||||
|
"data": {
|
||||||
|
"_resource_child_name": "YB_Bioyond_Deck",
|
||||||
|
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_YB_Deck"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"protocol_type": []
|
||||||
|
},
|
||||||
|
"data": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "YB_Bioyond_Deck",
|
||||||
|
"name": "YB_Bioyond_Deck",
|
||||||
|
"children": [],
|
||||||
|
"parent": "bioyond_cell_workstation",
|
||||||
|
"type": "deck",
|
||||||
|
"class": "BIOYOND_YB_Deck",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"type": "BIOYOND_YB_Deck",
|
||||||
|
"setup": true,
|
||||||
|
"rotation": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0,
|
||||||
|
"type": "Rotation"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "BatteryStation",
|
||||||
|
"name": "扣电工作站",
|
||||||
|
"parent": null,
|
||||||
|
"children": [
|
||||||
|
"coin_cell_deck"
|
||||||
|
],
|
||||||
|
"type": "device",
|
||||||
|
"class":"coincellassemblyworkstation_device",
|
||||||
|
"config": {
|
||||||
|
"deck": {
|
||||||
|
"data": {
|
||||||
|
"_resource_child_name": "YB_YH_Deck",
|
||||||
|
"_resource_type": "unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials:CoincellDeck"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"protocol_type": []
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"size": {"height": 1450, "width": 1450, "depth": 2100},
|
||||||
|
"position": {
|
||||||
|
"x": -1500,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "YB_YH_Deck",
|
||||||
|
"name": "YB_YH_Deck",
|
||||||
|
"children": [],
|
||||||
|
"parent": "BatteryStation",
|
||||||
|
"type": "deck",
|
||||||
|
"class": "CoincellDeck",
|
||||||
|
"config": {
|
||||||
|
"type": "CoincellDeck",
|
||||||
|
"setup": true,
|
||||||
|
"rotation": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0,
|
||||||
|
"type": "Rotation"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -367,10 +367,37 @@ def main():
|
|||||||
graph, resource_tree_set, resource_links = read_node_link_json(request_startup_json)
|
graph, resource_tree_set, resource_links = read_node_link_json(request_startup_json)
|
||||||
else:
|
else:
|
||||||
if not os.path.isfile(file_path):
|
if not os.path.isfile(file_path):
|
||||||
|
# 尝试从 main.py 向上两级目录查找
|
||||||
temp_file_path = os.path.abspath(str(os.path.join(__file__, "..", "..", file_path)))
|
temp_file_path = os.path.abspath(str(os.path.join(__file__, "..", "..", file_path)))
|
||||||
if os.path.isfile(temp_file_path):
|
if os.path.isfile(temp_file_path):
|
||||||
print_status(f"使用相对路径{temp_file_path}", "info")
|
print_status(f"使用相对路径{temp_file_path}", "info")
|
||||||
file_path = temp_file_path
|
file_path = temp_file_path
|
||||||
|
else:
|
||||||
|
# 尝试在 working_dir 中查找
|
||||||
|
working_dir_file_path = os.path.join(working_dir, file_path)
|
||||||
|
if os.path.isfile(working_dir_file_path):
|
||||||
|
print_status(f"在工作目录中找到文件: {working_dir_file_path}", "info")
|
||||||
|
file_path = working_dir_file_path
|
||||||
|
else:
|
||||||
|
# 尝试使用文件名在 working_dir 中查找
|
||||||
|
file_name = os.path.basename(file_path)
|
||||||
|
working_dir_file_path = os.path.join(working_dir, file_name)
|
||||||
|
if os.path.isfile(working_dir_file_path):
|
||||||
|
print_status(f"在工作目录中找到文件: {working_dir_file_path}", "info")
|
||||||
|
file_path = working_dir_file_path
|
||||||
|
# 最终检查文件是否存在
|
||||||
|
if not os.path.isfile(file_path):
|
||||||
|
print_status(
|
||||||
|
f"无法找到设备加载文件: {file_path}\n"
|
||||||
|
f"已尝试在以下位置查找:\n"
|
||||||
|
f" 1. 原始路径: {args_dict.get('graph', BasicConfig.startup_json_path)}\n"
|
||||||
|
f" 2. 相对路径: {os.path.abspath(str(os.path.join(__file__, '..', '..', args_dict.get('graph', BasicConfig.startup_json_path) or '')))}\n"
|
||||||
|
f" 3. 工作目录: {os.path.join(working_dir, args_dict.get('graph', BasicConfig.startup_json_path) or '')}\n"
|
||||||
|
f" 4. 工作目录(仅文件名): {os.path.join(working_dir, os.path.basename(args_dict.get('graph', BasicConfig.startup_json_path) or ''))}\n"
|
||||||
|
f"请使用 -g 参数指定正确的文件路径,或在工作目录 {working_dir} 中放置文件",
|
||||||
|
"error"
|
||||||
|
)
|
||||||
|
os._exit(1)
|
||||||
if file_path.endswith(".json"):
|
if file_path.endswith(".json"):
|
||||||
graph, resource_tree_set, resource_links = read_node_link_json(file_path)
|
graph, resource_tree_set, resource_links = read_node_link_json(file_path)
|
||||||
else:
|
else:
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,23 @@
|
|||||||
{
|
{
|
||||||
"nodes": [
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "bioyond_cell_workstation",
|
||||||
|
"name": "配液分液工站",
|
||||||
|
"children": [
|
||||||
|
],
|
||||||
|
"parent": null,
|
||||||
|
"type": "device",
|
||||||
|
"class": "bioyond_cell",
|
||||||
|
"config": {
|
||||||
|
"protocol_type": [],
|
||||||
|
"station_resource": {}
|
||||||
|
|
||||||
|
},
|
||||||
|
"data": {}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "BatteryStation",
|
"id": "BatteryStation",
|
||||||
"name": "扣电工作站",
|
"name": "扣电组装工作站",
|
||||||
"children": [
|
"children": [
|
||||||
"coin_cell_deck"
|
"coin_cell_deck"
|
||||||
],
|
],
|
||||||
@@ -98,7 +113,7 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazine_four",
|
"type": "MagazineHolder_4",
|
||||||
"size_x": 80,
|
"size_x": 80,
|
||||||
"size_y": 80,
|
"size_y": 80,
|
||||||
"size_z": 10,
|
"size_z": 10,
|
||||||
@@ -139,7 +154,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -234,7 +249,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -329,7 +344,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -424,7 +439,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -522,7 +537,7 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazine_four",
|
"type": "MagazineHolder_4",
|
||||||
"size_x": 80,
|
"size_x": 80,
|
||||||
"size_y": 80,
|
"size_y": 80,
|
||||||
"size_z": 10,
|
"size_z": 10,
|
||||||
@@ -563,7 +578,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -658,7 +673,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -753,7 +768,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -848,7 +863,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -948,7 +963,7 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazine",
|
"type": "MagazineHolder_6",
|
||||||
"size_x": 80,
|
"size_x": 80,
|
||||||
"size_y": 80,
|
"size_y": 80,
|
||||||
"size_z": 10,
|
"size_z": 10,
|
||||||
@@ -991,7 +1006,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -1086,7 +1101,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -1181,7 +1196,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -1276,7 +1291,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -1371,7 +1386,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -1466,7 +1481,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -1566,7 +1581,7 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazine",
|
"type": "MagazineHolder_6",
|
||||||
"size_x": 80,
|
"size_x": 80,
|
||||||
"size_y": 80,
|
"size_y": 80,
|
||||||
"size_z": 10,
|
"size_z": 10,
|
||||||
@@ -1609,7 +1624,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -1704,7 +1719,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -1799,7 +1814,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -1894,7 +1909,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -1989,7 +2004,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -2084,7 +2099,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -2184,7 +2199,7 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazine",
|
"type": "MagazineHolder_6",
|
||||||
"size_x": 80,
|
"size_x": 80,
|
||||||
"size_y": 80,
|
"size_y": 80,
|
||||||
"size_z": 10,
|
"size_z": 10,
|
||||||
@@ -2227,7 +2242,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -2322,7 +2337,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -2417,7 +2432,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -2512,7 +2527,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -2607,7 +2622,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -2702,7 +2717,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -2802,7 +2817,7 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazine",
|
"type": "MagazineHolder_6",
|
||||||
"size_x": 80,
|
"size_x": 80,
|
||||||
"size_y": 80,
|
"size_y": 80,
|
||||||
"size_z": 10,
|
"size_z": 10,
|
||||||
@@ -2845,7 +2860,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -2940,7 +2955,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -3035,7 +3050,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -3130,7 +3145,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -3225,7 +3240,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -3320,7 +3335,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -3420,7 +3435,7 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazine",
|
"type": "MagazineHolder_6",
|
||||||
"size_x": 80,
|
"size_x": 80,
|
||||||
"size_y": 80,
|
"size_y": 80,
|
||||||
"size_z": 10,
|
"size_z": 10,
|
||||||
@@ -3463,7 +3478,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -3558,7 +3573,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -3653,7 +3668,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -3748,7 +3763,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -3843,7 +3858,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -3938,7 +3953,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -4038,7 +4053,7 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazine",
|
"type": "MagazineHolder_6",
|
||||||
"size_x": 80,
|
"size_x": 80,
|
||||||
"size_y": 80,
|
"size_y": 80,
|
||||||
"size_z": 10,
|
"size_z": 10,
|
||||||
@@ -4081,7 +4096,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -4176,7 +4191,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -4271,7 +4286,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -4366,7 +4381,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -4461,7 +4476,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -4556,7 +4571,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "ClipMagazineHole",
|
"type": "Magazine",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
Binary file not shown.
@@ -0,0 +1,7 @@
|
|||||||
|
material_name
|
||||||
|
LiPF6
|
||||||
|
LiDFOB
|
||||||
|
DTD
|
||||||
|
LiFSI
|
||||||
|
LiPO2F2
|
||||||
|
|
||||||
|
@@ -47,8 +47,8 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
print("开始初始化 BioyondV1RPC")
|
print("开始初始化 BioyondV1RPC")
|
||||||
self.config = config
|
self.config = config
|
||||||
self.api_key = config["api_key"]
|
self.api_key = config.get("api_key", "")
|
||||||
self.host = config["api_host"]
|
self.host = config.get("api_host", "") or config.get("base_url", "")
|
||||||
self._logger = SimpleLogger()
|
self._logger = SimpleLogger()
|
||||||
self.material_cache = {}
|
self.material_cache = {}
|
||||||
self._load_material_cache()
|
self._load_material_cache()
|
||||||
@@ -61,7 +61,7 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
|
|
||||||
:return: 当前时间的 ISO 8601 格式字符串
|
:return: 当前时间的 ISO 8601 格式字符串
|
||||||
"""
|
"""
|
||||||
current_time = datetime.now(timezone.utc).isoformat(
|
current_time = datetime.now().isoformat(
|
||||||
timespec='milliseconds'
|
timespec='milliseconds'
|
||||||
)
|
)
|
||||||
# 替换时区部分为 'Z'
|
# 替换时区部分为 'Z'
|
||||||
@@ -192,23 +192,6 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return []
|
return []
|
||||||
return str(response.get("data", {}))
|
return str(response.get("data", {}))
|
||||||
|
|
||||||
def material_type_list(self) -> list:
|
|
||||||
"""查询物料类型列表
|
|
||||||
|
|
||||||
返回值:
|
|
||||||
list: 物料类型数组,失败返回空列表
|
|
||||||
"""
|
|
||||||
response = self.post(
|
|
||||||
url=f'{self.host}/api/lims/storage/material-type-list',
|
|
||||||
params={
|
|
||||||
"apiKey": self.api_key,
|
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
|
||||||
"data": {},
|
|
||||||
})
|
|
||||||
if not response or response['code'] != 1:
|
|
||||||
return []
|
|
||||||
return response.get("data", [])
|
|
||||||
|
|
||||||
def material_inbound(self, material_id: str, location_id: str) -> dict:
|
def material_inbound(self, material_id: str, location_id: str) -> dict:
|
||||||
"""
|
"""
|
||||||
描述:指定库位入库一个物料
|
描述:指定库位入库一个物料
|
||||||
@@ -229,34 +212,8 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
})
|
})
|
||||||
|
|
||||||
if not response or response['code'] != 1:
|
if not response or response['code'] != 1:
|
||||||
if response:
|
|
||||||
error_msg = response.get('message', '未知错误')
|
|
||||||
print(f"[ERROR] 物料入库失败: code={response.get('code')}, message={error_msg}")
|
|
||||||
else:
|
|
||||||
print(f"[ERROR] 物料入库失败: API 无响应")
|
|
||||||
return {}
|
return {}
|
||||||
# 入库成功时,即使没有 data 字段,也返回成功标识
|
return response.get("data", {})
|
||||||
return response.get("data") or {"success": True}
|
|
||||||
|
|
||||||
def batch_inbound(self, inbound_items: List[Dict[str, Any]]) -> int:
|
|
||||||
"""批量入库物料
|
|
||||||
|
|
||||||
参数:
|
|
||||||
inbound_items: 入库条目列表,每项包含 materialId/locationId/quantity 等
|
|
||||||
|
|
||||||
返回值:
|
|
||||||
int: 成功返回1,失败返回0
|
|
||||||
"""
|
|
||||||
response = self.post(
|
|
||||||
url=f'{self.host}/api/lims/storage/batch-inbound',
|
|
||||||
params={
|
|
||||||
"apiKey": self.api_key,
|
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
|
||||||
"data": inbound_items,
|
|
||||||
})
|
|
||||||
if not response or response['code'] != 1:
|
|
||||||
return 0
|
|
||||||
return response.get("code", 0)
|
|
||||||
|
|
||||||
def delete_material(self, material_id: str) -> dict:
|
def delete_material(self, material_id: str) -> dict:
|
||||||
"""
|
"""
|
||||||
@@ -276,7 +233,7 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return response.get("data", {})
|
return response.get("data", {})
|
||||||
|
|
||||||
def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict:
|
def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict:
|
||||||
"""指定库位出库物料(通过库位名称)"""
|
"""指定库位出库物料"""
|
||||||
location_id = LOCATION_MAPPING.get(location_name, location_name)
|
location_id = LOCATION_MAPPING.get(location_name, location_name)
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
@@ -293,98 +250,9 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
"data": params
|
"data": params
|
||||||
})
|
})
|
||||||
|
|
||||||
if not response or response['code'] != 1:
|
|
||||||
return None
|
|
||||||
return response
|
|
||||||
|
|
||||||
def material_outbound_by_id(self, material_id: str, location_id: str, quantity: int) -> dict:
|
|
||||||
"""指定库位出库物料(直接使用location_id)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
material_id: 物料ID
|
|
||||||
location_id: 库位ID(不是库位名称,是UUID)
|
|
||||||
quantity: 数量
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: API响应,失败返回None
|
|
||||||
"""
|
|
||||||
params = {
|
|
||||||
"materialId": material_id,
|
|
||||||
"locationId": location_id,
|
|
||||||
"quantity": quantity
|
|
||||||
}
|
|
||||||
|
|
||||||
response = self.post(
|
|
||||||
url=f'{self.host}/api/lims/storage/outbound',
|
|
||||||
params={
|
|
||||||
"apiKey": self.api_key,
|
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
|
||||||
"data": params
|
|
||||||
})
|
|
||||||
|
|
||||||
if not response or response['code'] != 1:
|
|
||||||
return None
|
|
||||||
return response
|
|
||||||
|
|
||||||
def batch_outbound(self, outbound_items: List[Dict[str, Any]]) -> int:
|
|
||||||
"""批量出库物料
|
|
||||||
|
|
||||||
参数:
|
|
||||||
outbound_items: 出库条目列表,每项包含 materialId/locationId/quantity 等
|
|
||||||
|
|
||||||
返回值:
|
|
||||||
int: 成功返回1,失败返回0
|
|
||||||
"""
|
|
||||||
response = self.post(
|
|
||||||
url=f'{self.host}/api/lims/storage/batch-outbound',
|
|
||||||
params={
|
|
||||||
"apiKey": self.api_key,
|
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
|
||||||
"data": outbound_items,
|
|
||||||
})
|
|
||||||
if not response or response['code'] != 1:
|
|
||||||
return 0
|
|
||||||
return response.get("code", 0)
|
|
||||||
|
|
||||||
def material_info(self, material_id: str) -> dict:
|
|
||||||
"""查询物料详情
|
|
||||||
|
|
||||||
参数:
|
|
||||||
material_id: 物料ID
|
|
||||||
|
|
||||||
返回值:
|
|
||||||
dict: 物料信息字典,失败返回空字典
|
|
||||||
"""
|
|
||||||
response = self.post(
|
|
||||||
url=f'{self.host}/api/lims/storage/material-info',
|
|
||||||
params={
|
|
||||||
"apiKey": self.api_key,
|
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
|
||||||
"data": material_id,
|
|
||||||
})
|
|
||||||
if not response or response['code'] != 1:
|
if not response or response['code'] != 1:
|
||||||
return {}
|
return {}
|
||||||
return response.get("data", {})
|
return response
|
||||||
|
|
||||||
def reset_location(self, location_id: str) -> int:
|
|
||||||
"""复位库位
|
|
||||||
|
|
||||||
参数:
|
|
||||||
location_id: 库位ID
|
|
||||||
|
|
||||||
返回值:
|
|
||||||
int: 成功返回1,失败返回0
|
|
||||||
"""
|
|
||||||
response = self.post(
|
|
||||||
url=f'{self.host}/api/lims/storage/reset-location',
|
|
||||||
params={
|
|
||||||
"apiKey": self.api_key,
|
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
|
||||||
"data": location_id,
|
|
||||||
})
|
|
||||||
if not response or response['code'] != 1:
|
|
||||||
return 0
|
|
||||||
return response.get("code", 0)
|
|
||||||
|
|
||||||
# ==================== 工作流查询相关接口 ====================
|
# ==================== 工作流查询相关接口 ====================
|
||||||
|
|
||||||
@@ -429,66 +297,6 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return {}
|
return {}
|
||||||
return response.get("data", {})
|
return response.get("data", {})
|
||||||
|
|
||||||
def split_workflow_list(self, params: Dict[str, Any]) -> dict:
|
|
||||||
"""查询可拆分工作流列表
|
|
||||||
|
|
||||||
参数:
|
|
||||||
params: 查询条件参数
|
|
||||||
|
|
||||||
返回值:
|
|
||||||
dict: 返回数据字典,失败返回空字典
|
|
||||||
"""
|
|
||||||
response = self.post(
|
|
||||||
url=f'{self.host}/api/lims/workflow/split-workflow-list',
|
|
||||||
params={
|
|
||||||
"apiKey": self.api_key,
|
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
|
||||||
"data": params,
|
|
||||||
})
|
|
||||||
if not response or response['code'] != 1:
|
|
||||||
return {}
|
|
||||||
return response.get("data", {})
|
|
||||||
|
|
||||||
def merge_workflow(self, data: Dict[str, Any]) -> dict:
|
|
||||||
"""合并工作流(无参数版)
|
|
||||||
|
|
||||||
参数:
|
|
||||||
data: 合并请求体,包含待合并的子工作流信息
|
|
||||||
|
|
||||||
返回值:
|
|
||||||
dict: 合并结果,失败返回空字典
|
|
||||||
"""
|
|
||||||
response = self.post(
|
|
||||||
url=f'{self.host}/api/lims/workflow/merge-workflow',
|
|
||||||
params={
|
|
||||||
"apiKey": self.api_key,
|
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
|
||||||
"data": data,
|
|
||||||
})
|
|
||||||
if not response or response['code'] != 1:
|
|
||||||
return {}
|
|
||||||
return response.get("data", {})
|
|
||||||
|
|
||||||
def merge_workflow_with_parameters(self, data: Dict[str, Any]) -> dict:
|
|
||||||
"""合并工作流(携带参数)
|
|
||||||
|
|
||||||
参数:
|
|
||||||
data: 合并请求体,包含 name、workflows 以及 stepParameters 等
|
|
||||||
|
|
||||||
返回值:
|
|
||||||
dict: 合并结果,失败返回空字典
|
|
||||||
"""
|
|
||||||
response = self.post(
|
|
||||||
url=f'{self.host}/api/lims/workflow/merge-workflow-with-parameters',
|
|
||||||
params={
|
|
||||||
"apiKey": self.api_key,
|
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
|
||||||
"data": data,
|
|
||||||
})
|
|
||||||
if not response or response['code'] != 1:
|
|
||||||
return {}
|
|
||||||
return response.get("data", {})
|
|
||||||
|
|
||||||
def validate_workflow_parameters(self, workflows: List[Dict[str, Any]]) -> Dict[str, Any]:
|
def validate_workflow_parameters(self, workflows: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||||
"""验证工作流参数格式"""
|
"""验证工作流参数格式"""
|
||||||
try:
|
try:
|
||||||
@@ -651,15 +459,18 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return {}
|
return {}
|
||||||
return response.get("data", {})
|
return response.get("data", {})
|
||||||
|
|
||||||
def order_report(self, order_id: str) -> dict:
|
def order_report(self, json_str: str) -> dict:
|
||||||
"""查询订单报告
|
|
||||||
|
|
||||||
参数:
|
|
||||||
order_id: 订单ID
|
|
||||||
|
|
||||||
返回值:
|
|
||||||
dict: 报告数据,失败返回空字典
|
|
||||||
"""
|
"""
|
||||||
|
描述:查询某个任务明细
|
||||||
|
json_str 格式为JSON字符串:
|
||||||
|
'{"order_id": "order123"}'
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = json.loads(json_str)
|
||||||
|
order_id = data.get("order_id", "")
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return {}
|
||||||
|
|
||||||
response = self.post(
|
response = self.post(
|
||||||
url=f'{self.host}/api/lims/order/order-report',
|
url=f'{self.host}/api/lims/order/order-report',
|
||||||
params={
|
params={
|
||||||
@@ -667,18 +478,16 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
"requestTime": self.get_current_time_iso8601(),
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
"data": order_id,
|
"data": order_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
if not response or response['code'] != 1:
|
if not response or response['code'] != 1:
|
||||||
return {}
|
return {}
|
||||||
return response.get("data", {})
|
return response.get("data", {})
|
||||||
|
|
||||||
def order_takeout(self, json_str: str) -> int:
|
def order_takeout(self, json_str: str) -> int:
|
||||||
"""取出任务产物
|
"""
|
||||||
|
描述:取出任务产物
|
||||||
参数:
|
json_str 格式为JSON字符串:
|
||||||
json_str: JSON字符串,包含 order_id 与 preintake_id
|
'{"order_id": "order123", "preintake_id": "preintake123"}'
|
||||||
|
|
||||||
返回值:
|
|
||||||
int: 成功返回1,失败返回0
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = json.loads(json_str)
|
data = json.loads(json_str)
|
||||||
@@ -701,15 +510,14 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return 0
|
return 0
|
||||||
return response.get("code", 0)
|
return response.get("code", 0)
|
||||||
|
|
||||||
|
|
||||||
def sample_waste_removal(self, order_id: str) -> dict:
|
def sample_waste_removal(self, order_id: str) -> dict:
|
||||||
"""样品/废料取出
|
"""
|
||||||
|
样品/废料取出接口
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
order_id: 订单ID
|
- order_id: 订单ID
|
||||||
|
|
||||||
返回值:
|
返回: 取出结果
|
||||||
dict: 取出结果,失败返回空字典
|
|
||||||
"""
|
"""
|
||||||
params = {"orderId": order_id}
|
params = {"orderId": order_id}
|
||||||
|
|
||||||
@@ -731,13 +539,10 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return response.get("data", {})
|
return response.get("data", {})
|
||||||
|
|
||||||
def cancel_order(self, json_str: str) -> bool:
|
def cancel_order(self, json_str: str) -> bool:
|
||||||
"""取消指定任务
|
"""
|
||||||
|
描述:取消指定任务
|
||||||
参数:
|
json_str 格式为JSON字符串:
|
||||||
json_str: JSON字符串,包含 order_id
|
'{"order_id": "order123"}'
|
||||||
|
|
||||||
返回值:
|
|
||||||
bool: 成功返回 True,失败返回 False
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = json.loads(json_str)
|
data = json.loads(json_str)
|
||||||
@@ -757,126 +562,6 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def cancel_experiment(self, order_id: str) -> int:
|
|
||||||
"""取消指定实验
|
|
||||||
|
|
||||||
参数:
|
|
||||||
order_id: 订单ID
|
|
||||||
|
|
||||||
返回值:
|
|
||||||
int: 成功返回1,失败返回0
|
|
||||||
"""
|
|
||||||
response = self.post(
|
|
||||||
url=f'{self.host}/api/lims/order/cancel-experiment',
|
|
||||||
params={
|
|
||||||
"apiKey": self.api_key,
|
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
|
||||||
"data": order_id,
|
|
||||||
})
|
|
||||||
if not response or response['code'] != 1:
|
|
||||||
return 0
|
|
||||||
return response.get("code", 0)
|
|
||||||
|
|
||||||
def batch_cancel_experiment(self, order_ids: List[str]) -> int:
|
|
||||||
"""批量取消实验
|
|
||||||
|
|
||||||
参数:
|
|
||||||
order_ids: 订单ID列表
|
|
||||||
|
|
||||||
返回值:
|
|
||||||
int: 成功返回1,失败返回0
|
|
||||||
"""
|
|
||||||
response = self.post(
|
|
||||||
url=f'{self.host}/api/lims/order/batch-cancel-experiment',
|
|
||||||
params={
|
|
||||||
"apiKey": self.api_key,
|
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
|
||||||
"data": order_ids,
|
|
||||||
})
|
|
||||||
if not response or response['code'] != 1:
|
|
||||||
return 0
|
|
||||||
return response.get("code", 0)
|
|
||||||
|
|
||||||
def gantts_by_order_id(self, order_id: str) -> dict:
|
|
||||||
"""查询订单甘特图数据
|
|
||||||
|
|
||||||
参数:
|
|
||||||
order_id: 订单ID
|
|
||||||
|
|
||||||
返回值:
|
|
||||||
dict: 甘特数据,失败返回空字典
|
|
||||||
"""
|
|
||||||
response = self.post(
|
|
||||||
url=f'{self.host}/api/lims/order/gantts-by-order-id',
|
|
||||||
params={
|
|
||||||
"apiKey": self.api_key,
|
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
|
||||||
"data": order_id,
|
|
||||||
})
|
|
||||||
if not response or response['code'] != 1:
|
|
||||||
return {}
|
|
||||||
return response.get("data", {})
|
|
||||||
|
|
||||||
def simulation_gantt_by_order_id(self, order_id: str) -> dict:
|
|
||||||
"""查询订单模拟甘特图数据
|
|
||||||
|
|
||||||
参数:
|
|
||||||
order_id: 订单ID
|
|
||||||
|
|
||||||
返回值:
|
|
||||||
dict: 模拟甘特数据,失败返回空字典
|
|
||||||
"""
|
|
||||||
response = self.post(
|
|
||||||
url=f'{self.host}/api/lims/order/simulation-gantt-by-order-id',
|
|
||||||
params={
|
|
||||||
"apiKey": self.api_key,
|
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
|
||||||
"data": order_id,
|
|
||||||
})
|
|
||||||
if not response or response['code'] != 1:
|
|
||||||
return {}
|
|
||||||
return response.get("data", {})
|
|
||||||
|
|
||||||
def reset_order_status(self, order_id: str) -> int:
|
|
||||||
"""复位订单状态
|
|
||||||
|
|
||||||
参数:
|
|
||||||
order_id: 订单ID
|
|
||||||
|
|
||||||
返回值:
|
|
||||||
int: 成功返回1,失败返回0
|
|
||||||
"""
|
|
||||||
response = self.post(
|
|
||||||
url=f'{self.host}/api/lims/order/reset-order-status',
|
|
||||||
params={
|
|
||||||
"apiKey": self.api_key,
|
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
|
||||||
"data": order_id,
|
|
||||||
})
|
|
||||||
if not response or response['code'] != 1:
|
|
||||||
return 0
|
|
||||||
return response.get("code", 0)
|
|
||||||
|
|
||||||
def gantt_with_simulation_by_order_id(self, order_id: str) -> dict:
|
|
||||||
"""查询订单甘特与模拟联合数据
|
|
||||||
|
|
||||||
参数:
|
|
||||||
order_id: 订单ID
|
|
||||||
|
|
||||||
返回值:
|
|
||||||
dict: 联合数据,失败返回空字典
|
|
||||||
"""
|
|
||||||
response = self.post(
|
|
||||||
url=f'{self.host}/api/lims/order/gantt-with-simulation-by-order-id',
|
|
||||||
params={
|
|
||||||
"apiKey": self.api_key,
|
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
|
||||||
"data": order_id,
|
|
||||||
})
|
|
||||||
if not response or response['code'] != 1:
|
|
||||||
return {}
|
|
||||||
return response.get("data", {})
|
|
||||||
|
|
||||||
# ==================== 设备管理相关接口 ====================
|
# ==================== 设备管理相关接口 ====================
|
||||||
|
|
||||||
def device_list(self, json_str: str = "") -> list:
|
def device_list(self, json_str: str = "") -> list:
|
||||||
@@ -908,13 +593,9 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return response.get("data", [])
|
return response.get("data", [])
|
||||||
|
|
||||||
def device_operation(self, json_str: str) -> int:
|
def device_operation(self, json_str: str) -> int:
|
||||||
"""设备操作
|
"""
|
||||||
|
描述:操作设备
|
||||||
参数:
|
json_str 格式为JSON字符串
|
||||||
json_str: JSON字符串,包含 device_no/operationType/operationParams
|
|
||||||
|
|
||||||
返回值:
|
|
||||||
int: 成功返回1,失败返回0
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = json.loads(json_str)
|
data = json.loads(json_str)
|
||||||
@@ -927,7 +608,7 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
response = self.post(
|
response = self.post(
|
||||||
url=f'{self.host}/api/lims/device/execute-operation',
|
url=f'{self.host}/api/lims/device/device-operation',
|
||||||
params={
|
params={
|
||||||
"apiKey": self.api_key,
|
"apiKey": self.api_key,
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
@@ -938,30 +619,9 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return 0
|
return 0
|
||||||
return response.get("code", 0)
|
return response.get("code", 0)
|
||||||
|
|
||||||
def reset_devices(self) -> int:
|
|
||||||
"""复位设备集合
|
|
||||||
|
|
||||||
返回值:
|
|
||||||
int: 成功返回1,失败返回0
|
|
||||||
"""
|
|
||||||
response = self.post(
|
|
||||||
url=f'{self.host}/api/lims/device/reset-devices',
|
|
||||||
params={
|
|
||||||
"apiKey": self.api_key,
|
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
|
||||||
})
|
|
||||||
if not response or response['code'] != 1:
|
|
||||||
return 0
|
|
||||||
return response.get("code", 0)
|
|
||||||
|
|
||||||
# ==================== 调度器相关接口 ====================
|
# ==================== 调度器相关接口 ====================
|
||||||
|
|
||||||
def scheduler_status(self) -> dict:
|
def scheduler_status(self) -> dict:
|
||||||
"""查询调度器状态
|
|
||||||
|
|
||||||
返回值:
|
|
||||||
dict: 包含 schedulerStatus/hasTask/creationTime 等
|
|
||||||
"""
|
|
||||||
response = self.post(
|
response = self.post(
|
||||||
url=f'{self.host}/api/lims/scheduler/scheduler-status',
|
url=f'{self.host}/api/lims/scheduler/scheduler-status',
|
||||||
params={
|
params={
|
||||||
@@ -974,7 +634,7 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return response.get("data", {})
|
return response.get("data", {})
|
||||||
|
|
||||||
def scheduler_start(self) -> int:
|
def scheduler_start(self) -> int:
|
||||||
"""启动调度器"""
|
"""描述:启动调度器"""
|
||||||
response = self.post(
|
response = self.post(
|
||||||
url=f'{self.host}/api/lims/scheduler/start',
|
url=f'{self.host}/api/lims/scheduler/start',
|
||||||
params={
|
params={
|
||||||
@@ -987,22 +647,9 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return response.get("code", 0)
|
return response.get("code", 0)
|
||||||
|
|
||||||
def scheduler_pause(self) -> int:
|
def scheduler_pause(self) -> int:
|
||||||
"""暂停调度器"""
|
"""描述:暂停调度器"""
|
||||||
response = self.post(
|
response = self.post(
|
||||||
url=f'{self.host}/api/lims/scheduler/pause',
|
url=f'{self.host}/api/lims/scheduler/scheduler-pause',
|
||||||
params={
|
|
||||||
"apiKey": self.api_key,
|
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
|
||||||
})
|
|
||||||
|
|
||||||
if not response or response['code'] != 1:
|
|
||||||
return 0
|
|
||||||
return response.get("code", 0)
|
|
||||||
|
|
||||||
def scheduler_smart_pause(self) -> int:
|
|
||||||
"""智能暂停调度器"""
|
|
||||||
response = self.post(
|
|
||||||
url=f'{self.host}/api/lims/scheduler/smart-pause',
|
|
||||||
params={
|
params={
|
||||||
"apiKey": self.api_key,
|
"apiKey": self.api_key,
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
@@ -1013,9 +660,8 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return response.get("code", 0)
|
return response.get("code", 0)
|
||||||
|
|
||||||
def scheduler_continue(self) -> int:
|
def scheduler_continue(self) -> int:
|
||||||
"""继续调度器"""
|
|
||||||
response = self.post(
|
response = self.post(
|
||||||
url=f'{self.host}/api/lims/scheduler/continue',
|
url=f'{self.host}/api/lims/scheduler/scheduler-continue',
|
||||||
params={
|
params={
|
||||||
"apiKey": self.api_key,
|
"apiKey": self.api_key,
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
@@ -1026,9 +672,9 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return response.get("code", 0)
|
return response.get("code", 0)
|
||||||
|
|
||||||
def scheduler_stop(self) -> int:
|
def scheduler_stop(self) -> int:
|
||||||
"""停止调度器"""
|
"""描述:停止调度器"""
|
||||||
response = self.post(
|
response = self.post(
|
||||||
url=f'{self.host}/api/lims/scheduler/stop',
|
url=f'{self.host}/api/lims/scheduler/scheduler-stop',
|
||||||
params={
|
params={
|
||||||
"apiKey": self.api_key,
|
"apiKey": self.api_key,
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
@@ -1039,9 +685,9 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return response.get("code", 0)
|
return response.get("code", 0)
|
||||||
|
|
||||||
def scheduler_reset(self) -> int:
|
def scheduler_reset(self) -> int:
|
||||||
"""复位调度器"""
|
"""描述:重置调度器"""
|
||||||
response = self.post(
|
response = self.post(
|
||||||
url=f'{self.host}/api/lims/scheduler/reset',
|
url=f'{self.host}/api/lims/scheduler/scheduler-reset',
|
||||||
params={
|
params={
|
||||||
"apiKey": self.api_key,
|
"apiKey": self.api_key,
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
@@ -1051,26 +697,6 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return 0
|
return 0
|
||||||
return response.get("code", 0)
|
return response.get("code", 0)
|
||||||
|
|
||||||
def scheduler_reply_error_handling(self, data: Dict[str, Any]) -> int:
|
|
||||||
"""调度错误处理回复
|
|
||||||
|
|
||||||
参数:
|
|
||||||
data: 错误处理参数
|
|
||||||
|
|
||||||
返回值:
|
|
||||||
int: 成功返回1,失败返回0
|
|
||||||
"""
|
|
||||||
response = self.post(
|
|
||||||
url=f'{self.host}/api/lims/scheduler/reply-error-handling',
|
|
||||||
params={
|
|
||||||
"apiKey": self.api_key,
|
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
|
||||||
"data": data,
|
|
||||||
})
|
|
||||||
if not response or response['code'] != 1:
|
|
||||||
return 0
|
|
||||||
return response.get("code", 0)
|
|
||||||
|
|
||||||
# ==================== 辅助方法 ====================
|
# ==================== 辅助方法 ====================
|
||||||
|
|
||||||
def _load_material_cache(self):
|
def _load_material_cache(self):
|
||||||
@@ -1079,7 +705,7 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
print("正在加载材料列表缓存...")
|
print("正在加载材料列表缓存...")
|
||||||
|
|
||||||
# 加载所有类型的材料:耗材(0)、样品(1)、试剂(2)
|
# 加载所有类型的材料:耗材(0)、样品(1)、试剂(2)
|
||||||
material_types = [0, 1, 2]
|
material_types = [1, 2]
|
||||||
|
|
||||||
for type_mode in material_types:
|
for type_mode in material_types:
|
||||||
print(f"正在加载类型 {type_mode} 的材料...")
|
print(f"正在加载类型 {type_mode} 的材料...")
|
||||||
@@ -1134,23 +760,3 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
def get_available_materials(self):
|
def get_available_materials(self):
|
||||||
"""获取所有可用的材料名称列表"""
|
"""获取所有可用的材料名称列表"""
|
||||||
return list(self.material_cache.keys())
|
return list(self.material_cache.keys())
|
||||||
|
|
||||||
def get_scheduler_state(self) -> Optional[MachineState]:
|
|
||||||
"""将调度状态字符串映射为枚举值
|
|
||||||
|
|
||||||
返回值:
|
|
||||||
Optional[MachineState]: 映射后的枚举,失败返回 None
|
|
||||||
"""
|
|
||||||
data = self.scheduler_status()
|
|
||||||
if not isinstance(data, dict):
|
|
||||||
return None
|
|
||||||
status = data.get("schedulerStatus")
|
|
||||||
mapping = {
|
|
||||||
"Init": MachineState.INITIAL,
|
|
||||||
"Stop": MachineState.STOPPED,
|
|
||||||
"Running": MachineState.RUNNING,
|
|
||||||
"Pause": MachineState.PAUSED,
|
|
||||||
"ErrorPause": MachineState.ERROR_PAUSED,
|
|
||||||
"ErrorStop": MachineState.ERROR_STOPPED,
|
|
||||||
}
|
|
||||||
return mapping.get(status)
|
|
||||||
|
|||||||
@@ -2,141 +2,330 @@
|
|||||||
"""
|
"""
|
||||||
配置文件 - 包含所有配置信息和映射关系
|
配置文件 - 包含所有配置信息和映射关系
|
||||||
"""
|
"""
|
||||||
|
import os
|
||||||
|
|
||||||
# API配置
|
# ==================== API 基础配置 ====================
|
||||||
|
# BioyondCellWorkstation 默认配置(包含所有必需参数)
|
||||||
API_CONFIG = {
|
API_CONFIG = {
|
||||||
"api_key": "",
|
# API 连接配置
|
||||||
"api_host": ""
|
# "api_host": os.getenv("BIOYOND_API_HOST", "http://172.16.1.143:44389"),#实机
|
||||||
}
|
"api_host": os.getenv("BIOYOND_API_HOST", "http://172.16.11.219:44388"),# 仿真机
|
||||||
|
"api_key": os.getenv("BIOYOND_API_KEY", "8A819E5C"),
|
||||||
|
"timeout": int(os.getenv("BIOYOND_TIMEOUT", "30")),
|
||||||
|
|
||||||
# 工作流映射配置
|
# 报送配置
|
||||||
WORKFLOW_MAPPINGS = {
|
"report_token": os.getenv("BIOYOND_REPORT_TOKEN", "CHANGE_ME_TOKEN"),
|
||||||
"reactor_taken_out": "",
|
|
||||||
"reactor_taken_in": "",
|
|
||||||
"Solid_feeding_vials": "",
|
|
||||||
"Liquid_feeding_vials(non-titration)": "",
|
|
||||||
"Liquid_feeding_solvents": "",
|
|
||||||
"Liquid_feeding(titration)": "",
|
|
||||||
"liquid_feeding_beaker": "",
|
|
||||||
"Drip_back": "",
|
|
||||||
}
|
|
||||||
|
|
||||||
# 工作流名称到DisplaySectionName的映射
|
# HTTP 服务配置
|
||||||
WORKFLOW_TO_SECTION_MAP = {
|
"HTTP_host": os.getenv("BIOYOND_HTTP_HOST", "172.16.11.6"), # HTTP服务监听地址,监听计算机飞连ip地址
|
||||||
'reactor_taken_in': '反应器放入',
|
"HTTP_port": int(os.getenv("BIOYOND_HTTP_PORT", "8080")),
|
||||||
'liquid_feeding_beaker': '液体投料-烧杯',
|
"debug_mode": False,# 调试模式
|
||||||
'Liquid_feeding_vials(non-titration)': '液体投料-小瓶(非滴定)',
|
|
||||||
'Liquid_feeding_solvents': '液体投料-溶剂',
|
|
||||||
'Solid_feeding_vials': '固体投料-小瓶',
|
|
||||||
'Liquid_feeding(titration)': '液体投料-滴定',
|
|
||||||
'reactor_taken_out': '反应器取出'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# 库位映射配置
|
# 库位映射配置
|
||||||
WAREHOUSE_MAPPING = {
|
WAREHOUSE_MAPPING = {
|
||||||
"粉末堆栈": {
|
"粉末加样头堆栈": {
|
||||||
"uuid": "",
|
"uuid": "",
|
||||||
"site_uuids": {
|
"site_uuids": {
|
||||||
# 样品板
|
"A01": "3a19da56-1379-ff7c-1745-07e200b44ce2",
|
||||||
"A1": "3a14198e-6929-31f0-8a22-0f98f72260df",
|
"B01": "3a19da56-1379-2424-d751-fe6e94cef938",
|
||||||
"A2": "3a14198e-6929-4379-affa-9a2935c17f99",
|
"C01": "3a19da56-1379-271c-03e3-6bdb590e395e",
|
||||||
"A3": "3a14198e-6929-56da-9a1c-7f5fbd4ae8af",
|
"D01": "3a19da56-1379-277f-2b1b-0d11f7cf92c6",
|
||||||
"A4": "3a14198e-6929-5e99-2b79-80720f7cfb54",
|
"E01": "3a19da56-1379-2f1c-a15b-e01db90eb39a",
|
||||||
"B1": "3a14198e-6929-f525-9a1b-1857552b28ee",
|
"F01": "3a19da56-1379-3fa1-846b-088158ac0b3d",
|
||||||
"B2": "3a14198e-6929-bf98-0fd5-26e1d68bf62d",
|
"G01": "3a19da56-1379-5aeb-d0cd-d3b4609d66e1",
|
||||||
"B3": "3a14198e-6929-2d86-a468-602175a2b5aa",
|
"H01": "3a19da56-1379-6077-8258-bdc036870b78",
|
||||||
"B4": "3a14198e-6929-1a98-ae57-e97660c489ad",
|
"I01": "3a19da56-1379-863b-a120-f606baf04617",
|
||||||
# 分装板
|
"J01": "3a19da56-1379-8a74-74e5-35a9b41d4fd5",
|
||||||
"C1": "3a14198e-6929-46fe-841e-03dd753f1e4a",
|
"K01": "3a19da56-1379-b270-b7af-f18773918abe",
|
||||||
"C2": "3a14198e-6929-1bc9-a9bd-3b7ca66e7f95",
|
"L01": "3a19da56-1379-ba54-6d78-fd770a671ffc",
|
||||||
"C3": "3a14198e-6929-72ac-32ce-9b50245682b8",
|
"M01": "3a19da56-1379-c22d-c96f-0ceb5eb54a04",
|
||||||
"C4": "3a14198e-6929-3bd8-e6c7-4a9fd93be118",
|
"N01": "3a19da56-1379-d64e-c6c5-c72ea4829888",
|
||||||
"D1": "3a14198e-6929-8a0b-b686-6f4a2955c4e2",
|
"O01": "3a19da56-1379-d887-1a3c-6f9cce90f90e",
|
||||||
"D2": "3a14198e-6929-dde1-fc78-34a84b71afdf",
|
"P01": "3a19da56-1379-e77d-0e65-7463b238a3b9",
|
||||||
"D3": "3a14198e-6929-a0ec-5f15-c0f9f339f963",
|
"Q01": "3a19da56-1379-edf6-1472-802ddb628774",
|
||||||
"D4": "3a14198e-6929-7ac8-915a-fea51cb2e884"
|
"R01": "3a19da56-1379-f281-0273-e0ef78f0fd97",
|
||||||
|
"S01": "3a19da56-1379-f924-7f68-df1fa51489f4",
|
||||||
|
"T01": "3a19da56-1379-ff7c-1745-07e200b44ce2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"溶液堆栈": {
|
"配液站内试剂仓库": {
|
||||||
"uuid": "",
|
"uuid": "",
|
||||||
"site_uuids": {
|
"site_uuids": {
|
||||||
"A1": "3a14198e-d724-e036-afdc-2ae39a7f3383",
|
"A01": "3a19da43-57b5-294f-d663-154a1cc32270",
|
||||||
"A2": "3a14198e-d724-afa4-fc82-0ac8a9016791",
|
"B01": "3a19da43-57b5-7394-5f49-54efe2c9bef2",
|
||||||
"A3": "3a14198e-d724-ca48-bb9e-7e85751e55b6",
|
"C01": "3a19da43-57b5-5e75-552f-8dbd0ad1075f",
|
||||||
"A4": "3a14198e-d724-df6d-5e32-5483b3cab583",
|
"A02": "3a19da43-57b5-8441-db94-b4d3875a4b6c",
|
||||||
"B1": "3a14198e-d724-d818-6d4f-5725191a24b5",
|
"B02": "3a19da43-57b5-3e41-c181-5119dddaf50c",
|
||||||
"B2": "3a14198e-d724-be8a-5e0b-012675e195c6",
|
"C02": "3a19da43-57b5-269b-282d-fba61fe8ce96",
|
||||||
"B3": "3a14198e-d724-cc1e-5c2c-228a130f40a8",
|
"A03": "3a19da43-57b5-7c1e-d02e-c40e8c33f8a1",
|
||||||
"B4": "3a14198e-d724-1e28-c885-574c3df468d0",
|
"B03": "3a19da43-57b5-659f-621f-1dcf3f640363",
|
||||||
"C1": "3a14198e-d724-b5bb-adf3-4c5a0da6fb31",
|
"C03": "3a19da43-57b5-855a-6e71-f398e376dee1",
|
||||||
"C2": "3a14198e-d724-ab4e-48cb-817c3c146707",
|
|
||||||
"C3": "3a14198e-d724-7f18-1853-39d0c62e1d33",
|
|
||||||
"C4": "3a14198e-d724-28a2-a760-baa896f46b66",
|
|
||||||
"D1": "3a14198e-d724-d378-d266-2508a224a19f",
|
|
||||||
"D2": "3a14198e-d724-f56e-468b-0110a8feb36a",
|
|
||||||
"D3": "3a14198e-d724-0cf1-dea9-a1f40fe7e13c",
|
|
||||||
"D4": "3a14198e-d724-0ddd-9654-f9352a421de9"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"试剂堆栈": {
|
"试剂替换仓库": {
|
||||||
"uuid": "",
|
"uuid": "",
|
||||||
"site_uuids": {
|
"site_uuids": {
|
||||||
"A1": "3a14198c-c2cf-8b40-af28-b467808f1c36",
|
"A01": "3a19da51-8f4e-30f3-ea08-4f8498e9b097",
|
||||||
"A2": "3a14198c-c2d0-f3e7-871a-e470d144296f",
|
"B01": "3a19da51-8f4e-1da7-beb0-80a4a01e67a8",
|
||||||
"A3": "3a14198c-c2d0-dc7d-b8d0-e1d88cee3094",
|
"C01": "3a19da51-8f4e-337d-2675-bfac46880b06",
|
||||||
"A4": "3a14198c-c2d0-2070-efc8-44e245f10c6f",
|
"D01": "3a19da51-8f4e-e514-b92c-9c44dc5e489d",
|
||||||
"B1": "3a14198c-c2d0-354f-39ad-642e1a72fcb8",
|
"E01": "3a19da51-8f4e-22d1-dd5b-9774ddc80402",
|
||||||
"B2": "3a14198c-c2d0-1559-105d-0ea30682cab4",
|
"F01": "3a19da51-8f4e-273a-4871-dff41c29bfd9",
|
||||||
"B3": "3a14198c-c2d0-725e-523d-34c037ac2440",
|
"G01": "3a19da51-8f4e-b32f-454f-74bc1a665653",
|
||||||
"B4": "3a14198c-c2d0-efce-0939-69ca5a7dfd39"
|
"H01": "3a19da51-8f4e-8c93-68c9-0b4382320f59",
|
||||||
|
"I01": "3a19da51-8f4e-360c-0149-291b47c6089b",
|
||||||
|
"J01": "3a19da51-8f4e-4152-9bca-8d64df8c1af0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"自动堆栈-左": {
|
||||||
|
"uuid": "",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "3a19debc-84b5-4c1c-d3a1-26830cf273ff",
|
||||||
|
"A02": "3a19debc-84b5-033b-b31f-6b87f7c2bf52",
|
||||||
|
"B01": "3a19debc-84b5-3924-172f-719ab01b125c",
|
||||||
|
"B02": "3a19debc-84b5-aad8-70c6-b8c6bb2d8750"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"自动堆栈-右": {
|
||||||
|
"uuid": "",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "3a19debe-5200-7df2-1dd9-7d202f158864",
|
||||||
|
"A02": "3a19debe-5200-573b-6120-8b51f50e1e50",
|
||||||
|
"B01": "3a19debe-5200-7cd8-7666-851b0a97e309",
|
||||||
|
"B02": "3a19debe-5200-e6d3-96a3-baa6e3d5e484"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"手动堆栈": {
|
||||||
|
"uuid": "",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "3a19deae-2c7a-36f5-5e41-02c5b66feaea",
|
||||||
|
"A02": "3a19deae-2c7a-dc6d-c41e-ef285d946cfe",
|
||||||
|
"A03": "3a19deae-2c7a-5876-c454-6b7e224ca927",
|
||||||
|
"B01": "3a19deae-2c7a-2426-6d71-e9de3cb158b1",
|
||||||
|
"B02": "3a19deae-2c7a-79b0-5e44-efaafd1e4cf3",
|
||||||
|
"B03": "3a19deae-2c7a-b9eb-f4e3-e308e0cf839a",
|
||||||
|
"C01": "3a19deae-2c7a-32bc-768e-556647e292f3",
|
||||||
|
"C02": "3a19deae-2c7a-e97a-8484-f5a4599447c4",
|
||||||
|
"C03": "3a19deae-2c7a-3056-6504-10dc73fbc276",
|
||||||
|
"D01": "3a19deae-2c7a-ffad-875e-8c4cda61d440",
|
||||||
|
"D02": "3a19deae-2c7a-61be-601c-b6fb5610499a",
|
||||||
|
"D03": "3a19deae-2c7a-c0f7-05a7-e3fe2491e560",
|
||||||
|
"E01": "3a19deae-2c7a-a6f4-edd1-b436a7576363",
|
||||||
|
"E02": "3a19deae-2c7a-4367-96dd-1ca2186f4910",
|
||||||
|
"E03": "3a19deae-2c7a-b163-2219-23df15200311",
|
||||||
|
"F01": "3a19deae-2c7a-d594-fd6a-0d20de3c7c4a",
|
||||||
|
"F02": "3a19deae-2c7a-a194-ea63-8b342b8d8679",
|
||||||
|
"F03": "3a19deae-2c7a-f7c4-12bd-425799425698",
|
||||||
|
"G01": "3a19deae-2c7a-0b56-72f1-8ab86e53b955",
|
||||||
|
"G02": "3a19deae-2c7a-204e-95ed-1f1950f28343",
|
||||||
|
"G03": "3a19deae-2c7a-392b-62f1-4907c66343f8",
|
||||||
|
"H01": "3a19deae-2c7a-5602-e876-d27aca4e3201",
|
||||||
|
"H02": "3a19deae-2c7a-f15c-70e0-25b58a8c9702",
|
||||||
|
"H03": "3a19deae-2c7a-780b-8965-2e1345f7e834",
|
||||||
|
"I01": "3a19deae-2c7a-8849-e172-07de14ede928",
|
||||||
|
"I02": "3a19deae-2c7a-4772-a37f-ff99270bafc0",
|
||||||
|
"I03": "3a19deae-2c7a-cce7-6e4a-25ea4a2068c4",
|
||||||
|
"J01": "3a19deae-2c7a-1848-de92-b5d5ed054cc6",
|
||||||
|
"J02": "3a19deae-2c7a-1d45-b4f8-6f866530e205",
|
||||||
|
"J03": "3a19deae-2c7a-f237-89d9-8fe19025dee9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"4号手套箱内部堆栈": {
|
||||||
|
"uuid": "",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "3a1baa20-a7b1-c665-8b9c-d8099d07d2f6",
|
||||||
|
"A02": "3a1baa20-a7b1-93a7-c988-f9c8ad6c58c9",
|
||||||
|
"A03": "3a1baa20-a7b1-00ee-f751-da9b20b6c464",
|
||||||
|
"A04": "3a1baa20-a7b1-4712-c37b-0b5b658ef7b9",
|
||||||
|
"B01": "3a1baa20-a7b1-9847-fc9c-96d604cd1a8e",
|
||||||
|
"B02": "3a1baa20-a7b1-4ae9-e604-0601db06249c",
|
||||||
|
"B03": "3a1baa20-a7b1-8329-ea75-81ca559d9ce1",
|
||||||
|
"B04": "3a1baa20-a7b1-89c5-d96f-36e98a8f7268",
|
||||||
|
"C01": "3a1baa20-a7b1-32ec-39e6-8044733839d6",
|
||||||
|
"C02": "3a1baa20-a7b1-b573-e426-4c86040348b2",
|
||||||
|
"C03": "3a1baa20-a7b1-cca7-781e-0522b729bf5d",
|
||||||
|
"C04": "3a1baa20-a7b1-7c98-5fd9-5855355ae4b3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"大分液瓶堆栈": {
|
||||||
|
"uuid": "",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "3a19da3d-4f3d-bcac-2932-7542041e10e0",
|
||||||
|
"A02": "3a19da3d-4f3d-4d75-38ac-fb58ad0687c3",
|
||||||
|
"A03": "3a19da3d-4f3d-b25e-f2b1-85342a5b7eae",
|
||||||
|
"B01": "3a19da3d-4f3d-fd3e-058a-2733a0925767",
|
||||||
|
"B02": "3a19da3d-4f3d-37bd-a944-c391ad56857f",
|
||||||
|
"B03": "3a19da3d-4f3d-e353-7862-c6d1d4bc667f",
|
||||||
|
"C01": "3a19da3d-4f3d-9519-5da7-76179c958e70",
|
||||||
|
"C02": "3a19da3d-4f3d-b586-d7ed-9ec244f6f937",
|
||||||
|
"C03": "3a19da3d-4f3d-5061-249b-35dfef732811"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"小分液瓶堆栈": {
|
||||||
|
"uuid": "",
|
||||||
|
"site_uuids": {
|
||||||
|
"C03": "3a19da40-55bf-8943-d20d-a8b3ea0d16c0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"站内Tip头盒堆栈": {
|
||||||
|
"uuid": "",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "3a19deab-d5cc-be1e-5c37-4e9e5a664388",
|
||||||
|
"A02": "3a19deab-d5cc-b394-8141-27cb3853e8ea",
|
||||||
|
"B01": "3a19deab-d5cc-4dca-596e-ca7414d5f501",
|
||||||
|
"B02": "3a19deab-d5cc-9bc0-442b-12d9d59aa62a",
|
||||||
|
"C01": "3a19deab-d5cc-2eaf-b6a4-f0d54e4f1246",
|
||||||
|
"C02": "3a19deab-d5cc-d9f4-25df-b8018c372bc7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"配液站内配液大板仓库(无需提前上料)": {
|
||||||
|
"uuid": "",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "3a1a21dc-06af-3915-9cb9-80a9dc42f386"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"配液站内配液小板仓库(无需以前入料)": {
|
||||||
|
"uuid": "",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "3a1a21de-8e8b-7938-2d06-858b36c10e31"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"移液站内大瓶板仓库(无需提前如料)": {
|
||||||
|
"uuid": "",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "3a1a224c-c727-fa62-1f2b-0037a84b9fca"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"移液站内小瓶板仓库(无需提前入料)": {
|
||||||
|
"uuid": "",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "3a1a224d-ed49-710c-a9c3-3fc61d479cbb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"适配器位仓库": {
|
||||||
|
"uuid": "",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "3a1abd46-18fe-1f56-6ced-a1f7fe08e36c"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"1号2号手套箱交接堆栈": {
|
||||||
|
"uuid": "",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "3a1baa49-7f77-35aa-60b1-e55a45d065fa"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"2号手套箱内部堆栈": {
|
||||||
|
"uuid": "",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "3a1baa4b-393e-9f86-3921-7a18b0a8e371",
|
||||||
|
"A02": "3a1baa4b-393e-9425-928b-ee0f6f679d44",
|
||||||
|
"A03": "3a1baa4b-393e-0baf-632b-59dfdc931a3a",
|
||||||
|
"B01": "3a1baa4b-393e-f8aa-c8a9-956f3132f05c",
|
||||||
|
"B02": "3a1baa4b-393e-ef05-42f6-53f4c6e89d70",
|
||||||
|
"B03": "3a1baa4b-393e-c07b-a924-a9f0dfda9711",
|
||||||
|
"C01": "3a1baa4b-393e-4c2b-821a-16a7fe025c48",
|
||||||
|
"C02": "3a1baa4b-393e-2eaf-61a1-9063c832d5a2",
|
||||||
|
"C03": "3a1baa4b-393e-034e-8e28-8626d934a85f"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# 物料类型配置
|
# 物料类型配置
|
||||||
MATERIAL_TYPE_MAPPINGS = {
|
MATERIAL_TYPE_MAPPINGS = {
|
||||||
"烧杯": ("BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"),
|
"100ml液体": ("YB_100ml_yeti", "d37166b3-ecaa-481e-bd84-3032b795ba07"),
|
||||||
"试剂瓶": ("BIOYOND_PolymerStation_1BottleCarrier", ""),
|
"液": ("YB_ye", "3a190ca1-2add-2b23-f8e1-bbd348b7f790"),
|
||||||
"样品板": ("BIOYOND_PolymerStation_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"),
|
"高粘液": ("YB_gaonianye", "abe8df30-563d-43d2-85e0-cabec59ddc16"),
|
||||||
"分装板": ("BIOYOND_PolymerStation_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"),
|
"加样头(大)": ("YB_jia_yang_tou_da_Carrier", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
||||||
"样品瓶": ("BIOYOND_PolymerStation_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"),
|
# "加样头(大)板": ("YB_jia_yang_tou_da", "a8e714ae-2a4e-4eb9-9614-e4c140ec3f16"),
|
||||||
"90%分装小瓶": ("BIOYOND_PolymerStation_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"),
|
"5ml分液瓶板": ("YB_5ml_fenyepingban", "3a192fa4-007d-ec7b-456e-2a8be7a13f23"),
|
||||||
"10%分装小瓶": ("BIOYOND_PolymerStation_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"),
|
"5ml分液瓶": ("YB_5ml_fenyeping", "3a192c2a-ebb7-58a1-480d-8b3863bf74f4"),
|
||||||
|
"20ml分液瓶板": ("YB_20ml_fenyepingban", "3a192fa4-47db-3449-162a-eaf8aba57e27"),
|
||||||
|
"20ml分液瓶": ("YB_20ml_fenyeping", "3a192c2b-19e8-f0a3-035e-041ca8ca1035"),
|
||||||
|
"配液瓶(小)板": ("YB_peiyepingxiaoban", "3a190c8b-3284-af78-d29f-9a69463ad047"),
|
||||||
|
"配液瓶(小)": ("YB_pei_ye_xiao_Bottle", "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"),
|
||||||
|
"配液瓶(大)板": ("YB_peiyepingdaban", "53e50377-32dc-4781-b3c0-5ce45bc7dc27"),
|
||||||
|
"配液瓶(大)": ("YB_pei_ye_da_Bottle", "19c52ad1-51c5-494f-8854-576f4ca9c6ca"),
|
||||||
|
"适配器块": ("YB_shi_pei_qi_kuai", "efc3bb32-d504-4890-91c0-b64ed3ac80cf"),
|
||||||
|
"枪头盒": ("YB_qiang_tou_he", "3a192c2e-20f3-a44a-0334-c8301839d0b3"),
|
||||||
|
"枪头": ("YB_qiang_tou", "b6196971-1050-46da-9927-333e8dea062d"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# 步骤参数配置(各工作流的步骤UUID)
|
SOLID_LIQUID_MAPPINGS = {
|
||||||
WORKFLOW_STEP_IDS = {
|
# 固体
|
||||||
"reactor_taken_in": {
|
"LiDFOB": {
|
||||||
"config": ""
|
"typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469",
|
||||||
|
"code": "",
|
||||||
|
"barCode": "",
|
||||||
|
"name": "LiDFOB",
|
||||||
|
"unit": "g",
|
||||||
|
"parameters": "",
|
||||||
|
"quantity": "2",
|
||||||
|
"warningQuantity": "1",
|
||||||
|
"details": []
|
||||||
},
|
},
|
||||||
"liquid_feeding_beaker": {
|
# "LiPF6": {
|
||||||
"liquid": "",
|
# "typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469",
|
||||||
"observe": ""
|
# "code": "",
|
||||||
},
|
# "barCode": "",
|
||||||
"liquid_feeding_vials_non_titration": {
|
# "name": "LiPF6",
|
||||||
"liquid": "",
|
# "unit": "g",
|
||||||
"observe": ""
|
# "parameters": "",
|
||||||
},
|
# "quantity": 2,
|
||||||
"liquid_feeding_solvents": {
|
# "warningQuantity": 1,
|
||||||
"liquid": "",
|
# "details": []
|
||||||
"observe": ""
|
# },
|
||||||
},
|
# "LiFSI": {
|
||||||
"solid_feeding_vials": {
|
# "typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469",
|
||||||
"feeding": "",
|
# "code": "",
|
||||||
"observe": ""
|
# "barCode": "",
|
||||||
},
|
# "name": "LiFSI",
|
||||||
"liquid_feeding_titration": {
|
# "unit": "g",
|
||||||
"liquid": "",
|
# "parameters": "",
|
||||||
"observe": ""
|
# "quantity": 2,
|
||||||
},
|
# "warningQuantity": 1,
|
||||||
"drip_back": {
|
# "details": []
|
||||||
"liquid": "",
|
# },
|
||||||
"observe": ""
|
# "DTC": {
|
||||||
}
|
# "typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469",
|
||||||
|
# "code": "",
|
||||||
|
# "barCode": "",
|
||||||
|
# "name": "DTC",
|
||||||
|
# "unit": "g",
|
||||||
|
# "parameters": "",
|
||||||
|
# "quantity": 2,
|
||||||
|
# "warningQuantity": 1,
|
||||||
|
# "details": []
|
||||||
|
# },
|
||||||
|
# "LiPO2F2": {
|
||||||
|
# "typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469",
|
||||||
|
# "code": "",
|
||||||
|
# "barCode": "",
|
||||||
|
# "name": "LiPO2F2",
|
||||||
|
# "unit": "g",
|
||||||
|
# "parameters": "",
|
||||||
|
# "quantity": 2,
|
||||||
|
# "warningQuantity": 1,
|
||||||
|
# "details": []
|
||||||
|
# },
|
||||||
|
# 液体
|
||||||
|
# "SA": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
||||||
|
# "EC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
||||||
|
# "VC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
||||||
|
# "AND": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
||||||
|
# "HTCN": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
||||||
|
# "DENE": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
||||||
|
# "TMSP": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
||||||
|
# "TMSB": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
||||||
|
# "EP": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
||||||
|
# "DEC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
||||||
|
# "EMC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
||||||
|
# "SN": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
||||||
|
# "DMC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
||||||
|
# "FEC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WORKFLOW_MAPPINGS = {}
|
||||||
|
|
||||||
LOCATION_MAPPING = {}
|
LOCATION_MAPPING = {}
|
||||||
|
|
||||||
ACTION_NAMES = {}
|
|
||||||
|
|
||||||
HTTP_SERVICE_CONFIG = {}
|
|
||||||
@@ -1,25 +1,8 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import json
|
import json
|
||||||
import time
|
|
||||||
from typing import Optional, Dict, Any, List
|
|
||||||
from typing_extensions import TypedDict
|
|
||||||
import requests
|
|
||||||
from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG
|
|
||||||
|
|
||||||
from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondException
|
from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondException
|
||||||
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation
|
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation
|
||||||
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
import importlib
|
|
||||||
|
|
||||||
class ComputeExperimentDesignReturn(TypedDict):
|
|
||||||
solutions: list
|
|
||||||
titration: dict
|
|
||||||
solvents: dict
|
|
||||||
feeding_order: list
|
|
||||||
return_info: str
|
|
||||||
|
|
||||||
|
|
||||||
class BioyondDispensingStation(BioyondWorkstation):
|
class BioyondDispensingStation(BioyondWorkstation):
|
||||||
@@ -40,111 +23,6 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
# self._logger = SimpleLogger()
|
# self._logger = SimpleLogger()
|
||||||
# self.is_running = False
|
# self.is_running = False
|
||||||
|
|
||||||
# 用于跟踪任务完成状态的字典: {orderCode: {status, order_id, timestamp}}
|
|
||||||
self.order_completion_status = {}
|
|
||||||
|
|
||||||
def _post_project_api(self, endpoint: str, data: Any) -> Dict[str, Any]:
|
|
||||||
"""项目接口通用POST调用
|
|
||||||
|
|
||||||
参数:
|
|
||||||
endpoint: 接口路径(例如 /api/lims/order/brief-step-paramerers)
|
|
||||||
data: 请求体中的 data 字段内容
|
|
||||||
|
|
||||||
返回:
|
|
||||||
dict: 服务端响应,失败时返回 {code:0,message,...}
|
|
||||||
"""
|
|
||||||
request_data = {
|
|
||||||
"apiKey": API_CONFIG["api_key"],
|
|
||||||
"requestTime": self.hardware_interface.get_current_time_iso8601(),
|
|
||||||
"data": data
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
response = requests.post(
|
|
||||||
f"{self.hardware_interface.host}{endpoint}",
|
|
||||||
json=request_data,
|
|
||||||
headers={"Content-Type": "application/json"},
|
|
||||||
timeout=30
|
|
||||||
)
|
|
||||||
result = response.json()
|
|
||||||
return result if isinstance(result, dict) else {"code": 0, "message": "非JSON响应"}
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
return {"code": 0, "message": "非JSON响应"}
|
|
||||||
except requests.exceptions.Timeout:
|
|
||||||
return {"code": 0, "message": "请求超时"}
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
return {"code": 0, "message": str(e)}
|
|
||||||
|
|
||||||
def _delete_project_api(self, endpoint: str, data: Any) -> Dict[str, Any]:
|
|
||||||
"""项目接口通用DELETE调用
|
|
||||||
|
|
||||||
参数:
|
|
||||||
endpoint: 接口路径(例如 /api/lims/order/workflows)
|
|
||||||
data: 请求体中的 data 字段内容
|
|
||||||
|
|
||||||
返回:
|
|
||||||
dict: 服务端响应,失败时返回 {code:0,message,...}
|
|
||||||
"""
|
|
||||||
request_data = {
|
|
||||||
"apiKey": API_CONFIG["api_key"],
|
|
||||||
"requestTime": self.hardware_interface.get_current_time_iso8601(),
|
|
||||||
"data": data
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
response = requests.delete(
|
|
||||||
f"{self.hardware_interface.host}{endpoint}",
|
|
||||||
json=request_data,
|
|
||||||
headers={"Content-Type": "application/json"},
|
|
||||||
timeout=30
|
|
||||||
)
|
|
||||||
result = response.json()
|
|
||||||
return result if isinstance(result, dict) else {"code": 0, "message": "非JSON响应"}
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
return {"code": 0, "message": "非JSON响应"}
|
|
||||||
except requests.exceptions.Timeout:
|
|
||||||
return {"code": 0, "message": "请求超时"}
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
return {"code": 0, "message": str(e)}
|
|
||||||
|
|
||||||
def compute_experiment_design(
|
|
||||||
self,
|
|
||||||
ratio: dict,
|
|
||||||
wt_percent: str = "0.25",
|
|
||||||
m_tot: str = "70",
|
|
||||||
titration_percent: str = "0.03",
|
|
||||||
) -> ComputeExperimentDesignReturn:
|
|
||||||
try:
|
|
||||||
if isinstance(ratio, str):
|
|
||||||
try:
|
|
||||||
ratio = json.loads(ratio)
|
|
||||||
except Exception:
|
|
||||||
ratio = {}
|
|
||||||
root = str(Path(__file__).resolve().parents[3])
|
|
||||||
if root not in sys.path:
|
|
||||||
sys.path.append(root)
|
|
||||||
try:
|
|
||||||
mod = importlib.import_module("tem.compute")
|
|
||||||
except Exception as e:
|
|
||||||
raise BioyondException(f"无法导入计算模块: {e}")
|
|
||||||
try:
|
|
||||||
wp = float(wt_percent) if isinstance(wt_percent, str) else wt_percent
|
|
||||||
mt = float(m_tot) if isinstance(m_tot, str) else m_tot
|
|
||||||
tp = float(titration_percent) if isinstance(titration_percent, str) else titration_percent
|
|
||||||
except Exception as e:
|
|
||||||
raise BioyondException(f"参数解析失败: {e}")
|
|
||||||
res = mod.generate_experiment_design(ratio=ratio, wt_percent=wp, m_tot=mt, titration_percent=tp)
|
|
||||||
out = {
|
|
||||||
"solutions": res.get("solutions", []),
|
|
||||||
"titration": res.get("titration", {}),
|
|
||||||
"solvents": res.get("solvents", {}),
|
|
||||||
"feeding_order": res.get("feeding_order", []),
|
|
||||||
"return_info": json.dumps(res, ensure_ascii=False)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
except BioyondException:
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
raise BioyondException(str(e))
|
|
||||||
|
|
||||||
# 90%10%小瓶投料任务创建方法
|
# 90%10%小瓶投料任务创建方法
|
||||||
def create_90_10_vial_feeding_task(self,
|
def create_90_10_vial_feeding_task(self,
|
||||||
order_name: str = None,
|
order_name: str = None,
|
||||||
@@ -392,45 +270,7 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
# 7. 调用create_order方法创建任务
|
# 7. 调用create_order方法创建任务
|
||||||
result = self.hardware_interface.create_order(json_str)
|
result = self.hardware_interface.create_order(json_str)
|
||||||
self.hardware_interface._logger.info(f"创建90%10%小瓶投料任务结果: {result}")
|
self.hardware_interface._logger.info(f"创建90%10%小瓶投料任务结果: {result}")
|
||||||
|
return json.dumps({"suc": True})
|
||||||
# 8. 解析结果获取order_id
|
|
||||||
order_id = None
|
|
||||||
if isinstance(result, str):
|
|
||||||
# result 格式: "{'3a1d895c-4d39-d504-1398-18f5a40bac1e': [{'id': '...', ...}]}"
|
|
||||||
# 第一个键就是order_id (UUID)
|
|
||||||
try:
|
|
||||||
# 尝试解析字符串为字典
|
|
||||||
import ast
|
|
||||||
result_dict = ast.literal_eval(result)
|
|
||||||
# 获取第一个键作为order_id
|
|
||||||
if result_dict and isinstance(result_dict, dict):
|
|
||||||
first_key = list(result_dict.keys())[0]
|
|
||||||
order_id = first_key
|
|
||||||
self.hardware_interface._logger.info(f"✓ 成功提取order_id: {order_id}")
|
|
||||||
else:
|
|
||||||
self.hardware_interface._logger.warning(f"result_dict格式异常: {result_dict}")
|
|
||||||
except Exception as e:
|
|
||||||
self.hardware_interface._logger.error(f"✗ 无法从结果中提取order_id: {e}, result类型={type(result)}")
|
|
||||||
elif isinstance(result, dict):
|
|
||||||
# 如果已经是字典
|
|
||||||
if result:
|
|
||||||
first_key = list(result.keys())[0]
|
|
||||||
order_id = first_key
|
|
||||||
self.hardware_interface._logger.info(f"✓ 成功提取order_id(dict): {order_id}")
|
|
||||||
|
|
||||||
if not order_id:
|
|
||||||
self.hardware_interface._logger.warning(
|
|
||||||
f"⚠ 未能提取order_id,result={result[:100] if isinstance(result, str) else result}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 返回成功结果和构建的JSON数据
|
|
||||||
return json.dumps({
|
|
||||||
"suc": True,
|
|
||||||
"order_code": order_code,
|
|
||||||
"order_id": order_id,
|
|
||||||
"result": result,
|
|
||||||
"order_params": order_data
|
|
||||||
})
|
|
||||||
|
|
||||||
except BioyondException:
|
except BioyondException:
|
||||||
# 重新抛出BioyondException
|
# 重新抛出BioyondException
|
||||||
@@ -558,37 +398,7 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
result = self.hardware_interface.create_order(json_str)
|
result = self.hardware_interface.create_order(json_str)
|
||||||
self.hardware_interface._logger.info(f"创建二胺溶液配置任务结果: {result}")
|
self.hardware_interface._logger.info(f"创建二胺溶液配置任务结果: {result}")
|
||||||
|
|
||||||
# 8. 解析结果获取order_id
|
return json.dumps({"suc": True})
|
||||||
order_id = None
|
|
||||||
if isinstance(result, str):
|
|
||||||
try:
|
|
||||||
import ast
|
|
||||||
result_dict = ast.literal_eval(result)
|
|
||||||
if result_dict and isinstance(result_dict, dict):
|
|
||||||
first_key = list(result_dict.keys())[0]
|
|
||||||
order_id = first_key
|
|
||||||
self.hardware_interface._logger.info(f"✓ 成功提取order_id: {order_id}")
|
|
||||||
else:
|
|
||||||
self.hardware_interface._logger.warning(f"result_dict格式异常: {result_dict}")
|
|
||||||
except Exception as e:
|
|
||||||
self.hardware_interface._logger.error(f"✗ 无法从结果中提取order_id: {e}")
|
|
||||||
elif isinstance(result, dict):
|
|
||||||
if result:
|
|
||||||
first_key = list(result.keys())[0]
|
|
||||||
order_id = first_key
|
|
||||||
self.hardware_interface._logger.info(f"✓ 成功提取order_id(dict): {order_id}")
|
|
||||||
|
|
||||||
if not order_id:
|
|
||||||
self.hardware_interface._logger.warning(f"⚠ 未能提取order_id")
|
|
||||||
|
|
||||||
# 返回成功结果和构建的JSON数据
|
|
||||||
return json.dumps({
|
|
||||||
"suc": True,
|
|
||||||
"order_code": order_code,
|
|
||||||
"order_id": order_id,
|
|
||||||
"result": result,
|
|
||||||
"order_params": order_data
|
|
||||||
})
|
|
||||||
|
|
||||||
except BioyondException:
|
except BioyondException:
|
||||||
# 重新抛出BioyondException
|
# 重新抛出BioyondException
|
||||||
@@ -689,24 +499,15 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
hold_m_name=hold_m_name
|
hold_m_name=hold_m_name
|
||||||
)
|
)
|
||||||
|
|
||||||
# 解析返回结果以获取order_code和order_id
|
|
||||||
result_data = json.loads(result) if isinstance(result, str) else result
|
|
||||||
order_code = result_data.get("order_code")
|
|
||||||
order_id = result_data.get("order_id")
|
|
||||||
order_params = result_data.get("order_params", {})
|
|
||||||
|
|
||||||
results.append({
|
results.append({
|
||||||
"index": idx + 1,
|
"index": idx + 1,
|
||||||
"name": name,
|
"name": name,
|
||||||
"success": True,
|
"success": True,
|
||||||
"order_code": order_code,
|
"hold_m_name": hold_m_name
|
||||||
"order_id": order_id,
|
|
||||||
"hold_m_name": hold_m_name,
|
|
||||||
"order_params": order_params
|
|
||||||
})
|
})
|
||||||
success_count += 1
|
success_count += 1
|
||||||
self.hardware_interface._logger.info(
|
self.hardware_interface._logger.info(
|
||||||
f"成功创建二胺溶液配置任务: {name}, order_code={order_code}, order_id={order_id}"
|
f"成功创建二胺溶液配置任务: {name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
except BioyondException as e:
|
except BioyondException as e:
|
||||||
@@ -732,17 +533,11 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
f"创建第 {idx + 1} 个任务时发生未知错误: {str(e)}"
|
f"创建第 {idx + 1} 个任务时发生未知错误: {str(e)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 提取所有成功任务的order_code和order_id
|
|
||||||
order_codes = [r["order_code"] for r in results if r["success"]]
|
|
||||||
order_ids = [r["order_id"] for r in results if r["success"]]
|
|
||||||
|
|
||||||
# 返回汇总结果
|
# 返回汇总结果
|
||||||
summary = {
|
summary = {
|
||||||
"total": len(solutions),
|
"total": len(solutions),
|
||||||
"success": success_count,
|
"success": success_count,
|
||||||
"failed": failed_count,
|
"failed": failed_count,
|
||||||
"order_codes": order_codes,
|
|
||||||
"order_ids": order_ids,
|
|
||||||
"details": results
|
"details": results
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -751,13 +546,8 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
f"成功={success_count}, 失败={failed_count}"
|
f"成功={success_count}, 失败={failed_count}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 构建返回结果
|
# 返回JSON字符串格式
|
||||||
summary["return_info"] = {
|
return json.dumps(summary, ensure_ascii=False)
|
||||||
"order_codes": order_codes,
|
|
||||||
"order_ids": order_ids,
|
|
||||||
}
|
|
||||||
|
|
||||||
return summary
|
|
||||||
|
|
||||||
except BioyondException:
|
except BioyondException:
|
||||||
raise
|
raise
|
||||||
@@ -766,40 +556,6 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
self.hardware_interface._logger.error(error_msg)
|
self.hardware_interface._logger.error(error_msg)
|
||||||
raise BioyondException(error_msg)
|
raise BioyondException(error_msg)
|
||||||
|
|
||||||
def brief_step_parameters(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
"""获取简要步骤参数(站点项目接口)
|
|
||||||
|
|
||||||
参数:
|
|
||||||
data: 查询参数字典
|
|
||||||
|
|
||||||
返回值:
|
|
||||||
dict: 接口返回数据
|
|
||||||
"""
|
|
||||||
return self._post_project_api("/api/lims/order/brief-step-paramerers", data)
|
|
||||||
|
|
||||||
def project_order_report(self, order_id: str) -> Dict[str, Any]:
|
|
||||||
"""查询项目端订单报告(兼容旧路径)
|
|
||||||
|
|
||||||
参数:
|
|
||||||
order_id: 订单ID
|
|
||||||
|
|
||||||
返回值:
|
|
||||||
dict: 报告数据
|
|
||||||
"""
|
|
||||||
return self._post_project_api("/api/lims/order/project-order-report", order_id)
|
|
||||||
|
|
||||||
def workflow_sample_locations(self, workflow_id: str) -> Dict[str, Any]:
|
|
||||||
"""查询工作流样品库位(站点项目接口)
|
|
||||||
|
|
||||||
参数:
|
|
||||||
workflow_id: 工作流ID
|
|
||||||
|
|
||||||
返回值:
|
|
||||||
dict: 位置信息数据
|
|
||||||
"""
|
|
||||||
return self._post_project_api("/api/lims/storage/workflow-sample-locations", workflow_id)
|
|
||||||
|
|
||||||
|
|
||||||
# 批量创建90%10%小瓶投料任务
|
# 批量创建90%10%小瓶投料任务
|
||||||
def batch_create_90_10_vial_feeding_tasks(self,
|
def batch_create_90_10_vial_feeding_tasks(self,
|
||||||
titration,
|
titration,
|
||||||
@@ -857,15 +613,22 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
if not all([name, main_portion is not None, titration_portion is not None, titration_solvent is not None]):
|
if not all([name, main_portion is not None, titration_portion is not None, titration_solvent is not None]):
|
||||||
raise BioyondException("titration 数据缺少必要参数")
|
raise BioyondException("titration 数据缺少必要参数")
|
||||||
|
|
||||||
|
# 将main_portion平均分成3份作为90%物料(3个小瓶)
|
||||||
|
portion_90 = main_portion / 3
|
||||||
|
|
||||||
# 调用单个任务创建方法
|
# 调用单个任务创建方法
|
||||||
result = self.create_90_10_vial_feeding_task(
|
result = self.create_90_10_vial_feeding_task(
|
||||||
order_name=f"90%10%小瓶投料-{name}",
|
order_name=f"90%10%小瓶投料-{name}",
|
||||||
speed=speed,
|
speed=speed,
|
||||||
temperature=temperature,
|
temperature=temperature,
|
||||||
delay_time=delay_time,
|
delay_time=delay_time,
|
||||||
# 90%物料 - 主称固体直接使用main_portion
|
# 90%物料 - 主称固体平均分成3份
|
||||||
percent_90_1_assign_material_name=name,
|
percent_90_1_assign_material_name=name,
|
||||||
percent_90_1_target_weigh=str(round(main_portion, 6)),
|
percent_90_1_target_weigh=str(round(portion_90, 6)),
|
||||||
|
percent_90_2_assign_material_name=name,
|
||||||
|
percent_90_2_target_weigh=str(round(portion_90, 6)),
|
||||||
|
percent_90_3_assign_material_name=name,
|
||||||
|
percent_90_3_target_weigh=str(round(portion_90, 6)),
|
||||||
# 10%物料 - 滴定固体 + 滴定溶剂(只使用第1个10%小瓶)
|
# 10%物料 - 滴定固体 + 滴定溶剂(只使用第1个10%小瓶)
|
||||||
percent_10_1_assign_material_name=name,
|
percent_10_1_assign_material_name=name,
|
||||||
percent_10_1_target_weigh=str(round(titration_portion, 6)),
|
percent_10_1_target_weigh=str(round(titration_portion, 6)),
|
||||||
@@ -874,54 +637,29 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
hold_m_name=hold_m_name
|
hold_m_name=hold_m_name
|
||||||
)
|
)
|
||||||
|
|
||||||
# 解析返回结果以获取order_code和order_id
|
summary = {
|
||||||
result_data = json.loads(result) if isinstance(result, str) else result
|
|
||||||
order_code = result_data.get("order_code")
|
|
||||||
order_id = result_data.get("order_id")
|
|
||||||
order_params = result_data.get("order_params", {})
|
|
||||||
|
|
||||||
# 构建详细信息(保持原有结构)
|
|
||||||
detail = {
|
|
||||||
"index": 1,
|
|
||||||
"name": name,
|
|
||||||
"success": True,
|
"success": True,
|
||||||
"order_code": order_code,
|
|
||||||
"order_id": order_id,
|
|
||||||
"hold_m_name": hold_m_name,
|
"hold_m_name": hold_m_name,
|
||||||
|
"material_name": name,
|
||||||
"90_vials": {
|
"90_vials": {
|
||||||
"count": 1,
|
"count": 3,
|
||||||
"weight_per_vial": round(main_portion, 6),
|
"weight_per_vial": round(portion_90, 6),
|
||||||
"total_weight": round(main_portion, 6)
|
"total_weight": round(main_portion, 6)
|
||||||
},
|
},
|
||||||
"10_vials": {
|
"10_vials": {
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"solid_weight": round(titration_portion, 6),
|
"solid_weight": round(titration_portion, 6),
|
||||||
"liquid_volume": round(titration_solvent, 6)
|
"liquid_volume": round(titration_solvent, 6)
|
||||||
},
|
}
|
||||||
"order_params": order_params
|
|
||||||
}
|
|
||||||
|
|
||||||
# 构建批量结果格式(与diamine_solution_tasks保持一致)
|
|
||||||
summary = {
|
|
||||||
"total": 1,
|
|
||||||
"success": 1,
|
|
||||||
"failed": 0,
|
|
||||||
"order_codes": [order_code],
|
|
||||||
"order_ids": [order_id],
|
|
||||||
"details": [detail]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.hardware_interface._logger.info(
|
self.hardware_interface._logger.info(
|
||||||
f"成功创建90%10%小瓶投料任务: {name}, order_code={order_code}, order_id={order_id}"
|
f"成功创建90%10%小瓶投料任务: {hold_m_name}, "
|
||||||
|
f"90%物料={portion_90:.6f}g×3, 10%物料={titration_portion:.6f}g+{titration_solvent:.6f}mL"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 构建返回结果
|
# 返回JSON字符串格式
|
||||||
summary["return_info"] = {
|
return json.dumps(summary, ensure_ascii=False)
|
||||||
"order_codes": [order_code],
|
|
||||||
"order_ids": [order_id],
|
|
||||||
}
|
|
||||||
|
|
||||||
return summary
|
|
||||||
|
|
||||||
except BioyondException:
|
except BioyondException:
|
||||||
raise
|
raise
|
||||||
@@ -930,571 +668,6 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
self.hardware_interface._logger.error(error_msg)
|
self.hardware_interface._logger.error(error_msg)
|
||||||
raise BioyondException(error_msg)
|
raise BioyondException(error_msg)
|
||||||
|
|
||||||
def _extract_actuals_from_report(self, report) -> Dict[str, Any]:
|
|
||||||
data = report.get('data') if isinstance(report, dict) else None
|
|
||||||
actual_target_weigh = None
|
|
||||||
actual_volume = None
|
|
||||||
if data:
|
|
||||||
extra = data.get('extraProperties') or {}
|
|
||||||
if isinstance(extra, dict):
|
|
||||||
for v in extra.values():
|
|
||||||
obj = None
|
|
||||||
try:
|
|
||||||
obj = json.loads(v) if isinstance(v, str) else v
|
|
||||||
except Exception:
|
|
||||||
obj = None
|
|
||||||
if isinstance(obj, dict):
|
|
||||||
tw = obj.get('targetWeigh')
|
|
||||||
vol = obj.get('volume')
|
|
||||||
if tw is not None:
|
|
||||||
try:
|
|
||||||
actual_target_weigh = float(tw)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if vol is not None:
|
|
||||||
try:
|
|
||||||
actual_volume = float(vol)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return {
|
|
||||||
'actualTargetWeigh': actual_target_weigh,
|
|
||||||
'actualVolume': actual_volume
|
|
||||||
}
|
|
||||||
|
|
||||||
# 等待多个任务完成并获取实验报告
|
|
||||||
def wait_for_multiple_orders_and_get_reports(self,
|
|
||||||
batch_create_result: str = None,
|
|
||||||
timeout: int = 7200,
|
|
||||||
check_interval: int = 10) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
同时等待多个任务完成并获取实验报告
|
|
||||||
|
|
||||||
参数说明:
|
|
||||||
- batch_create_result: 批量创建任务的返回结果JSON字符串,包含order_codes和order_ids数组
|
|
||||||
- timeout: 超时时间(秒),默认7200秒(2小时)
|
|
||||||
- check_interval: 检查间隔(秒),默认10秒
|
|
||||||
|
|
||||||
返回: 包含所有任务状态和报告的字典
|
|
||||||
{
|
|
||||||
"total": 2,
|
|
||||||
"completed": 2,
|
|
||||||
"timeout": 0,
|
|
||||||
"elapsed_time": 120.5,
|
|
||||||
"reports": [
|
|
||||||
{
|
|
||||||
"order_code": "task_vial_1",
|
|
||||||
"order_id": "uuid1",
|
|
||||||
"status": "completed",
|
|
||||||
"completion_status": 30,
|
|
||||||
"report": {...}
|
|
||||||
},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
异常:
|
|
||||||
- BioyondException: 所有任务都超时或发生错误
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# 参数类型转换
|
|
||||||
timeout = int(timeout) if timeout else 7200
|
|
||||||
check_interval = int(check_interval) if check_interval else 10
|
|
||||||
|
|
||||||
# 验证batch_create_result参数
|
|
||||||
if not batch_create_result or batch_create_result == "":
|
|
||||||
raise BioyondException("batch_create_result参数为空,请确保从batch_create节点正确连接handle")
|
|
||||||
|
|
||||||
# 解析batch_create_result JSON对象
|
|
||||||
try:
|
|
||||||
# 清理可能存在的截断标记 [...]
|
|
||||||
if isinstance(batch_create_result, str) and '[...]' in batch_create_result:
|
|
||||||
batch_create_result = batch_create_result.replace('[...]', '[]')
|
|
||||||
|
|
||||||
result_obj = json.loads(batch_create_result) if isinstance(batch_create_result, str) else batch_create_result
|
|
||||||
|
|
||||||
# 兼容外层包装格式 {error, suc, return_value}
|
|
||||||
if isinstance(result_obj, dict) and "return_value" in result_obj:
|
|
||||||
inner = result_obj.get("return_value")
|
|
||||||
if isinstance(inner, str):
|
|
||||||
result_obj = json.loads(inner)
|
|
||||||
elif isinstance(inner, dict):
|
|
||||||
result_obj = inner
|
|
||||||
|
|
||||||
# 从summary对象中提取order_codes和order_ids
|
|
||||||
order_codes = result_obj.get("order_codes", [])
|
|
||||||
order_ids = result_obj.get("order_ids", [])
|
|
||||||
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
raise BioyondException(f"解析batch_create_result失败: {e}")
|
|
||||||
except Exception as e:
|
|
||||||
raise BioyondException(f"处理batch_create_result时出错: {e}")
|
|
||||||
|
|
||||||
# 验证提取的数据
|
|
||||||
if not order_codes:
|
|
||||||
raise BioyondException("batch_create_result中未找到order_codes字段或为空")
|
|
||||||
if not order_ids:
|
|
||||||
raise BioyondException("batch_create_result中未找到order_ids字段或为空")
|
|
||||||
|
|
||||||
# 确保order_codes和order_ids是列表类型
|
|
||||||
if not isinstance(order_codes, list):
|
|
||||||
order_codes = [order_codes] if order_codes else []
|
|
||||||
if not isinstance(order_ids, list):
|
|
||||||
order_ids = [order_ids] if order_ids else []
|
|
||||||
|
|
||||||
codes_list = order_codes
|
|
||||||
ids_list = order_ids
|
|
||||||
|
|
||||||
if len(codes_list) != len(ids_list):
|
|
||||||
raise BioyondException(
|
|
||||||
f"order_codes数量({len(codes_list)})与order_ids数量({len(ids_list)})不匹配"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not codes_list or not ids_list:
|
|
||||||
raise BioyondException("order_codes和order_ids不能为空")
|
|
||||||
|
|
||||||
# 初始化跟踪变量
|
|
||||||
total = len(codes_list)
|
|
||||||
pending_orders = {code: {"order_id": ids_list[i], "completed": False}
|
|
||||||
for i, code in enumerate(codes_list)}
|
|
||||||
reports = []
|
|
||||||
|
|
||||||
start_time = time.time()
|
|
||||||
self.hardware_interface._logger.info(
|
|
||||||
f"开始等待 {total} 个任务完成: {', '.join(codes_list)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 轮询检查任务状态
|
|
||||||
while pending_orders:
|
|
||||||
elapsed_time = time.time() - start_time
|
|
||||||
|
|
||||||
# 检查超时
|
|
||||||
if elapsed_time > timeout:
|
|
||||||
# 收集超时任务
|
|
||||||
timeout_orders = list(pending_orders.keys())
|
|
||||||
self.hardware_interface._logger.error(
|
|
||||||
f"等待任务完成超时,剩余未完成任务: {', '.join(timeout_orders)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 为超时任务添加记录
|
|
||||||
for order_code in timeout_orders:
|
|
||||||
reports.append({
|
|
||||||
"order_code": order_code,
|
|
||||||
"order_id": pending_orders[order_code]["order_id"],
|
|
||||||
"status": "timeout",
|
|
||||||
"completion_status": None,
|
|
||||||
"report": None,
|
|
||||||
"extracted": None,
|
|
||||||
"elapsed_time": elapsed_time
|
|
||||||
})
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
# 检查每个待完成的任务
|
|
||||||
completed_in_this_round = []
|
|
||||||
for order_code in list(pending_orders.keys()):
|
|
||||||
order_id = pending_orders[order_code]["order_id"]
|
|
||||||
|
|
||||||
# 检查任务是否完成
|
|
||||||
if order_code in self.order_completion_status:
|
|
||||||
completion_info = self.order_completion_status[order_code]
|
|
||||||
self.hardware_interface._logger.info(
|
|
||||||
f"检测到任务 {order_code} 已完成,状态: {completion_info.get('status')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 获取实验报告
|
|
||||||
try:
|
|
||||||
report = self.project_order_report(order_id)
|
|
||||||
|
|
||||||
if not report:
|
|
||||||
self.hardware_interface._logger.warning(
|
|
||||||
f"任务 {order_code} 已完成但无法获取报告"
|
|
||||||
)
|
|
||||||
report = {"error": "无法获取报告"}
|
|
||||||
else:
|
|
||||||
self.hardware_interface._logger.info(
|
|
||||||
f"成功获取任务 {order_code} 的实验报告"
|
|
||||||
)
|
|
||||||
|
|
||||||
reports.append({
|
|
||||||
"order_code": order_code,
|
|
||||||
"order_id": order_id,
|
|
||||||
"status": "completed",
|
|
||||||
"completion_status": completion_info.get('status'),
|
|
||||||
"report": report,
|
|
||||||
"extracted": self._extract_actuals_from_report(report),
|
|
||||||
"elapsed_time": elapsed_time
|
|
||||||
})
|
|
||||||
|
|
||||||
# 标记为已完成
|
|
||||||
completed_in_this_round.append(order_code)
|
|
||||||
|
|
||||||
# 清理完成状态记录
|
|
||||||
del self.order_completion_status[order_code]
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.hardware_interface._logger.error(
|
|
||||||
f"查询任务 {order_code} 报告失败: {str(e)}"
|
|
||||||
)
|
|
||||||
reports.append({
|
|
||||||
"order_code": order_code,
|
|
||||||
"order_id": order_id,
|
|
||||||
"status": "error",
|
|
||||||
"completion_status": completion_info.get('status'),
|
|
||||||
"report": None,
|
|
||||||
"extracted": None,
|
|
||||||
"error": str(e),
|
|
||||||
"elapsed_time": elapsed_time
|
|
||||||
})
|
|
||||||
completed_in_this_round.append(order_code)
|
|
||||||
|
|
||||||
# 从待完成列表中移除已完成的任务
|
|
||||||
for order_code in completed_in_this_round:
|
|
||||||
del pending_orders[order_code]
|
|
||||||
|
|
||||||
# 如果还有待完成的任务,等待后继续
|
|
||||||
if pending_orders:
|
|
||||||
time.sleep(check_interval)
|
|
||||||
|
|
||||||
# 每分钟记录一次等待状态
|
|
||||||
new_elapsed_time = time.time() - start_time
|
|
||||||
if int(new_elapsed_time) % 60 == 0 and new_elapsed_time > 0:
|
|
||||||
self.hardware_interface._logger.info(
|
|
||||||
f"批量等待任务中... 已完成 {len(reports)}/{total}, "
|
|
||||||
f"待完成: {', '.join(pending_orders.keys())}, "
|
|
||||||
f"已等待 {int(new_elapsed_time/60)} 分钟"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 统计结果
|
|
||||||
completed_count = sum(1 for r in reports if r['status'] == 'completed')
|
|
||||||
timeout_count = sum(1 for r in reports if r['status'] == 'timeout')
|
|
||||||
error_count = sum(1 for r in reports if r['status'] == 'error')
|
|
||||||
|
|
||||||
final_elapsed_time = time.time() - start_time
|
|
||||||
|
|
||||||
summary = {
|
|
||||||
"total": total,
|
|
||||||
"completed": completed_count,
|
|
||||||
"timeout": timeout_count,
|
|
||||||
"error": error_count,
|
|
||||||
"elapsed_time": round(final_elapsed_time, 2),
|
|
||||||
"reports": reports
|
|
||||||
}
|
|
||||||
|
|
||||||
self.hardware_interface._logger.info(
|
|
||||||
f"批量等待任务完成: 总数={total}, 成功={completed_count}, "
|
|
||||||
f"超时={timeout_count}, 错误={error_count}, 耗时={final_elapsed_time:.1f}秒"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 返回字典格式,在顶层包含统计信息
|
|
||||||
return {
|
|
||||||
"return_info": json.dumps(summary, ensure_ascii=False)
|
|
||||||
}
|
|
||||||
|
|
||||||
except BioyondException:
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = f"批量等待任务完成时发生未预期的错误: {str(e)}"
|
|
||||||
self.hardware_interface._logger.error(error_msg)
|
|
||||||
raise BioyondException(error_msg)
|
|
||||||
|
|
||||||
def process_order_finish_report(self, report_request, used_materials) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
重写父类方法,处理任务完成报送并记录到 order_completion_status
|
|
||||||
|
|
||||||
Args:
|
|
||||||
report_request: WorkstationReportRequest 对象,包含任务完成信息
|
|
||||||
used_materials: 物料使用记录列表
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, Any]: 处理结果
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# 调用父类方法
|
|
||||||
result = super().process_order_finish_report(report_request, used_materials)
|
|
||||||
|
|
||||||
# 记录任务完成状态
|
|
||||||
data = report_request.data
|
|
||||||
order_code = data.get('orderCode')
|
|
||||||
|
|
||||||
if order_code:
|
|
||||||
self.order_completion_status[order_code] = {
|
|
||||||
'status': data.get('status'),
|
|
||||||
'order_name': data.get('orderName'),
|
|
||||||
'timestamp': datetime.now().isoformat(),
|
|
||||||
'start_time': data.get('startTime'),
|
|
||||||
'end_time': data.get('endTime')
|
|
||||||
}
|
|
||||||
|
|
||||||
self.hardware_interface._logger.info(
|
|
||||||
f"已记录任务完成状态: {order_code}, status={data.get('status')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.hardware_interface._logger.error(f"处理任务完成报送失败: {e}")
|
|
||||||
return {"processed": False, "error": str(e)}
|
|
||||||
|
|
||||||
def transfer_materials_to_reaction_station(
|
|
||||||
self,
|
|
||||||
target_device_id: str,
|
|
||||||
transfer_groups: list
|
|
||||||
) -> dict:
|
|
||||||
"""
|
|
||||||
将配液站完成的物料转移到指定反应站的堆栈库位
|
|
||||||
支持多组转移任务,每组包含物料名称、目标堆栈和目标库位
|
|
||||||
|
|
||||||
Args:
|
|
||||||
target_device_id: 目标反应站设备ID(所有转移组使用同一个设备)
|
|
||||||
transfer_groups: 转移任务组列表,每组包含:
|
|
||||||
- materials: 物料名称(字符串,将通过RPC查询)
|
|
||||||
- target_stack: 目标堆栈名称(如"堆栈1左")
|
|
||||||
- target_sites: 目标库位(如"A01")
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: 转移结果
|
|
||||||
{
|
|
||||||
"success": bool,
|
|
||||||
"total_groups": int,
|
|
||||||
"successful_groups": int,
|
|
||||||
"failed_groups": int,
|
|
||||||
"target_device_id": str,
|
|
||||||
"details": [...]
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# 验证参数
|
|
||||||
if not target_device_id:
|
|
||||||
raise ValueError("目标设备ID不能为空")
|
|
||||||
|
|
||||||
if not transfer_groups:
|
|
||||||
raise ValueError("转移任务组列表不能为空")
|
|
||||||
|
|
||||||
if not isinstance(transfer_groups, list):
|
|
||||||
raise ValueError("transfer_groups必须是列表类型")
|
|
||||||
|
|
||||||
# 标准化设备ID格式: 确保以 /devices/ 开头
|
|
||||||
if not target_device_id.startswith("/devices/"):
|
|
||||||
if target_device_id.startswith("/"):
|
|
||||||
target_device_id = f"/devices{target_device_id}"
|
|
||||||
else:
|
|
||||||
target_device_id = f"/devices/{target_device_id}"
|
|
||||||
|
|
||||||
self.hardware_interface._logger.info(
|
|
||||||
f"目标设备ID标准化为: {target_device_id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.hardware_interface._logger.info(
|
|
||||||
f"开始执行批量物料转移: {len(transfer_groups)}组任务 -> {target_device_id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
from .config import WAREHOUSE_MAPPING
|
|
||||||
results = []
|
|
||||||
successful_count = 0
|
|
||||||
failed_count = 0
|
|
||||||
|
|
||||||
for idx, group in enumerate(transfer_groups, 1):
|
|
||||||
try:
|
|
||||||
# 提取参数
|
|
||||||
material_name = group.get("materials", "")
|
|
||||||
target_stack = group.get("target_stack", "")
|
|
||||||
target_sites = group.get("target_sites", "")
|
|
||||||
|
|
||||||
# 验证必填参数
|
|
||||||
if not material_name:
|
|
||||||
raise ValueError(f"第{idx}组: 物料名称不能为空")
|
|
||||||
if not target_stack:
|
|
||||||
raise ValueError(f"第{idx}组: 目标堆栈不能为空")
|
|
||||||
if not target_sites:
|
|
||||||
raise ValueError(f"第{idx}组: 目标库位不能为空")
|
|
||||||
|
|
||||||
self.hardware_interface._logger.info(
|
|
||||||
f"处理第{idx}组转移: {material_name} -> "
|
|
||||||
f"{target_device_id}/{target_stack}/{target_sites}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 通过物料名称从deck获取ResourcePLR对象
|
|
||||||
try:
|
|
||||||
material_resource = self.deck.get_resource(material_name)
|
|
||||||
if not material_resource:
|
|
||||||
raise ValueError(f"在deck中未找到物料: {material_name}")
|
|
||||||
|
|
||||||
self.hardware_interface._logger.info(
|
|
||||||
f"从deck获取到物料 {material_name}: {material_resource}"
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
raise ValueError(
|
|
||||||
f"获取物料 {material_name} 失败: {str(e)},请确认物料已正确加载到deck中"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 验证目标堆栈是否存在
|
|
||||||
if target_stack not in WAREHOUSE_MAPPING:
|
|
||||||
raise ValueError(
|
|
||||||
f"未知的堆栈名称: {target_stack},"
|
|
||||||
f"可选值: {list(WAREHOUSE_MAPPING.keys())}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 验证库位是否有效
|
|
||||||
stack_sites = WAREHOUSE_MAPPING[target_stack].get("site_uuids", {})
|
|
||||||
if target_sites not in stack_sites:
|
|
||||||
raise ValueError(
|
|
||||||
f"库位 {target_sites} 不存在于堆栈 {target_stack} 中,"
|
|
||||||
f"可选库位: {list(stack_sites.keys())}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 获取目标库位的UUID
|
|
||||||
target_site_uuid = stack_sites[target_sites]
|
|
||||||
if not target_site_uuid:
|
|
||||||
raise ValueError(
|
|
||||||
f"库位 {target_sites} 的 UUID 未配置,请在 WAREHOUSE_MAPPING 中完善"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 目标位点(包含UUID)
|
|
||||||
future = ROS2DeviceNode.run_async_func(
|
|
||||||
self._ros_node.get_resource_with_dir,
|
|
||||||
True,
|
|
||||||
**{
|
|
||||||
"resource_id": f"/reaction_station_bioyond/Bioyond_Deck/{target_stack}",
|
|
||||||
"with_children": True,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
# 等待异步完成后再获取结果
|
|
||||||
if not future:
|
|
||||||
raise ValueError(f"获取目标堆栈资源future无效: {target_stack}")
|
|
||||||
while not future.done():
|
|
||||||
time.sleep(0.1)
|
|
||||||
target_site_resource = future.result()
|
|
||||||
|
|
||||||
# 调用父类的 transfer_resource_to_another 方法
|
|
||||||
# 传入ResourcePLR对象和目标位点资源
|
|
||||||
future = self.transfer_resource_to_another(
|
|
||||||
resource=[material_resource],
|
|
||||||
mount_resource=[target_site_resource],
|
|
||||||
sites=[target_sites],
|
|
||||||
mount_device_id=target_device_id
|
|
||||||
)
|
|
||||||
|
|
||||||
# 等待异步任务完成(轮询直到完成,再取结果)
|
|
||||||
if future:
|
|
||||||
try:
|
|
||||||
while not future.done():
|
|
||||||
time.sleep(0.1)
|
|
||||||
future.result()
|
|
||||||
self.hardware_interface._logger.info(
|
|
||||||
f"异步转移任务已完成: {material_name}"
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
raise ValueError(f"转移任务执行失败: {str(e)}")
|
|
||||||
|
|
||||||
self.hardware_interface._logger.info(
|
|
||||||
f"第{idx}组转移成功: {material_name} -> "
|
|
||||||
f"{target_device_id}/{target_stack}/{target_sites}"
|
|
||||||
)
|
|
||||||
|
|
||||||
successful_count += 1
|
|
||||||
results.append({
|
|
||||||
"group_index": idx,
|
|
||||||
"success": True,
|
|
||||||
"material_name": material_name,
|
|
||||||
"target_stack": target_stack,
|
|
||||||
"target_site": target_sites,
|
|
||||||
"message": "转移成功"
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = f"第{idx}组转移失败: {str(e)}"
|
|
||||||
self.hardware_interface._logger.error(error_msg)
|
|
||||||
failed_count += 1
|
|
||||||
results.append({
|
|
||||||
"group_index": idx,
|
|
||||||
"success": False,
|
|
||||||
"material_name": group.get("materials", ""),
|
|
||||||
"error": str(e)
|
|
||||||
})
|
|
||||||
|
|
||||||
# 返回汇总结果
|
|
||||||
return {
|
|
||||||
"success": failed_count == 0,
|
|
||||||
"total_groups": len(transfer_groups),
|
|
||||||
"successful_groups": successful_count,
|
|
||||||
"failed_groups": failed_count,
|
|
||||||
"target_device_id": target_device_id,
|
|
||||||
"details": results,
|
|
||||||
"message": f"完成 {len(transfer_groups)} 组转移任务到 {target_device_id}: "
|
|
||||||
f"{successful_count} 成功, {failed_count} 失败"
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = f"批量转移物料失败: {str(e)}"
|
|
||||||
self.hardware_interface._logger.error(error_msg)
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"total_groups": len(transfer_groups) if transfer_groups else 0,
|
|
||||||
"successful_groups": 0,
|
|
||||||
"failed_groups": len(transfer_groups) if transfer_groups else 0,
|
|
||||||
"target_device_id": target_device_id if target_device_id else "",
|
|
||||||
"error": error_msg
|
|
||||||
}
|
|
||||||
|
|
||||||
def query_resource_by_name(self, material_name: str):
|
|
||||||
"""
|
|
||||||
通过物料名称查询资源对象(适用于Bioyond系统)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
material_name: 物料名称
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
物料ID或None
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Bioyond系统使用material_cache存储物料信息
|
|
||||||
if not hasattr(self.hardware_interface, 'material_cache'):
|
|
||||||
self.hardware_interface._logger.error(
|
|
||||||
"hardware_interface没有material_cache属性"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
material_cache = self.hardware_interface.material_cache
|
|
||||||
|
|
||||||
self.hardware_interface._logger.info(
|
|
||||||
f"查询物料 '{material_name}', 缓存中共有 {len(material_cache)} 个物料"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 调试: 打印前几个物料信息
|
|
||||||
if material_cache:
|
|
||||||
cache_items = list(material_cache.items())[:5]
|
|
||||||
for name, material_id in cache_items:
|
|
||||||
self.hardware_interface._logger.debug(
|
|
||||||
f"缓存物料: name={name}, id={material_id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 直接从缓存中查找
|
|
||||||
if material_name in material_cache:
|
|
||||||
material_id = material_cache[material_name]
|
|
||||||
self.hardware_interface._logger.info(
|
|
||||||
f"找到物料: {material_name} -> ID: {material_id}"
|
|
||||||
)
|
|
||||||
return material_id
|
|
||||||
|
|
||||||
self.hardware_interface._logger.warning(
|
|
||||||
f"未找到物料: {material_name} (缓存中无此物料)"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 打印所有可用物料名称供参考
|
|
||||||
available_materials = list(material_cache.keys())
|
|
||||||
if available_materials:
|
|
||||||
self.hardware_interface._logger.info(
|
|
||||||
f"可用物料列表(前10个): {available_materials[:10]}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.hardware_interface._logger.error(
|
|
||||||
f"查询物料失败 {material_name}: {str(e)}"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
bioyond = BioyondDispensingStation(config={
|
bioyond = BioyondDispensingStation(config={
|
||||||
@@ -1916,3 +1089,4 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# id = "3a1bce3c-4f31-c8f3-5525-f3b273bc34dc"
|
# id = "3a1bce3c-4f31-c8f3-5525-f3b273bc34dc"
|
||||||
# bioyond.sample_waste_removal(id)
|
# bioyond.sample_waste_removal(id)
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
import time
|
|
||||||
import requests
|
import requests
|
||||||
from typing import List, Dict, Any
|
from typing import List, Dict, Any
|
||||||
from pathlib import Path
|
|
||||||
from datetime import datetime
|
|
||||||
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation
|
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation
|
||||||
from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import MachineState
|
|
||||||
from unilabos.ros.msgs.message_converter import convert_to_ros_msg, Float64, String
|
|
||||||
from unilabos.devices.workstation.bioyond_studio.config import (
|
from unilabos.devices.workstation.bioyond_studio.config import (
|
||||||
WORKFLOW_STEP_IDS,
|
WORKFLOW_STEP_IDS,
|
||||||
WORKFLOW_TO_SECTION_MAP,
|
WORKFLOW_TO_SECTION_MAP,
|
||||||
@@ -15,37 +10,6 @@ from unilabos.devices.workstation.bioyond_studio.config import (
|
|||||||
from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG
|
from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG
|
||||||
|
|
||||||
|
|
||||||
class BioyondReactor:
|
|
||||||
def __init__(self, config: dict = None, deck=None, protocol_type=None, **kwargs):
|
|
||||||
self.in_temperature = 0.0
|
|
||||||
self.out_temperature = 0.0
|
|
||||||
self.pt100_temperature = 0.0
|
|
||||||
self.sensor_average_temperature = 0.0
|
|
||||||
self.target_temperature = 0.0
|
|
||||||
self.setting_temperature = 0.0
|
|
||||||
self.viscosity = 0.0
|
|
||||||
self.average_viscosity = 0.0
|
|
||||||
self.speed = 0.0
|
|
||||||
self.force = 0.0
|
|
||||||
|
|
||||||
def update_metrics(self, payload: Dict[str, Any]):
|
|
||||||
def _f(v):
|
|
||||||
try:
|
|
||||||
return float(v)
|
|
||||||
except Exception:
|
|
||||||
return 0.0
|
|
||||||
self.target_temperature = _f(payload.get("targetTemperature"))
|
|
||||||
self.setting_temperature = _f(payload.get("settingTemperature"))
|
|
||||||
self.in_temperature = _f(payload.get("inTemperature"))
|
|
||||||
self.out_temperature = _f(payload.get("outTemperature"))
|
|
||||||
self.pt100_temperature = _f(payload.get("pt100Temperature"))
|
|
||||||
self.sensor_average_temperature = _f(payload.get("sensorAverageTemperature"))
|
|
||||||
self.speed = _f(payload.get("speed"))
|
|
||||||
self.force = _f(payload.get("force"))
|
|
||||||
self.viscosity = _f(payload.get("viscosity"))
|
|
||||||
self.average_viscosity = _f(payload.get("averageViscosity"))
|
|
||||||
|
|
||||||
|
|
||||||
class BioyondReactionStation(BioyondWorkstation):
|
class BioyondReactionStation(BioyondWorkstation):
|
||||||
"""Bioyond反应站类
|
"""Bioyond反应站类
|
||||||
|
|
||||||
@@ -73,19 +37,6 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
print(f"BioyondReactionStation初始化完成 - workflow_mappings: {self.workflow_mappings}")
|
print(f"BioyondReactionStation初始化完成 - workflow_mappings: {self.workflow_mappings}")
|
||||||
print(f"workflow_mappings长度: {len(self.workflow_mappings)}")
|
print(f"workflow_mappings长度: {len(self.workflow_mappings)}")
|
||||||
|
|
||||||
self.in_temperature = 0.0
|
|
||||||
self.out_temperature = 0.0
|
|
||||||
self.pt100_temperature = 0.0
|
|
||||||
self.sensor_average_temperature = 0.0
|
|
||||||
self.target_temperature = 0.0
|
|
||||||
self.setting_temperature = 0.0
|
|
||||||
self.viscosity = 0.0
|
|
||||||
self.average_viscosity = 0.0
|
|
||||||
self.speed = 0.0
|
|
||||||
self.force = 0.0
|
|
||||||
|
|
||||||
self._frame_to_reactor_id = {1: "reactor_1", 2: "reactor_2", 3: "reactor_3", 4: "reactor_4", 5: "reactor_5"}
|
|
||||||
|
|
||||||
# ==================== 工作流方法 ====================
|
# ==================== 工作流方法 ====================
|
||||||
|
|
||||||
def reactor_taken_out(self):
|
def reactor_taken_out(self):
|
||||||
@@ -281,7 +232,7 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
temperature: 温度设定(°C)
|
temperature: 温度设定(°C)
|
||||||
"""
|
"""
|
||||||
# 处理 volume 参数:优先使用直接传入的 volume,否则从 solvents 中提取
|
# 处理 volume 参数:优先使用直接传入的 volume,否则从 solvents 中提取
|
||||||
if not volume and solvents is not None:
|
if volume is None and solvents is not None:
|
||||||
# 参数类型转换:如果是字符串则解析为字典
|
# 参数类型转换:如果是字符串则解析为字典
|
||||||
if isinstance(solvents, str):
|
if isinstance(solvents, str):
|
||||||
try:
|
try:
|
||||||
@@ -340,39 +291,22 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
|
|
||||||
def liquid_feeding_titration(
|
def liquid_feeding_titration(
|
||||||
self,
|
self,
|
||||||
|
volume_formula: str,
|
||||||
assign_material_name: str,
|
assign_material_name: str,
|
||||||
volume_formula: str = None,
|
titration_type: str = "1",
|
||||||
x_value: str = None,
|
|
||||||
feeding_order_data: str = None,
|
|
||||||
extracted_actuals: str = None,
|
|
||||||
titration_type: str = "2",
|
|
||||||
time: str = "90",
|
time: str = "90",
|
||||||
torque_variation: int = 2,
|
torque_variation: int = 2,
|
||||||
temperature: float = 25.00
|
temperature: float = 25.00
|
||||||
):
|
):
|
||||||
"""液体进料(滴定)
|
"""液体进料(滴定)
|
||||||
|
|
||||||
支持两种模式:
|
|
||||||
1. 直接提供 volume_formula (传统方式)
|
|
||||||
2. 自动计算公式: 提供 x_value, feeding_order_data, extracted_actuals (新方式)
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
volume_formula: 分液公式(μL)
|
||||||
assign_material_name: 物料名称
|
assign_material_name: 物料名称
|
||||||
volume_formula: 分液公式(μL),如果提供则直接使用,否则自动计算
|
titration_type: 是否滴定(1=否, 2=是)
|
||||||
x_value: 手工输入的x值,格式如 "1-2-3"
|
|
||||||
feeding_order_data: feeding_order JSON字符串或对象,用于获取m二酐值
|
|
||||||
extracted_actuals: 从报告提取的实际加料量JSON字符串,包含actualTargetWeigh和actualVolume
|
|
||||||
titration_type: 是否滴定(1=否, 2=是),默认2
|
|
||||||
time: 观察时间(分钟)
|
time: 观察时间(分钟)
|
||||||
torque_variation: 是否观察(int类型, 1=否, 2=是)
|
torque_variation: 是否观察(int类型, 1=否, 2=是)
|
||||||
temperature: 温度(°C)
|
temperature: 温度(°C)
|
||||||
|
|
||||||
自动公式模板: 1000*(m二酐-x)*V二酐滴定/m二酐滴定
|
|
||||||
其中:
|
|
||||||
- m二酐滴定 = actualTargetWeigh (从extracted_actuals获取)
|
|
||||||
- V二酐滴定 = actualVolume (从extracted_actuals获取)
|
|
||||||
- x = x_value (手工输入)
|
|
||||||
- m二酐 = feeding_order中type为"main_anhydride"的amount值
|
|
||||||
"""
|
"""
|
||||||
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding(titration)"}')
|
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding(titration)"}')
|
||||||
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
||||||
@@ -382,84 +316,6 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
if isinstance(temperature, str):
|
if isinstance(temperature, str):
|
||||||
temperature = float(temperature)
|
temperature = float(temperature)
|
||||||
|
|
||||||
# 如果没有直接提供volume_formula,则自动计算
|
|
||||||
if not volume_formula and x_value and feeding_order_data and extracted_actuals:
|
|
||||||
# 1. 解析 feeding_order_data 获取 m二酐
|
|
||||||
if isinstance(feeding_order_data, str):
|
|
||||||
try:
|
|
||||||
feeding_order_data = json.loads(feeding_order_data)
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
raise ValueError(f"feeding_order_data JSON解析失败: {str(e)}")
|
|
||||||
|
|
||||||
# 支持两种格式:
|
|
||||||
# 格式1: 直接是数组 [{...}, {...}]
|
|
||||||
# 格式2: 对象包裹 {"feeding_order": [{...}, {...}]}
|
|
||||||
if isinstance(feeding_order_data, list):
|
|
||||||
feeding_order_list = feeding_order_data
|
|
||||||
elif isinstance(feeding_order_data, dict):
|
|
||||||
feeding_order_list = feeding_order_data.get("feeding_order", [])
|
|
||||||
else:
|
|
||||||
raise ValueError("feeding_order_data 必须是数组或包含feeding_order的字典")
|
|
||||||
|
|
||||||
# 从feeding_order中找到main_anhydride的amount
|
|
||||||
m_anhydride = None
|
|
||||||
for item in feeding_order_list:
|
|
||||||
if item.get("type") == "main_anhydride":
|
|
||||||
m_anhydride = item.get("amount")
|
|
||||||
break
|
|
||||||
|
|
||||||
if m_anhydride is None:
|
|
||||||
raise ValueError("在feeding_order中未找到type为'main_anhydride'的条目")
|
|
||||||
|
|
||||||
# 2. 解析 extracted_actuals 获取 actualTargetWeigh 和 actualVolume
|
|
||||||
if isinstance(extracted_actuals, str):
|
|
||||||
try:
|
|
||||||
extracted_actuals_obj = json.loads(extracted_actuals)
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
raise ValueError(f"extracted_actuals JSON解析失败: {str(e)}")
|
|
||||||
else:
|
|
||||||
extracted_actuals_obj = extracted_actuals
|
|
||||||
|
|
||||||
# 获取actuals数组
|
|
||||||
actuals_list = extracted_actuals_obj.get("actuals", [])
|
|
||||||
if not actuals_list:
|
|
||||||
# actuals为空,无法自动生成公式,回退到手动模式
|
|
||||||
print(f"警告: extracted_actuals中actuals数组为空,无法自动生成公式,请手动提供volume_formula")
|
|
||||||
volume_formula = None # 清空,触发后续的错误检查
|
|
||||||
else:
|
|
||||||
# 根据assign_material_name匹配对应的actual数据
|
|
||||||
# 假设order_code中包含物料名称
|
|
||||||
matched_actual = None
|
|
||||||
for actual in actuals_list:
|
|
||||||
order_code = actual.get("order_code", "")
|
|
||||||
# 简单匹配:如果order_code包含物料名称
|
|
||||||
if assign_material_name in order_code:
|
|
||||||
matched_actual = actual
|
|
||||||
break
|
|
||||||
|
|
||||||
# 如果没有匹配到,使用第一个
|
|
||||||
if not matched_actual and actuals_list:
|
|
||||||
matched_actual = actuals_list[0]
|
|
||||||
|
|
||||||
if not matched_actual:
|
|
||||||
raise ValueError("无法从extracted_actuals中获取实际加料量数据")
|
|
||||||
|
|
||||||
m_anhydride_titration = matched_actual.get("actualTargetWeigh") # m二酐滴定
|
|
||||||
v_anhydride_titration = matched_actual.get("actualVolume") # V二酐滴定
|
|
||||||
|
|
||||||
if m_anhydride_titration is None or v_anhydride_titration is None:
|
|
||||||
raise ValueError(f"实际加料量数据不完整: actualTargetWeigh={m_anhydride_titration}, actualVolume={v_anhydride_titration}")
|
|
||||||
|
|
||||||
# 3. 构建公式: 1000*(m二酐-x)*V二酐滴定/m二酐滴定
|
|
||||||
# x_value 格式如 "{{1-2-3}}",保留完整格式(包括花括号)直接替换到公式中
|
|
||||||
volume_formula = f"1000*({m_anhydride}-{x_value})*{v_anhydride_titration}/{m_anhydride_titration}"
|
|
||||||
|
|
||||||
print(f"自动生成滴定公式: {volume_formula}")
|
|
||||||
print(f" m二酐={m_anhydride}, x={x_value}, V二酐滴定={v_anhydride_titration}, m二酐滴定={m_anhydride_titration}")
|
|
||||||
|
|
||||||
elif not volume_formula:
|
|
||||||
raise ValueError("必须提供 volume_formula 或 (x_value + feeding_order_data + extracted_actuals)")
|
|
||||||
|
|
||||||
liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["liquid"]
|
liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["liquid"]
|
||||||
observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["observe"]
|
observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["observe"]
|
||||||
|
|
||||||
@@ -487,288 +343,9 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
print(f"当前队列长度: {len(self.pending_task_params)}")
|
print(f"当前队列长度: {len(self.pending_task_params)}")
|
||||||
return json.dumps({"suc": True})
|
return json.dumps({"suc": True})
|
||||||
|
|
||||||
def _extract_actuals_from_report(self, report) -> Dict[str, Any]:
|
|
||||||
data = report.get('data') if isinstance(report, dict) else None
|
|
||||||
actual_target_weigh = None
|
|
||||||
actual_volume = None
|
|
||||||
if data:
|
|
||||||
extra = data.get('extraProperties') or {}
|
|
||||||
if isinstance(extra, dict):
|
|
||||||
for v in extra.values():
|
|
||||||
obj = None
|
|
||||||
try:
|
|
||||||
obj = json.loads(v) if isinstance(v, str) else v
|
|
||||||
except Exception:
|
|
||||||
obj = None
|
|
||||||
if isinstance(obj, dict):
|
|
||||||
tw = obj.get('targetWeigh')
|
|
||||||
vol = obj.get('volume')
|
|
||||||
if tw is not None:
|
|
||||||
try:
|
|
||||||
actual_target_weigh = float(tw)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if vol is not None:
|
|
||||||
try:
|
|
||||||
actual_volume = float(vol)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return {
|
|
||||||
'actualTargetWeigh': actual_target_weigh,
|
|
||||||
'actualVolume': actual_volume
|
|
||||||
}
|
|
||||||
|
|
||||||
def extract_actuals_from_batch_reports(self, batch_reports_result: str) -> dict:
|
|
||||||
print(f"[DEBUG] extract_actuals 收到原始数据: {batch_reports_result[:500]}...") # 打印前500字符
|
|
||||||
try:
|
|
||||||
obj = json.loads(batch_reports_result) if isinstance(batch_reports_result, str) else batch_reports_result
|
|
||||||
if isinstance(obj, dict) and "return_info" in obj:
|
|
||||||
inner = obj["return_info"]
|
|
||||||
obj = json.loads(inner) if isinstance(inner, str) else inner
|
|
||||||
reports = obj.get("reports", []) if isinstance(obj, dict) else []
|
|
||||||
print(f"[DEBUG] 解析后的 reports 数组长度: {len(reports)}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[DEBUG] 解析异常: {e}")
|
|
||||||
reports = []
|
|
||||||
|
|
||||||
actuals = []
|
|
||||||
for i, r in enumerate(reports):
|
|
||||||
print(f"[DEBUG] 处理 report[{i}]: order_code={r.get('order_code')}, has_extracted={r.get('extracted') is not None}, has_report={r.get('report') is not None}")
|
|
||||||
order_code = r.get("order_code")
|
|
||||||
order_id = r.get("order_id")
|
|
||||||
ex = r.get("extracted")
|
|
||||||
if isinstance(ex, dict) and (ex.get("actualTargetWeigh") is not None or ex.get("actualVolume") is not None):
|
|
||||||
print(f"[DEBUG] 从 extracted 字段提取: actualTargetWeigh={ex.get('actualTargetWeigh')}, actualVolume={ex.get('actualVolume')}")
|
|
||||||
actuals.append({
|
|
||||||
"order_code": order_code,
|
|
||||||
"order_id": order_id,
|
|
||||||
"actualTargetWeigh": ex.get("actualTargetWeigh"),
|
|
||||||
"actualVolume": ex.get("actualVolume")
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
report = r.get("report")
|
|
||||||
vals = self._extract_actuals_from_report(report) if report else {"actualTargetWeigh": None, "actualVolume": None}
|
|
||||||
print(f"[DEBUG] 从 report 字段提取: {vals}")
|
|
||||||
actuals.append({
|
|
||||||
"order_code": order_code,
|
|
||||||
"order_id": order_id,
|
|
||||||
**vals
|
|
||||||
})
|
|
||||||
|
|
||||||
print(f"[DEBUG] 最终提取的 actuals 数组长度: {len(actuals)}")
|
|
||||||
result = {
|
|
||||||
"return_info": json.dumps({"actuals": actuals}, ensure_ascii=False)
|
|
||||||
}
|
|
||||||
print(f"[DEBUG] 返回结果: {result}")
|
|
||||||
return result
|
|
||||||
|
|
||||||
def process_temperature_cutoff_report(self, report_request) -> Dict[str, Any]:
|
|
||||||
try:
|
|
||||||
data = report_request.data
|
|
||||||
def _f(v):
|
|
||||||
try:
|
|
||||||
return float(v)
|
|
||||||
except Exception:
|
|
||||||
return 0.0
|
|
||||||
self.target_temperature = _f(data.get("targetTemperature"))
|
|
||||||
self.setting_temperature = _f(data.get("settingTemperature"))
|
|
||||||
self.in_temperature = _f(data.get("inTemperature"))
|
|
||||||
self.out_temperature = _f(data.get("outTemperature"))
|
|
||||||
self.pt100_temperature = _f(data.get("pt100Temperature"))
|
|
||||||
self.sensor_average_temperature = _f(data.get("sensorAverageTemperature"))
|
|
||||||
self.speed = _f(data.get("speed"))
|
|
||||||
self.force = _f(data.get("force"))
|
|
||||||
self.viscosity = _f(data.get("viscosity"))
|
|
||||||
self.average_viscosity = _f(data.get("averageViscosity"))
|
|
||||||
|
|
||||||
try:
|
|
||||||
if hasattr(self, "_ros_node") and self._ros_node is not None:
|
|
||||||
props = [
|
|
||||||
"in_temperature","out_temperature","pt100_temperature","sensor_average_temperature",
|
|
||||||
"target_temperature","setting_temperature","viscosity","average_viscosity",
|
|
||||||
"speed","force"
|
|
||||||
]
|
|
||||||
for name in props:
|
|
||||||
pub = self._ros_node._property_publishers.get(name)
|
|
||||||
if pub:
|
|
||||||
pub.publish_property()
|
|
||||||
frame = data.get("frameCode")
|
|
||||||
reactor_id = None
|
|
||||||
try:
|
|
||||||
reactor_id = self._frame_to_reactor_id.get(int(frame))
|
|
||||||
except Exception:
|
|
||||||
reactor_id = None
|
|
||||||
if reactor_id and hasattr(self._ros_node, "sub_devices"):
|
|
||||||
child = self._ros_node.sub_devices.get(reactor_id)
|
|
||||||
if child and hasattr(child, "driver_instance"):
|
|
||||||
child.driver_instance.update_metrics(data)
|
|
||||||
pubs = getattr(child.ros_node_instance, "_property_publishers", {})
|
|
||||||
for name in props:
|
|
||||||
p = pubs.get(name)
|
|
||||||
if p:
|
|
||||||
p.publish_property()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
event = {
|
|
||||||
"frameCode": data.get("frameCode"),
|
|
||||||
"generateTime": data.get("generateTime"),
|
|
||||||
"targetTemperature": data.get("targetTemperature"),
|
|
||||||
"settingTemperature": data.get("settingTemperature"),
|
|
||||||
"inTemperature": data.get("inTemperature"),
|
|
||||||
"outTemperature": data.get("outTemperature"),
|
|
||||||
"pt100Temperature": data.get("pt100Temperature"),
|
|
||||||
"sensorAverageTemperature": data.get("sensorAverageTemperature"),
|
|
||||||
"speed": data.get("speed"),
|
|
||||||
"force": data.get("force"),
|
|
||||||
"viscosity": data.get("viscosity"),
|
|
||||||
"averageViscosity": data.get("averageViscosity"),
|
|
||||||
"request_time": report_request.request_time,
|
|
||||||
"timestamp": datetime.now().isoformat(),
|
|
||||||
"reactor_id": self._frame_to_reactor_id.get(int(data.get("frameCode", 0))) if str(data.get("frameCode", "")).isdigit() else None,
|
|
||||||
}
|
|
||||||
|
|
||||||
base_dir = Path(__file__).resolve().parents[3] / "unilabos_data"
|
|
||||||
base_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
out_file = base_dir / "temperature_cutoff_events.json"
|
|
||||||
try:
|
|
||||||
existing = json.loads(out_file.read_text(encoding="utf-8")) if out_file.exists() else []
|
|
||||||
if not isinstance(existing, list):
|
|
||||||
existing = []
|
|
||||||
except Exception:
|
|
||||||
existing = []
|
|
||||||
existing.append(event)
|
|
||||||
out_file.write_text(json.dumps(existing, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
||||||
|
|
||||||
if hasattr(self, "_ros_node") and self._ros_node is not None:
|
|
||||||
ns = self._ros_node.namespace
|
|
||||||
topics = {
|
|
||||||
"targetTemperature": f"{ns}/metrics/temperature_cutoff/target_temperature",
|
|
||||||
"settingTemperature": f"{ns}/metrics/temperature_cutoff/setting_temperature",
|
|
||||||
"inTemperature": f"{ns}/metrics/temperature_cutoff/in_temperature",
|
|
||||||
"outTemperature": f"{ns}/metrics/temperature_cutoff/out_temperature",
|
|
||||||
"pt100Temperature": f"{ns}/metrics/temperature_cutoff/pt100_temperature",
|
|
||||||
"sensorAverageTemperature": f"{ns}/metrics/temperature_cutoff/sensor_average_temperature",
|
|
||||||
"speed": f"{ns}/metrics/temperature_cutoff/speed",
|
|
||||||
"force": f"{ns}/metrics/temperature_cutoff/force",
|
|
||||||
"viscosity": f"{ns}/metrics/temperature_cutoff/viscosity",
|
|
||||||
"averageViscosity": f"{ns}/metrics/temperature_cutoff/average_viscosity",
|
|
||||||
}
|
|
||||||
for k, t in topics.items():
|
|
||||||
v = data.get(k)
|
|
||||||
if v is not None:
|
|
||||||
pub = self._ros_node.create_publisher(Float64, t, 10)
|
|
||||||
pub.publish(convert_to_ros_msg(Float64, float(v)))
|
|
||||||
|
|
||||||
evt_pub = self._ros_node.create_publisher(String, f"{ns}/events/temperature_cutoff", 10)
|
|
||||||
evt_pub.publish(convert_to_ros_msg(String, json.dumps(event, ensure_ascii=False)))
|
|
||||||
|
|
||||||
return {"processed": True, "frame": data.get("frameCode")}
|
|
||||||
except Exception as e:
|
|
||||||
return {"processed": False, "error": str(e)}
|
|
||||||
|
|
||||||
def wait_for_multiple_orders_and_get_reports(self, batch_create_result: str = None, timeout: int = 7200, check_interval: int = 10) -> Dict[str, Any]:
|
|
||||||
try:
|
|
||||||
timeout = int(timeout) if timeout else 7200
|
|
||||||
check_interval = int(check_interval) if check_interval else 10
|
|
||||||
if not batch_create_result or batch_create_result == "":
|
|
||||||
raise ValueError("batch_create_result为空")
|
|
||||||
try:
|
|
||||||
if isinstance(batch_create_result, str) and '[...]' in batch_create_result:
|
|
||||||
batch_create_result = batch_create_result.replace('[...]', '[]')
|
|
||||||
result_obj = json.loads(batch_create_result) if isinstance(batch_create_result, str) else batch_create_result
|
|
||||||
if isinstance(result_obj, dict) and "return_value" in result_obj:
|
|
||||||
inner = result_obj.get("return_value")
|
|
||||||
if isinstance(inner, str):
|
|
||||||
result_obj = json.loads(inner)
|
|
||||||
elif isinstance(inner, dict):
|
|
||||||
result_obj = inner
|
|
||||||
order_codes = result_obj.get("order_codes", [])
|
|
||||||
order_ids = result_obj.get("order_ids", [])
|
|
||||||
except Exception as e:
|
|
||||||
raise ValueError(f"解析batch_create_result失败: {e}")
|
|
||||||
if not order_codes or not order_ids:
|
|
||||||
raise ValueError("缺少order_codes或order_ids")
|
|
||||||
if not isinstance(order_codes, list):
|
|
||||||
order_codes = [order_codes]
|
|
||||||
if not isinstance(order_ids, list):
|
|
||||||
order_ids = [order_ids]
|
|
||||||
if len(order_codes) != len(order_ids):
|
|
||||||
raise ValueError("order_codes与order_ids数量不匹配")
|
|
||||||
total = len(order_codes)
|
|
||||||
pending = {c: {"order_id": order_ids[i], "completed": False} for i, c in enumerate(order_codes)}
|
|
||||||
reports = []
|
|
||||||
start_time = time.time()
|
|
||||||
while pending:
|
|
||||||
elapsed_time = time.time() - start_time
|
|
||||||
if elapsed_time > timeout:
|
|
||||||
for oc in list(pending.keys()):
|
|
||||||
reports.append({
|
|
||||||
"order_code": oc,
|
|
||||||
"order_id": pending[oc]["order_id"],
|
|
||||||
"status": "timeout",
|
|
||||||
"completion_status": None,
|
|
||||||
"report": None,
|
|
||||||
"extracted": None,
|
|
||||||
"elapsed_time": elapsed_time
|
|
||||||
})
|
|
||||||
break
|
|
||||||
completed_round = []
|
|
||||||
for oc in list(pending.keys()):
|
|
||||||
oid = pending[oc]["order_id"]
|
|
||||||
if oc in self.order_completion_status:
|
|
||||||
info = self.order_completion_status[oc]
|
|
||||||
try:
|
|
||||||
rep = self.hardware_interface.order_report(oid)
|
|
||||||
if not rep:
|
|
||||||
rep = {"error": "无法获取报告"}
|
|
||||||
reports.append({
|
|
||||||
"order_code": oc,
|
|
||||||
"order_id": oid,
|
|
||||||
"status": "completed",
|
|
||||||
"completion_status": info.get('status'),
|
|
||||||
"report": rep,
|
|
||||||
"extracted": self._extract_actuals_from_report(rep),
|
|
||||||
"elapsed_time": elapsed_time
|
|
||||||
})
|
|
||||||
completed_round.append(oc)
|
|
||||||
del self.order_completion_status[oc]
|
|
||||||
except Exception as e:
|
|
||||||
reports.append({
|
|
||||||
"order_code": oc,
|
|
||||||
"order_id": oid,
|
|
||||||
"status": "error",
|
|
||||||
"completion_status": info.get('status') if 'info' in locals() else None,
|
|
||||||
"report": None,
|
|
||||||
"extracted": None,
|
|
||||||
"error": str(e),
|
|
||||||
"elapsed_time": elapsed_time
|
|
||||||
})
|
|
||||||
completed_round.append(oc)
|
|
||||||
for oc in completed_round:
|
|
||||||
del pending[oc]
|
|
||||||
if pending:
|
|
||||||
time.sleep(check_interval)
|
|
||||||
completed_count = sum(1 for r in reports if r['status'] == 'completed')
|
|
||||||
timeout_count = sum(1 for r in reports if r['status'] == 'timeout')
|
|
||||||
error_count = sum(1 for r in reports if r['status'] == 'error')
|
|
||||||
final_elapsed_time = time.time() - start_time
|
|
||||||
summary = {
|
|
||||||
"total": total,
|
|
||||||
"completed": completed_count,
|
|
||||||
"timeout": timeout_count,
|
|
||||||
"error": error_count,
|
|
||||||
"elapsed_time": round(final_elapsed_time, 2),
|
|
||||||
"reports": reports
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
"return_info": json.dumps(summary, ensure_ascii=False)
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
raise
|
|
||||||
|
|
||||||
def liquid_feeding_beaker(
|
def liquid_feeding_beaker(
|
||||||
self,
|
self,
|
||||||
volume: str = "350",
|
volume: str = "35000",
|
||||||
assign_material_name: str = "BAPP",
|
assign_material_name: str = "BAPP",
|
||||||
time: str = "0",
|
time: str = "0",
|
||||||
torque_variation: int = 1,
|
torque_variation: int = 1,
|
||||||
@@ -778,7 +355,7 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
"""液体进料烧杯
|
"""液体进料烧杯
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
volume: 分液质量(g)
|
volume: 分液量(μL)
|
||||||
assign_material_name: 物料名称(试剂瓶位)
|
assign_material_name: 物料名称(试剂瓶位)
|
||||||
time: 观察时间(分钟)
|
time: 观察时间(分钟)
|
||||||
torque_variation: 是否观察(int类型, 1=否, 2=是)
|
torque_variation: 是否观察(int类型, 1=否, 2=是)
|
||||||
@@ -912,106 +489,6 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
"""
|
"""
|
||||||
return self.hardware_interface.create_order(json_str)
|
return self.hardware_interface.create_order(json_str)
|
||||||
|
|
||||||
def hard_delete_merged_workflows(self, workflow_ids: List[str]) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
调用新接口:硬删除合并后的工作流
|
|
||||||
|
|
||||||
Args:
|
|
||||||
workflow_ids: 要删除的工作流ID数组
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
删除结果
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if not isinstance(workflow_ids, list):
|
|
||||||
raise ValueError("workflow_ids必须是字符串数组")
|
|
||||||
return self._delete_project_api("/api/lims/order/workflows", workflow_ids)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ 硬删除异常: {str(e)}")
|
|
||||||
return {"code": 0, "message": str(e), "timestamp": int(time.time())}
|
|
||||||
|
|
||||||
# ==================== 项目接口通用方法 ====================
|
|
||||||
|
|
||||||
def _post_project_api(self, endpoint: str, data: Any) -> Dict[str, Any]:
|
|
||||||
"""项目接口通用POST调用
|
|
||||||
|
|
||||||
参数:
|
|
||||||
endpoint: 接口路径(例如 /api/lims/order/skip-titration-steps)
|
|
||||||
data: 请求体中的 data 字段内容
|
|
||||||
|
|
||||||
返回:
|
|
||||||
dict: 服务端响应,失败时返回 {code:0,message,...}
|
|
||||||
"""
|
|
||||||
request_data = {
|
|
||||||
"apiKey": API_CONFIG["api_key"],
|
|
||||||
"requestTime": self.hardware_interface.get_current_time_iso8601(),
|
|
||||||
"data": data
|
|
||||||
}
|
|
||||||
print(f"\n📤 项目POST请求: {self.hardware_interface.host}{endpoint}")
|
|
||||||
print(json.dumps(request_data, indent=4, ensure_ascii=False))
|
|
||||||
try:
|
|
||||||
response = requests.post(
|
|
||||||
f"{self.hardware_interface.host}{endpoint}",
|
|
||||||
json=request_data,
|
|
||||||
headers={"Content-Type": "application/json"},
|
|
||||||
timeout=30
|
|
||||||
)
|
|
||||||
result = response.json()
|
|
||||||
if result.get("code") == 1:
|
|
||||||
print("✅ 请求成功")
|
|
||||||
else:
|
|
||||||
print(f"❌ 请求失败: {result.get('message','未知错误')}")
|
|
||||||
return result
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
print("❌ 非JSON响应")
|
|
||||||
return {"code": 0, "message": "非JSON响应", "timestamp": int(time.time())}
|
|
||||||
except requests.exceptions.Timeout:
|
|
||||||
print("❌ 请求超时")
|
|
||||||
return {"code": 0, "message": "请求超时", "timestamp": int(time.time())}
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
print(f"❌ 网络异常: {str(e)}")
|
|
||||||
return {"code": 0, "message": str(e), "timestamp": int(time.time())}
|
|
||||||
|
|
||||||
def _delete_project_api(self, endpoint: str, data: Any) -> Dict[str, Any]:
|
|
||||||
"""项目接口通用DELETE调用
|
|
||||||
|
|
||||||
参数:
|
|
||||||
endpoint: 接口路径(例如 /api/lims/order/workflows)
|
|
||||||
data: 请求体中的 data 字段内容
|
|
||||||
|
|
||||||
返回:
|
|
||||||
dict: 服务端响应,失败时返回 {code:0,message,...}
|
|
||||||
"""
|
|
||||||
request_data = {
|
|
||||||
"apiKey": API_CONFIG["api_key"],
|
|
||||||
"requestTime": self.hardware_interface.get_current_time_iso8601(),
|
|
||||||
"data": data
|
|
||||||
}
|
|
||||||
print(f"\n📤 项目DELETE请求: {self.hardware_interface.host}{endpoint}")
|
|
||||||
print(json.dumps(request_data, indent=4, ensure_ascii=False))
|
|
||||||
try:
|
|
||||||
response = requests.delete(
|
|
||||||
f"{self.hardware_interface.host}{endpoint}",
|
|
||||||
json=request_data,
|
|
||||||
headers={"Content-Type": "application/json"},
|
|
||||||
timeout=30
|
|
||||||
)
|
|
||||||
result = response.json()
|
|
||||||
if result.get("code") == 1:
|
|
||||||
print("✅ 请求成功")
|
|
||||||
else:
|
|
||||||
print(f"❌ 请求失败: {result.get('message','未知错误')}")
|
|
||||||
return result
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
print("❌ 非JSON响应")
|
|
||||||
return {"code": 0, "message": "非JSON响应", "timestamp": int(time.time())}
|
|
||||||
except requests.exceptions.Timeout:
|
|
||||||
print("❌ 请求超时")
|
|
||||||
return {"code": 0, "message": "请求超时", "timestamp": int(time.time())}
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
print(f"❌ 网络异常: {str(e)}")
|
|
||||||
return {"code": 0, "message": str(e), "timestamp": int(time.time())}
|
|
||||||
|
|
||||||
# ==================== 工作流执行核心方法 ====================
|
# ==================== 工作流执行核心方法 ====================
|
||||||
|
|
||||||
def process_web_workflows(self, web_workflow_json: str) -> List[Dict[str, str]]:
|
def process_web_workflows(self, web_workflow_json: str) -> List[Dict[str, str]]:
|
||||||
@@ -1042,6 +519,69 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
print(f"错误:处理工作流失败: {e}")
|
print(f"错误:处理工作流失败: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def process_and_execute_workflow(self, workflow_name: str, task_name: str) -> dict:
|
||||||
|
"""
|
||||||
|
一站式处理工作流程:解析网页工作流列表,合并工作流(带参数),然后发布任务
|
||||||
|
|
||||||
|
Args:
|
||||||
|
workflow_name: 合并后的工作流名称
|
||||||
|
task_name: 任务名称
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
任务创建结果
|
||||||
|
"""
|
||||||
|
web_workflow_list = self.get_workflow_sequence()
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"📋 处理网页工作流列表: {web_workflow_list}")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
|
||||||
|
web_workflow_json = json.dumps({"web_workflow_list": web_workflow_list})
|
||||||
|
workflows_result = self.process_web_workflows(web_workflow_json)
|
||||||
|
|
||||||
|
if not workflows_result:
|
||||||
|
return self._create_error_result("处理网页工作流列表失败", "process_web_workflows")
|
||||||
|
|
||||||
|
print(f"workflows_result 类型: {type(workflows_result)}")
|
||||||
|
print(f"workflows_result 内容: {workflows_result}")
|
||||||
|
|
||||||
|
workflows_with_params = self._build_workflows_with_parameters(workflows_result)
|
||||||
|
|
||||||
|
merge_data = {
|
||||||
|
"name": workflow_name,
|
||||||
|
"workflows": workflows_with_params
|
||||||
|
}
|
||||||
|
|
||||||
|
# print(f"\n🔄 合并工作流(带参数),名称: {workflow_name}")
|
||||||
|
merged_workflow = self.merge_workflow_with_parameters(json.dumps(merge_data))
|
||||||
|
|
||||||
|
if not merged_workflow:
|
||||||
|
return self._create_error_result("合并工作流失败", "merge_workflow_with_parameters")
|
||||||
|
|
||||||
|
workflow_id = merged_workflow.get("subWorkflows", [{}])[0].get("id", "")
|
||||||
|
# print(f"\n📤 使用工作流创建任务: {workflow_name} (ID: {workflow_id})")
|
||||||
|
|
||||||
|
order_params = [{
|
||||||
|
"orderCode": f"task_{self.hardware_interface.get_current_time_iso8601()}",
|
||||||
|
"orderName": task_name,
|
||||||
|
"workFlowId": workflow_id,
|
||||||
|
"borderNumber": 1,
|
||||||
|
"paramValues": {}
|
||||||
|
}]
|
||||||
|
|
||||||
|
result = self.create_order(json.dumps(order_params))
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
return self._create_error_result("创建任务失败", "create_order")
|
||||||
|
|
||||||
|
# 清空工作流序列和参数,防止下次执行时累积重复
|
||||||
|
self.pending_task_params = []
|
||||||
|
self.clear_workflows() # 清空工作流序列,避免重复累积
|
||||||
|
|
||||||
|
# print(f"\n✅ 任务创建成功: {result}")
|
||||||
|
# print(f"\n✅ 任务创建成功")
|
||||||
|
print(f"{'='*60}\n")
|
||||||
|
return json.dumps({"success": True, "result": result})
|
||||||
|
|
||||||
def _build_workflows_with_parameters(self, workflows_result: list) -> list:
|
def _build_workflows_with_parameters(self, workflows_result: list) -> list:
|
||||||
"""
|
"""
|
||||||
构建带参数的工作流列表
|
构建带参数的工作流列表
|
||||||
@@ -1241,90 +781,3 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
print(f" ❌ 工作流ID验证失败: {e}")
|
print(f" ❌ 工作流ID验证失败: {e}")
|
||||||
print(f" 💡 将重新合并工作流")
|
print(f" 💡 将重新合并工作流")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def process_and_execute_workflow(self, workflow_name: str, task_name: str) -> dict:
|
|
||||||
"""
|
|
||||||
一站式处理工作流程:解析网页工作流列表,合并工作流(带参数),然后发布任务
|
|
||||||
|
|
||||||
Args:
|
|
||||||
workflow_name: 合并后的工作流名称
|
|
||||||
task_name: 任务名称
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
任务创建结果
|
|
||||||
"""
|
|
||||||
web_workflow_list = self.get_workflow_sequence()
|
|
||||||
print(f"\n{'='*60}")
|
|
||||||
print(f"📋 处理网页工作流列表: {web_workflow_list}")
|
|
||||||
print(f"{'='*60}")
|
|
||||||
|
|
||||||
web_workflow_json = json.dumps({"web_workflow_list": web_workflow_list})
|
|
||||||
workflows_result = self.process_web_workflows(web_workflow_json)
|
|
||||||
|
|
||||||
if not workflows_result:
|
|
||||||
return self._create_error_result("处理网页工作流列表失败", "process_web_workflows")
|
|
||||||
|
|
||||||
print(f"workflows_result 类型: {type(workflows_result)}")
|
|
||||||
print(f"workflows_result 内容: {workflows_result}")
|
|
||||||
|
|
||||||
workflows_with_params = self._build_workflows_with_parameters(workflows_result)
|
|
||||||
|
|
||||||
merge_data = {
|
|
||||||
"name": workflow_name,
|
|
||||||
"workflows": workflows_with_params
|
|
||||||
}
|
|
||||||
|
|
||||||
# print(f"\n🔄 合并工作流(带参数),名称: {workflow_name}")
|
|
||||||
merged_workflow = self.merge_workflow_with_parameters(json.dumps(merge_data))
|
|
||||||
|
|
||||||
if not merged_workflow:
|
|
||||||
return self._create_error_result("合并工作流失败", "merge_workflow_with_parameters")
|
|
||||||
|
|
||||||
workflow_id = merged_workflow.get("subWorkflows", [{}])[0].get("id", "")
|
|
||||||
# print(f"\n📤 使用工作流创建任务: {workflow_name} (ID: {workflow_id})")
|
|
||||||
|
|
||||||
order_params = [{
|
|
||||||
"orderCode": f"task_{self.hardware_interface.get_current_time_iso8601()}",
|
|
||||||
"orderName": task_name,
|
|
||||||
"workFlowId": workflow_id,
|
|
||||||
"borderNumber": 1,
|
|
||||||
"paramValues": {}
|
|
||||||
}]
|
|
||||||
|
|
||||||
result = self.create_order(json.dumps(order_params))
|
|
||||||
|
|
||||||
if not result:
|
|
||||||
return self._create_error_result("创建任务失败", "create_order")
|
|
||||||
|
|
||||||
# 清空工作流序列和参数,防止下次执行时累积重复
|
|
||||||
self.pending_task_params = []
|
|
||||||
self.clear_workflows() # 清空工作流序列,避免重复累积
|
|
||||||
|
|
||||||
# print(f"\n✅ 任务创建成功: {result}")
|
|
||||||
# print(f"\n✅ 任务创建成功")
|
|
||||||
print(f"{'='*60}\n")
|
|
||||||
|
|
||||||
# 返回结果,包含合并后的工作流数据和订单参数
|
|
||||||
return json.dumps({
|
|
||||||
"success": True,
|
|
||||||
"result": result,
|
|
||||||
"merged_workflow": merged_workflow,
|
|
||||||
"order_params": order_params
|
|
||||||
})
|
|
||||||
|
|
||||||
# ==================== 反应器操作接口 ====================
|
|
||||||
|
|
||||||
def skip_titration_steps(self, preintake_id: str) -> Dict[str, Any]:
|
|
||||||
"""跳过当前正在进行的滴定步骤
|
|
||||||
|
|
||||||
Args:
|
|
||||||
preintake_id: 通量ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, Any]: 服务器响应,包含状态码、消息和时间戳
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return self._post_project_api("/api/lims/order/skip-titration-steps", preintake_id)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ 跳过滴定异常: {str(e)}")
|
|
||||||
return {"code": 0, "message": str(e), "timestamp": int(time.time())}
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,639 @@
|
|||||||
|
"""
|
||||||
|
纽扣电池组装工作站物料类定义
|
||||||
|
Button Battery Assembly Station Resource Classes
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
from typing import Any, Dict, List, Optional, TypedDict, Union, cast
|
||||||
|
|
||||||
|
from pylabrobot.resources.coordinate import Coordinate
|
||||||
|
from pylabrobot.resources.container import Container
|
||||||
|
from pylabrobot.resources.deck import Deck
|
||||||
|
from pylabrobot.resources.itemized_resource import ItemizedResource
|
||||||
|
from pylabrobot.resources.resource import Resource
|
||||||
|
from pylabrobot.resources.resource_stack import ResourceStack
|
||||||
|
from pylabrobot.resources.tip_rack import TipRack, TipSpot
|
||||||
|
from pylabrobot.resources.trash import Trash
|
||||||
|
from pylabrobot.resources.utils import create_ordered_items_2d
|
||||||
|
|
||||||
|
from unilabos.resources.battery.magazine import MagazineHolder_4_Cathode, MagazineHolder_6_Cathode, MagazineHolder_6_Anode, MagazineHolder_6_Battery
|
||||||
|
from unilabos.resources.battery.bottle_carriers import YIHUA_Electrolyte_12VialCarrier
|
||||||
|
from unilabos.resources.battery.electrode_sheet import ElectrodeSheet
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: 这个应该只能放一个极片
|
||||||
|
class MaterialHoleState(TypedDict):
|
||||||
|
diameter: int
|
||||||
|
depth: int
|
||||||
|
max_sheets: int
|
||||||
|
info: Optional[str] # 附加信息
|
||||||
|
|
||||||
|
class MaterialHole(Resource):
|
||||||
|
"""料板洞位类"""
|
||||||
|
children: List[ElectrodeSheet] = []
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
size_x: float,
|
||||||
|
size_y: float,
|
||||||
|
size_z: float,
|
||||||
|
category: str = "material_hole",
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
size_x=size_x,
|
||||||
|
size_y=size_y,
|
||||||
|
size_z=size_z,
|
||||||
|
category=category,
|
||||||
|
)
|
||||||
|
self._unilabos_state: MaterialHoleState = MaterialHoleState(
|
||||||
|
diameter=20,
|
||||||
|
depth=10,
|
||||||
|
max_sheets=1,
|
||||||
|
info=None
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_all_sheet_info(self):
|
||||||
|
info_list = []
|
||||||
|
for sheet in self.children:
|
||||||
|
info_list.append(sheet._unilabos_state["info"])
|
||||||
|
return info_list
|
||||||
|
|
||||||
|
#这个函数函数好像没用,一般不会集中赋值质量
|
||||||
|
def set_all_sheet_mass(self):
|
||||||
|
for sheet in self.children:
|
||||||
|
sheet._unilabos_state["mass"] = 0.5 # 示例:设置质量为0.5g
|
||||||
|
|
||||||
|
def load_state(self, state: Dict[str, Any]) -> None:
|
||||||
|
"""格式不变"""
|
||||||
|
super().load_state(state)
|
||||||
|
self._unilabos_state = state
|
||||||
|
|
||||||
|
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
||||||
|
"""格式不变"""
|
||||||
|
data = super().serialize_state()
|
||||||
|
data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等)
|
||||||
|
return data
|
||||||
|
#移动极片前先取出对象
|
||||||
|
def get_sheet_with_name(self, name: str) -> Optional[ElectrodeSheet]:
|
||||||
|
for sheet in self.children:
|
||||||
|
if sheet.name == name:
|
||||||
|
return sheet
|
||||||
|
return None
|
||||||
|
|
||||||
|
def has_electrode_sheet(self) -> bool:
|
||||||
|
"""检查洞位是否有极片"""
|
||||||
|
return len(self.children) > 0
|
||||||
|
|
||||||
|
def assign_child_resource(
|
||||||
|
self,
|
||||||
|
resource: ElectrodeSheet,
|
||||||
|
location: Optional[Coordinate],
|
||||||
|
reassign: bool = True,
|
||||||
|
):
|
||||||
|
"""放置极片"""
|
||||||
|
# TODO: 这里要改,diameter找不到,加入._unilabos_state后应该没问题
|
||||||
|
#if resource._unilabos_state["diameter"] > self._unilabos_state["diameter"]:
|
||||||
|
# raise ValueError(f"极片直径 {resource._unilabos_state['diameter']} 超过洞位直径 {self._unilabos_state['diameter']}")
|
||||||
|
#if len(self.children) >= self._unilabos_state["max_sheets"]:
|
||||||
|
# raise ValueError(f"洞位已满,无法放置更多极片")
|
||||||
|
super().assign_child_resource(resource, location, reassign)
|
||||||
|
|
||||||
|
# 根据children的编号取物料对象。
|
||||||
|
def get_electrode_sheet_info(self, index: int) -> ElectrodeSheet:
|
||||||
|
return self.children[index]
|
||||||
|
|
||||||
|
|
||||||
|
class MaterialPlateState(TypedDict):
|
||||||
|
hole_spacing_x: float
|
||||||
|
hole_spacing_y: float
|
||||||
|
hole_diameter: float
|
||||||
|
info: Optional[str] # 附加信息
|
||||||
|
|
||||||
|
class MaterialPlate(ItemizedResource[MaterialHole]):
|
||||||
|
"""料板类 - 4x4个洞位,每个洞位放1个极片"""
|
||||||
|
|
||||||
|
children: List[MaterialHole]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
size_x: float,
|
||||||
|
size_y: float,
|
||||||
|
size_z: float,
|
||||||
|
ordered_items: Optional[Dict[str, MaterialHole]] = None,
|
||||||
|
ordering: Optional[OrderedDict[str, str]] = None,
|
||||||
|
category: str = "material_plate",
|
||||||
|
model: Optional[str] = None,
|
||||||
|
fill: bool = False
|
||||||
|
):
|
||||||
|
"""初始化料板
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 料板名称
|
||||||
|
size_x: 长度 (mm)
|
||||||
|
size_y: 宽度 (mm)
|
||||||
|
size_z: 高度 (mm)
|
||||||
|
hole_diameter: 洞直径 (mm)
|
||||||
|
hole_depth: 洞深度 (mm)
|
||||||
|
hole_spacing_x: X方向洞位间距 (mm)
|
||||||
|
hole_spacing_y: Y方向洞位间距 (mm)
|
||||||
|
number: 编号
|
||||||
|
category: 类别
|
||||||
|
model: 型号
|
||||||
|
"""
|
||||||
|
self._unilabos_state: MaterialPlateState = MaterialPlateState(
|
||||||
|
hole_spacing_x=24.0,
|
||||||
|
hole_spacing_y=24.0,
|
||||||
|
hole_diameter=20.0,
|
||||||
|
info="",
|
||||||
|
)
|
||||||
|
# 创建4x4的洞位
|
||||||
|
# TODO: 这里要改,对应不同形状
|
||||||
|
holes = create_ordered_items_2d(
|
||||||
|
klass=MaterialHole,
|
||||||
|
num_items_x=4,
|
||||||
|
num_items_y=4,
|
||||||
|
dx=(size_x - 4 * self._unilabos_state["hole_spacing_x"]) / 2, # 居中
|
||||||
|
dy=(size_y - 4 * self._unilabos_state["hole_spacing_y"]) / 2, # 居中
|
||||||
|
dz=size_z,
|
||||||
|
item_dx=self._unilabos_state["hole_spacing_x"],
|
||||||
|
item_dy=self._unilabos_state["hole_spacing_y"],
|
||||||
|
size_x = 16,
|
||||||
|
size_y = 16,
|
||||||
|
size_z = 16,
|
||||||
|
)
|
||||||
|
if fill:
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
size_x=size_x,
|
||||||
|
size_y=size_y,
|
||||||
|
size_z=size_z,
|
||||||
|
ordered_items=holes,
|
||||||
|
category=category,
|
||||||
|
model=model,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
size_x=size_x,
|
||||||
|
size_y=size_y,
|
||||||
|
size_z=size_z,
|
||||||
|
ordered_items=ordered_items,
|
||||||
|
ordering=ordering,
|
||||||
|
category=category,
|
||||||
|
model=model,
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_locations(self):
|
||||||
|
# TODO:调多次相加
|
||||||
|
holes = create_ordered_items_2d(
|
||||||
|
klass=MaterialHole,
|
||||||
|
num_items_x=4,
|
||||||
|
num_items_y=4,
|
||||||
|
dx=(self._size_x - 3 * self._unilabos_state["hole_spacing_x"]) / 2, # 居中
|
||||||
|
dy=(self._size_y - 3 * self._unilabos_state["hole_spacing_y"]) / 2, # 居中
|
||||||
|
dz=self._size_z,
|
||||||
|
item_dx=self._unilabos_state["hole_spacing_x"],
|
||||||
|
item_dy=self._unilabos_state["hole_spacing_y"],
|
||||||
|
size_x = 1,
|
||||||
|
size_y = 1,
|
||||||
|
size_z = 1,
|
||||||
|
)
|
||||||
|
for item, original_item in zip(holes.items(), self.children):
|
||||||
|
original_item.location = item[1].location
|
||||||
|
|
||||||
|
|
||||||
|
class PlateSlot(ResourceStack):
|
||||||
|
"""板槽位类 - 1个槽上能堆放8个板,移板只能操作最上方的板"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
size_x: float,
|
||||||
|
size_y: float,
|
||||||
|
size_z: float,
|
||||||
|
max_plates: int = 8,
|
||||||
|
category: str = "plate_slot",
|
||||||
|
model: Optional[str] = None
|
||||||
|
):
|
||||||
|
"""初始化板槽位
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 槽位名称
|
||||||
|
max_plates: 最大板数量
|
||||||
|
category: 类别
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
direction="z", # Z方向堆叠
|
||||||
|
resources=[],
|
||||||
|
)
|
||||||
|
self.max_plates = max_plates
|
||||||
|
self.category = category
|
||||||
|
|
||||||
|
def can_add_plate(self) -> bool:
|
||||||
|
"""检查是否可以添加板"""
|
||||||
|
return len(self.children) < self.max_plates
|
||||||
|
|
||||||
|
def add_plate(self, plate: MaterialPlate) -> None:
|
||||||
|
"""添加料板"""
|
||||||
|
if not self.can_add_plate():
|
||||||
|
raise ValueError(f"槽位 {self.name} 已满,无法添加更多板")
|
||||||
|
self.assign_child_resource(plate)
|
||||||
|
|
||||||
|
def get_top_plate(self) -> MaterialPlate:
|
||||||
|
"""获取最上方的板"""
|
||||||
|
if len(self.children) == 0:
|
||||||
|
raise ValueError(f"槽位 {self.name} 为空")
|
||||||
|
return cast(MaterialPlate, self.get_top_item())
|
||||||
|
|
||||||
|
def take_top_plate(self) -> MaterialPlate:
|
||||||
|
"""取出最上方的板"""
|
||||||
|
top_plate = self.get_top_plate()
|
||||||
|
self.unassign_child_resource(top_plate)
|
||||||
|
return top_plate
|
||||||
|
|
||||||
|
def can_access_for_picking(self) -> bool:
|
||||||
|
"""检查是否可以进行取料操作(只有最上方的板能进行取料操作)"""
|
||||||
|
return len(self.children) > 0
|
||||||
|
|
||||||
|
def serialize(self) -> dict:
|
||||||
|
return {
|
||||||
|
**super().serialize(),
|
||||||
|
"max_plates": self.max_plates,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#是一种类型注解,不用self
|
||||||
|
class BatteryState(TypedDict):
|
||||||
|
"""电池状态字典"""
|
||||||
|
diameter: float
|
||||||
|
height: float
|
||||||
|
assembly_pressure: float
|
||||||
|
electrolyte_volume: float
|
||||||
|
electrolyte_name: str
|
||||||
|
|
||||||
|
class Battery(Resource):
|
||||||
|
"""电池类 - 可容纳极片"""
|
||||||
|
children: List[ElectrodeSheet] = []
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
size_x=1,
|
||||||
|
size_y=1,
|
||||||
|
size_z=1,
|
||||||
|
category: str = "battery",
|
||||||
|
):
|
||||||
|
"""初始化电池
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 电池名称
|
||||||
|
diameter: 直径 (mm)
|
||||||
|
height: 高度 (mm)
|
||||||
|
max_volume: 最大容量 (μL)
|
||||||
|
barcode: 二维码编号
|
||||||
|
category: 类别
|
||||||
|
model: 型号
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
size_x=1,
|
||||||
|
size_y=1,
|
||||||
|
size_z=1,
|
||||||
|
category=category,
|
||||||
|
)
|
||||||
|
self._unilabos_state: BatteryState = BatteryState(
|
||||||
|
diameter = 1.0,
|
||||||
|
height = 1.0,
|
||||||
|
assembly_pressure = 1.0,
|
||||||
|
electrolyte_volume = 1.0,
|
||||||
|
electrolyte_name = "DP001"
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_electrolyte_with_bottle(self, bottle: Bottle) -> bool:
|
||||||
|
to_add_name = bottle._unilabos_state["electrolyte_name"]
|
||||||
|
if bottle.aspirate_electrolyte(10):
|
||||||
|
if self.add_electrolyte(to_add_name, 10):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
bottle._unilabos_state["electrolyte_volume"] += 10
|
||||||
|
|
||||||
|
def set_electrolyte(self, name: str, volume: float) -> None:
|
||||||
|
"""设置电解液信息"""
|
||||||
|
self._unilabos_state["electrolyte_name"] = name
|
||||||
|
self._unilabos_state["electrolyte_volume"] = volume
|
||||||
|
#这个应该没用,不会有加了后再加的事情
|
||||||
|
def add_electrolyte(self, name: str, volume: float) -> bool:
|
||||||
|
"""添加电解液信息"""
|
||||||
|
if name != self._unilabos_state["electrolyte_name"]:
|
||||||
|
return False
|
||||||
|
self._unilabos_state["electrolyte_volume"] += volume
|
||||||
|
|
||||||
|
def load_state(self, state: Dict[str, Any]) -> None:
|
||||||
|
"""格式不变"""
|
||||||
|
super().load_state(state)
|
||||||
|
self._unilabos_state = state
|
||||||
|
|
||||||
|
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
||||||
|
"""格式不变"""
|
||||||
|
data = super().serialize_state()
|
||||||
|
data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等)
|
||||||
|
return data
|
||||||
|
|
||||||
|
# 电解液作为属性放进去
|
||||||
|
|
||||||
|
class BatteryPressSlotState(TypedDict):
|
||||||
|
"""电池状态字典"""
|
||||||
|
diameter: float =20.0
|
||||||
|
depth: float = 4.0
|
||||||
|
|
||||||
|
class BatteryPressSlot(Resource):
|
||||||
|
"""电池压制槽类 - 设备,可容纳一个电池"""
|
||||||
|
children: List[Battery] = []
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str = "BatteryPressSlot",
|
||||||
|
category: str = "battery_press_slot",
|
||||||
|
):
|
||||||
|
"""初始化电池压制槽
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 压制槽名称
|
||||||
|
diameter: 直径 (mm)
|
||||||
|
depth: 深度 (mm)
|
||||||
|
category: 类别
|
||||||
|
model: 型号
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
size_x=10,
|
||||||
|
size_y=12,
|
||||||
|
size_z=13,
|
||||||
|
category=category,
|
||||||
|
)
|
||||||
|
self._unilabos_state: BatteryPressSlotState = BatteryPressSlotState()
|
||||||
|
|
||||||
|
def has_battery(self) -> bool:
|
||||||
|
"""检查是否有电池"""
|
||||||
|
return len(self.children) > 0
|
||||||
|
|
||||||
|
def load_state(self, state: Dict[str, Any]) -> None:
|
||||||
|
"""格式不变"""
|
||||||
|
super().load_state(state)
|
||||||
|
self._unilabos_state = state
|
||||||
|
|
||||||
|
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
||||||
|
"""格式不变"""
|
||||||
|
data = super().serialize_state()
|
||||||
|
data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def assign_child_resource(
|
||||||
|
self,
|
||||||
|
resource: Battery,
|
||||||
|
location: Optional[Coordinate],
|
||||||
|
reassign: bool = True,
|
||||||
|
):
|
||||||
|
"""放置极片"""
|
||||||
|
# TODO: 让高京看下槽位只有一个电池时是否这么写。
|
||||||
|
if self.has_battery():
|
||||||
|
raise ValueError(f"槽位已含有一个电池,无法再放置其他电池")
|
||||||
|
super().assign_child_resource(resource, location, reassign)
|
||||||
|
|
||||||
|
# 根据children的编号取物料对象。
|
||||||
|
def get_battery_info(self, index: int) -> Battery:
|
||||||
|
return self.children[0]
|
||||||
|
|
||||||
|
|
||||||
|
def TipBox64(
|
||||||
|
name: str,
|
||||||
|
size_x: float = 127.8,
|
||||||
|
size_y: float = 85.5,
|
||||||
|
size_z: float = 60.0,
|
||||||
|
category: str = "tip_rack",
|
||||||
|
model: Optional[str] = None,
|
||||||
|
):
|
||||||
|
"""64孔枪头盒类"""
|
||||||
|
from pylabrobot.resources.tip import Tip
|
||||||
|
|
||||||
|
# 创建12x8=96个枪头位
|
||||||
|
def make_tip():
|
||||||
|
return Tip(
|
||||||
|
has_filter=False,
|
||||||
|
total_tip_length=20.0,
|
||||||
|
maximal_volume=1000, # 1mL
|
||||||
|
fitting_depth=8.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
tip_spots = create_ordered_items_2d(
|
||||||
|
klass=TipSpot,
|
||||||
|
num_items_x=12,
|
||||||
|
num_items_y=8,
|
||||||
|
dx=8.0,
|
||||||
|
dy=8.0,
|
||||||
|
dz=0.0,
|
||||||
|
item_dx=9.0,
|
||||||
|
item_dy=9.0,
|
||||||
|
size_x=10,
|
||||||
|
size_y=10,
|
||||||
|
size_z=0.0,
|
||||||
|
make_tip=make_tip,
|
||||||
|
)
|
||||||
|
idx_available = list(range(0, 32)) + list(range(64, 96))
|
||||||
|
tip_spots_available = {k: v for i, (k, v) in enumerate(tip_spots.items()) if i in idx_available}
|
||||||
|
tip_rack = TipRack(
|
||||||
|
name=name,
|
||||||
|
size_x=size_x,
|
||||||
|
size_y=size_y,
|
||||||
|
size_z=size_z,
|
||||||
|
# ordered_items=tip_spots_available,
|
||||||
|
ordered_items=tip_spots,
|
||||||
|
category=category,
|
||||||
|
model=model,
|
||||||
|
with_tips=False,
|
||||||
|
)
|
||||||
|
tip_rack.set_tip_state([True]*32 + [False]*32 + [True]*32) # 前32和后32个有枪头,中间32个无枪头
|
||||||
|
return tip_rack
|
||||||
|
|
||||||
|
|
||||||
|
class WasteTipBoxstate(TypedDict):
|
||||||
|
""""废枪头盒状态字典"""
|
||||||
|
max_tips: int = 100
|
||||||
|
tip_count: int = 0
|
||||||
|
|
||||||
|
#枪头不是一次性的(同一溶液则反复使用),根据寄存器判断
|
||||||
|
class WasteTipBox(Trash):
|
||||||
|
"""废枪头盒类 - 100个枪头容量"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
size_x: float = 127.8,
|
||||||
|
size_y: float = 85.5,
|
||||||
|
size_z: float = 60.0,
|
||||||
|
material_z_thickness=0,
|
||||||
|
max_volume=float("inf"),
|
||||||
|
category="trash",
|
||||||
|
model=None,
|
||||||
|
compute_volume_from_height=None,
|
||||||
|
compute_height_from_volume=None,
|
||||||
|
):
|
||||||
|
"""初始化废枪头盒
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 废枪头盒名称
|
||||||
|
size_x: 长度 (mm)
|
||||||
|
size_y: 宽度 (mm)
|
||||||
|
size_z: 高度 (mm)
|
||||||
|
max_tips: 最大枪头容量
|
||||||
|
category: 类别
|
||||||
|
model: 型号
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
size_x=size_x,
|
||||||
|
size_y=size_y,
|
||||||
|
size_z=size_z,
|
||||||
|
category=category,
|
||||||
|
model=model,
|
||||||
|
)
|
||||||
|
self._unilabos_state: WasteTipBoxstate = WasteTipBoxstate()
|
||||||
|
|
||||||
|
def add_tip(self) -> None:
|
||||||
|
"""添加废枪头"""
|
||||||
|
if self._unilabos_state["tip_count"] >= self._unilabos_state["max_tips"]:
|
||||||
|
raise ValueError(f"废枪头盒 {self.name} 已满")
|
||||||
|
self._unilabos_state["tip_count"] += 1
|
||||||
|
|
||||||
|
def get_tip_count(self) -> int:
|
||||||
|
"""获取枪头数量"""
|
||||||
|
return self._unilabos_state["tip_count"]
|
||||||
|
|
||||||
|
def empty(self) -> None:
|
||||||
|
"""清空废枪头盒"""
|
||||||
|
self._unilabos_state["tip_count"] = 0
|
||||||
|
|
||||||
|
|
||||||
|
def load_state(self, state: Dict[str, Any]) -> None:
|
||||||
|
"""格式不变"""
|
||||||
|
super().load_state(state)
|
||||||
|
self._unilabos_state = state
|
||||||
|
|
||||||
|
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
||||||
|
"""格式不变"""
|
||||||
|
data = super().serialize_state()
|
||||||
|
data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class CoincellDeck(Deck):
|
||||||
|
"""纽扣电池组装工作站台面类"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str = "coin_cell_deck",
|
||||||
|
size_x: float = 1450.0, # 1m
|
||||||
|
size_y: float = 1450.0, # 1m
|
||||||
|
size_z: float = 100.0, # 0.9m
|
||||||
|
origin: Coordinate = Coordinate(-2200, 0, 0),
|
||||||
|
category: str = "coin_cell_deck",
|
||||||
|
setup: bool = False, # 是否自动执行 setup
|
||||||
|
):
|
||||||
|
"""初始化纽扣电池组装工作站台面
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 台面名称
|
||||||
|
size_x: 长度 (mm) - 1m
|
||||||
|
size_y: 宽度 (mm) - 1m
|
||||||
|
size_z: 高度 (mm) - 0.9m
|
||||||
|
origin: 原点坐标
|
||||||
|
category: 类别
|
||||||
|
setup: 是否自动执行 setup 配置标准布局
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
size_x=1450.0,
|
||||||
|
size_y=1450.0,
|
||||||
|
size_z=100.0,
|
||||||
|
origin=origin,
|
||||||
|
)
|
||||||
|
if setup:
|
||||||
|
self.setup()
|
||||||
|
|
||||||
|
def setup(self) -> None:
|
||||||
|
"""设置工作站的标准布局 - 包含子弹夹、料盘、瓶架等完整配置"""
|
||||||
|
# ====================================== 子弹夹 ============================================
|
||||||
|
|
||||||
|
# 正极片(4个洞位,2x2布局)
|
||||||
|
zhengji_zip = MagazineHolder_4_Cathode("正极&铝箔弹夹")
|
||||||
|
self.assign_child_resource(zhengji_zip, Coordinate(x=402.0, y=830.0, z=0))
|
||||||
|
|
||||||
|
# 正极壳、平垫片(6个洞位,2x2+2布局)
|
||||||
|
zhengjike_zip = MagazineHolder_6_Cathode("正极壳&平垫片弹夹")
|
||||||
|
self.assign_child_resource(zhengjike_zip, Coordinate(x=566.0, y=272.0, z=0))
|
||||||
|
|
||||||
|
# 负极壳、弹垫片(6个洞位,2x2+2布局)
|
||||||
|
fujike_zip = MagazineHolder_6_Anode("负极壳&弹垫片弹夹")
|
||||||
|
self.assign_child_resource(fujike_zip, Coordinate(x=474.0, y=276.0, z=0))
|
||||||
|
|
||||||
|
# 成品弹夹(6个洞位,3x2布局)
|
||||||
|
chengpindanjia_zip = MagazineHolder_6_Battery("成品弹夹")
|
||||||
|
self.assign_child_resource(chengpindanjia_zip, Coordinate(x=260.0, y=156.0, z=0))
|
||||||
|
|
||||||
|
# ====================================== 物料板 ============================================
|
||||||
|
# 创建物料板(料盘carrier)- 4x4布局
|
||||||
|
# 负极料盘
|
||||||
|
fujiliaopan = MaterialPlate(name="负极料盘", size_x=120, size_y=100, size_z=10.0, fill=True)
|
||||||
|
self.assign_child_resource(fujiliaopan, Coordinate(x=708.0, y=794.0, z=0))
|
||||||
|
# for i in range(16):
|
||||||
|
# fujipian = ElectrodeSheet(name=f"{fujiliaopan.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
|
||||||
|
# fujiliaopan.children[i].assign_child_resource(fujipian, location=None)
|
||||||
|
|
||||||
|
# 隔膜料盘
|
||||||
|
gemoliaopan = MaterialPlate(name="隔膜料盘", size_x=120, size_y=100, size_z=10.0, fill=True)
|
||||||
|
self.assign_child_resource(gemoliaopan, Coordinate(x=718.0, y=918.0, z=0))
|
||||||
|
# for i in range(16):
|
||||||
|
# gemopian = ElectrodeSheet(name=f"{gemoliaopan.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
|
||||||
|
# gemoliaopan.children[i].assign_child_resource(gemopian, location=None)
|
||||||
|
|
||||||
|
# ====================================== 瓶架、移液枪 ============================================
|
||||||
|
# 在台面上放置 3x4 瓶架、6x2 瓶架 与 64孔移液枪头盒
|
||||||
|
# 奔耀上料5ml分液瓶小板 - 由奔曜跨站转运而来,不单独写,但是这里应该有一个堆栈用于摆放分液瓶小板
|
||||||
|
|
||||||
|
# bottle_rack_3x4 = BottleRack(
|
||||||
|
# name="bottle_rack_3x4",
|
||||||
|
# size_x=210.0,
|
||||||
|
# size_y=140.0,
|
||||||
|
# size_z=100.0,
|
||||||
|
# num_items_x=2,
|
||||||
|
# num_items_y=4,
|
||||||
|
# position_spacing=35.0,
|
||||||
|
# orientation="vertical",
|
||||||
|
# )
|
||||||
|
# self.assign_child_resource(bottle_rack_3x4, Coordinate(x=1542.0, y=717.0, z=0))
|
||||||
|
|
||||||
|
# 电解液缓存位 - 6x2布局
|
||||||
|
bottle_rack_6x2 = YIHUA_Electrolyte_12VialCarrier(name="bottle_rack_6x2")
|
||||||
|
self.assign_child_resource(bottle_rack_6x2, Coordinate(x=1050.0, y=358.0, z=0))
|
||||||
|
# 电解液回收位6x2
|
||||||
|
bottle_rack_6x2_2 = YIHUA_Electrolyte_12VialCarrier(name="bottle_rack_6x2_2")
|
||||||
|
self.assign_child_resource(bottle_rack_6x2_2, Coordinate(x=914.0, y=358.0, z=0))
|
||||||
|
|
||||||
|
tip_box = TipBox64(name="tip_box_64")
|
||||||
|
self.assign_child_resource(tip_box, Coordinate(x=782.0, y=514.0, z=0))
|
||||||
|
|
||||||
|
waste_tip_box = WasteTipBox(name="waste_tip_box")
|
||||||
|
self.assign_child_resource(waste_tip_box, Coordinate(x=778.0, y=622.0, z=0))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
deck = create_coin_cell_deck()
|
||||||
|
print(deck)
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,44 +1,133 @@
|
|||||||
import csv
|
import csv
|
||||||
|
import inspect
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import types
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
from pylabrobot.resources import Resource as PLRResource
|
from functools import wraps
|
||||||
|
from pylabrobot.resources import Deck, Resource as PLRResource
|
||||||
from unilabos_msgs.msg import Resource
|
from unilabos_msgs.msg import Resource
|
||||||
from unilabos.device_comms.modbus_plc.client import ModbusTcpClient
|
from unilabos.device_comms.modbus_plc.client import ModbusTcpClient
|
||||||
from unilabos.devices.workstation.coin_cell_assembly.button_battery_station import MaterialHole, MaterialPlate
|
|
||||||
from unilabos.devices.workstation.workstation_base import WorkstationBase
|
from unilabos.devices.workstation.workstation_base import WorkstationBase
|
||||||
from unilabos.device_comms.modbus_plc.client import TCPClient, ModbusNode, PLCWorkflow, ModbusWorkflow, WorkflowAction, BaseClient
|
from unilabos.device_comms.modbus_plc.client import TCPClient, ModbusNode, PLCWorkflow, ModbusWorkflow, WorkflowAction, BaseClient
|
||||||
from unilabos.device_comms.modbus_plc.modbus import DeviceType, Base as ModbusNodeBase, DataType, WorderOrder
|
from unilabos.device_comms.modbus_plc.modbus import DeviceType, Base as ModbusNodeBase, DataType, WorderOrder
|
||||||
from unilabos.devices.workstation.coin_cell_assembly.button_battery_station import *
|
from unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials import *
|
||||||
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode
|
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode
|
||||||
from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
|
from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
|
||||||
|
from unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials import CoincellDeck
|
||||||
|
from unilabos.resources.graphio import convert_resources_to_type
|
||||||
|
from unilabos.utils.log import logger
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_modbus_slave_kw_alias(modbus_client):
|
||||||
|
if modbus_client is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
method_names = [
|
||||||
|
"read_coils",
|
||||||
|
"write_coils",
|
||||||
|
"write_coil",
|
||||||
|
"read_discrete_inputs",
|
||||||
|
"read_holding_registers",
|
||||||
|
"write_register",
|
||||||
|
"write_registers",
|
||||||
|
]
|
||||||
|
|
||||||
|
def _wrap(func):
|
||||||
|
signature = inspect.signature(func)
|
||||||
|
has_var_kwargs = any(param.kind == param.VAR_KEYWORD for param in signature.parameters.values())
|
||||||
|
accepts_unit = has_var_kwargs or "unit" in signature.parameters
|
||||||
|
accepts_slave = has_var_kwargs or "slave" in signature.parameters
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def _wrapped(self, *args, **kwargs):
|
||||||
|
if "slave" in kwargs and not accepts_slave:
|
||||||
|
slave_value = kwargs.pop("slave")
|
||||||
|
if accepts_unit and "unit" not in kwargs:
|
||||||
|
kwargs["unit"] = slave_value
|
||||||
|
if "unit" in kwargs and not accepts_unit:
|
||||||
|
unit_value = kwargs.pop("unit")
|
||||||
|
if accepts_slave and "slave" not in kwargs:
|
||||||
|
kwargs["slave"] = unit_value
|
||||||
|
return func(self, *args, **kwargs)
|
||||||
|
|
||||||
|
_wrapped._has_slave_alias = True
|
||||||
|
return _wrapped
|
||||||
|
|
||||||
|
for name in method_names:
|
||||||
|
if not hasattr(modbus_client, name):
|
||||||
|
continue
|
||||||
|
bound_method = getattr(modbus_client, name)
|
||||||
|
func = getattr(bound_method, "__func__", None)
|
||||||
|
if func is None:
|
||||||
|
continue
|
||||||
|
if getattr(func, "_has_slave_alias", False):
|
||||||
|
continue
|
||||||
|
wrapped = _wrap(func)
|
||||||
|
setattr(modbus_client, name, types.MethodType(wrapped, modbus_client))
|
||||||
|
|
||||||
|
|
||||||
|
def _coerce_deck_input(deck: Any) -> Optional[Deck]:
|
||||||
|
if deck is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(deck, Deck):
|
||||||
|
return deck
|
||||||
|
|
||||||
|
if isinstance(deck, PLRResource):
|
||||||
|
return deck if isinstance(deck, Deck) else None
|
||||||
|
|
||||||
|
candidates = None
|
||||||
|
if isinstance(deck, dict):
|
||||||
|
if "nodes" in deck and isinstance(deck["nodes"], list):
|
||||||
|
candidates = deck["nodes"]
|
||||||
|
else:
|
||||||
|
candidates = [deck]
|
||||||
|
elif isinstance(deck, list):
|
||||||
|
candidates = deck
|
||||||
|
|
||||||
|
if candidates is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
converted = convert_resources_to_type(resources_list=candidates, resource_type=Deck)
|
||||||
|
if isinstance(converted, Deck):
|
||||||
|
return converted
|
||||||
|
if isinstance(converted, list):
|
||||||
|
for item in converted:
|
||||||
|
if isinstance(item, Deck):
|
||||||
|
return item
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(f"deck 转换 Deck 失败: {exc}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
#构建物料系统
|
#构建物料系统
|
||||||
|
|
||||||
class CoinCellAssemblyWorkstation(WorkstationBase):
|
class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||||
def __init__(
|
def __init__(self,
|
||||||
self,
|
config: dict = None,
|
||||||
deck: CoincellDeck,
|
deck=None,
|
||||||
address: str = "192.168.1.20",
|
address: str = "172.16.28.102",
|
||||||
port: str = "502",
|
port: str = "502",
|
||||||
debug_mode: bool = True,
|
debug_mode: bool = False,
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs):
|
||||||
):
|
|
||||||
super().__init__(
|
if deck is None and config:
|
||||||
#桌子
|
deck = config.get('deck')
|
||||||
deck=deck,
|
if deck is None:
|
||||||
*args,
|
logger.info("没有传入依华deck,检查启动json文件")
|
||||||
**kwargs,
|
super().__init__(deck=deck, *args, **kwargs,)
|
||||||
)
|
|
||||||
self.debug_mode = debug_mode
|
self.debug_mode = debug_mode
|
||||||
self.deck = deck
|
|
||||||
""" 连接初始化 """
|
""" 连接初始化 """
|
||||||
modbus_client = TCPClient(addr=address, port=port)
|
modbus_client = TCPClient(addr=address, port=port)
|
||||||
print("modbus_client", modbus_client)
|
logger.debug(f"创建 Modbus 客户端: {modbus_client}")
|
||||||
|
_ensure_modbus_slave_kw_alias(modbus_client.client)
|
||||||
if not debug_mode:
|
if not debug_mode:
|
||||||
modbus_client.client.connect()
|
modbus_client.client.connect()
|
||||||
count = 100
|
count = 100
|
||||||
@@ -49,26 +138,19 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
if not modbus_client.client.is_socket_open():
|
if not modbus_client.client.is_socket_open():
|
||||||
raise ValueError('modbus tcp connection failed')
|
raise ValueError('modbus tcp connection failed')
|
||||||
|
self.nodes = BaseClient.load_csv(os.path.join(os.path.dirname(__file__), 'coin_cell_assembly_1105.csv'))
|
||||||
|
self.client = modbus_client.register_node_list(self.nodes)
|
||||||
else:
|
else:
|
||||||
print("测试模式,跳过连接")
|
print("测试模式,跳过连接")
|
||||||
|
self.nodes, self.client = None, None
|
||||||
""" 工站的配置 """
|
""" 工站的配置 """
|
||||||
self.nodes = BaseClient.load_csv(os.path.join(os.path.dirname(__file__), 'coin_cell_assembly_a.csv'))
|
|
||||||
self.client = modbus_client.register_node_list(self.nodes)
|
|
||||||
self.success = False
|
self.success = False
|
||||||
self.allow_data_read = False #允许读取函数运行标志位
|
self.allow_data_read = False #允许读取函数运行标志位
|
||||||
self.csv_export_thread = None
|
self.csv_export_thread = None
|
||||||
self.csv_export_running = False
|
self.csv_export_running = False
|
||||||
self.csv_export_file = None
|
self.csv_export_file = None
|
||||||
#创建一个物料台面,包含两个极片板
|
self.coin_num_N = 0 #已组装电池数量
|
||||||
#self.deck = create_a_coin_cell_deck()
|
|
||||||
|
|
||||||
#self._ros_node.update_resource(self.deck)
|
|
||||||
|
|
||||||
#ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
|
|
||||||
# "resources": [self.deck]
|
|
||||||
#})
|
|
||||||
|
|
||||||
|
|
||||||
def post_init(self, ros_node: ROS2WorkstationNode):
|
def post_init(self, ros_node: ROS2WorkstationNode):
|
||||||
self._ros_node = ros_node
|
self._ros_node = ros_node
|
||||||
@@ -77,6 +159,27 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
"resources": [self.deck]
|
"resources": [self.deck]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def sync_transfer_resources(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
供跨工站转运完成后调用,强制将当前台面资源同步到云端/前端。
|
||||||
|
"""
|
||||||
|
if not hasattr(self, "_ros_node") or self._ros_node is None:
|
||||||
|
return {"status": "failed", "error": "ros_node_not_ready"}
|
||||||
|
if self.deck is None:
|
||||||
|
return {"status": "failed", "error": "deck_not_initialized"}
|
||||||
|
try:
|
||||||
|
future = ROS2DeviceNode.run_async_func(
|
||||||
|
self._ros_node.update_resource,
|
||||||
|
True,
|
||||||
|
resources=[self.deck],
|
||||||
|
)
|
||||||
|
if future:
|
||||||
|
future.result()
|
||||||
|
return {"status": "success"}
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error(f"同步转运资源失败: {exc}", exc_info=True)
|
||||||
|
return {"status": "failed", "error": str(exc)}
|
||||||
|
|
||||||
# 批量操作在这里写
|
# 批量操作在这里写
|
||||||
async def change_hole_sheet_to_2(self, hole: MaterialHole):
|
async def change_hole_sheet_to_2(self, hole: MaterialHole):
|
||||||
hole._unilabos_state["max_sheets"] = 2
|
hole._unilabos_state["max_sheets"] = 2
|
||||||
@@ -491,11 +594,11 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
try:
|
try:
|
||||||
# 尝试不同的字节序读取
|
# 尝试不同的字节序读取
|
||||||
code_little, read_err = self.client.use_node('REG_DATA_COIN_CELL_CODE').read(10, word_order=WorderOrder.LITTLE)
|
code_little, read_err = self.client.use_node('REG_DATA_COIN_CELL_CODE').read(10, word_order=WorderOrder.LITTLE)
|
||||||
print(code_little)
|
# logger.debug(f"读取电池二维码原始数据: {code_little}")
|
||||||
clean_code = code_little[-8:][::-1]
|
clean_code = code_little[-8:][::-1]
|
||||||
return clean_code
|
return clean_code
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"读取电池二维码失败: {e}")
|
logger.error(f"读取电池二维码失败: {e}")
|
||||||
return "N/A"
|
return "N/A"
|
||||||
|
|
||||||
|
|
||||||
@@ -504,11 +607,11 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
try:
|
try:
|
||||||
# 尝试不同的字节序读取
|
# 尝试不同的字节序读取
|
||||||
code_little, read_err = self.client.use_node('REG_DATA_ELECTROLYTE_CODE').read(10, word_order=WorderOrder.LITTLE)
|
code_little, read_err = self.client.use_node('REG_DATA_ELECTROLYTE_CODE').read(10, word_order=WorderOrder.LITTLE)
|
||||||
print(code_little)
|
# logger.debug(f"读取电解液二维码原始数据: {code_little}")
|
||||||
clean_code = code_little[-8:][::-1]
|
clean_code = code_little[-8:][::-1]
|
||||||
return clean_code
|
return clean_code
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"读取电解液二维码失败: {e}")
|
logger.error(f"读取电解液二维码失败: {e}")
|
||||||
return "N/A"
|
return "N/A"
|
||||||
|
|
||||||
# ===================== 环境监控区 ======================
|
# ===================== 环境监控区 ======================
|
||||||
@@ -606,7 +709,8 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
print("waiting for start_cmd")
|
print("waiting for start_cmd")
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
def func_pack_send_bottle_num(self, bottle_num: int):
|
def func_pack_send_bottle_num(self, bottle_num):
|
||||||
|
bottle_num = int(bottle_num)
|
||||||
#发送电解液平台数
|
#发送电解液平台数
|
||||||
print("启动")
|
print("启动")
|
||||||
while (self._unilab_rece_electrolyte_bottle_num()) == False:
|
while (self._unilab_rece_electrolyte_bottle_num()) == False:
|
||||||
@@ -654,16 +758,25 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
# self.success = True
|
# self.success = True
|
||||||
# return self.success
|
# return self.success
|
||||||
|
|
||||||
def func_pack_send_msg_cmd(self, elec_use_num) -> bool:
|
def func_pack_send_msg_cmd(self, elec_use_num, elec_vol, assembly_type, assembly_pressure) -> bool:
|
||||||
"""UNILAB写参数"""
|
"""UNILAB写参数"""
|
||||||
while (self.request_rec_msg_status) == False:
|
while (self.request_rec_msg_status) == False:
|
||||||
print("wait for request_rec_msg_status to True")
|
print("wait for request_rec_msg_status to True")
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
self.success = False
|
self.success = False
|
||||||
#self._unilab_send_msg_electrolyte_num(elec_num)
|
#self._unilab_send_msg_electrolyte_num(elec_num)
|
||||||
time.sleep(1)
|
#设置平行样数目
|
||||||
self._unilab_send_msg_electrolyte_use_num(elec_use_num)
|
self._unilab_send_msg_electrolyte_use_num(elec_use_num)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
#发送电解液加注量
|
||||||
|
self._unilab_send_msg_electrolyte_vol(elec_vol)
|
||||||
|
time.sleep(1)
|
||||||
|
#发送电解液组装类型
|
||||||
|
self._unilab_send_msg_assembly_type(assembly_type)
|
||||||
|
time.sleep(1)
|
||||||
|
#发送电池压制力
|
||||||
|
self._unilab_send_msg_assembly_pressure(assembly_pressure)
|
||||||
|
time.sleep(1)
|
||||||
self._unilab_send_msg_succ_cmd(True)
|
self._unilab_send_msg_succ_cmd(True)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
while (self.request_rec_msg_status) == True:
|
while (self.request_rec_msg_status) == True:
|
||||||
@@ -688,15 +801,32 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
data_coin_num = self.data_coin_num
|
data_coin_num = self.data_coin_num
|
||||||
data_electrolyte_code = self.data_electrolyte_code
|
data_electrolyte_code = self.data_electrolyte_code
|
||||||
data_coin_cell_code = self.data_coin_cell_code
|
data_coin_cell_code = self.data_coin_cell_code
|
||||||
print("data_open_circuit_voltage", data_open_circuit_voltage)
|
logger.debug(f"data_open_circuit_voltage: {data_open_circuit_voltage}")
|
||||||
print("data_pole_weight", data_pole_weight)
|
logger.debug(f"data_pole_weight: {data_pole_weight}")
|
||||||
print("data_assembly_time", data_assembly_time)
|
logger.debug(f"data_assembly_time: {data_assembly_time}")
|
||||||
print("data_assembly_pressure", data_assembly_pressure)
|
logger.debug(f"data_assembly_pressure: {data_assembly_pressure}")
|
||||||
print("data_electrolyte_volume", data_electrolyte_volume)
|
logger.debug(f"data_electrolyte_volume: {data_electrolyte_volume}")
|
||||||
print("data_coin_num", data_coin_num)
|
logger.debug(f"data_coin_num: {data_coin_num}")
|
||||||
print("data_electrolyte_code", data_electrolyte_code)
|
logger.debug(f"data_electrolyte_code: {data_electrolyte_code}")
|
||||||
print("data_coin_cell_code", data_coin_cell_code)
|
logger.debug(f"data_coin_cell_code: {data_coin_cell_code}")
|
||||||
#接收完信息后,读取完毕标志位置True
|
#接收完信息后,读取完毕标志位置True
|
||||||
|
liaopan3 = self.deck.get_resource("成品弹夹")
|
||||||
|
#把物料解绑后放到另一盘上
|
||||||
|
battery = ElectrodeSheet(name=f"battery_{self.coin_num_N}", size_x=14, size_y=14, size_z=2)
|
||||||
|
battery._unilabos_state = {
|
||||||
|
"electrolyte_name": data_coin_cell_code,
|
||||||
|
"data_electrolyte_code": data_electrolyte_code,
|
||||||
|
"open_circuit_voltage": data_open_circuit_voltage,
|
||||||
|
"assembly_pressure": data_assembly_pressure,
|
||||||
|
"electrolyte_volume": data_electrolyte_volume
|
||||||
|
}
|
||||||
|
liaopan3.children[self.coin_num_N].assign_child_resource(battery, location=None)
|
||||||
|
#print(jipian2.parent)
|
||||||
|
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
|
||||||
|
"resources": [self.deck]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
self._unilab_rec_msg_succ_cmd(True)
|
self._unilab_rec_msg_succ_cmd(True)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
#等待允许读取标志位置False
|
#等待允许读取标志位置False
|
||||||
@@ -754,11 +884,25 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
self.success = True
|
self.success = True
|
||||||
return self.success
|
return self.success
|
||||||
|
|
||||||
|
def qiming_coin_cell_code(self, fujipian_panshu:int, fujipian_juzhendianwei:int=0, gemopanshu:int=0, gemo_juzhendianwei:int=0, lvbodian:bool=True, battery_pressure_mode:bool=True, battery_pressure:int=4000, battery_clean_ignore:bool=False) -> bool:
|
||||||
|
self.success = False
|
||||||
|
self.client.use_node('REG_MSG_NE_PLATE_NUM').write(fujipian_panshu)
|
||||||
|
self.client.use_node('REG_MSG_NE_PLATE_MATRIX').write(fujipian_juzhendianwei)
|
||||||
|
self.client.use_node('REG_MSG_SEPARATOR_PLATE_NUM').write(gemopanshu)
|
||||||
|
self.client.use_node('REG_MSG_SEPARATOR_PLATE_MATRIX').write(gemo_juzhendianwei)
|
||||||
|
self.client.use_node('COIL_ALUMINUM_FOIL').write(not lvbodian)
|
||||||
|
self.client.use_node('REG_MSG_PRESS_MODE').write(not battery_pressure_mode)
|
||||||
|
# self.client.use_node('REG_MSG_ASSEMBLY_PRESSURE').write(battery_pressure)
|
||||||
|
self.client.use_node('REG_MSG_BATTERY_CLEAN_IGNORE').write(battery_clean_ignore)
|
||||||
|
self.success = True
|
||||||
|
|
||||||
|
return self.success
|
||||||
|
|
||||||
def func_allpack_cmd(self, elec_num, elec_use_num, file_path: str="D:\\coin_cell_data") -> bool:
|
def func_allpack_cmd(self, elec_num, elec_use_num, elec_vol:int=50, assembly_type:int=7, assembly_pressure:int=4200, file_path: str="/Users/sml/work") -> bool:
|
||||||
|
elec_num, elec_use_num, elec_vol, assembly_type, assembly_pressure = int(elec_num), int(elec_use_num), int(elec_vol), int(assembly_type), int(assembly_pressure)
|
||||||
summary_csv_file = os.path.join(file_path, "duandian.csv")
|
summary_csv_file = os.path.join(file_path, "duandian.csv")
|
||||||
# 如果断点文件存在,先读取之前的进度
|
# 如果断点文件存在,先读取之前的进度
|
||||||
|
|
||||||
if os.path.exists(summary_csv_file):
|
if os.path.exists(summary_csv_file):
|
||||||
read_status_flag = True
|
read_status_flag = True
|
||||||
with open(summary_csv_file, 'r', newline='', encoding='utf-8') as csvfile:
|
with open(summary_csv_file, 'r', newline='', encoding='utf-8') as csvfile:
|
||||||
@@ -784,53 +928,37 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
elec_num_N = 0
|
elec_num_N = 0
|
||||||
elec_use_num_N = 0
|
elec_use_num_N = 0
|
||||||
coin_num_N = 0
|
coin_num_N = 0
|
||||||
|
for i in range(20):
|
||||||
print(f"剩余电解液瓶数: {elec_num}, 已组装电池数: {elec_use_num}")
|
print(f"剩余电解液瓶数: {elec_num}, 已组装电池数: {elec_use_num}")
|
||||||
|
print(f"剩余电解液瓶数: {type(elec_num)}, 已组装电池数: {type(elec_use_num)}")
|
||||||
|
print(f"剩余电解液瓶数: {type(int(elec_num))}, 已组装电池数: {type(int(elec_use_num))}")
|
||||||
|
|
||||||
#如果是第一次运行,则进行初始化、切换自动、启动, 如果是断点重启则跳过。
|
#如果是第一次运行,则进行初始化、切换自动、启动, 如果是断点重启则跳过。
|
||||||
if read_status_flag == False:
|
if read_status_flag == False:
|
||||||
|
pass
|
||||||
#初始化
|
#初始化
|
||||||
self.func_pack_device_init()
|
#self.func_pack_device_init()
|
||||||
#切换自动
|
#切换自动
|
||||||
self.func_pack_device_auto()
|
#self.func_pack_device_auto()
|
||||||
#启动,小车收回
|
#启动,小车收回
|
||||||
self.func_pack_device_start()
|
#self.func_pack_device_start()
|
||||||
#发送电解液瓶数量,启动搬运,多搬运没事
|
#发送电解液瓶数量,启动搬运,多搬运没事
|
||||||
self.func_pack_send_bottle_num(elec_num)
|
#self.func_pack_send_bottle_num(elec_num)
|
||||||
last_i = elec_num_N
|
last_i = elec_num_N
|
||||||
last_j = elec_use_num_N
|
last_j = elec_use_num_N
|
||||||
for i in range(last_i, elec_num):
|
for i in range(last_i, elec_num):
|
||||||
print(f"开始第{last_i+i+1}瓶电解液的组装")
|
print(f"开始第{last_i+i+1}瓶电解液的组装")
|
||||||
#第一个循环从上次断点继续,后续循环从0开始
|
#第一个循环从上次断点继续,后续循环从0开始
|
||||||
j_start = last_j if i == last_i else 0
|
j_start = last_j if i == last_i else 0
|
||||||
self.func_pack_send_msg_cmd(elec_use_num-j_start)
|
self.func_pack_send_msg_cmd(elec_use_num-j_start, elec_vol, assembly_type, assembly_pressure)
|
||||||
|
|
||||||
for j in range(j_start, elec_use_num):
|
for j in range(j_start, elec_use_num):
|
||||||
print(f"开始第{last_i+i+1}瓶电解液的第{j+j_start+1}个电池组装")
|
print(f"开始第{last_i+i+1}瓶电解液的第{j+j_start+1}个电池组装")
|
||||||
#读取电池组装数据并存入csv
|
#读取电池组装数据并存入csv
|
||||||
self.func_pack_get_msg_cmd(file_path)
|
self.func_pack_get_msg_cmd(file_path)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
#这里定义物料系统
|
|
||||||
# TODO:读完再将电池数加一还是进入循环就将电池数加一需要考虑
|
# TODO:读完再将电池数加一还是进入循环就将电池数加一需要考虑
|
||||||
liaopan1 = self.deck.get_resource("liaopan1")
|
|
||||||
liaopan4 = self.deck.get_resource("liaopan4")
|
|
||||||
jipian1 = liaopan1.children[coin_num_N].children[0]
|
|
||||||
jipian4 = liaopan4.children[coin_num_N].children[0]
|
|
||||||
#print(jipian1)
|
|
||||||
#从料盘上去物料解绑后放到另一盘上
|
|
||||||
jipian1.parent.unassign_child_resource(jipian1)
|
|
||||||
jipian4.parent.unassign_child_resource(jipian4)
|
|
||||||
|
|
||||||
#print(jipian2.parent)
|
|
||||||
battery = Battery(name = f"battery_{coin_num_N}")
|
|
||||||
battery.assign_child_resource(jipian1, location=None)
|
|
||||||
battery.assign_child_resource(jipian4, location=None)
|
|
||||||
|
|
||||||
zidanjia6 = self.deck.get_resource("zi_dan_jia6")
|
|
||||||
|
|
||||||
zidanjia6.children[0].assign_child_resource(battery, location=None)
|
|
||||||
|
|
||||||
|
|
||||||
# 生成断点文件
|
# 生成断点文件
|
||||||
@@ -842,6 +970,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
writer.writerow([elec_num, elec_use_num, elec_num_N, elec_use_num_N, coin_num_N, timestamp])
|
writer.writerow([elec_num, elec_use_num, elec_num_N, elec_use_num_N, coin_num_N, timestamp])
|
||||||
csvfile.flush()
|
csvfile.flush()
|
||||||
coin_num_N += 1
|
coin_num_N += 1
|
||||||
|
self.coin_num_N = coin_num_N
|
||||||
elec_use_num_N += 1
|
elec_use_num_N += 1
|
||||||
elec_num_N += 1
|
elec_num_N += 1
|
||||||
elec_use_num_N = 0
|
elec_use_num_N = 0
|
||||||
@@ -876,38 +1005,54 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
#self.success = True
|
#self.success = True
|
||||||
#return self.success
|
#return self.success
|
||||||
|
|
||||||
|
def run_packaging_workflow(self, workflow_config: Dict[str, Any]) -> "CoinCellAssemblyWorkstation":
|
||||||
|
config = workflow_config or {}
|
||||||
|
|
||||||
|
qiming_params = config.get("qiming") or {}
|
||||||
|
if qiming_params:
|
||||||
|
self.qiming_coin_cell_code(**qiming_params)
|
||||||
|
|
||||||
|
if config.get("init", True):
|
||||||
|
self.func_pack_device_init()
|
||||||
|
if config.get("auto", True):
|
||||||
|
self.func_pack_device_auto()
|
||||||
|
if config.get("start", True):
|
||||||
|
self.func_pack_device_start()
|
||||||
|
|
||||||
|
packaging_config = config.get("packaging") or {}
|
||||||
|
bottle_num = packaging_config.get("bottle_num")
|
||||||
|
if bottle_num is not None:
|
||||||
|
self.func_pack_send_bottle_num(bottle_num)
|
||||||
|
|
||||||
|
allpack_params = packaging_config.get("command") or {}
|
||||||
|
if allpack_params:
|
||||||
|
self.func_allpack_cmd(**allpack_params)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
def fun_wuliao_test(self) -> bool:
|
def fun_wuliao_test(self) -> bool:
|
||||||
#找到data_init中构建的2个物料盘
|
#找到data_init中构建的2个物料盘
|
||||||
#liaopan1 = self.deck.get_resource("liaopan1")
|
liaopan3 = self.deck.get_resource("\u7535\u6c60\u6599\u76d8")
|
||||||
#liaopan4 = self.deck.get_resource("liaopan4")
|
for i in range(16):
|
||||||
#for coin_num_N in range(16):
|
battery = ElectrodeSheet(name=f"battery_{i}", size_x=16, size_y=16, size_z=2)
|
||||||
# liaopan1 = self.deck.get_resource("liaopan1")
|
battery._unilabos_state = {
|
||||||
# liaopan4 = self.deck.get_resource("liaopan4")
|
"diameter": 20.0,
|
||||||
# jipian1 = liaopan1.children[coin_num_N].children[0]
|
"height": 20.0,
|
||||||
# jipian4 = liaopan4.children[coin_num_N].children[0]
|
"assembly_pressure": i,
|
||||||
# #print(jipian1)
|
"electrolyte_volume": 20.0,
|
||||||
# #从料盘上去物料解绑后放到另一盘上
|
"electrolyte_name": f"DP{i}"
|
||||||
# jipian1.parent.unassign_child_resource(jipian1)
|
}
|
||||||
# jipian4.parent.unassign_child_resource(jipian4)
|
liaopan3.children[i].assign_child_resource(battery, location=None)
|
||||||
#
|
|
||||||
# #print(jipian2.parent)
|
|
||||||
# battery = Battery(name = f"battery_{coin_num_N}")
|
|
||||||
# battery.assign_child_resource(jipian1, location=None)
|
|
||||||
# battery.assign_child_resource(jipian4, location=None)
|
|
||||||
#
|
|
||||||
# zidanjia6 = self.deck.get_resource("zi_dan_jia6")
|
|
||||||
# zidanjia6.children[0].assign_child_resource(battery, location=None)
|
|
||||||
# ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
|
|
||||||
# "resources": [self.deck]
|
|
||||||
# })
|
|
||||||
# time.sleep(2)
|
|
||||||
for i in range(20):
|
|
||||||
print(f"输出{i}")
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
|
|
||||||
|
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
|
||||||
|
"resources": [self.deck]
|
||||||
|
})
|
||||||
|
# for i in range(40):
|
||||||
|
# print(f"fun_wuliao_test 运行结束{i}")
|
||||||
|
# time.sleep(1)
|
||||||
|
# time.sleep(40)
|
||||||
# 数据读取与输出
|
# 数据读取与输出
|
||||||
def func_read_data_and_output(self, file_path: str="D:\\coin_cell_data"):
|
def func_read_data_and_output(self, file_path: str="/Users/sml/work"):
|
||||||
# 检查CSV导出是否正在运行,已运行则跳出,防止同时启动两个while循环
|
# 检查CSV导出是否正在运行,已运行则跳出,防止同时启动两个while循环
|
||||||
if self.csv_export_running:
|
if self.csv_export_running:
|
||||||
return False, "读取已在运行中"
|
return False, "读取已在运行中"
|
||||||
@@ -1012,7 +1157,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
# else:
|
# else:
|
||||||
# print("子弹夹洞位0没有极片")
|
# print("子弹夹洞位0没有极片")
|
||||||
#
|
#
|
||||||
# #把电解液从瓶中取到电池夹子中
|
# # TODO:#把电解液从瓶中取到电池夹子中
|
||||||
# battery_site = deck.get_resource("battery_press_1")
|
# battery_site = deck.get_resource("battery_press_1")
|
||||||
# clip_magazine_battery = deck.get_resource("clip_magazine_battery")
|
# clip_magazine_battery = deck.get_resource("clip_magazine_battery")
|
||||||
# if battery_site.has_battery():
|
# if battery_site.has_battery():
|
||||||
@@ -1101,42 +1246,92 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
def run_coin_cell_assembly_workflow(
|
||||||
|
self,
|
||||||
|
workflow_config: Optional[Dict[str, Any]] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
config: Dict[str, Any]
|
||||||
|
if workflow_config is None:
|
||||||
|
config = {}
|
||||||
|
elif isinstance(workflow_config, list):
|
||||||
|
config = {"materials": workflow_config}
|
||||||
|
else:
|
||||||
|
config = workflow_config
|
||||||
|
qiming_defaults = {
|
||||||
|
"fujipian_panshu": 1,
|
||||||
|
"fujipian_juzhendianwei": 0,
|
||||||
|
"gemopanshu": 1,
|
||||||
|
"gemo_juzhendianwei": 0,
|
||||||
|
"lvbodian": True,
|
||||||
|
"battery_pressure_mode": True,
|
||||||
|
"battery_pressure": 4200,
|
||||||
|
"battery_clean_ignore": False,
|
||||||
|
}
|
||||||
|
qiming_params = {**qiming_defaults, **(config.get("qiming") or {})}
|
||||||
|
qiming_success = self.qiming_coin_cell_code(**qiming_params)
|
||||||
|
|
||||||
|
step_results: Dict[str, Any] = {}
|
||||||
|
try:
|
||||||
|
self.func_pack_device_init()
|
||||||
|
step_results["init"] = True
|
||||||
|
except Exception as exc:
|
||||||
|
step_results["init"] = f"error: {exc}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.func_pack_device_auto()
|
||||||
|
step_results["auto"] = True
|
||||||
|
except Exception as exc:
|
||||||
|
step_results["auto"] = f"error: {exc}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.func_pack_device_start()
|
||||||
|
step_results["start"] = True
|
||||||
|
except Exception as exc:
|
||||||
|
step_results["start"] = f"error: {exc}"
|
||||||
|
|
||||||
|
packaging_cfg = config.get("packaging") or {}
|
||||||
|
bottle_num = packaging_cfg.get("bottle_num", 1)
|
||||||
|
try:
|
||||||
|
self.func_pack_send_bottle_num(bottle_num)
|
||||||
|
step_results["send_bottle_num"] = True
|
||||||
|
except Exception as exc:
|
||||||
|
step_results["send_bottle_num"] = f"error: {exc}"
|
||||||
|
|
||||||
|
command_defaults = {
|
||||||
|
"elec_num": 1,
|
||||||
|
"elec_use_num": 1,
|
||||||
|
"elec_vol": 50,
|
||||||
|
"assembly_type": 7,
|
||||||
|
"assembly_pressure": 4200,
|
||||||
|
"file_path": "/Users/sml/work",
|
||||||
|
}
|
||||||
|
command_params = {**command_defaults, **(packaging_cfg.get("command") or {})}
|
||||||
|
packaging_result = self.func_allpack_cmd(**command_params)
|
||||||
|
|
||||||
|
finished_result = self.func_pack_send_finished_cmd()
|
||||||
|
stop_result = self.func_pack_device_stop()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"qiming": {
|
||||||
|
"params": qiming_params,
|
||||||
|
"success": qiming_success,
|
||||||
|
},
|
||||||
|
"workflow_steps": step_results,
|
||||||
|
"packaging": {
|
||||||
|
"bottle_num": bottle_num,
|
||||||
|
"command": command_params,
|
||||||
|
"result": packaging_result,
|
||||||
|
},
|
||||||
|
"finish": {
|
||||||
|
"send_finished": finished_result,
|
||||||
|
"stop": stop_result,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
from pylabrobot.resources import Resource
|
deck = CoincellDeck(setup=True, name="coin_cell_deck")
|
||||||
Coin_Cell = CoinCellAssemblyWorkstation(Resource("1", 1, 1, 1), debug_mode=True)
|
w = CoinCellAssemblyWorkstation(deck=deck, address="172.16.28.102", port="502", debug_mode=False)
|
||||||
#Coin_Cell.func_pack_device_init()
|
w.run_coin_cell_assembly_workflow()
|
||||||
#Coin_Cell.func_pack_device_auto()
|
|
||||||
#Coin_Cell.func_pack_device_start()
|
|
||||||
#Coin_Cell.func_pack_send_bottle_num(2)
|
|
||||||
#Coin_Cell.func_pack_send_msg_cmd(2)
|
|
||||||
#Coin_Cell.func_pack_get_msg_cmd()
|
|
||||||
#Coin_Cell.func_pack_get_msg_cmd()
|
|
||||||
#Coin_Cell.func_pack_send_finished_cmd()
|
|
||||||
#
|
|
||||||
#Coin_Cell.func_allpack_cmd(3, 2)
|
|
||||||
#print(Coin_Cell.data_stack_vision_code)
|
|
||||||
#print("success")
|
|
||||||
#创建一个物料台面
|
|
||||||
|
|
||||||
#deck = create_a_coin_cell_deck()
|
|
||||||
|
|
||||||
##在台面上找到料盘和极片
|
|
||||||
#liaopan1 = deck.get_resource("liaopan1")
|
|
||||||
#liaopan2 = deck.get_resource("liaopan2")
|
|
||||||
#jipian1 = liaopan1.children[1].children[0]
|
|
||||||
#
|
|
||||||
##print(jipian1)
|
|
||||||
##把物料解绑后放到另一盘上
|
|
||||||
#jipian1.parent.unassign_child_resource(jipian1)
|
|
||||||
#liaopan2.children[1].assign_child_resource(jipian1, location=None)
|
|
||||||
##print(jipian2.parent)
|
|
||||||
from unilabos.resources.graphio import resource_ulab_to_plr, convert_resources_to_type
|
|
||||||
|
|
||||||
with open("./button_battery_decks_unilab.json", "r", encoding="utf-8") as f:
|
|
||||||
bioyond_resources_unilab = json.load(f)
|
|
||||||
print(f"成功读取 JSON 文件,包含 {len(bioyond_resources_unilab)} 个资源")
|
|
||||||
ulab_resources = convert_resources_to_type(bioyond_resources_unilab, List[PLRResource])
|
|
||||||
print(f"转换结果类型: {type(ulab_resources)}")
|
|
||||||
print(ulab_resources)
|
|
||||||
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
Name,DataType,InitValue,Comment,Attribute,DeviceType,Address,
|
||||||
|
COIL_SYS_START_CMD,BOOL,,,,coil,9010,
|
||||||
|
COIL_SYS_STOP_CMD,BOOL,,,,coil,9020,
|
||||||
|
COIL_SYS_RESET_CMD,BOOL,,,,coil,9030,
|
||||||
|
COIL_SYS_HAND_CMD,BOOL,,,,coil,9040,
|
||||||
|
COIL_SYS_AUTO_CMD,BOOL,,,,coil,9050,
|
||||||
|
COIL_SYS_INIT_CMD,BOOL,,,,coil,9060,
|
||||||
|
COIL_UNILAB_SEND_MSG_SUCC_CMD,BOOL,,,,coil,9700,
|
||||||
|
COIL_UNILAB_REC_MSG_SUCC_CMD,BOOL,,,,coil,9710,unilab_rec_msg_succ_cmd
|
||||||
|
COIL_SYS_START_STATUS,BOOL,,,,coil,9210,
|
||||||
|
COIL_SYS_STOP_STATUS,BOOL,,,,coil,9220,
|
||||||
|
COIL_SYS_RESET_STATUS,BOOL,,,,coil,9230,
|
||||||
|
COIL_SYS_HAND_STATUS,BOOL,,,,coil,9240,
|
||||||
|
COIL_SYS_AUTO_STATUS,BOOL,,,,coil,9250,
|
||||||
|
COIL_SYS_INIT_STATUS,BOOL,,,,coil,9260,
|
||||||
|
COIL_REQUEST_REC_MSG_STATUS,BOOL,,,,coil,9500,
|
||||||
|
COIL_REQUEST_SEND_MSG_STATUS,BOOL,,,,coil,9510,request_send_msg_status
|
||||||
|
REG_MSG_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,17000,
|
||||||
|
REG_MSG_ELECTROLYTE_NUM,INT16,,,,hold_register,17002,unilab_send_msg_electrolyte_num
|
||||||
|
REG_MSG_ELECTROLYTE_VOLUME,INT16,,,,hold_register,17004,unilab_send_msg_electrolyte_vol
|
||||||
|
REG_MSG_ASSEMBLY_TYPE,INT16,,,,hold_register,17006,unilab_send_msg_assembly_type
|
||||||
|
REG_MSG_ASSEMBLY_PRESSURE,INT16,,,,hold_register,17008,unilab_send_msg_assembly_pressure
|
||||||
|
REG_DATA_ASSEMBLY_COIN_CELL_NUM,INT16,,,,hold_register,16000,data_assembly_coin_cell_num
|
||||||
|
REG_DATA_OPEN_CIRCUIT_VOLTAGE,FLOAT32,,,,hold_register,16002,data_open_circuit_voltage
|
||||||
|
REG_DATA_AXIS_X_POS,FLOAT32,,,,hold_register,16004,
|
||||||
|
REG_DATA_AXIS_Y_POS,FLOAT32,,,,hold_register,16006,
|
||||||
|
REG_DATA_AXIS_Z_POS,FLOAT32,,,,hold_register,16008,
|
||||||
|
REG_DATA_POLE_WEIGHT,FLOAT32,,,,hold_register,16010,data_pole_weight
|
||||||
|
REG_DATA_ASSEMBLY_PER_TIME,FLOAT32,,,,hold_register,16012,data_assembly_time
|
||||||
|
REG_DATA_ASSEMBLY_PRESSURE,INT16,,,,hold_register,16014,data_assembly_pressure
|
||||||
|
REG_DATA_ELECTROLYTE_VOLUME,INT16,,,,hold_register,16016,data_electrolyte_volume
|
||||||
|
REG_DATA_COIN_NUM,INT16,,,,hold_register,16018,data_coin_num
|
||||||
|
REG_DATA_ELECTROLYTE_CODE,STRING,,,,hold_register,16020,data_electrolyte_code()
|
||||||
|
REG_DATA_COIN_CELL_CODE,STRING,,,,hold_register,16030,data_coin_cell_code()
|
||||||
|
REG_DATA_STACK_VISON_CODE,STRING,,,,hold_register,18004,data_stack_vision_code()
|
||||||
|
REG_DATA_GLOVE_BOX_PRESSURE,FLOAT32,,,,hold_register,16050,data_glove_box_pressure
|
||||||
|
REG_DATA_GLOVE_BOX_WATER_CONTENT,FLOAT32,,,,hold_register,16052,data_glove_box_water_content
|
||||||
|
REG_DATA_GLOVE_BOX_O2_CONTENT,FLOAT32,,,,hold_register,16054,data_glove_box_o2_content
|
||||||
|
UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,9720,
|
||||||
|
UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,9520,
|
||||||
|
REG_MSG_ELECTROLYTE_NUM_USED,INT16,,,,hold_register,17496,
|
||||||
|
REG_DATA_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,16000,
|
||||||
|
UNILAB_SEND_FINISHED_CMD,BOOL,,,,coil,9730,
|
||||||
|
UNILAB_RECE_FINISHED_CMD,BOOL,,,,coil,9530,
|
||||||
|
REG_DATA_ASSEMBLY_TYPE,INT16,,,,hold_register,16018,ASSEMBLY_TYPE7or8
|
||||||
|
COIL_ALUMINUM_FOIL,BOOL,,使用铝箔垫,,coil,9340,
|
||||||
|
REG_MSG_NE_PLATE_MATRIX,INT16,,负极片矩阵点位,,hold_register,17440,
|
||||||
|
REG_MSG_SEPARATOR_PLATE_MATRIX,INT16,,隔膜矩阵点位,,hold_register,17450,
|
||||||
|
REG_MSG_TIP_BOX_MATRIX,INT16,,移液枪头矩阵点位,,hold_register,17480,
|
||||||
|
REG_MSG_NE_PLATE_NUM,INT16,,负极片盘数,,hold_register,17443,
|
||||||
|
REG_MSG_SEPARATOR_PLATE_NUM,INT16,,隔膜盘数,,hold_register,17453,
|
||||||
|
REG_MSG_PRESS_MODE,BOOL,,压制模式(false:压力检测模式,True:距离模式),,coil,9360,电池压制模式
|
||||||
|
,,,,,,,
|
||||||
|
,BOOL,,视觉对位(false:使用,true:忽略),,coil,9300,视觉对位
|
||||||
|
,BOOL,,复检(false:使用,true:忽略),,coil,9310,视觉复检
|
||||||
|
,BOOL,,手套箱_左仓(false:使用,true:忽略),,coil,9320,手套箱左仓
|
||||||
|
,BOOL,,手套箱_右仓(false:使用,true:忽略),,coil,9420,手套箱右仓
|
||||||
|
,BOOL,,真空检知(false:使用,true:忽略),,coil,9350,真空检知
|
||||||
|
,BOOL,,电解液添加模式(false:单次滴液,true:二次滴液),,coil,9370,滴液模式
|
||||||
|
,BOOL,,正极片称重(false:使用,true:忽略),,coil,9380,正极片称重
|
||||||
|
,BOOL,,正负极片组装方式(false:正装,true:倒装),,coil,9390,正负极反装
|
||||||
|
,BOOL,,压制清洁(false:使用,true:忽略),,coil,9400,压制清洁
|
||||||
|
,BOOL,,物料盘摆盘方式(false:水平摆盘,true:堆叠摆盘),,coil,9410,负极片摆盘方式
|
||||||
|
REG_MSG_BATTERY_CLEAN_IGNORE,BOOL,,忽略电池清洁(false:使用,true:忽略),,coil,9460,
|
||||||
|
@@ -0,0 +1,64 @@
|
|||||||
|
Name,DataType,InitValue,Comment,Attribute,DeviceType,Address,
|
||||||
|
COIL_SYS_START_CMD,BOOL,,,,coil,8010,
|
||||||
|
COIL_SYS_STOP_CMD,BOOL,,,,coil,8020,
|
||||||
|
COIL_SYS_RESET_CMD,BOOL,,,,coil,8030,
|
||||||
|
COIL_SYS_HAND_CMD,BOOL,,,,coil,8040,
|
||||||
|
COIL_SYS_AUTO_CMD,BOOL,,,,coil,8050,
|
||||||
|
COIL_SYS_INIT_CMD,BOOL,,,,coil,8060,
|
||||||
|
COIL_UNILAB_SEND_MSG_SUCC_CMD,BOOL,,,,coil,8700,
|
||||||
|
COIL_UNILAB_REC_MSG_SUCC_CMD,BOOL,,,,coil,8710,unilab_rec_msg_succ_cmd
|
||||||
|
COIL_SYS_START_STATUS,BOOL,,,,coil,8210,
|
||||||
|
COIL_SYS_STOP_STATUS,BOOL,,,,coil,8220,
|
||||||
|
COIL_SYS_RESET_STATUS,BOOL,,,,coil,8230,
|
||||||
|
COIL_SYS_HAND_STATUS,BOOL,,,,coil,8240,
|
||||||
|
COIL_SYS_AUTO_STATUS,BOOL,,,,coil,8250,
|
||||||
|
COIL_SYS_INIT_STATUS,BOOL,,,,coil,8260,
|
||||||
|
COIL_REQUEST_REC_MSG_STATUS,BOOL,,,,coil,8500,
|
||||||
|
COIL_REQUEST_SEND_MSG_STATUS,BOOL,,,,coil,8510,request_send_msg_status
|
||||||
|
REG_MSG_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,11000,
|
||||||
|
REG_MSG_ELECTROLYTE_NUM,INT16,,,,hold_register,11002,unilab_send_msg_electrolyte_num
|
||||||
|
REG_MSG_ELECTROLYTE_VOLUME,INT16,,,,hold_register,11004,unilab_send_msg_electrolyte_vol
|
||||||
|
REG_MSG_ASSEMBLY_TYPE,INT16,,,,hold_register,11006,unilab_send_msg_assembly_type
|
||||||
|
REG_MSG_ASSEMBLY_PRESSURE,INT16,,,,hold_register,11008,unilab_send_msg_assembly_pressure
|
||||||
|
REG_DATA_ASSEMBLY_COIN_CELL_NUM,INT16,,,,hold_register,10000,data_assembly_coin_cell_num
|
||||||
|
REG_DATA_OPEN_CIRCUIT_VOLTAGE,FLOAT32,,,,hold_register,10002,data_open_circuit_voltage
|
||||||
|
REG_DATA_AXIS_X_POS,FLOAT32,,,,hold_register,10004,
|
||||||
|
REG_DATA_AXIS_Y_POS,FLOAT32,,,,hold_register,10006,
|
||||||
|
REG_DATA_AXIS_Z_POS,FLOAT32,,,,hold_register,10008,
|
||||||
|
REG_DATA_POLE_WEIGHT,FLOAT32,,,,hold_register,10010,data_pole_weight
|
||||||
|
REG_DATA_ASSEMBLY_PER_TIME,FLOAT32,,,,hold_register,10012,data_assembly_time
|
||||||
|
REG_DATA_ASSEMBLY_PRESSURE,INT16,,,,hold_register,10014,data_assembly_pressure
|
||||||
|
REG_DATA_ELECTROLYTE_VOLUME,INT16,,,,hold_register,10016,data_electrolyte_volume
|
||||||
|
REG_DATA_COIN_NUM,INT16,,,,hold_register,10018,data_coin_num
|
||||||
|
REG_DATA_ELECTROLYTE_CODE,STRING,,,,hold_register,10020,data_electrolyte_code()
|
||||||
|
REG_DATA_COIN_CELL_CODE,STRING,,,,hold_register,10030,data_coin_cell_code()
|
||||||
|
REG_DATA_STACK_VISON_CODE,STRING,,,,hold_register,12004,data_stack_vision_code()
|
||||||
|
REG_DATA_GLOVE_BOX_PRESSURE,FLOAT32,,,,hold_register,10050,data_glove_box_pressure
|
||||||
|
REG_DATA_GLOVE_BOX_WATER_CONTENT,FLOAT32,,,,hold_register,10052,data_glove_box_water_content
|
||||||
|
REG_DATA_GLOVE_BOX_O2_CONTENT,FLOAT32,,,,hold_register,10054,data_glove_box_o2_content
|
||||||
|
UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,8720,
|
||||||
|
UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,8520,
|
||||||
|
REG_MSG_ELECTROLYTE_NUM_USED,INT16,,,,hold_register,496,
|
||||||
|
REG_DATA_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,10000,
|
||||||
|
UNILAB_SEND_FINISHED_CMD,BOOL,,,,coil,8730,
|
||||||
|
UNILAB_RECE_FINISHED_CMD,BOOL,,,,coil,8530,
|
||||||
|
REG_DATA_ASSEMBLY_TYPE,INT16,,,,hold_register,10018,ASSEMBLY_TYPE7or8
|
||||||
|
COIL_ALUMINUM_FOIL,BOOL,,使用铝箔垫,,coil,8340,
|
||||||
|
REG_MSG_NE_PLATE_MATRIX,INT16,,负极片矩阵点位,,hold_register,440,
|
||||||
|
REG_MSG_SEPARATOR_PLATE_MATRIX,INT16,,隔膜矩阵点位,,hold_register,450,
|
||||||
|
REG_MSG_TIP_BOX_MATRIX,INT16,,移液枪头矩阵点位,,hold_register,480,
|
||||||
|
REG_MSG_NE_PLATE_NUM,INT16,,负极片盘数,,hold_register,443,
|
||||||
|
REG_MSG_SEPARATOR_PLATE_NUM,INT16,,隔膜盘数,,hold_register,453,
|
||||||
|
REG_MSG_PRESS_MODE,BOOL,,压制模式(false:压力检测模式,True:距离模式),,coil,8360,电池压制模式
|
||||||
|
,,,,,,,
|
||||||
|
,BOOL,,视觉对位(false:使用,true:忽略),,coil,8300,视觉对位
|
||||||
|
,BOOL,,复检(false:使用,true:忽略),,coil,8310,视觉复检
|
||||||
|
,BOOL,,手套箱_左仓(false:使用,true:忽略),,coil,8320,手套箱左仓
|
||||||
|
,BOOL,,手套箱_右仓(false:使用,true:忽略),,coil,8420,手套箱右仓
|
||||||
|
,BOOL,,真空检知(false:使用,true:忽略),,coil,8350,真空检知
|
||||||
|
,BOOL,,电解液添加模式(false:单次滴液,true:二次滴液),,coil,8370,滴液模式
|
||||||
|
,BOOL,,正极片称重(false:使用,true:忽略),,coil,8380,正极片称重
|
||||||
|
,BOOL,,正负极片组装方式(false:正装,true:倒装),,coil,8390,正负极反装
|
||||||
|
,BOOL,,压制清洁(false:使用,true:忽略),,coil,8400,压制清洁
|
||||||
|
,BOOL,,物料盘摆盘方式(false:水平摆盘,true:堆叠摆盘),,coil,8410,负极片摆盘方式
|
||||||
|
REG_MSG_BATTERY_CLEAN_IGNORE,BOOL,,忽略电池清洁(false:使用,true:忽略),,coil,8460,
|
||||||
|
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "bioyond_cell_workstation",
|
||||||
|
"name": "配液分液工站",
|
||||||
|
"children": [
|
||||||
|
],
|
||||||
|
"parent": null,
|
||||||
|
"type": "device",
|
||||||
|
"class": "bioyond_cell",
|
||||||
|
"config": {
|
||||||
|
"protocol_type": [],
|
||||||
|
"station_resource": {}
|
||||||
|
},
|
||||||
|
"data": {}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "BatteryStation",
|
||||||
|
"name": "扣电工作站",
|
||||||
|
"children": [
|
||||||
|
"coin_cell_deck"
|
||||||
|
],
|
||||||
|
"parent": null,
|
||||||
|
"type": "device",
|
||||||
|
"class": "coincellassemblyworkstation_device",
|
||||||
|
"position": {
|
||||||
|
"x": -600,
|
||||||
|
"y": -400,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"debug_mode": false,
|
||||||
|
"protocol_type": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": []
|
||||||
|
}
|
||||||
@@ -147,7 +147,7 @@ class WorkstationBase(ABC):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
deck: Optional[Deck],
|
deck: Deck,
|
||||||
*args,
|
*args,
|
||||||
**kwargs, # 必须有kwargs
|
**kwargs, # 必须有kwargs
|
||||||
):
|
):
|
||||||
@@ -349,5 +349,5 @@ class WorkstationBase(ABC):
|
|||||||
|
|
||||||
|
|
||||||
class ProtocolNode(WorkstationBase):
|
class ProtocolNode(WorkstationBase):
|
||||||
def __init__(self, protocol_type: List[str], deck: Optional[PLRResource], *args, **kwargs):
|
def __init__(self, deck: Optional[PLRResource], *args, **kwargs):
|
||||||
super().__init__(deck, *args, **kwargs)
|
super().__init__(deck, *args, **kwargs)
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from dataclasses import dataclass, asdict
|
from dataclasses import dataclass, asdict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from unilabos.utils.log import logger
|
from unilabos.utils.log import logger
|
||||||
|
|
||||||
@@ -77,14 +76,6 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
logger.info(f"收到工作站报送: {endpoint} - {request_data.get('token', 'unknown')}")
|
logger.info(f"收到工作站报送: {endpoint} - {request_data.get('token', 'unknown')}")
|
||||||
|
|
||||||
try:
|
|
||||||
payload_for_log = {"method": "POST", **request_data}
|
|
||||||
self._save_raw_request(endpoint, payload_for_log)
|
|
||||||
if hasattr(self.workstation, '_reports_received_count'):
|
|
||||||
self.workstation._reports_received_count += 1
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 统一的报送端点路由(基于LIMS协议规范)
|
# 统一的报送端点路由(基于LIMS协议规范)
|
||||||
if endpoint == '/report/step_finish':
|
if endpoint == '/report/step_finish':
|
||||||
response = self._handle_step_finish_report(request_data)
|
response = self._handle_step_finish_report(request_data)
|
||||||
@@ -99,8 +90,6 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
response = self._handle_material_change_report(request_data)
|
response = self._handle_material_change_report(request_data)
|
||||||
elif endpoint == '/report/error_handling':
|
elif endpoint == '/report/error_handling':
|
||||||
response = self._handle_error_handling_report(request_data)
|
response = self._handle_error_handling_report(request_data)
|
||||||
elif endpoint == '/report/temperature-cutoff':
|
|
||||||
response = self._handle_temperature_cutoff_report(request_data)
|
|
||||||
# 保留LIMS协议端点以兼容现有系统
|
# 保留LIMS协议端点以兼容现有系统
|
||||||
elif endpoint == '/LIMS/step_finish':
|
elif endpoint == '/LIMS/step_finish':
|
||||||
response = self._handle_step_finish_report(request_data)
|
response = self._handle_step_finish_report(request_data)
|
||||||
@@ -118,8 +107,7 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
"/report/order_finish",
|
"/report/order_finish",
|
||||||
"/report/batch_update",
|
"/report/batch_update",
|
||||||
"/report/material_change",
|
"/report/material_change",
|
||||||
"/report/error_handling",
|
"/report/error_handling"
|
||||||
"/report/temperature-cutoff"
|
|
||||||
]}
|
]}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -140,11 +128,6 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
parsed_path = urlparse(self.path)
|
parsed_path = urlparse(self.path)
|
||||||
endpoint = parsed_path.path
|
endpoint = parsed_path.path
|
||||||
|
|
||||||
try:
|
|
||||||
self._save_raw_request(endpoint, {"method": "GET"})
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if endpoint == '/status':
|
if endpoint == '/status':
|
||||||
response = self._handle_status_check()
|
response = self._handle_status_check()
|
||||||
elif endpoint == '/health':
|
elif endpoint == '/health':
|
||||||
@@ -335,50 +318,6 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
message=f"任务完成报送处理失败: {str(e)}"
|
message=f"任务完成报送处理失败: {str(e)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _handle_temperature_cutoff_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
|
||||||
try:
|
|
||||||
required_fields = ['token', 'request_time', 'data']
|
|
||||||
if missing := [f for f in required_fields if f not in request_data]:
|
|
||||||
return HttpResponse(success=False, message=f"缺少必要字段: {', '.join(missing)}")
|
|
||||||
|
|
||||||
data = request_data['data']
|
|
||||||
metrics = [
|
|
||||||
'frameCode',
|
|
||||||
'generateTime',
|
|
||||||
'targetTemperature',
|
|
||||||
'settingTemperature',
|
|
||||||
'inTemperature',
|
|
||||||
'outTemperature',
|
|
||||||
'pt100Temperature',
|
|
||||||
'sensorAverageTemperature',
|
|
||||||
'speed',
|
|
||||||
'force',
|
|
||||||
'viscosity',
|
|
||||||
'averageViscosity'
|
|
||||||
]
|
|
||||||
if miss := [f for f in metrics if f not in data]:
|
|
||||||
return HttpResponse(success=False, message=f"data字段缺少必要内容: {', '.join(miss)}")
|
|
||||||
|
|
||||||
report_request = WorkstationReportRequest(
|
|
||||||
token=request_data['token'],
|
|
||||||
request_time=request_data['request_time'],
|
|
||||||
data=data
|
|
||||||
)
|
|
||||||
|
|
||||||
result = {}
|
|
||||||
if hasattr(self.workstation, 'process_temperature_cutoff_report'):
|
|
||||||
result = self.workstation.process_temperature_cutoff_report(report_request)
|
|
||||||
|
|
||||||
return HttpResponse(
|
|
||||||
success=True,
|
|
||||||
message=f"温度/粘度报送已处理: 帧{data['frameCode']}",
|
|
||||||
acknowledgment_id=f"TEMP_CUTOFF_{int(time.time()*1000)}_{data['frameCode']}",
|
|
||||||
data=result
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"处理温度/粘度报送失败: {e}\n{traceback.format_exc()}")
|
|
||||||
return HttpResponse(success=False, message=f"温度/粘度报送处理失败: {str(e)}")
|
|
||||||
|
|
||||||
def _handle_batch_update_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
def _handle_batch_update_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||||||
"""处理批量报送"""
|
"""处理批量报送"""
|
||||||
try:
|
try:
|
||||||
@@ -549,18 +488,11 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
def _handle_status_check(self) -> HttpResponse:
|
def _handle_status_check(self) -> HttpResponse:
|
||||||
"""处理状态查询"""
|
"""处理状态查询"""
|
||||||
try:
|
try:
|
||||||
# 安全地获取 device_id
|
|
||||||
device_id = "unknown"
|
|
||||||
if hasattr(self.workstation, 'device_id'):
|
|
||||||
device_id = self.workstation.device_id
|
|
||||||
elif hasattr(self.workstation, '_ros_node') and hasattr(self.workstation._ros_node, 'device_id'):
|
|
||||||
device_id = self.workstation._ros_node.device_id
|
|
||||||
|
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
success=True,
|
success=True,
|
||||||
message="工作站报送服务正常运行",
|
message="工作站报送服务正常运行",
|
||||||
data={
|
data={
|
||||||
"workstation_id": device_id,
|
"workstation_id": self.workstation.device_id,
|
||||||
"service_type": "unified_reporting_service",
|
"service_type": "unified_reporting_service",
|
||||||
"uptime": time.time() - getattr(self.workstation, '_start_time', time.time()),
|
"uptime": time.time() - getattr(self.workstation, '_start_time', time.time()),
|
||||||
"reports_received": getattr(self.workstation, '_reports_received_count', 0),
|
"reports_received": getattr(self.workstation, '_reports_received_count', 0),
|
||||||
@@ -571,7 +503,6 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
"POST /report/batch_update",
|
"POST /report/batch_update",
|
||||||
"POST /report/material_change",
|
"POST /report/material_change",
|
||||||
"POST /report/error_handling",
|
"POST /report/error_handling",
|
||||||
"POST /report/temperature-cutoff",
|
|
||||||
"GET /status",
|
"GET /status",
|
||||||
"GET /health"
|
"GET /health"
|
||||||
]
|
]
|
||||||
@@ -609,22 +540,6 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
"""重写日志方法"""
|
"""重写日志方法"""
|
||||||
logger.debug(f"HTTP请求: {format % args}")
|
logger.debug(f"HTTP请求: {format % args}")
|
||||||
|
|
||||||
def _save_raw_request(self, endpoint: str, request_data: Dict[str, Any]) -> None:
|
|
||||||
try:
|
|
||||||
base_dir = Path(__file__).resolve().parents[3] / "unilabos_data" / "http_reports"
|
|
||||||
base_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
log_path = getattr(self.workstation, "_http_log_path", None)
|
|
||||||
log_file = Path(log_path) if log_path else (base_dir / f"http_{int(time.time()*1000)}.log")
|
|
||||||
payload = {
|
|
||||||
"endpoint": endpoint,
|
|
||||||
"received_at": datetime.now().isoformat(),
|
|
||||||
"body": request_data
|
|
||||||
}
|
|
||||||
with open(log_file, "a", encoding="utf-8") as f:
|
|
||||||
f.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class WorkstationHTTPService:
|
class WorkstationHTTPService:
|
||||||
"""工作站HTTP服务"""
|
"""工作站HTTP服务"""
|
||||||
@@ -650,23 +565,12 @@ class WorkstationHTTPService:
|
|||||||
|
|
||||||
# 创建HTTP服务器
|
# 创建HTTP服务器
|
||||||
self.server = HTTPServer((self.host, self.port), handler_factory)
|
self.server = HTTPServer((self.host, self.port), handler_factory)
|
||||||
base_dir = Path(__file__).resolve().parents[3] / "unilabos_data" / "http_reports"
|
|
||||||
base_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
session_log = base_dir / f"http_{int(time.time()*1000)}.log"
|
|
||||||
setattr(self.workstation, "_http_log_path", str(session_log))
|
|
||||||
|
|
||||||
# 安全地获取 device_id 用于线程命名
|
|
||||||
device_id = "unknown"
|
|
||||||
if hasattr(self.workstation, 'device_id'):
|
|
||||||
device_id = self.workstation.device_id
|
|
||||||
elif hasattr(self.workstation, '_ros_node') and hasattr(self.workstation._ros_node, 'device_id'):
|
|
||||||
device_id = self.workstation._ros_node.device_id
|
|
||||||
|
|
||||||
# 在单独线程中运行服务器
|
# 在单独线程中运行服务器
|
||||||
self.server_thread = threading.Thread(
|
self.server_thread = threading.Thread(
|
||||||
target=self._run_server,
|
target=self._run_server,
|
||||||
daemon=True,
|
daemon=True,
|
||||||
name=f"WorkstationHTTP-{device_id}"
|
name=f"WorkstationHTTP-{self.workstation.device_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.running = True
|
self.running = True
|
||||||
@@ -681,7 +585,6 @@ class WorkstationHTTPService:
|
|||||||
logger.info("扩展报送端点:")
|
logger.info("扩展报送端点:")
|
||||||
logger.info(" - POST /report/material_change # 物料变更报送")
|
logger.info(" - POST /report/material_change # 物料变更报送")
|
||||||
logger.info(" - POST /report/error_handling # 错误处理报送")
|
logger.info(" - POST /report/error_handling # 错误处理报送")
|
||||||
logger.info(" - POST /report/temperature-cutoff # 温度/粘度报送")
|
|
||||||
logger.info("兼容端点:")
|
logger.info("兼容端点:")
|
||||||
logger.info(" - POST /LIMS/step_finish # 兼容LIMS步骤完成")
|
logger.info(" - POST /LIMS/step_finish # 兼容LIMS步骤完成")
|
||||||
logger.info(" - POST /LIMS/preintake_finish # 兼容LIMS通量完成")
|
logger.info(" - POST /LIMS/preintake_finish # 兼容LIMS通量完成")
|
||||||
@@ -765,7 +668,7 @@ __all__ = [
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# 简单测试HTTP服务
|
# 简单测试HTTP服务
|
||||||
class BioyondWorkstation:
|
class DummyWorkstation:
|
||||||
device_id = "WS-001"
|
device_id = "WS-001"
|
||||||
|
|
||||||
def process_step_finish_report(self, report_request):
|
def process_step_finish_report(self, report_request):
|
||||||
@@ -783,10 +686,7 @@ if __name__ == "__main__":
|
|||||||
def handle_external_error(self, error_data):
|
def handle_external_error(self, error_data):
|
||||||
return {"handled": True}
|
return {"handled": True}
|
||||||
|
|
||||||
def process_temperature_cutoff_report(self, report_request):
|
workstation = DummyWorkstation()
|
||||||
return {"processed": True, "metrics": report_request.data}
|
|
||||||
|
|
||||||
workstation = BioyondWorkstation()
|
|
||||||
http_service = WorkstationHTTPService(workstation)
|
http_service = WorkstationHTTPService(workstation)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -809,3 +709,4 @@ if __name__ == "__main__":
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"服务器运行错误: {e}")
|
print(f"服务器运行错误: {e}")
|
||||||
http_service.stop()
|
http_service.stop()
|
||||||
|
|
||||||
|
|||||||
1285
unilabos/registry/devices/bioyond_cell.yaml
Normal file
1285
unilabos/registry/devices/bioyond_cell.yaml
Normal file
File diff suppressed because it is too large
Load Diff
751
unilabos/registry/devices/coin_cell_workstation.yaml
Normal file
751
unilabos/registry/devices/coin_cell_workstation.yaml
Normal file
@@ -0,0 +1,751 @@
|
|||||||
|
coincellassemblyworkstation_device:
|
||||||
|
category:
|
||||||
|
- coin_cell_workstation
|
||||||
|
class:
|
||||||
|
action_value_mappings:
|
||||||
|
auto-change_hole_sheet_to_2:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
hole: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
hole:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- hole
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: change_hole_sheet_to_2参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommandAsync
|
||||||
|
auto-fill_plate:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: fill_plate参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommandAsync
|
||||||
|
auto-fun_wuliao_test:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: fun_wuliao_test参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_allpack_cmd:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
assembly_pressure: 4200
|
||||||
|
assembly_type: 7
|
||||||
|
elec_num: null
|
||||||
|
elec_use_num: null
|
||||||
|
elec_vol: 50
|
||||||
|
file_path: /Users/sml/work
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
assembly_pressure:
|
||||||
|
default: 4200
|
||||||
|
type: integer
|
||||||
|
assembly_type:
|
||||||
|
default: 7
|
||||||
|
type: integer
|
||||||
|
elec_num:
|
||||||
|
type: string
|
||||||
|
elec_use_num:
|
||||||
|
type: string
|
||||||
|
elec_vol:
|
||||||
|
default: 50
|
||||||
|
type: integer
|
||||||
|
file_path:
|
||||||
|
default: /Users/sml/work
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- elec_num
|
||||||
|
- elec_use_num
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_allpack_cmd参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_get_csv_export_status:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_get_csv_export_status参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_pack_device_auto:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_pack_device_auto参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_pack_device_init:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_pack_device_init参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_pack_device_start:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_pack_device_start参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_pack_device_stop:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_pack_device_stop参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_pack_get_msg_cmd:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
file_path: D:\coin_cell_data
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
file_path:
|
||||||
|
default: D:\coin_cell_data
|
||||||
|
type: string
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_pack_get_msg_cmd参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_pack_send_bottle_num:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
bottle_num: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
bottle_num:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- bottle_num
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_pack_send_bottle_num参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_pack_send_finished_cmd:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_pack_send_finished_cmd参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_pack_send_msg_cmd:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
assembly_pressure: null
|
||||||
|
assembly_type: null
|
||||||
|
elec_use_num: null
|
||||||
|
elec_vol: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
assembly_pressure:
|
||||||
|
type: string
|
||||||
|
assembly_type:
|
||||||
|
type: string
|
||||||
|
elec_use_num:
|
||||||
|
type: string
|
||||||
|
elec_vol:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- elec_use_num
|
||||||
|
- elec_vol
|
||||||
|
- assembly_type
|
||||||
|
- assembly_pressure
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_pack_send_msg_cmd参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_read_data_and_output:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
file_path: /Users/sml/work
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
file_path:
|
||||||
|
default: /Users/sml/work
|
||||||
|
type: string
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_read_data_and_output参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_stop_read_data:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_stop_read_data参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-modify_deck_name:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
resource_name: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
resource_name:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- resource_name
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: modify_deck_name参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-post_init:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
ros_node: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
ros_node:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- ros_node
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: post_init参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-qiming_coin_cell_code:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
battery_clean_ignore: false
|
||||||
|
battery_pressure: 4000
|
||||||
|
battery_pressure_mode: true
|
||||||
|
fujipian_juzhendianwei: 0
|
||||||
|
fujipian_panshu: null
|
||||||
|
gemo_juzhendianwei: 0
|
||||||
|
gemopanshu: 0
|
||||||
|
lvbodian: true
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
battery_clean_ignore:
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
battery_pressure:
|
||||||
|
default: 4000
|
||||||
|
type: integer
|
||||||
|
battery_pressure_mode:
|
||||||
|
default: true
|
||||||
|
type: boolean
|
||||||
|
fujipian_juzhendianwei:
|
||||||
|
default: 0
|
||||||
|
type: integer
|
||||||
|
fujipian_panshu:
|
||||||
|
type: integer
|
||||||
|
gemo_juzhendianwei:
|
||||||
|
default: 0
|
||||||
|
type: integer
|
||||||
|
gemopanshu:
|
||||||
|
default: 0
|
||||||
|
type: integer
|
||||||
|
lvbodian:
|
||||||
|
default: true
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- fujipian_panshu
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: qiming_coin_cell_code参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-run_coin_cell_assembly_workflow:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
workflow_config:
|
||||||
|
type: object
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
goal_default:
|
||||||
|
workflow_config: {}
|
||||||
|
handles:
|
||||||
|
input:
|
||||||
|
- data_key: workflow_config
|
||||||
|
data_source: handle
|
||||||
|
data_type: resource
|
||||||
|
handler_key: WorkflowConfig
|
||||||
|
label: Workflow Config
|
||||||
|
output:
|
||||||
|
- data_key: qiming
|
||||||
|
data_source: executor
|
||||||
|
data_type: resource
|
||||||
|
handler_key: QimingResult
|
||||||
|
label: Qiming Result
|
||||||
|
- data_key: workflow_steps
|
||||||
|
data_source: executor
|
||||||
|
data_type: resource
|
||||||
|
handler_key: WorkflowSteps
|
||||||
|
label: Workflow Steps
|
||||||
|
- data_key: packaging
|
||||||
|
data_source: executor
|
||||||
|
data_type: resource
|
||||||
|
handler_key: PackagingResult
|
||||||
|
label: Packaging Result
|
||||||
|
- data_key: finish
|
||||||
|
data_source: executor
|
||||||
|
data_type: resource
|
||||||
|
handler_key: FinishResult
|
||||||
|
label: Finish Result
|
||||||
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
properties:
|
||||||
|
finish:
|
||||||
|
properties:
|
||||||
|
send_finished:
|
||||||
|
type: object
|
||||||
|
stop:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- send_finished
|
||||||
|
- stop
|
||||||
|
type: object
|
||||||
|
packaging:
|
||||||
|
properties:
|
||||||
|
bottle_num:
|
||||||
|
type: integer
|
||||||
|
command:
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- bottle_num
|
||||||
|
- command
|
||||||
|
- result
|
||||||
|
type: object
|
||||||
|
qiming:
|
||||||
|
properties:
|
||||||
|
params:
|
||||||
|
type: object
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- params
|
||||||
|
- success
|
||||||
|
type: object
|
||||||
|
workflow_steps:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- qiming
|
||||||
|
- workflow_steps
|
||||||
|
- packaging
|
||||||
|
- finish
|
||||||
|
type: object
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
workflow_config:
|
||||||
|
type: object
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
properties:
|
||||||
|
finish:
|
||||||
|
properties:
|
||||||
|
send_finished:
|
||||||
|
type: object
|
||||||
|
stop:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- send_finished
|
||||||
|
- stop
|
||||||
|
type: object
|
||||||
|
packaging:
|
||||||
|
properties:
|
||||||
|
bottle_num:
|
||||||
|
type: integer
|
||||||
|
command:
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- bottle_num
|
||||||
|
- command
|
||||||
|
- result
|
||||||
|
type: object
|
||||||
|
qiming:
|
||||||
|
properties:
|
||||||
|
params:
|
||||||
|
type: object
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- params
|
||||||
|
- success
|
||||||
|
type: object
|
||||||
|
workflow_steps:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- qiming
|
||||||
|
- workflow_steps
|
||||||
|
- packaging
|
||||||
|
- finish
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: run_coin_cell_assembly_workflow参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-run_packaging_workflow:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
workflow_config: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
workflow_config:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- workflow_config
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: run_packaging_workflow参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
module: unilabos.devices.workstation.coin_cell_assembly.coin_cell_assembly:CoinCellAssemblyWorkstation
|
||||||
|
status_types:
|
||||||
|
data_assembly_coin_cell_num: int
|
||||||
|
data_assembly_pressure: int
|
||||||
|
data_assembly_time: float
|
||||||
|
data_axis_x_pos: float
|
||||||
|
data_axis_y_pos: float
|
||||||
|
data_axis_z_pos: float
|
||||||
|
data_coin_cell_code: str
|
||||||
|
data_coin_num: int
|
||||||
|
data_electrolyte_code: str
|
||||||
|
data_electrolyte_volume: int
|
||||||
|
data_glove_box_o2_content: float
|
||||||
|
data_glove_box_pressure: float
|
||||||
|
data_glove_box_water_content: float
|
||||||
|
data_open_circuit_voltage: float
|
||||||
|
data_pole_weight: float
|
||||||
|
request_rec_msg_status: bool
|
||||||
|
request_send_msg_status: bool
|
||||||
|
sys_mode: str
|
||||||
|
sys_status: str
|
||||||
|
type: python
|
||||||
|
config_info: []
|
||||||
|
description: 扣电工站
|
||||||
|
handles: []
|
||||||
|
icon: koudian.webp
|
||||||
|
init_param_schema:
|
||||||
|
config:
|
||||||
|
properties:
|
||||||
|
address:
|
||||||
|
default: 172.16.28.102
|
||||||
|
type: string
|
||||||
|
config:
|
||||||
|
type: object
|
||||||
|
debug_mode:
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
deck:
|
||||||
|
type: string
|
||||||
|
port:
|
||||||
|
default: '502'
|
||||||
|
type: string
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
data:
|
||||||
|
properties:
|
||||||
|
data_assembly_coin_cell_num:
|
||||||
|
type: integer
|
||||||
|
data_assembly_pressure:
|
||||||
|
type: integer
|
||||||
|
data_assembly_time:
|
||||||
|
type: number
|
||||||
|
data_axis_x_pos:
|
||||||
|
type: number
|
||||||
|
data_axis_y_pos:
|
||||||
|
type: number
|
||||||
|
data_axis_z_pos:
|
||||||
|
type: number
|
||||||
|
data_coin_cell_code:
|
||||||
|
type: string
|
||||||
|
data_coin_num:
|
||||||
|
type: integer
|
||||||
|
data_electrolyte_code:
|
||||||
|
type: string
|
||||||
|
data_electrolyte_volume:
|
||||||
|
type: integer
|
||||||
|
data_glove_box_o2_content:
|
||||||
|
type: number
|
||||||
|
data_glove_box_pressure:
|
||||||
|
type: number
|
||||||
|
data_glove_box_water_content:
|
||||||
|
type: number
|
||||||
|
data_open_circuit_voltage:
|
||||||
|
type: number
|
||||||
|
data_pole_weight:
|
||||||
|
type: number
|
||||||
|
request_rec_msg_status:
|
||||||
|
type: boolean
|
||||||
|
request_send_msg_status:
|
||||||
|
type: boolean
|
||||||
|
sys_mode:
|
||||||
|
type: string
|
||||||
|
sys_status:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- sys_status
|
||||||
|
- sys_mode
|
||||||
|
- request_rec_msg_status
|
||||||
|
- request_send_msg_status
|
||||||
|
- data_assembly_coin_cell_num
|
||||||
|
- data_assembly_time
|
||||||
|
- data_open_circuit_voltage
|
||||||
|
- data_axis_x_pos
|
||||||
|
- data_axis_y_pos
|
||||||
|
- data_axis_z_pos
|
||||||
|
- data_pole_weight
|
||||||
|
- data_assembly_pressure
|
||||||
|
- data_electrolyte_volume
|
||||||
|
- data_coin_num
|
||||||
|
- data_coin_cell_code
|
||||||
|
- data_electrolyte_code
|
||||||
|
- data_glove_box_pressure
|
||||||
|
- data_glove_box_o2_content
|
||||||
|
- data_glove_box_water_content
|
||||||
|
type: object
|
||||||
|
registry_type: device
|
||||||
|
version: 1.0.0
|
||||||
4
unilabos/resources/battery/__init__.py
Normal file
4
unilabos/resources/battery/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
"""Battery-related resource classes for coin cell assembly"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
45
unilabos/resources/battery/bottle_carriers.py
Normal file
45
unilabos/resources/battery/bottle_carriers.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
"""
|
||||||
|
瓶架类定义 - 用于纽扣电池组装工作站
|
||||||
|
Bottle Carrier Resource Classes
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
from pylabrobot.resources import ResourceHolder
|
||||||
|
from pylabrobot.resources.utils import create_ordered_items_2d
|
||||||
|
from unilabos.resources.itemized_carrier import ItemizedCarrier
|
||||||
|
|
||||||
|
|
||||||
|
def YIHUA_Electrolyte_12VialCarrier(name: str) -> ItemizedCarrier:
|
||||||
|
"""依华电解液12瓶架 - 3x4布局
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 瓶架名称
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ItemizedCarrier: 包含12个瓶位的瓶架
|
||||||
|
"""
|
||||||
|
sites = create_ordered_items_2d(
|
||||||
|
klass=ResourceHolder,
|
||||||
|
num_items_x=4,
|
||||||
|
num_items_y=3,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=5.0,
|
||||||
|
item_dx=70.0,
|
||||||
|
item_dy=26.67,
|
||||||
|
size_x=60.0,
|
||||||
|
size_y=20.0,
|
||||||
|
size_z=70.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ItemizedCarrier(
|
||||||
|
name=name,
|
||||||
|
size_x=300.0,
|
||||||
|
size_y=100.0,
|
||||||
|
size_z=80.0,
|
||||||
|
num_items_x=4,
|
||||||
|
num_items_y=3,
|
||||||
|
sites=sites,
|
||||||
|
category="bottle_carrier",
|
||||||
|
)
|
||||||
|
|
||||||
67
unilabos/resources/battery/electrode_sheet.py
Normal file
67
unilabos/resources/battery/electrode_sheet.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
"""
|
||||||
|
电极片类定义
|
||||||
|
Electrode Sheet Resource Classes
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
from pylabrobot.resources.resource import Resource
|
||||||
|
|
||||||
|
|
||||||
|
class ElectrodeSheet(Resource):
|
||||||
|
"""电极片类 - 用于纽扣电池组装"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
size_x: float = 12.0,
|
||||||
|
size_y: float = 12.0,
|
||||||
|
size_z: float = 0.1,
|
||||||
|
category: str = "electrode_sheet",
|
||||||
|
electrode_type: str = "anode", # "anode" 负极, "cathode" 正极, "separator" 隔膜
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
"""初始化电极片
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 电极片名称
|
||||||
|
size_x: X方向尺寸 (mm)
|
||||||
|
size_y: Y方向尺寸 (mm)
|
||||||
|
size_z: Z方向尺寸/厚度 (mm)
|
||||||
|
category: 类别
|
||||||
|
electrode_type: 电极类型
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
size_x=size_x,
|
||||||
|
size_y=size_y,
|
||||||
|
size_z=size_z,
|
||||||
|
category=category,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
self._electrode_type = electrode_type
|
||||||
|
self._unilabos_state: Dict[str, Any] = {
|
||||||
|
"electrode_type": electrode_type,
|
||||||
|
"material": "",
|
||||||
|
"thickness": size_z,
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def electrode_type(self) -> str:
|
||||||
|
"""获取电极类型"""
|
||||||
|
return self._electrode_type
|
||||||
|
|
||||||
|
def load_state(self, state: Dict[str, Any]) -> None:
|
||||||
|
"""加载状态"""
|
||||||
|
super().load_state(state)
|
||||||
|
if isinstance(state, dict):
|
||||||
|
self._unilabos_state.update(state)
|
||||||
|
|
||||||
|
def serialize_state(self) -> Dict[str, Any]:
|
||||||
|
"""序列化状态"""
|
||||||
|
data = super().serialize_state()
|
||||||
|
data.update(self._unilabos_state)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
152
unilabos/resources/battery/magazine.py
Normal file
152
unilabos/resources/battery/magazine.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
"""
|
||||||
|
弹夹架类定义 - 用于纽扣电池组装工作站
|
||||||
|
Magazine Holder Resource Classes
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import List, Optional
|
||||||
|
from pylabrobot.resources.coordinate import Coordinate
|
||||||
|
from pylabrobot.resources import ResourceHolder
|
||||||
|
from pylabrobot.resources.utils import create_ordered_items_2d
|
||||||
|
from unilabos.resources.itemized_carrier import ItemizedCarrier
|
||||||
|
|
||||||
|
|
||||||
|
def MagazineHolder_4_Cathode(name: str) -> ItemizedCarrier:
|
||||||
|
"""正极&铝箔弹夹 - 4个洞位 (2x2布局)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 弹夹名称
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ItemizedCarrier: 包含4个槽位的弹夹架
|
||||||
|
"""
|
||||||
|
sites = create_ordered_items_2d(
|
||||||
|
klass=ResourceHolder,
|
||||||
|
num_items_x=2,
|
||||||
|
num_items_y=2,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=0.0,
|
||||||
|
item_dx=50.0,
|
||||||
|
item_dy=30.0,
|
||||||
|
size_x=40.0,
|
||||||
|
size_y=25.0,
|
||||||
|
size_z=40.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ItemizedCarrier(
|
||||||
|
name=name,
|
||||||
|
size_x=120.0,
|
||||||
|
size_y=80.0,
|
||||||
|
size_z=50.0,
|
||||||
|
num_items_x=2,
|
||||||
|
num_items_y=2,
|
||||||
|
sites=sites,
|
||||||
|
category="magazine_holder",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def MagazineHolder_6_Cathode(name: str) -> ItemizedCarrier:
|
||||||
|
"""正极壳&平垫片弹夹 - 6个洞位 (2x3布局)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 弹夹名称
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ItemizedCarrier: 包含6个槽位的弹夹架
|
||||||
|
"""
|
||||||
|
sites = create_ordered_items_2d(
|
||||||
|
klass=ResourceHolder,
|
||||||
|
num_items_x=3,
|
||||||
|
num_items_y=2,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=0.0,
|
||||||
|
item_dx=40.0,
|
||||||
|
item_dy=30.0,
|
||||||
|
size_x=35.0,
|
||||||
|
size_y=25.0,
|
||||||
|
size_z=40.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ItemizedCarrier(
|
||||||
|
name=name,
|
||||||
|
size_x=150.0,
|
||||||
|
size_y=80.0,
|
||||||
|
size_z=50.0,
|
||||||
|
num_items_x=3,
|
||||||
|
num_items_y=2,
|
||||||
|
sites=sites,
|
||||||
|
category="magazine_holder",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def MagazineHolder_6_Anode(name: str) -> ItemizedCarrier:
|
||||||
|
"""负极壳&弹垫片弹夹 - 6个洞位 (2x3布局)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 弹夹名称
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ItemizedCarrier: 包含6个槽位的弹夹架
|
||||||
|
"""
|
||||||
|
sites = create_ordered_items_2d(
|
||||||
|
klass=ResourceHolder,
|
||||||
|
num_items_x=3,
|
||||||
|
num_items_y=2,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=0.0,
|
||||||
|
item_dx=40.0,
|
||||||
|
item_dy=30.0,
|
||||||
|
size_x=35.0,
|
||||||
|
size_y=25.0,
|
||||||
|
size_z=40.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ItemizedCarrier(
|
||||||
|
name=name,
|
||||||
|
size_x=150.0,
|
||||||
|
size_y=80.0,
|
||||||
|
size_z=50.0,
|
||||||
|
num_items_x=3,
|
||||||
|
num_items_y=2,
|
||||||
|
sites=sites,
|
||||||
|
category="magazine_holder",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def MagazineHolder_6_Battery(name: str) -> ItemizedCarrier:
|
||||||
|
"""成品弹夹 - 6个洞位 (3x2布局)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 弹夹名称
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ItemizedCarrier: 包含6个槽位的弹夹架
|
||||||
|
"""
|
||||||
|
sites = create_ordered_items_2d(
|
||||||
|
klass=ResourceHolder,
|
||||||
|
num_items_x=3,
|
||||||
|
num_items_y=2,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=0.0,
|
||||||
|
item_dx=33.0,
|
||||||
|
item_dy=40.0,
|
||||||
|
size_x=30.0,
|
||||||
|
size_y=35.0,
|
||||||
|
size_z=40.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ItemizedCarrier(
|
||||||
|
name=name,
|
||||||
|
size_x=120.0,
|
||||||
|
size_y=100.0,
|
||||||
|
size_z=50.0,
|
||||||
|
num_items_x=3,
|
||||||
|
num_items_y=2,
|
||||||
|
sites=sites,
|
||||||
|
category="magazine_holder",
|
||||||
|
)
|
||||||
|
|
||||||
Reference in New Issue
Block a user