mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-17 13:01:12 +00:00
Workstation templates: Resources and its CRUD, and workstation tasks (#95)
* coin_cell_station draft * refactor: rename "station_resource" to "deck" * add standardized BIOYOND resources: bottle_carrier, bottle * refactor and add BIOYOND resources tests * add BIOYOND deck assignment and pass all tests * fix: update resource with correct structure; remove deprecated liquid_handler set_group action * feat: 将新威电池测试系统驱动与配置文件并入 workstation_dev_YB2 (#92) * feat: 新威电池测试系统驱动与注册文件 * feat: bring neware driver & battery.json into workstation_dev_YB2 * add bioyond studio draft * bioyond station with communication init and resource sync * fix bioyond station and registry * create/update resources with POST/PUT for big amount/ small amount data * refactor: add itemized_carrier instead of carrier consists of ResourceHolder * create warehouse by factory func * update bioyond launch json * add child_size for itemized_carrier * fix bioyond resource io --------- Co-authored-by: h840473807 <47357934+h840473807@users.noreply.github.com> Co-authored-by: Xie Qiming <97236197+Andy6M@users.noreply.github.com>
This commit is contained in:
@@ -41,7 +41,7 @@
|
|||||||
"HydrogenateProtocol",
|
"HydrogenateProtocol",
|
||||||
"RecrystallizeProtocol"
|
"RecrystallizeProtocol"
|
||||||
],
|
],
|
||||||
"station_resource": {
|
"deck": {
|
||||||
"data": {
|
"data": {
|
||||||
"_resource_child_name": "deck",
|
"_resource_child_name": "deck",
|
||||||
"_resource_type": "pylabrobot.resources.opentrons.deck:OTDeck"
|
"_resource_type": "pylabrobot.resources.opentrons.deck:OTDeck"
|
||||||
|
|||||||
69
test/experiments/reaction_station_bioyond_test.json
Normal file
69
test/experiments/reaction_station_bioyond_test.json
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "reaction_station_bioyond",
|
||||||
|
"name": "reaction_station_bioyond",
|
||||||
|
"parent": null,
|
||||||
|
"children": [
|
||||||
|
"Bioyond_Deck"
|
||||||
|
],
|
||||||
|
"type": "device",
|
||||||
|
"class": "workstation.bioyond",
|
||||||
|
"config": {
|
||||||
|
"bioyond_config": {
|
||||||
|
"api_key": "DE9BDDA0",
|
||||||
|
"api_host": "http://192.168.1.200:44388",
|
||||||
|
"workflow_mappings": {
|
||||||
|
"reactor_taken_out": "3a16081e-4788-ca37-eff4-ceed8d7019d1",
|
||||||
|
"reactor_taken_in": "3a160df6-76b3-0957-9eb0-cb496d5721c6",
|
||||||
|
"Solid_feeding_vials": "3a160877-87e7-7699-7bc6-ec72b05eb5e6",
|
||||||
|
"Liquid_feeding_vials(non-titration)": "3a167d99-6158-c6f0-15b5-eb030f7d8e47",
|
||||||
|
"Liquid_feeding_solvents": "3a160824-0665-01ed-285a-51ef817a9046",
|
||||||
|
"Liquid_feeding(titration)": "3a160824-0665-01ed-285a-51ef817a9046",
|
||||||
|
"Liquid_feeding_beaker": "3a16087e-124f-8ddb-8ec1-c2dff09ca784",
|
||||||
|
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
|
||||||
|
},
|
||||||
|
"material_type_mappings": {
|
||||||
|
"烧杯": "BIOYOND_PolymerStation_1FlaskCarrier",
|
||||||
|
"试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier",
|
||||||
|
"样品板": "BIOYOND_PolymerStation_6VialCarrier"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deck": {
|
||||||
|
"data": {
|
||||||
|
"_resource_child_name": "Bioyond_Deck",
|
||||||
|
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"protocol_type": []
|
||||||
|
},
|
||||||
|
"data": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Bioyond_Deck",
|
||||||
|
"name": "Bioyond_Deck",
|
||||||
|
"sample_id": null,
|
||||||
|
"children": [
|
||||||
|
],
|
||||||
|
"parent": "reaction_station_bioyond",
|
||||||
|
"type": "deck",
|
||||||
|
"class": "BIOYOND_PolymerReactionStation_Deck",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"type": "BIOYOND_PolymerReactionStation_Deck",
|
||||||
|
"setup": true,
|
||||||
|
"rotation": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0,
|
||||||
|
"type": "Rotation"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
198
test/resources/bioyond_materials.json
Normal file
198
test/resources/bioyond_materials.json
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "3a1c67a9-aed7-b94d-9e24-bfdf10c8baa9",
|
||||||
|
"typeName": "烧杯",
|
||||||
|
"code": "0006-00160",
|
||||||
|
"barCode": "",
|
||||||
|
"name": "ODA",
|
||||||
|
"quantity": 120000.00000000000000000000000,
|
||||||
|
"lockQuantity": 695374.00000000000000000000000,
|
||||||
|
"unit": "微升",
|
||||||
|
"status": 1,
|
||||||
|
"isUse": false,
|
||||||
|
"locations": [
|
||||||
|
{
|
||||||
|
"id": "3a14aa17-0d49-11d7-a6e1-f236b3e5e5a3",
|
||||||
|
"whid": "3a14aa17-0d49-dce4-486e-4b5c85c8b366",
|
||||||
|
"whName": "堆栈1",
|
||||||
|
"code": "0001-0001",
|
||||||
|
"x": 1,
|
||||||
|
"y": 1,
|
||||||
|
"z": 1,
|
||||||
|
"quantity": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"detail": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3a1c67a9-aed9-1ade-5fe1-cc04b24b171c",
|
||||||
|
"typeName": "烧杯",
|
||||||
|
"code": "0006-00161",
|
||||||
|
"barCode": "",
|
||||||
|
"name": "MPDA",
|
||||||
|
"quantity": 120000.00000000000000000000000,
|
||||||
|
"lockQuantity": 681618.00000000000000000000000,
|
||||||
|
"unit": "",
|
||||||
|
"status": 1,
|
||||||
|
"isUse": false,
|
||||||
|
"locations": [
|
||||||
|
{
|
||||||
|
"id": "3a14aa17-0d49-4bc5-8836-517b75473f5f",
|
||||||
|
"whid": "3a14aa17-0d49-dce4-486e-4b5c85c8b366",
|
||||||
|
"whName": "堆栈1",
|
||||||
|
"code": "0001-0002",
|
||||||
|
"x": 1,
|
||||||
|
"y": 2,
|
||||||
|
"z": 1,
|
||||||
|
"quantity": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"detail": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3a1c67a9-aed9-2864-6783-2cee4e701ba6",
|
||||||
|
"typeName": "试剂瓶",
|
||||||
|
"code": "0004-00041",
|
||||||
|
"barCode": "",
|
||||||
|
"name": "NMP",
|
||||||
|
"quantity": 300000.00000000000000000000000,
|
||||||
|
"lockQuantity": 380000.00000000000000000000000,
|
||||||
|
"unit": "微升",
|
||||||
|
"status": 1,
|
||||||
|
"isUse": false,
|
||||||
|
"locations": [
|
||||||
|
{
|
||||||
|
"id": "3a14aa3b-9fab-adac-7b9c-e1ee446b51d5",
|
||||||
|
"whid": "3a14aa3b-9fab-9d8e-d1a7-828f01f51f0c",
|
||||||
|
"whName": "站内试剂存放堆栈",
|
||||||
|
"code": "0003-0001",
|
||||||
|
"x": 1,
|
||||||
|
"y": 1,
|
||||||
|
"z": 1,
|
||||||
|
"quantity": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"detail": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3a1c67a9-aed9-32c7-5809-3ba1b8db1aa1",
|
||||||
|
"typeName": "试剂瓶",
|
||||||
|
"code": "0004-00042",
|
||||||
|
"barCode": "",
|
||||||
|
"name": "PGME",
|
||||||
|
"quantity": 300000.00000000000000000000000,
|
||||||
|
"lockQuantity": 337892.00000000000000000000000,
|
||||||
|
"unit": "",
|
||||||
|
"status": 1,
|
||||||
|
"isUse": false,
|
||||||
|
"locations": [
|
||||||
|
{
|
||||||
|
"id": "3a14aa3b-9fab-ca72-febc-b7c304476c78",
|
||||||
|
"whid": "3a14aa3b-9fab-9d8e-d1a7-828f01f51f0c",
|
||||||
|
"whName": "站内试剂存放堆栈",
|
||||||
|
"code": "0003-0002",
|
||||||
|
"x": 1,
|
||||||
|
"y": 2,
|
||||||
|
"z": 1,
|
||||||
|
"quantity": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"detail": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3a1c68c8-0574-d748-725e-97a2e549f085",
|
||||||
|
"typeName": "样品板",
|
||||||
|
"code": "0001-00004",
|
||||||
|
"barCode": "",
|
||||||
|
"name": "0917",
|
||||||
|
"quantity": 1.0000000000000000000000000000,
|
||||||
|
"lockQuantity": 4.0000000000000000000000000000,
|
||||||
|
"unit": "块",
|
||||||
|
"status": 1,
|
||||||
|
"isUse": false,
|
||||||
|
"locations": [
|
||||||
|
{
|
||||||
|
"id": "3a14aa17-0d49-f49c-6b66-b27f185a3b32",
|
||||||
|
"whid": "3a14aa17-0d49-dce4-486e-4b5c85c8b366",
|
||||||
|
"whName": "堆栈1",
|
||||||
|
"code": "0001-0009",
|
||||||
|
"x": 2,
|
||||||
|
"y": 1,
|
||||||
|
"z": 1,
|
||||||
|
"quantity": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"detail": [
|
||||||
|
{
|
||||||
|
"id": "3a1c68c8-0574-69a1-9858-4637e0193451",
|
||||||
|
"detailMaterialId": "3a1c68c8-0574-3630-bd42-bbf3623c5208",
|
||||||
|
"code": null,
|
||||||
|
"name": "SIDA",
|
||||||
|
"quantity": "300000",
|
||||||
|
"lockQuantity": "4",
|
||||||
|
"unit": "微升",
|
||||||
|
"x": 1,
|
||||||
|
"y": 2,
|
||||||
|
"z": 1,
|
||||||
|
"associateId": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3a1c68c8-0574-8d51-3191-a31f5be421e5",
|
||||||
|
"detailMaterialId": "3a1c68c8-0574-3b20-9ad7-90755f123d53",
|
||||||
|
"code": null,
|
||||||
|
"name": "BTDA-2",
|
||||||
|
"quantity": "300000",
|
||||||
|
"lockQuantity": "4",
|
||||||
|
"unit": "微升",
|
||||||
|
"x": 2,
|
||||||
|
"y": 2,
|
||||||
|
"z": 1,
|
||||||
|
"associateId": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3a1c68c8-0574-da80-735b-53ae2197a360",
|
||||||
|
"detailMaterialId": "3a1c68c8-0574-f2e4-33b3-90d813567939",
|
||||||
|
"code": null,
|
||||||
|
"name": "BTDA-DD",
|
||||||
|
"quantity": "300000",
|
||||||
|
"lockQuantity": "28",
|
||||||
|
"unit": "微升",
|
||||||
|
"x": 1,
|
||||||
|
"y": 1,
|
||||||
|
"z": 1,
|
||||||
|
"associateId": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3a1c68c8-0574-e717-1b1b-99891f875455",
|
||||||
|
"detailMaterialId": "3a1c68c8-0574-a0ef-e636-68cdc98960e2",
|
||||||
|
"code": null,
|
||||||
|
"name": "BTDA-3",
|
||||||
|
"quantity": "300000",
|
||||||
|
"lockQuantity": "4",
|
||||||
|
"unit": "微升",
|
||||||
|
"x": 2,
|
||||||
|
"y": 3,
|
||||||
|
"z": 1,
|
||||||
|
"associateId": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3a1c68c8-0574-e9bd-6cca-5e261b4f89cb",
|
||||||
|
"detailMaterialId": "3a1c68c8-0574-9d11-5115-283e8e5510b1",
|
||||||
|
"code": null,
|
||||||
|
"name": "BTDA-1",
|
||||||
|
"quantity": "300000",
|
||||||
|
"lockQuantity": "4",
|
||||||
|
"unit": "微升",
|
||||||
|
"x": 2,
|
||||||
|
"y": 1,
|
||||||
|
"z": 1,
|
||||||
|
"associateId": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"code": 1,
|
||||||
|
"message": "",
|
||||||
|
"timestamp": 1758560573511
|
||||||
|
}
|
||||||
48
test/resources/test_bottle_carrier.py
Normal file
48
test/resources/test_bottle_carrier.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from unilabos.resources.bioyond.bottle_carriers import BIOYOND_Electrolyte_6VialCarrier, BIOYOND_Electrolyte_1BottleCarrier
|
||||||
|
from unilabos.resources.bioyond.bottles import BIOYOND_PolymerStation_Solid_Vial, BIOYOND_PolymerStation_Solution_Beaker, BIOYOND_PolymerStation_Reagent_Bottle
|
||||||
|
|
||||||
|
|
||||||
|
def test_bottle_carrier() -> "BottleCarrier":
|
||||||
|
print("创建载架...")
|
||||||
|
|
||||||
|
# 创建6瓶载架
|
||||||
|
bottle_carrier = BIOYOND_Electrolyte_6VialCarrier("powder_carrier_01")
|
||||||
|
print(f"6瓶载架: {bottle_carrier.name}, 位置数: {len(bottle_carrier.sites)}")
|
||||||
|
|
||||||
|
# 创建1烧杯载架
|
||||||
|
beaker_carrier = BIOYOND_Electrolyte_1BottleCarrier("solution_carrier_01")
|
||||||
|
print(f"1烧杯载架: {beaker_carrier.name}, 位置数: {len(beaker_carrier.sites)}")
|
||||||
|
|
||||||
|
# 创建瓶子和烧杯
|
||||||
|
powder_bottle = BIOYOND_PolymerStation_Solid_Vial("powder_bottle_01")
|
||||||
|
solution_beaker = BIOYOND_PolymerStation_Solution_Beaker("solution_beaker_01")
|
||||||
|
reagent_bottle = BIOYOND_PolymerStation_Reagent_Bottle("reagent_bottle_01")
|
||||||
|
|
||||||
|
print(f"\n创建的物料:")
|
||||||
|
print(f"粉末瓶: {powder_bottle.name} - {powder_bottle.diameter}mm x {powder_bottle.height}mm, {powder_bottle.max_volume}μL")
|
||||||
|
print(f"溶液烧杯: {solution_beaker.name} - {solution_beaker.diameter}mm x {solution_beaker.height}mm, {solution_beaker.max_volume}μL")
|
||||||
|
print(f"试剂瓶: {reagent_bottle.name} - {reagent_bottle.diameter}mm x {reagent_bottle.height}mm, {reagent_bottle.max_volume}μL")
|
||||||
|
|
||||||
|
# 测试放置容器
|
||||||
|
print(f"\n测试放置容器...")
|
||||||
|
|
||||||
|
# 通过载架的索引操作来放置容器
|
||||||
|
# bottle_carrier[0] = powder_bottle # 放置粉末瓶到第一个位置
|
||||||
|
print(f"粉末瓶已放置到6瓶载架的位置 0")
|
||||||
|
|
||||||
|
# beaker_carrier[0] = solution_beaker # 放置烧杯到第一个位置
|
||||||
|
print(f"溶液烧杯已放置到1烧杯载架的位置 0")
|
||||||
|
|
||||||
|
# 验证放置结果
|
||||||
|
print(f"\n验证放置结果:")
|
||||||
|
bottle_at_0 = bottle_carrier[0].resource
|
||||||
|
beaker_at_0 = beaker_carrier[0].resource
|
||||||
|
|
||||||
|
if bottle_at_0:
|
||||||
|
print(f"位置 0 的瓶子: {bottle_at_0.name}")
|
||||||
|
if beaker_at_0:
|
||||||
|
print(f"位置 0 的烧杯: {beaker_at_0.name}")
|
||||||
|
|
||||||
|
print("\n载架设置完成!")
|
||||||
35
test/resources/test_converter_bioyond.py
Normal file
35
test/resources/test_converter_bioyond.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import pytest
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
from unilabos.resources.graphio import resource_bioyond_to_plr
|
||||||
|
from unilabos.registry.registry import lab_registry
|
||||||
|
|
||||||
|
from unilabos.resources.bioyond.decks import BIOYOND_PolymerReactionStation_Deck
|
||||||
|
|
||||||
|
lab_registry.setup()
|
||||||
|
|
||||||
|
|
||||||
|
type_mapping = {
|
||||||
|
"烧杯": "BIOYOND_PolymerStation_1FlaskCarrier",
|
||||||
|
"试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier",
|
||||||
|
"样品板": "BIOYOND_PolymerStation_6VialCarrier",
|
||||||
|
}
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def bioyond_materials() -> list[dict]:
|
||||||
|
print("加载 BioYond 物料数据...")
|
||||||
|
print(os.getcwd())
|
||||||
|
with open("bioyond_materials.json", "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)["data"]
|
||||||
|
print(f"加载了 {len(data)} 条物料数据")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def test_bioyond_to_plr(bioyond_materials) -> list[dict]:
|
||||||
|
deck = BIOYOND_PolymerReactionStation_Deck("test_deck")
|
||||||
|
print("将 BioYond 物料数据转换为 PLR 格式...")
|
||||||
|
output = resource_bioyond_to_plr(bioyond_materials, type_mapping=type_mapping, deck=deck)
|
||||||
|
print(deck.summary())
|
||||||
|
print([resource.serialize() for resource in output])
|
||||||
|
print([resource.serialize_all_state() for resource in output])
|
||||||
29
unilabos/devices/battery/battery.json
Normal file
29
unilabos/devices/battery/battery.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "NEWARE_BATTERY_TEST_SYSTEM",
|
||||||
|
"name": "Neware Battery Test System",
|
||||||
|
"parent": null,
|
||||||
|
"type": "device",
|
||||||
|
"class": "neware_battery_test_system",
|
||||||
|
"position": {
|
||||||
|
"x": 620.6111111111111,
|
||||||
|
"y": 171,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"port": 502,
|
||||||
|
"machine_id": 1,
|
||||||
|
"devtype": "27",
|
||||||
|
"timeout": 20,
|
||||||
|
"size_x": 500.0,
|
||||||
|
"size_y": 500.0,
|
||||||
|
"size_z": 2000.0
|
||||||
|
},
|
||||||
|
"data": {},
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": []
|
||||||
|
}
|
||||||
1042
unilabos/devices/battery/neware_battery_test_system.py
Normal file
1042
unilabos/devices/battery/neware_battery_test_system.py
Normal file
File diff suppressed because it is too large
Load Diff
1058
unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py
Normal file
1058
unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py
Normal file
File diff suppressed because it is too large
Load Diff
398
unilabos/devices/workstation/bioyond_studio/experiment.py
Normal file
398
unilabos/devices/workstation/bioyond_studio/experiment.py
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
# experiment_workflow.py
|
||||||
|
"""
|
||||||
|
实验流程主程序
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from bioyond_rpc import BioyondV1RPC
|
||||||
|
from config import API_CONFIG, WORKFLOW_MAPPINGS
|
||||||
|
|
||||||
|
|
||||||
|
def run_experiment():
|
||||||
|
"""运行实验流程"""
|
||||||
|
|
||||||
|
# 初始化Bioyond客户端
|
||||||
|
config = {
|
||||||
|
**API_CONFIG,
|
||||||
|
"workflow_mappings": WORKFLOW_MAPPINGS
|
||||||
|
}
|
||||||
|
|
||||||
|
Bioyond = BioyondV1RPC(config)
|
||||||
|
|
||||||
|
print("\n============= 多工作流参数测试(简化接口+材料缓存)=============")
|
||||||
|
|
||||||
|
# 显示可用的材料名称(前20个)
|
||||||
|
available_materials = Bioyond.get_available_materials()
|
||||||
|
print(f"可用材料名称(前20个): {available_materials[:20]}")
|
||||||
|
print(f"总共有 {len(available_materials)} 个材料可用\n")
|
||||||
|
|
||||||
|
# 1. 反应器放入
|
||||||
|
print("1. 添加反应器放入工作流,带参数...")
|
||||||
|
Bioyond.reactor_taken_in(
|
||||||
|
assign_material_name="BTDA-DD",
|
||||||
|
cutoff="10000",
|
||||||
|
temperature="-10"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. 液体投料-烧杯 (第一个)
|
||||||
|
print("2. 添加液体投料-烧杯,带参数...")
|
||||||
|
Bioyond.liquid_feeding_beaker(
|
||||||
|
volume="34768.7",
|
||||||
|
assign_material_name="ODA",
|
||||||
|
time="0",
|
||||||
|
torque_variation="1",
|
||||||
|
titrationType="1",
|
||||||
|
temperature=-10
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. 液体投料-烧杯 (第二个)
|
||||||
|
print("3. 添加液体投料-烧杯,带参数...")
|
||||||
|
Bioyond.liquid_feeding_beaker(
|
||||||
|
volume="34080.9",
|
||||||
|
assign_material_name="MPDA",
|
||||||
|
time="5",
|
||||||
|
torque_variation="2",
|
||||||
|
titrationType="1",
|
||||||
|
temperature=0
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. 液体投料-小瓶非滴定
|
||||||
|
print("4. 添加液体投料-小瓶非滴定,带参数...")
|
||||||
|
Bioyond.liquid_feeding_vials_non_titration(
|
||||||
|
volumeFormula="639.5",
|
||||||
|
assign_material_name="SIDA",
|
||||||
|
titration_type="1",
|
||||||
|
time="0",
|
||||||
|
torque_variation="1",
|
||||||
|
temperature=-10
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5. 液体投料溶剂
|
||||||
|
print("5. 添加液体投料溶剂,带参数...")
|
||||||
|
Bioyond.liquid_feeding_solvents(
|
||||||
|
assign_material_name="NMP",
|
||||||
|
volume="19000",
|
||||||
|
titration_type="1",
|
||||||
|
time="5",
|
||||||
|
torque_variation="2",
|
||||||
|
temperature=-10
|
||||||
|
)
|
||||||
|
|
||||||
|
# 6-8. 固体进料小瓶 (三个)
|
||||||
|
print("6. 添加固体进料小瓶,带参数...")
|
||||||
|
Bioyond.solid_feeding_vials(
|
||||||
|
material_id="3",
|
||||||
|
time="180",
|
||||||
|
torque_variation="2",
|
||||||
|
assign_material_name="BTDA-1",
|
||||||
|
temperature=-10.00
|
||||||
|
)
|
||||||
|
|
||||||
|
print("7. 添加固体进料小瓶,带参数...")
|
||||||
|
Bioyond.solid_feeding_vials(
|
||||||
|
material_id="3",
|
||||||
|
time="180",
|
||||||
|
torque_variation="2",
|
||||||
|
assign_material_name="BTDA-2",
|
||||||
|
temperature=25.00
|
||||||
|
)
|
||||||
|
|
||||||
|
print("8. 添加固体进料小瓶,带参数...")
|
||||||
|
Bioyond.solid_feeding_vials(
|
||||||
|
material_id="3",
|
||||||
|
time="480",
|
||||||
|
torque_variation="2",
|
||||||
|
assign_material_name="BTDA-3",
|
||||||
|
temperature=25.00
|
||||||
|
)
|
||||||
|
|
||||||
|
# 液体投料滴定(第一个)
|
||||||
|
print("9. 添加液体投料滴定,带参数...") # ODPA
|
||||||
|
Bioyond.liquid_feeding_titration(
|
||||||
|
volume_formula="1000",
|
||||||
|
assign_material_name="BTDA-DD",
|
||||||
|
titration_type="1",
|
||||||
|
time="360",
|
||||||
|
torque_variation="2",
|
||||||
|
temperature="25.00"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 液体投料滴定(第二个)
|
||||||
|
print("10. 添加液体投料滴定,带参数...") # ODPA
|
||||||
|
Bioyond.liquid_feeding_titration(
|
||||||
|
volume_formula="500",
|
||||||
|
assign_material_name="BTDA-DD",
|
||||||
|
titration_type="1",
|
||||||
|
time="360",
|
||||||
|
torque_variation="2",
|
||||||
|
temperature="25.00"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 液体投料滴定(第三个)
|
||||||
|
print("11. 添加液体投料滴定,带参数...") # ODPA
|
||||||
|
Bioyond.liquid_feeding_titration(
|
||||||
|
volume_formula="500",
|
||||||
|
assign_material_name="BTDA-DD",
|
||||||
|
titration_type="1",
|
||||||
|
time="360",
|
||||||
|
torque_variation="2",
|
||||||
|
temperature="25.00"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("12. 添加液体投料滴定,带参数...") # ODPA
|
||||||
|
Bioyond.liquid_feeding_titration(
|
||||||
|
volume_formula="500",
|
||||||
|
assign_material_name="BTDA-DD",
|
||||||
|
titration_type="1",
|
||||||
|
time="360",
|
||||||
|
torque_variation="2",
|
||||||
|
temperature="25.00"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("13. 添加液体投料滴定,带参数...") # ODPA
|
||||||
|
Bioyond.liquid_feeding_titration(
|
||||||
|
volume_formula="500",
|
||||||
|
assign_material_name="BTDA-DD",
|
||||||
|
titration_type="1",
|
||||||
|
time="360",
|
||||||
|
torque_variation="2",
|
||||||
|
temperature="25.00"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("14. 添加液体投料滴定,带参数...") # ODPA
|
||||||
|
Bioyond.liquid_feeding_titration(
|
||||||
|
volume_formula="500",
|
||||||
|
assign_material_name="BTDA-DD",
|
||||||
|
titration_type="1",
|
||||||
|
time="360",
|
||||||
|
torque_variation="2",
|
||||||
|
temperature="25.00"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
print("15. 添加液体投料溶剂,带参数...")
|
||||||
|
Bioyond.liquid_feeding_solvents(
|
||||||
|
assign_material_name="PGME",
|
||||||
|
volume="16894.6",
|
||||||
|
titration_type="1",
|
||||||
|
time="360",
|
||||||
|
torque_variation="2",
|
||||||
|
temperature=25.00
|
||||||
|
)
|
||||||
|
|
||||||
|
# 16. 反应器取出
|
||||||
|
print("16. 添加反应器取出工作流...")
|
||||||
|
Bioyond.reactor_taken_out()
|
||||||
|
|
||||||
|
# 显示当前工作流序列
|
||||||
|
sequence = Bioyond.get_workflow_sequence()
|
||||||
|
print("\n当前工作流执行顺序:")
|
||||||
|
print(sequence)
|
||||||
|
|
||||||
|
# 执行process_and_execute_workflow,合并工作流并创建任务
|
||||||
|
print("\n4. 执行process_and_execute_workflow...")
|
||||||
|
|
||||||
|
result = Bioyond.process_and_execute_workflow(
|
||||||
|
workflow_name="test3_86",
|
||||||
|
task_name="实验3_86"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 显示执行结果
|
||||||
|
print("\n5. 执行结果:")
|
||||||
|
if isinstance(result, str):
|
||||||
|
try:
|
||||||
|
result_dict = json.loads(result)
|
||||||
|
if result_dict.get("success"):
|
||||||
|
print("任务创建成功!")
|
||||||
|
print(f"- 工作流: {result_dict.get('workflow', {}).get('name')}")
|
||||||
|
print(f"- 工作流ID: {result_dict.get('workflow', {}).get('id')}")
|
||||||
|
print(f"- 任务结果: {result_dict.get('task')}")
|
||||||
|
else:
|
||||||
|
print(f"任务创建失败: {result_dict.get('error')}")
|
||||||
|
except:
|
||||||
|
print(f"结果解析失败: {result}")
|
||||||
|
else:
|
||||||
|
if result.get("success"):
|
||||||
|
print("任务创建成功!")
|
||||||
|
print(f"- 工作流: {result.get('workflow', {}).get('name')}")
|
||||||
|
print(f"- 工作流ID: {result.get('workflow', {}).get('id')}")
|
||||||
|
print(f"- 任务结果: {result.get('task')}")
|
||||||
|
else:
|
||||||
|
print(f"任务创建失败: {result.get('error')}")
|
||||||
|
|
||||||
|
# 可选:启动调度器
|
||||||
|
# Bioyond.scheduler_start()
|
||||||
|
|
||||||
|
return Bioyond
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_materials(bioyond):
|
||||||
|
"""准备实验材料(可选)"""
|
||||||
|
|
||||||
|
# 样品板材料数据定义
|
||||||
|
material_data_yp_1 = {
|
||||||
|
"typeId": "3a142339-80de-8f25-6093-1b1b1b6c322e",
|
||||||
|
"name": "样品板-1",
|
||||||
|
"unit": "个",
|
||||||
|
"quantity": 1,
|
||||||
|
"details": [
|
||||||
|
{
|
||||||
|
"typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
|
||||||
|
"name": "BPDA-DD-1",
|
||||||
|
"quantity": 1,
|
||||||
|
"x": 1,
|
||||||
|
"y": 1,
|
||||||
|
"Parameters": "{\"molecular\": 1}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
|
||||||
|
"name": "PEPA",
|
||||||
|
"quantity": 1,
|
||||||
|
"x": 1,
|
||||||
|
"y": 2,
|
||||||
|
"Parameters": "{\"molecular\": 1}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
|
||||||
|
"name": "BPDA-DD-2",
|
||||||
|
"quantity": 1,
|
||||||
|
"x": 1,
|
||||||
|
"y": 3,
|
||||||
|
"Parameters": "{\"molecular\": 1}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
|
||||||
|
"name": "BPDA-1",
|
||||||
|
"quantity": 1,
|
||||||
|
"x": 2,
|
||||||
|
"y": 1,
|
||||||
|
"Parameters": "{\"molecular\": 1}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
|
||||||
|
"name": "PMDA",
|
||||||
|
"quantity": 1,
|
||||||
|
"x": 2,
|
||||||
|
"y": 2,
|
||||||
|
"Parameters": "{\"molecular\": 1}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
|
||||||
|
"name": "BPDA-2",
|
||||||
|
"quantity": 1,
|
||||||
|
"x": 2,
|
||||||
|
"y": 3,
|
||||||
|
"Parameters": "{\"molecular\": 1}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Parameters": "{}"
|
||||||
|
}
|
||||||
|
|
||||||
|
material_data_yp_2 = {
|
||||||
|
"typeId": "3a142339-80de-8f25-6093-1b1b1b6c322e",
|
||||||
|
"name": "样品板-2",
|
||||||
|
"unit": "个",
|
||||||
|
"quantity": 1,
|
||||||
|
"details": [
|
||||||
|
{
|
||||||
|
"typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
|
||||||
|
"name": "BPDA-DD",
|
||||||
|
"quantity": 1,
|
||||||
|
"x": 1,
|
||||||
|
"y": 1,
|
||||||
|
"Parameters": "{\"molecular\": 1}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
|
||||||
|
"name": "SIDA",
|
||||||
|
"quantity": 1,
|
||||||
|
"x": 1,
|
||||||
|
"y": 2,
|
||||||
|
"Parameters": "{\"molecular\": 1}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
|
||||||
|
"name": "BTDA-1",
|
||||||
|
"quantity": 1,
|
||||||
|
"x": 2,
|
||||||
|
"y": 1,
|
||||||
|
"Parameters": "{\"molecular\": 1}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
|
||||||
|
"name": "BTDA-2",
|
||||||
|
"quantity": 1,
|
||||||
|
"x": 2,
|
||||||
|
"y": 2,
|
||||||
|
"Parameters": "{\"molecular\": 1}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
|
||||||
|
"name": "BTDA-3",
|
||||||
|
"quantity": 1,
|
||||||
|
"x": 2,
|
||||||
|
"y": 3,
|
||||||
|
"Parameters": "{\"molecular\": 1}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Parameters": "{}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 烧杯材料数据定义
|
||||||
|
beaker_materials = [
|
||||||
|
{
|
||||||
|
"typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6",
|
||||||
|
"name": "PDA-1",
|
||||||
|
"unit": "微升",
|
||||||
|
"quantity": 1,
|
||||||
|
"parameters": "{\"DeviceMaterialType\":\"NMP\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6",
|
||||||
|
"name": "TFDB",
|
||||||
|
"unit": "微升",
|
||||||
|
"quantity": 1,
|
||||||
|
"parameters": "{\"DeviceMaterialType\":\"NMP\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6",
|
||||||
|
"name": "ODA",
|
||||||
|
"unit": "微升",
|
||||||
|
"quantity": 1,
|
||||||
|
"parameters": "{\"DeviceMaterialType\":\"NMP\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6",
|
||||||
|
"name": "MPDA",
|
||||||
|
"unit": "微升",
|
||||||
|
"quantity": 1,
|
||||||
|
"parameters": "{\"DeviceMaterialType\":\"NMP\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6",
|
||||||
|
"name": "PDA-2",
|
||||||
|
"unit": "微升",
|
||||||
|
"quantity": 1,
|
||||||
|
"parameters": "{\"DeviceMaterialType\":\"NMP\"}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# 如果需要,可以在这里调用add_material方法添加材料
|
||||||
|
# 例如:
|
||||||
|
# result = bioyond.add_material(json.dumps(material_data_yp_1))
|
||||||
|
# print(f"添加材料结果: {result}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"sample_plates": [material_data_yp_1, material_data_yp_2],
|
||||||
|
"beakers": beaker_materials
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 运行主实验流程
|
||||||
|
bioyond_client = run_experiment()
|
||||||
|
|
||||||
|
# 可选:准备材料数据
|
||||||
|
# materials = prepare_materials(bioyond_client)
|
||||||
|
# print(f"\n准备的材料数据: {materials}")
|
||||||
400
unilabos/devices/workstation/bioyond_studio/station.py
Normal file
400
unilabos/devices/workstation/bioyond_studio/station.py
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
"""
|
||||||
|
Bioyond工作站实现
|
||||||
|
Bioyond Workstation Implementation
|
||||||
|
|
||||||
|
集成Bioyond物料管理的工作站示例
|
||||||
|
"""
|
||||||
|
import traceback
|
||||||
|
from typing import Dict, Any, List, Optional, Union
|
||||||
|
import json
|
||||||
|
|
||||||
|
from unilabos.devices.workstation.workstation_base import WorkstationBase, ResourceSynchronizer
|
||||||
|
from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondV1RPC
|
||||||
|
from unilabos.resources.warehouse import WareHouse
|
||||||
|
from unilabos.utils.log import logger
|
||||||
|
from unilabos.resources.graphio import resource_bioyond_to_plr
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode
|
||||||
|
from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
|
||||||
|
|
||||||
|
from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG, WORKFLOW_MAPPINGS, MATERIAL_TYPE_MAPPINGS
|
||||||
|
|
||||||
|
|
||||||
|
class BioyondResourceSynchronizer(ResourceSynchronizer):
|
||||||
|
"""Bioyond资源同步器
|
||||||
|
|
||||||
|
负责与Bioyond系统进行物料数据的同步
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, workstation: 'BioyondWorkstation'):
|
||||||
|
super().__init__(workstation)
|
||||||
|
self.bioyond_api_client = None
|
||||||
|
self.sync_interval = 60 # 默认60秒同步一次
|
||||||
|
self.last_sync_time = 0
|
||||||
|
self.initialize()
|
||||||
|
|
||||||
|
def initialize(self) -> bool:
|
||||||
|
"""初始化Bioyond资源同步器"""
|
||||||
|
try:
|
||||||
|
self.bioyond_api_client = self.workstation.hardware_interface
|
||||||
|
if self.bioyond_api_client is None:
|
||||||
|
logger.error("Bioyond API客户端未初始化")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 设置同步间隔
|
||||||
|
self.sync_interval = self.workstation.bioyond_config.get("sync_interval", 600)
|
||||||
|
|
||||||
|
logger.info("Bioyond资源同步器初始化完成")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Bioyond资源同步器初始化失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def sync_from_external(self) -> bool:
|
||||||
|
"""从Bioyond系统同步物料数据"""
|
||||||
|
try:
|
||||||
|
if self.bioyond_api_client is None:
|
||||||
|
logger.error("Bioyond API客户端未初始化")
|
||||||
|
return False
|
||||||
|
|
||||||
|
bioyond_data = self.bioyond_api_client.stock_material('{"typeMode": 2, "includeDetail": true}')
|
||||||
|
if not bioyond_data:
|
||||||
|
logger.warning("从Bioyond获取的物料数据为空")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 转换为UniLab格式
|
||||||
|
unilab_resources = resource_bioyond_to_plr(bioyond_data, type_mapping=self.workstation.bioyond_config["material_type_mappings"], deck=self.workstation.deck)
|
||||||
|
|
||||||
|
logger.info(f"从Bioyond同步了 {len(unilab_resources)} 个资源")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"从Bioyond同步物料数据失败: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def sync_to_external(self, resource: Any) -> bool:
|
||||||
|
"""将本地物料数据变更同步到Bioyond系统"""
|
||||||
|
try:
|
||||||
|
if self.bioyond_api_client is None:
|
||||||
|
logger.error("Bioyond API客户端未初始化")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 调用入库、出库操作
|
||||||
|
# bioyond_format_data = self._convert_resource_to_bioyond_format(resource)
|
||||||
|
# success = await self.bioyond_api_client.update_material(bioyond_format_data)
|
||||||
|
#
|
||||||
|
# if success
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handle_external_change(self, change_info: Dict[str, Any]) -> bool:
|
||||||
|
"""处理Bioyond系统的变更通知"""
|
||||||
|
try:
|
||||||
|
# 这里可以实现对Bioyond变更的处理逻辑
|
||||||
|
logger.info(f"处理Bioyond变更通知: {change_info}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理Bioyond变更通知失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class BioyondWorkstation(WorkstationBase):
|
||||||
|
"""Bioyond工作站
|
||||||
|
|
||||||
|
集成Bioyond物料管理的工作站实现
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
bioyond_config: Optional[Dict[str, Any]] = None,
|
||||||
|
deck: Optional[Any] = None,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
# 初始化父类
|
||||||
|
super().__init__(
|
||||||
|
# 桌子
|
||||||
|
deck=deck,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
self.deck.warehouses = {}
|
||||||
|
for resource in self.deck.children:
|
||||||
|
if isinstance(resource, WareHouse):
|
||||||
|
self.deck.warehouses[resource.name] = resource
|
||||||
|
|
||||||
|
self._create_communication_module(bioyond_config)
|
||||||
|
self.resource_synchronizer = BioyondResourceSynchronizer(self)
|
||||||
|
self.resource_synchronizer.sync_from_external()
|
||||||
|
|
||||||
|
# TODO: self._ros_node里面拿属性
|
||||||
|
logger.info(f"Bioyond工作站初始化完成")
|
||||||
|
|
||||||
|
def post_init(self, ros_node: ROS2WorkstationNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
#self.deck = create_a_coin_cell_deck()
|
||||||
|
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
|
||||||
|
"resources": [self.deck]
|
||||||
|
})
|
||||||
|
|
||||||
|
def _create_communication_module(self, config: Optional[Dict[str, Any]] = None) -> None:
|
||||||
|
"""创建Bioyond通信模块"""
|
||||||
|
self.bioyond_config = config or {
|
||||||
|
**API_CONFIG,
|
||||||
|
"workflow_mappings": WORKFLOW_MAPPINGS,
|
||||||
|
"material_type_mappings": MATERIAL_TYPE_MAPPINGS
|
||||||
|
}
|
||||||
|
self.hardware_interface = BioyondV1RPC(self.bioyond_config)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _register_supported_workflows(self):
|
||||||
|
"""注册Bioyond支持的工作流"""
|
||||||
|
from unilabos.devices.workstation.workstation_base import WorkflowInfo
|
||||||
|
|
||||||
|
# Bioyond物料同步工作流
|
||||||
|
self.supported_workflows["bioyond_sync"] = WorkflowInfo(
|
||||||
|
name="bioyond_sync",
|
||||||
|
description="从Bioyond系统同步物料",
|
||||||
|
parameters={
|
||||||
|
"sync_type": {"type": "string", "default": "full", "options": ["full", "incremental"]},
|
||||||
|
"force_sync": {"type": "boolean", "default": False}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Bioyond物料更新工作流
|
||||||
|
self.supported_workflows["bioyond_update"] = WorkflowInfo(
|
||||||
|
name="bioyond_update",
|
||||||
|
description="将本地物料变更同步到Bioyond",
|
||||||
|
parameters={
|
||||||
|
"material_ids": {"type": "list", "default": []},
|
||||||
|
"sync_all": {"type": "boolean", "default": True}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"注册了 {len(self.supported_workflows)} 个Bioyond工作流")
|
||||||
|
|
||||||
|
async def execute_bioyond_sync_workflow(self, parameters: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""执行Bioyond同步工作流"""
|
||||||
|
try:
|
||||||
|
sync_type = parameters.get("sync_type", "full")
|
||||||
|
force_sync = parameters.get("force_sync", False)
|
||||||
|
|
||||||
|
logger.info(f"开始执行Bioyond同步工作流: {sync_type}")
|
||||||
|
|
||||||
|
# 获取物料管理模块
|
||||||
|
material_manager = self.material_management
|
||||||
|
|
||||||
|
if sync_type == "full":
|
||||||
|
# 全量同步
|
||||||
|
success = await material_manager.sync_from_bioyond()
|
||||||
|
else:
|
||||||
|
# 增量同步(这里可以实现增量同步逻辑)
|
||||||
|
success = await material_manager.sync_from_bioyond()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
result = {
|
||||||
|
"status": "success",
|
||||||
|
"message": f"Bioyond同步完成: {sync_type}",
|
||||||
|
"synced_resources": len(material_manager.plr_resources)
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
result = {
|
||||||
|
"status": "failed",
|
||||||
|
"message": "Bioyond同步失败"
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"Bioyond同步工作流执行完成: {result['status']}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Bioyond同步工作流执行失败: {e}")
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute_bioyond_update_workflow(self, parameters: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""执行Bioyond更新工作流"""
|
||||||
|
try:
|
||||||
|
material_ids = parameters.get("material_ids", [])
|
||||||
|
sync_all = parameters.get("sync_all", True)
|
||||||
|
|
||||||
|
logger.info(f"开始执行Bioyond更新工作流: sync_all={sync_all}")
|
||||||
|
|
||||||
|
# 获取物料管理模块
|
||||||
|
material_manager = self.material_management
|
||||||
|
|
||||||
|
if sync_all:
|
||||||
|
# 同步所有物料
|
||||||
|
success_count = 0
|
||||||
|
for resource in material_manager.plr_resources.values():
|
||||||
|
success = await material_manager.sync_to_bioyond(resource)
|
||||||
|
if success:
|
||||||
|
success_count += 1
|
||||||
|
else:
|
||||||
|
# 同步指定物料
|
||||||
|
success_count = 0
|
||||||
|
for material_id in material_ids:
|
||||||
|
resource = material_manager.find_material_by_id(material_id)
|
||||||
|
if resource:
|
||||||
|
success = await material_manager.sync_to_bioyond(resource)
|
||||||
|
if success:
|
||||||
|
success_count += 1
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"status": "success",
|
||||||
|
"message": f"Bioyond更新完成",
|
||||||
|
"updated_resources": success_count,
|
||||||
|
"total_resources": len(material_ids) if not sync_all else len(material_manager.plr_resources)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"Bioyond更新工作流执行完成: {result['status']}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Bioyond更新工作流执行失败: {e}")
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
def load_bioyond_data_from_file(self, file_path: str) -> bool:
|
||||||
|
"""从文件加载Bioyond数据(用于测试)"""
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
bioyond_data = json.load(f)
|
||||||
|
|
||||||
|
# 获取物料管理模块
|
||||||
|
material_manager = self.material_management
|
||||||
|
|
||||||
|
# 转换为UniLab格式
|
||||||
|
if isinstance(bioyond_data, dict) and "data" in bioyond_data:
|
||||||
|
unilab_resources = material_manager.resource_bioyond_container_to_ulab(bioyond_data)
|
||||||
|
else:
|
||||||
|
unilab_resources = material_manager.resource_bioyond_to_ulab(bioyond_data)
|
||||||
|
|
||||||
|
# 分配到Deck
|
||||||
|
import asyncio
|
||||||
|
asyncio.create_task(material_manager._assign_resources_to_deck(unilab_resources))
|
||||||
|
|
||||||
|
logger.info(f"从文件 {file_path} 加载了 {len(unilab_resources)} 个Bioyond资源")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"从文件加载Bioyond数据失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# 使用示例
|
||||||
|
def create_bioyond_workstation_example():
|
||||||
|
"""创建Bioyond工作站示例"""
|
||||||
|
|
||||||
|
# 配置参数
|
||||||
|
device_id = "bioyond_workstation_001"
|
||||||
|
|
||||||
|
# 子资源配置
|
||||||
|
children = {
|
||||||
|
"plate_1": {
|
||||||
|
"name": "plate_1",
|
||||||
|
"type": "plate",
|
||||||
|
"position": {"x": 100, "y": 100, "z": 0},
|
||||||
|
"config": {
|
||||||
|
"size_x": 127.76,
|
||||||
|
"size_y": 85.48,
|
||||||
|
"size_z": 14.35,
|
||||||
|
"model": "Generic 96 Well Plate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Bioyond配置
|
||||||
|
bioyond_config = {
|
||||||
|
"base_url": "http://bioyond.example.com/api",
|
||||||
|
"api_key": "your_api_key_here",
|
||||||
|
"sync_interval": 60, # 60秒同步一次
|
||||||
|
"timeout": 30
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deck配置
|
||||||
|
deck_config = {
|
||||||
|
"size_x": 1000.0,
|
||||||
|
"size_y": 1000.0,
|
||||||
|
"size_z": 100.0,
|
||||||
|
"model": "BioyondDeck"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 创建工作站
|
||||||
|
workstation = BioyondWorkstation(
|
||||||
|
station_resource=deck_config,
|
||||||
|
bioyond_config=bioyond_config,
|
||||||
|
deck_config=deck_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
return workstation
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 创建示例工作站
|
||||||
|
#workstation = create_bioyond_workstation_example()
|
||||||
|
|
||||||
|
# 从文件加载测试数据
|
||||||
|
#workstation.load_bioyond_data_from_file("bioyond_test_yibin.json")
|
||||||
|
|
||||||
|
# 获取状态
|
||||||
|
#status = workstation.get_bioyond_status()
|
||||||
|
#print("Bioyond工作站状态:", status)
|
||||||
|
|
||||||
|
# 创建测试数据 - 使用resource_bioyond_container_to_ulab函数期望的格式
|
||||||
|
|
||||||
|
# 读取 bioyond_resources_unilab_output3 copy.json 文件
|
||||||
|
from unilabos.resources.graphio import resource_ulab_to_plr, convert_resources_to_type
|
||||||
|
from Bioyond_wuliao import *
|
||||||
|
from typing import List
|
||||||
|
from pylabrobot.resources import Resource as PLRResource
|
||||||
|
import json
|
||||||
|
from pylabrobot.resources.deck import Deck
|
||||||
|
from pylabrobot.resources.coordinate import Coordinate
|
||||||
|
|
||||||
|
with open("./bioyond_test_yibin3_unilab_result_corr.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(f"转换结果长度: {len(ulab_resources) if ulab_resources else 0}")
|
||||||
|
deck = Deck(size_x=2000,
|
||||||
|
size_y=653.5,
|
||||||
|
size_z=900)
|
||||||
|
|
||||||
|
Stack0 = Stack(name="Stack0", location=Coordinate(0, 100, 0))
|
||||||
|
Stack1 = Stack(name="Stack1", location=Coordinate(100, 100, 0))
|
||||||
|
Stack2 = Stack(name="Stack2", location=Coordinate(200, 100, 0))
|
||||||
|
Stack3 = Stack(name="Stack3", location=Coordinate(300, 100, 0))
|
||||||
|
Stack4 = Stack(name="Stack4", location=Coordinate(400, 100, 0))
|
||||||
|
Stack5 = Stack(name="Stack5", location=Coordinate(500, 100, 0))
|
||||||
|
|
||||||
|
deck.assign_child_resource(Stack1, Stack1.location)
|
||||||
|
deck.assign_child_resource(Stack2, Stack2.location)
|
||||||
|
deck.assign_child_resource(Stack3, Stack3.location)
|
||||||
|
deck.assign_child_resource(Stack4, Stack4.location)
|
||||||
|
deck.assign_child_resource(Stack5, Stack5.location)
|
||||||
|
|
||||||
|
Stack0.assign_child_resource(ulab_resources[0], Stack0.location)
|
||||||
|
Stack1.assign_child_resource(ulab_resources[1], Stack1.location)
|
||||||
|
Stack2.assign_child_resource(ulab_resources[2], Stack2.location)
|
||||||
|
Stack3.assign_child_resource(ulab_resources[3], Stack3.location)
|
||||||
|
Stack4.assign_child_resource(ulab_resources[4], Stack4.location)
|
||||||
|
Stack5.assign_child_resource(ulab_resources[5], Stack5.location)
|
||||||
|
|
||||||
|
from unilabos.resources.graphio import convert_resources_from_type
|
||||||
|
from unilabos.app.web.client import http_client
|
||||||
|
|
||||||
|
resources = convert_resources_from_type([deck], [PLRResource])
|
||||||
|
|
||||||
|
|
||||||
|
print(resources)
|
||||||
|
http_client.remote_addr = "https://uni-lab.bohrium.com/api/v1"
|
||||||
|
#http_client.auth = "9F05593C"
|
||||||
|
http_client.auth = "ED634D1C"
|
||||||
|
http_client.resource_add(resources, database_process_later=False)
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
14472
unilabos/devices/workstation/coin_cell_assembly/new_cellconfig4.json
Normal file
14472
unilabos/devices/workstation/coin_cell_assembly/new_cellconfig4.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -112,17 +112,17 @@ class ResourceSynchronizer(ABC):
|
|||||||
self.workstation = workstation
|
self.workstation = workstation
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def sync_from_external(self) -> bool:
|
def sync_from_external(self) -> bool:
|
||||||
"""从外部系统同步物料到本地deck"""
|
"""从外部系统同步物料到本地deck"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def sync_to_external(self, plr_resource: PLRResource) -> bool:
|
def sync_to_external(self, plr_resource: PLRResource) -> bool:
|
||||||
"""将本地物料同步到外部系统"""
|
"""将本地物料同步到外部系统"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def handle_external_change(self, change_info: Dict[str, Any]) -> bool:
|
def handle_external_change(self, change_info: Dict[str, Any]) -> bool:
|
||||||
"""处理外部系统的变更通知"""
|
"""处理外部系统的变更通知"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -147,17 +147,15 @@ class WorkstationBase(ABC):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
station_resource: PLRResource,
|
deck: Deck,
|
||||||
*args,
|
*args,
|
||||||
**kwargs, # 必须有kwargs
|
**kwargs, # 必须有kwargs
|
||||||
):
|
):
|
||||||
# 基本配置
|
|
||||||
print(station_resource)
|
|
||||||
self.deck_config = station_resource
|
|
||||||
|
|
||||||
# PLR 物料系统
|
# PLR 物料系统
|
||||||
self.deck: Optional[Deck] = None
|
self.deck: Optional[Deck] = deck
|
||||||
self.plr_resources: Dict[str, PLRResource] = {}
|
self.plr_resources: Dict[str, PLRResource] = {}
|
||||||
|
|
||||||
|
self.resource_synchronizer = None # type: Optional[ResourceSynchronizer]
|
||||||
# 硬件接口
|
# 硬件接口
|
||||||
self.hardware_interface: Union[Any, str] = None
|
self.hardware_interface: Union[Any, str] = None
|
||||||
|
|
||||||
@@ -173,46 +171,7 @@ class WorkstationBase(ABC):
|
|||||||
def post_init(self, ros_node: ROS2WorkstationNode) -> None:
|
def post_init(self, ros_node: ROS2WorkstationNode) -> None:
|
||||||
# 初始化物料系统
|
# 初始化物料系统
|
||||||
self._ros_node = ros_node
|
self._ros_node = ros_node
|
||||||
self._initialize_material_system()
|
self._ros_node.update_resource([self.deck])
|
||||||
|
|
||||||
def _initialize_material_system(self):
|
|
||||||
"""初始化物料系统 - 使用 graphio 转换"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _create_complete_resource_config(self) -> Dict[str, Any]:
|
|
||||||
"""创建完整的资源配置 - 合并 deck_config 和 children"""
|
|
||||||
# 创建主 deck 配置
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def _normalize_child_resource(self, resource_id: str, config: Dict[str, Any], parent_id: str) -> Dict[str, Any]:
|
|
||||||
"""标准化子资源配置"""
|
|
||||||
return {
|
|
||||||
"id": resource_id,
|
|
||||||
"name": config.get("name", resource_id),
|
|
||||||
"type": config.get("type", "container"),
|
|
||||||
"position": self._normalize_position(config.get("position", {})),
|
|
||||||
"config": config.get("config", {}),
|
|
||||||
"data": config.get("data", {}),
|
|
||||||
"children": [], # 简化版本:只支持一层子资源
|
|
||||||
"parent": parent_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
def _normalize_position(self, position: Any) -> Dict[str, float]:
|
|
||||||
"""标准化位置信息"""
|
|
||||||
if isinstance(position, dict):
|
|
||||||
return {
|
|
||||||
"x": float(position.get("x", 0)),
|
|
||||||
"y": float(position.get("y", 0)),
|
|
||||||
"z": float(position.get("z", 0)),
|
|
||||||
}
|
|
||||||
elif isinstance(position, (list, tuple)) and len(position) >= 2:
|
|
||||||
return {
|
|
||||||
"x": float(position[0]),
|
|
||||||
"y": float(position[1]),
|
|
||||||
"z": float(position[2]) if len(position) > 2 else 0.0,
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
return {"x": 0.0, "y": 0.0, "z": 0.0}
|
|
||||||
|
|
||||||
def _build_resource_mappings(self, deck: Deck):
|
def _build_resource_mappings(self, deck: Deck):
|
||||||
"""递归构建资源映射"""
|
"""递归构建资源映射"""
|
||||||
@@ -296,14 +255,14 @@ class WorkstationBase(ABC):
|
|||||||
"""按类型查找资源"""
|
"""按类型查找资源"""
|
||||||
return [res for res in self.plr_resources.values() if isinstance(res, resource_type)]
|
return [res for res in self.plr_resources.values() if isinstance(res, resource_type)]
|
||||||
|
|
||||||
async def sync_with_external_system(self) -> bool:
|
def sync_with_external_system(self) -> bool:
|
||||||
"""与外部物料系统同步"""
|
"""与外部物料系统同步"""
|
||||||
if not self.resource_synchronizer:
|
if not self.resource_synchronizer:
|
||||||
logger.info(f"工作站 {self._ros_node.device_id} 没有配置资源同步器")
|
logger.info(f"工作站 {self._ros_node.device_id} 没有配置资源同步器")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
success = await self.resource_synchronizer.sync_from_external()
|
success = self.resource_synchronizer.sync_from_external()
|
||||||
if success:
|
if success:
|
||||||
logger.info(f"工作站 {self._ros_node.device_id} 外部同步成功")
|
logger.info(f"工作站 {self._ros_node.device_id} 外部同步成功")
|
||||||
else:
|
else:
|
||||||
@@ -391,5 +350,5 @@ class WorkstationBase(ABC):
|
|||||||
|
|
||||||
|
|
||||||
class ProtocolNode(WorkstationBase):
|
class ProtocolNode(WorkstationBase):
|
||||||
def __init__(self, station_resource: Optional[PLRResource], *args, **kwargs):
|
def __init__(self, deck: Optional[PLRResource], *args, **kwargs):
|
||||||
super().__init__(station_resource, *args, **kwargs)
|
super().__init__(deck, *args, **kwargs)
|
||||||
|
|||||||
@@ -149,6 +149,22 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
)
|
)
|
||||||
self._send_response(error_response)
|
self._send_response(error_response)
|
||||||
|
|
||||||
|
def do_OPTIONS(self):
|
||||||
|
"""处理OPTIONS请求 - CORS预检请求"""
|
||||||
|
try:
|
||||||
|
# 发送CORS响应头
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Access-Control-Allow-Origin', '*')
|
||||||
|
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
||||||
|
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
|
||||||
|
self.send_header('Access-Control-Max-Age', '86400')
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"OPTIONS请求处理失败: {e}")
|
||||||
|
self.send_response(500)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
def _handle_step_finish_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
def _handle_step_finish_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||||||
"""处理步骤完成报送(统一LIMS协议规范)"""
|
"""处理步骤完成报送(统一LIMS协议规范)"""
|
||||||
try:
|
try:
|
||||||
@@ -206,7 +222,7 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
# 验证data字段内容
|
# 验证data字段内容
|
||||||
data = request_data['data']
|
data = request_data['data']
|
||||||
data_required_fields = ['orderCode', 'orderName', 'sampleId', 'startTime', 'endTime', 'Status']
|
data_required_fields = ['orderCode', 'orderName', 'sampleId', 'startTime', 'endTime', 'status']
|
||||||
if data_missing_fields := [field for field in data_required_fields if field not in data]:
|
if data_missing_fields := [field for field in data_required_fields if field not in data]:
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
success=False,
|
success=False,
|
||||||
@@ -227,7 +243,7 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
"0": "待生产", "2": "进样", "10": "开始",
|
"0": "待生产", "2": "进样", "10": "开始",
|
||||||
"20": "完成", "-2": "异常停止", "-3": "人工停止"
|
"20": "完成", "-2": "异常停止", "-3": "人工停止"
|
||||||
}
|
}
|
||||||
status_desc = status_names.get(str(data['Status']), f"状态{data['Status']}")
|
status_desc = status_names.get(str(data['status']), f"状态{data['status']}")
|
||||||
|
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
success=True,
|
success=True,
|
||||||
@@ -380,6 +396,21 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
"""处理物料变更报送"""
|
"""处理物料变更报送"""
|
||||||
try:
|
try:
|
||||||
# 验证必需字段
|
# 验证必需字段
|
||||||
|
if 'brand' in request_data:
|
||||||
|
if request_data['brand'] == "bioyond": # 奔曜
|
||||||
|
error_msg = request_data["text"]
|
||||||
|
logger.info(f"收到奔曜错误处理报送: {error_msg}")
|
||||||
|
return HttpResponse(
|
||||||
|
success=True,
|
||||||
|
message=f"错误处理报送已收到: {error_msg}",
|
||||||
|
acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{error_msg.get('action_id', 'unknown')}",
|
||||||
|
data=None
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return HttpResponse(
|
||||||
|
success=False,
|
||||||
|
message=f"缺少厂家信息(brand字段)"
|
||||||
|
)
|
||||||
required_fields = ['workstation_id', 'timestamp', 'resource_id', 'change_type']
|
required_fields = ['workstation_id', 'timestamp', 'resource_id', 'change_type']
|
||||||
if missing_fields := [field for field in required_fields if field not in request_data]:
|
if missing_fields := [field for field in required_fields if field not in request_data]:
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
@@ -407,7 +438,29 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
def _handle_error_handling_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
def _handle_error_handling_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||||||
"""处理错误处理报送"""
|
"""处理错误处理报送"""
|
||||||
try:
|
try:
|
||||||
# 验证必需字段
|
# 检查是否为奔曜格式的错误报送
|
||||||
|
if 'brand' in request_data and str(request_data['brand']).lower() == "bioyond":
|
||||||
|
# 奔曜格式处理
|
||||||
|
if 'text' not in request_data:
|
||||||
|
return HttpResponse(
|
||||||
|
success=False,
|
||||||
|
message="奔曜格式缺少text字段"
|
||||||
|
)
|
||||||
|
|
||||||
|
error_data = request_data["text"]
|
||||||
|
logger.info(f"收到奔曜错误处理报送: {error_data}")
|
||||||
|
|
||||||
|
# 调用工作站的处理方法
|
||||||
|
result = self.workstation.handle_external_error(error_data)
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
success=True,
|
||||||
|
message=f"错误处理报送已收到: 任务{error_data.get('task', 'unknown')}, 错误代码{error_data.get('code', 'unknown')}",
|
||||||
|
acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{error_data.get('task', 'unknown')}",
|
||||||
|
data=result
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 标准格式处理
|
||||||
required_fields = ['workstation_id', 'timestamp', 'error_type', 'error_message']
|
required_fields = ['workstation_id', 'timestamp', 'error_type', 'error_message']
|
||||||
if missing_fields := [field for field in required_fields if field not in request_data]:
|
if missing_fields := [field for field in required_fields if field not in request_data]:
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
@@ -548,13 +601,19 @@ class WorkstationHTTPService:
|
|||||||
"""停止HTTP服务"""
|
"""停止HTTP服务"""
|
||||||
try:
|
try:
|
||||||
if self.running and self.server:
|
if self.running and self.server:
|
||||||
|
logger.info("正在停止工作站HTTP报送服务...")
|
||||||
self.running = False
|
self.running = False
|
||||||
self.server.shutdown()
|
|
||||||
self.server.server_close()
|
|
||||||
|
|
||||||
|
# 停止serve_forever循环
|
||||||
|
self.server.shutdown()
|
||||||
|
|
||||||
|
# 等待服务器线程结束
|
||||||
if self.server_thread and self.server_thread.is_alive():
|
if self.server_thread and self.server_thread.is_alive():
|
||||||
self.server_thread.join(timeout=5.0)
|
self.server_thread.join(timeout=5.0)
|
||||||
|
|
||||||
|
# 关闭服务器套接字
|
||||||
|
self.server.server_close()
|
||||||
|
|
||||||
logger.info("工作站HTTP报送服务已停止")
|
logger.info("工作站HTTP报送服务已停止")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -563,11 +622,13 @@ class WorkstationHTTPService:
|
|||||||
def _run_server(self):
|
def _run_server(self):
|
||||||
"""运行HTTP服务器"""
|
"""运行HTTP服务器"""
|
||||||
try:
|
try:
|
||||||
while self.running:
|
# 使用serve_forever()让服务持续运行
|
||||||
self.server.handle_request()
|
self.server.serve_forever()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if self.running: # 只在非正常停止时记录错误
|
if self.running: # 只在非正常停止时记录错误
|
||||||
logger.error(f"HTTP服务运行错误: {e}")
|
logger.error(f"HTTP服务运行错误: {e}")
|
||||||
|
finally:
|
||||||
|
logger.info("HTTP服务器线程已退出")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_running(self) -> bool:
|
def is_running(self) -> bool:
|
||||||
@@ -603,3 +664,49 @@ __all__ = [
|
|||||||
'MaterialChangeReport',
|
'MaterialChangeReport',
|
||||||
'TaskExecutionReport'
|
'TaskExecutionReport'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 简单测试HTTP服务
|
||||||
|
class DummyWorkstation:
|
||||||
|
device_id = "WS-001"
|
||||||
|
|
||||||
|
def process_step_finish_report(self, report_request):
|
||||||
|
return {"processed": True}
|
||||||
|
|
||||||
|
def process_sample_finish_report(self, report_request):
|
||||||
|
return {"processed": True}
|
||||||
|
|
||||||
|
def process_order_finish_report(self, report_request, used_materials):
|
||||||
|
return {"processed": True}
|
||||||
|
|
||||||
|
def process_material_change_report(self, report_data):
|
||||||
|
return {"processed": True}
|
||||||
|
|
||||||
|
def handle_external_error(self, error_data):
|
||||||
|
return {"handled": True}
|
||||||
|
|
||||||
|
workstation = DummyWorkstation()
|
||||||
|
http_service = WorkstationHTTPService(workstation)
|
||||||
|
|
||||||
|
try:
|
||||||
|
http_service.start()
|
||||||
|
print(f"测试服务器已启动: {http_service.service_url}")
|
||||||
|
print("按 Ctrl+C 停止服务器")
|
||||||
|
print("服务将持续运行,等待接收HTTP请求...")
|
||||||
|
|
||||||
|
# 保持服务器运行 - 使用更好的等待机制
|
||||||
|
try:
|
||||||
|
while http_service.is_running:
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n接收到停止信号...")
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n正在停止服务器...")
|
||||||
|
http_service.stop()
|
||||||
|
print("服务器已停止")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"服务器运行错误: {e}")
|
||||||
|
http_service.stop()
|
||||||
|
|
||||||
|
|||||||
344
unilabos/registry/devices/neware_battery_test_system.yaml
Normal file
344
unilabos/registry/devices/neware_battery_test_system.yaml
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
neware_battery_test_system:
|
||||||
|
category:
|
||||||
|
- neware_battery_test_system
|
||||||
|
class:
|
||||||
|
action_value_mappings:
|
||||||
|
auto-post_init:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
ros_node: null
|
||||||
|
handles: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
ros_node:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- ros_node
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: post_init参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-print_status_summary:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: print_status_summary参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-test_connection:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: test_connection参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
export_status_json:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
filepath: filepath
|
||||||
|
goal_default:
|
||||||
|
filepath: bts_status.json
|
||||||
|
handles: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
|
schema:
|
||||||
|
description: 导出当前状态数据到JSON文件
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
filepath:
|
||||||
|
default: bts_status.json
|
||||||
|
description: 输出JSON文件路径
|
||||||
|
type: string
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
description: 导出操作结果信息
|
||||||
|
type: string
|
||||||
|
success:
|
||||||
|
description: 导出是否成功
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
get_device_summary:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
|
schema:
|
||||||
|
description: 获取设备级别的摘要统计信息
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
description: 设备摘要信息JSON格式
|
||||||
|
type: string
|
||||||
|
success:
|
||||||
|
description: 查询是否成功
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
get_plate_status:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
plate_num: plate_num
|
||||||
|
goal_default:
|
||||||
|
plate_num: 1
|
||||||
|
handles: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
|
schema:
|
||||||
|
description: 获取指定盘(1或2)的电池状态信息
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
plate_num:
|
||||||
|
description: 盘号 (1 或 2)
|
||||||
|
maximum: 2
|
||||||
|
minimum: 1
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- plate_num
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
description: 盘状态信息JSON格式
|
||||||
|
type: string
|
||||||
|
success:
|
||||||
|
description: 查询是否成功
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
print_status_summary_action:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
|
schema:
|
||||||
|
description: 打印通道状态摘要信息到控制台
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
description: 打印操作结果信息
|
||||||
|
type: string
|
||||||
|
success:
|
||||||
|
description: 打印是否成功
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
query_plate_action:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
string: plate_id
|
||||||
|
goal_default:
|
||||||
|
string: ''
|
||||||
|
handles: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
title: StrSingleInput_Feedback
|
||||||
|
type: object
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
string:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- string
|
||||||
|
title: StrSingleInput_Goal
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
type: string
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
|
title: StrSingleInput_Result
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: StrSingleInput
|
||||||
|
type: object
|
||||||
|
type: StrSingleInput
|
||||||
|
test_connection_action:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
|
schema:
|
||||||
|
description: 测试与电池测试系统的TCP连接
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
description: 连接测试结果信息
|
||||||
|
type: string
|
||||||
|
success:
|
||||||
|
description: 连接测试是否成功
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
module: unilabos.devices.battery.neware_battery_test_system:NewareBatteryTestSystem
|
||||||
|
status_types:
|
||||||
|
channel_status: dict
|
||||||
|
connection_info: dict
|
||||||
|
device_summary: dict
|
||||||
|
plate_status: dict
|
||||||
|
status: str
|
||||||
|
total_channels: int
|
||||||
|
type: python
|
||||||
|
config_info: []
|
||||||
|
description: 新威电池测试系统驱动,支持720个通道的电池测试状态监控和数据导出。通过TCP通信实现远程控制,包含完整的物料管理系统,支持2盘电池的状态映射和监控。
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema:
|
||||||
|
config:
|
||||||
|
properties:
|
||||||
|
devtype:
|
||||||
|
type: string
|
||||||
|
ip:
|
||||||
|
type: string
|
||||||
|
machine_id:
|
||||||
|
default: 1
|
||||||
|
type: integer
|
||||||
|
port:
|
||||||
|
type: integer
|
||||||
|
size_x:
|
||||||
|
default: 500.0
|
||||||
|
type: number
|
||||||
|
size_y:
|
||||||
|
default: 500.0
|
||||||
|
type: number
|
||||||
|
size_z:
|
||||||
|
default: 2000.0
|
||||||
|
type: number
|
||||||
|
timeout:
|
||||||
|
type: integer
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
data:
|
||||||
|
properties:
|
||||||
|
channel_status:
|
||||||
|
type: object
|
||||||
|
connection_info:
|
||||||
|
type: object
|
||||||
|
device_summary:
|
||||||
|
type: object
|
||||||
|
plate_status:
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
total_channels:
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- status
|
||||||
|
- channel_status
|
||||||
|
- connection_info
|
||||||
|
- total_channels
|
||||||
|
- plate_status
|
||||||
|
- device_summary
|
||||||
|
type: object
|
||||||
|
version: 1.0.0
|
||||||
@@ -6034,10 +6034,108 @@ workstation:
|
|||||||
init_param_schema:
|
init_param_schema:
|
||||||
config:
|
config:
|
||||||
properties:
|
properties:
|
||||||
station_resource:
|
deck:
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- station_resource
|
- deck
|
||||||
|
type: object
|
||||||
|
data:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
version: 1.0.0
|
||||||
|
workstation.bioyond:
|
||||||
|
category:
|
||||||
|
- work_station
|
||||||
|
class:
|
||||||
|
action_value_mappings:
|
||||||
|
auto-execute_bioyond_sync_workflow:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
parameters: null
|
||||||
|
handles: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
parameters:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- parameters
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: execute_bioyond_sync_workflow参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommandAsync
|
||||||
|
auto-execute_bioyond_update_workflow:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
parameters: null
|
||||||
|
handles: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
parameters:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- parameters
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: execute_bioyond_update_workflow参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommandAsync
|
||||||
|
auto-load_bioyond_data_from_file:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
file_path: null
|
||||||
|
handles: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
file_path:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- file_path
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: load_bioyond_data_from_file参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
module: unilabos.devices.workstation.bioyond_studio.station:BioyondWorkstation
|
||||||
|
status_types: {}
|
||||||
|
type: python
|
||||||
|
config_info: []
|
||||||
|
description: ''
|
||||||
|
handles: []
|
||||||
|
icon: 反应站.webp
|
||||||
|
init_param_schema:
|
||||||
|
config:
|
||||||
|
properties:
|
||||||
|
bioyond_config:
|
||||||
|
type: string
|
||||||
|
deck:
|
||||||
|
type: string
|
||||||
|
required: []
|
||||||
type: object
|
type: object
|
||||||
data:
|
data:
|
||||||
properties: {}
|
properties: {}
|
||||||
|
|||||||
36
unilabos/registry/resources/bioyond/bottle_carriers.yaml
Normal file
36
unilabos/registry/resources/bioyond/bottle_carriers.yaml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
BIOYOND_PolymerStation_1BottleCarrier:
|
||||||
|
category:
|
||||||
|
- bottle_carriers
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_1BottleCarrier
|
||||||
|
type: pylabrobot
|
||||||
|
description: BIOYOND_PolymerStation_1BottleCarrier
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
registry_type: resource
|
||||||
|
version: 1.0.0
|
||||||
|
BIOYOND_PolymerStation_1FlaskCarrier:
|
||||||
|
category:
|
||||||
|
- bottle_carriers
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_1FlaskCarrier
|
||||||
|
type: pylabrobot
|
||||||
|
description: BIOYOND_PolymerStation_1FlaskCarrier
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
registry_type: resource
|
||||||
|
version: 1.0.0
|
||||||
|
BIOYOND_PolymerStation_6VialCarrier:
|
||||||
|
category:
|
||||||
|
- bottle_carriers
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_6VialCarrier
|
||||||
|
type: pylabrobot
|
||||||
|
description: BIOYOND_PolymerStation_6VialCarrier
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
registry_type: resource
|
||||||
|
version: 1.0.0
|
||||||
24
unilabos/registry/resources/bioyond/deck.yaml
Normal file
24
unilabos/registry/resources/bioyond/deck.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
BIOYOND_PolymerReactionStation_Deck:
|
||||||
|
category:
|
||||||
|
- deck
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck
|
||||||
|
type: pylabrobot
|
||||||
|
description: BIOYOND PolymerReactionStation Deck
|
||||||
|
handles: []
|
||||||
|
icon: '反应站.webp'
|
||||||
|
init_param_schema: {}
|
||||||
|
registry_type: resource
|
||||||
|
version: 1.0.0
|
||||||
|
BIOYOND_PolymerPreparationStation_Deck:
|
||||||
|
category:
|
||||||
|
- deck
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.decks:BIOYOND_PolymerPreparationStation_Deck
|
||||||
|
type: pylabrobot
|
||||||
|
description: BIOYOND PolymerPreparationStation Deck
|
||||||
|
handles: []
|
||||||
|
icon: '配液站.webp'
|
||||||
|
init_param_schema: {}
|
||||||
|
registry_type: resource
|
||||||
|
version: 1.0.0
|
||||||
0
unilabos/resources/bioyond/__init__.py
Normal file
0
unilabos/resources/bioyond/__init__.py
Normal file
217
unilabos/resources/bioyond/bottle_carriers.py
Normal file
217
unilabos/resources/bioyond/bottle_carriers.py
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder, create_ordered_items_2d
|
||||||
|
|
||||||
|
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
|
||||||
|
from unilabos.resources.bioyond.bottles import BIOYOND_PolymerStation_Solid_Vial, BIOYOND_PolymerStation_Solution_Beaker, BIOYOND_PolymerStation_Reagent_Bottle
|
||||||
|
# 命名约定:试剂瓶-Bottle,烧杯-Beaker,烧瓶-Flask,小瓶-Vial
|
||||||
|
|
||||||
|
|
||||||
|
def BIOYOND_Electrolyte_6VialCarrier(name: str) -> BottleCarrier:
|
||||||
|
"""6瓶载架 - 2x3布局"""
|
||||||
|
|
||||||
|
# 载架尺寸 (mm)
|
||||||
|
carrier_size_x = 127.8
|
||||||
|
carrier_size_y = 85.5
|
||||||
|
carrier_size_z = 50.0
|
||||||
|
|
||||||
|
# 瓶位尺寸
|
||||||
|
bottle_diameter = 30.0
|
||||||
|
bottle_spacing_x = 42.0 # X方向间距
|
||||||
|
bottle_spacing_y = 35.0 # Y方向间距
|
||||||
|
|
||||||
|
# 计算起始位置 (居中排列)
|
||||||
|
start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2
|
||||||
|
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
|
||||||
|
|
||||||
|
sites = create_ordered_items_2d(
|
||||||
|
klass=ResourceHolder,
|
||||||
|
num_items_x=3,
|
||||||
|
num_items_y=2,
|
||||||
|
dx=start_x,
|
||||||
|
dy=start_y,
|
||||||
|
dz=5.0,
|
||||||
|
item_dx=bottle_spacing_x,
|
||||||
|
item_dy=bottle_spacing_y,
|
||||||
|
|
||||||
|
size_x=bottle_diameter,
|
||||||
|
size_y=bottle_diameter,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
)
|
||||||
|
for k, v in sites.items():
|
||||||
|
v.name = f"{name}_{v.name}"
|
||||||
|
|
||||||
|
carrier = BottleCarrier(
|
||||||
|
name=name,
|
||||||
|
size_x=carrier_size_x,
|
||||||
|
size_y=carrier_size_y,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
sites=sites,
|
||||||
|
model="BIOYOND_Electrolyte_6VialCarrier",
|
||||||
|
)
|
||||||
|
carrier.num_items_x = 3
|
||||||
|
carrier.num_items_y = 2
|
||||||
|
carrier.num_items_z = 1
|
||||||
|
for i in range(6):
|
||||||
|
carrier[i] = BIOYOND_PolymerStation_Solid_Vial(f"{name}_vial_{i+1}")
|
||||||
|
return carrier
|
||||||
|
|
||||||
|
|
||||||
|
def BIOYOND_Electrolyte_1BottleCarrier(name: str) -> BottleCarrier:
|
||||||
|
"""1瓶载架 - 单个中央位置"""
|
||||||
|
|
||||||
|
# 载架尺寸 (mm)
|
||||||
|
carrier_size_x = 127.8
|
||||||
|
carrier_size_y = 85.5
|
||||||
|
carrier_size_z = 100.0
|
||||||
|
|
||||||
|
# 烧杯尺寸
|
||||||
|
beaker_diameter = 80.0
|
||||||
|
|
||||||
|
# 计算中央位置
|
||||||
|
center_x = (carrier_size_x - beaker_diameter) / 2
|
||||||
|
center_y = (carrier_size_y - beaker_diameter) / 2
|
||||||
|
center_z = 5.0
|
||||||
|
|
||||||
|
carrier = BottleCarrier(
|
||||||
|
name=name,
|
||||||
|
size_x=carrier_size_x,
|
||||||
|
size_y=carrier_size_y,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
sites=create_homogeneous_resources(
|
||||||
|
klass=ResourceHolder,
|
||||||
|
locations=[Coordinate(center_x, center_y, center_z)],
|
||||||
|
resource_size_x=beaker_diameter,
|
||||||
|
resource_size_y=beaker_diameter,
|
||||||
|
name_prefix=name,
|
||||||
|
),
|
||||||
|
model="BIOYOND_Electrolyte_1BottleCarrier",
|
||||||
|
)
|
||||||
|
carrier.num_items_x = 1
|
||||||
|
carrier.num_items_y = 1
|
||||||
|
carrier.num_items_z = 1
|
||||||
|
carrier[0] = BIOYOND_PolymerStation_Solution_Beaker(f"{name}_beaker_1")
|
||||||
|
return carrier
|
||||||
|
|
||||||
|
|
||||||
|
def BIOYOND_PolymerStation_6VialCarrier(name: str) -> BottleCarrier:
|
||||||
|
"""6瓶载架 - 2x3布局"""
|
||||||
|
|
||||||
|
# 载架尺寸 (mm)
|
||||||
|
carrier_size_x = 127.8
|
||||||
|
carrier_size_y = 85.5
|
||||||
|
carrier_size_z = 50.0
|
||||||
|
|
||||||
|
# 瓶位尺寸
|
||||||
|
bottle_diameter = 30.0
|
||||||
|
bottle_spacing_x = 42.0 # X方向间距
|
||||||
|
bottle_spacing_y = 35.0 # Y方向间距
|
||||||
|
|
||||||
|
# 计算起始位置 (居中排列)
|
||||||
|
start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2
|
||||||
|
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
|
||||||
|
|
||||||
|
sites = create_ordered_items_2d(
|
||||||
|
klass=ResourceHolder,
|
||||||
|
num_items_x=3,
|
||||||
|
num_items_y=2,
|
||||||
|
dx=start_x,
|
||||||
|
dy=start_y,
|
||||||
|
dz=5.0,
|
||||||
|
item_dx=bottle_spacing_x,
|
||||||
|
item_dy=bottle_spacing_y,
|
||||||
|
|
||||||
|
size_x=bottle_diameter,
|
||||||
|
size_y=bottle_diameter,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
)
|
||||||
|
for k, v in sites.items():
|
||||||
|
v.name = f"{name}_{v.name}"
|
||||||
|
|
||||||
|
carrier = BottleCarrier(
|
||||||
|
name=name,
|
||||||
|
size_x=carrier_size_x,
|
||||||
|
size_y=carrier_size_y,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
sites=sites,
|
||||||
|
model="BIOYOND_PolymerStation_6VialCarrier",
|
||||||
|
)
|
||||||
|
carrier.num_items_x = 3
|
||||||
|
carrier.num_items_y = 2
|
||||||
|
carrier.num_items_z = 1
|
||||||
|
ordering = ["A1", "A2", "A3", "B1", "B2", "B3"] # 自定义顺序
|
||||||
|
for i in range(6):
|
||||||
|
carrier[i] = BIOYOND_PolymerStation_Solid_Vial(f"{name}_vial_{ordering[i]}")
|
||||||
|
return carrier
|
||||||
|
|
||||||
|
|
||||||
|
def BIOYOND_PolymerStation_1BottleCarrier(name: str) -> BottleCarrier:
|
||||||
|
"""1瓶载架 - 单个中央位置"""
|
||||||
|
|
||||||
|
# 载架尺寸 (mm)
|
||||||
|
carrier_size_x = 127.8
|
||||||
|
carrier_size_y = 85.5
|
||||||
|
carrier_size_z = 20.0
|
||||||
|
|
||||||
|
# 烧杯尺寸
|
||||||
|
beaker_diameter = 60.0
|
||||||
|
|
||||||
|
# 计算中央位置
|
||||||
|
center_x = (carrier_size_x - beaker_diameter) / 2
|
||||||
|
center_y = (carrier_size_y - beaker_diameter) / 2
|
||||||
|
center_z = 5.0
|
||||||
|
|
||||||
|
carrier = BottleCarrier(
|
||||||
|
name=name,
|
||||||
|
size_x=carrier_size_x,
|
||||||
|
size_y=carrier_size_y,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
sites=create_homogeneous_resources(
|
||||||
|
klass=ResourceHolder,
|
||||||
|
locations=[Coordinate(center_x, center_y, center_z)],
|
||||||
|
resource_size_x=beaker_diameter,
|
||||||
|
resource_size_y=beaker_diameter,
|
||||||
|
name_prefix=name,
|
||||||
|
),
|
||||||
|
model="BIOYOND_PolymerStation_1BottleCarrier",
|
||||||
|
)
|
||||||
|
carrier.num_items_x = 1
|
||||||
|
carrier.num_items_y = 1
|
||||||
|
carrier.num_items_z = 1
|
||||||
|
carrier[0] = BIOYOND_PolymerStation_Reagent_Bottle(f"{name}_flask_1")
|
||||||
|
return carrier
|
||||||
|
|
||||||
|
|
||||||
|
def BIOYOND_PolymerStation_1FlaskCarrier(name: str) -> BottleCarrier:
|
||||||
|
"""1瓶载架 - 单个中央位置"""
|
||||||
|
|
||||||
|
# 载架尺寸 (mm)
|
||||||
|
carrier_size_x = 127.8
|
||||||
|
carrier_size_y = 85.5
|
||||||
|
carrier_size_z = 20.0
|
||||||
|
|
||||||
|
# 烧杯尺寸
|
||||||
|
beaker_diameter = 70.0
|
||||||
|
|
||||||
|
# 计算中央位置
|
||||||
|
center_x = (carrier_size_x - beaker_diameter) / 2
|
||||||
|
center_y = (carrier_size_y - beaker_diameter) / 2
|
||||||
|
center_z = 5.0
|
||||||
|
|
||||||
|
carrier = BottleCarrier(
|
||||||
|
name=name,
|
||||||
|
size_x=carrier_size_x,
|
||||||
|
size_y=carrier_size_y,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
sites=create_homogeneous_resources(
|
||||||
|
klass=ResourceHolder,
|
||||||
|
locations=[Coordinate(center_x, center_y, center_z)],
|
||||||
|
resource_size_x=beaker_diameter,
|
||||||
|
resource_size_y=beaker_diameter,
|
||||||
|
name_prefix=name,
|
||||||
|
),
|
||||||
|
model="BIOYOND_PolymerStation_1FlaskCarrier",
|
||||||
|
)
|
||||||
|
carrier.num_items_x = 1
|
||||||
|
carrier.num_items_y = 1
|
||||||
|
carrier.num_items_z = 1
|
||||||
|
carrier[0] = BIOYOND_PolymerStation_Reagent_Bottle(f"{name}_bottle_1")
|
||||||
|
return carrier
|
||||||
56
unilabos/resources/bioyond/bottles.py
Normal file
56
unilabos/resources/bioyond/bottles.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
|
||||||
|
# 工厂函数
|
||||||
|
|
||||||
|
|
||||||
|
def BIOYOND_PolymerStation_Solid_Vial(
|
||||||
|
name: str,
|
||||||
|
diameter: float = 20.0,
|
||||||
|
height: float = 100.0,
|
||||||
|
max_volume: float = 30000.0, # 30mL
|
||||||
|
barcode: str = None,
|
||||||
|
) -> Bottle:
|
||||||
|
"""创建粉末瓶"""
|
||||||
|
return Bottle(
|
||||||
|
name=name,
|
||||||
|
diameter=diameter,
|
||||||
|
height=height,
|
||||||
|
max_volume=max_volume,
|
||||||
|
barcode=barcode,
|
||||||
|
model="BIOYOND_PolymerStation_Solid_Vial",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def BIOYOND_PolymerStation_Solution_Beaker(
|
||||||
|
name: str,
|
||||||
|
diameter: float = 60.0,
|
||||||
|
height: float = 70.0,
|
||||||
|
max_volume: float = 200000.0, # 200mL
|
||||||
|
barcode: str = None,
|
||||||
|
) -> Bottle:
|
||||||
|
"""创建溶液烧杯"""
|
||||||
|
return Bottle(
|
||||||
|
name=name,
|
||||||
|
diameter=diameter,
|
||||||
|
height=height,
|
||||||
|
max_volume=max_volume,
|
||||||
|
barcode=barcode,
|
||||||
|
model="BIOYOND_PolymerStation_Solution_Beaker",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def BIOYOND_PolymerStation_Reagent_Bottle(
|
||||||
|
name: str,
|
||||||
|
diameter: float = 70.0,
|
||||||
|
height: float = 120.0,
|
||||||
|
max_volume: float = 500000.0, # 500mL
|
||||||
|
barcode: str = None,
|
||||||
|
) -> Bottle:
|
||||||
|
"""创建试剂瓶"""
|
||||||
|
return Bottle(
|
||||||
|
name=name,
|
||||||
|
diameter=diameter,
|
||||||
|
height=height,
|
||||||
|
max_volume=max_volume,
|
||||||
|
barcode=barcode,
|
||||||
|
model="BIOYOND_PolymerStation_Reagent_Bottle",
|
||||||
|
)
|
||||||
68
unilabos/resources/bioyond/decks.py
Normal file
68
unilabos/resources/bioyond/decks.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
from pylabrobot.resources import Deck, Coordinate, Rotation
|
||||||
|
|
||||||
|
from unilabos.resources.bioyond.warehouses import bioyond_warehouse_1x4x4, bioyond_warehouse_1x4x2, bioyond_warehouse_liquid_and_lid_handling
|
||||||
|
|
||||||
|
|
||||||
|
class BIOYOND_PolymerReactionStation_Deck(Deck):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str = "PolymerReactionStation_Deck",
|
||||||
|
size_x: float = 2700.0,
|
||||||
|
size_y: float = 1080.0,
|
||||||
|
size_z: float = 1500.0,
|
||||||
|
category: str = "deck",
|
||||||
|
setup: bool = False
|
||||||
|
) -> None:
|
||||||
|
super().__init__(name=name, size_x=2700.0, size_y=1080.0, size_z=1500.0)
|
||||||
|
if setup:
|
||||||
|
self.setup()
|
||||||
|
|
||||||
|
def setup(self) -> None:
|
||||||
|
# 添加仓库
|
||||||
|
self.warehouses = {
|
||||||
|
"堆栈1": bioyond_warehouse_1x4x4("堆栈1"),
|
||||||
|
"堆栈2": bioyond_warehouse_1x4x4("堆栈2"),
|
||||||
|
"站内试剂存放堆栈": bioyond_warehouse_liquid_and_lid_handling("站内试剂存放堆栈"),
|
||||||
|
}
|
||||||
|
self.warehouse_locations = {
|
||||||
|
"堆栈1": Coordinate(0.0, 430.0, 0.0),
|
||||||
|
"堆栈2": Coordinate(2550.0, 430.0, 0.0),
|
||||||
|
"站内试剂存放堆栈": Coordinate(800.0, 475.0, 0.0),
|
||||||
|
}
|
||||||
|
self.warehouses["站内试剂存放堆栈"].rotation = Rotation(z=90)
|
||||||
|
|
||||||
|
for warehouse_name, warehouse in self.warehouses.items():
|
||||||
|
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
|
||||||
|
|
||||||
|
|
||||||
|
class BIOYOND_PolymerPreparationStation_Deck(Deck):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str = "PolymerPreparationStation_Deck",
|
||||||
|
size_x: float = 2700.0,
|
||||||
|
size_y: float = 1080.0,
|
||||||
|
size_z: float = 1500.0,
|
||||||
|
category: str = "deck",
|
||||||
|
setup: bool = False
|
||||||
|
) -> None:
|
||||||
|
super().__init__(name=name, size_x=2700.0, size_y=1080.0, size_z=1500.0)
|
||||||
|
if setup:
|
||||||
|
self.setup()
|
||||||
|
|
||||||
|
def setup(self) -> None:
|
||||||
|
# 添加仓库
|
||||||
|
self.warehouses = {
|
||||||
|
"io_warehouse_left": bioyond_warehouse_1x4x4("io_warehouse_left"),
|
||||||
|
"io_warehouse_right": bioyond_warehouse_1x4x4("io_warehouse_right"),
|
||||||
|
"solutions": bioyond_warehouse_1x4x2("warehouse_solutions"),
|
||||||
|
"liquid_and_lid_handling": bioyond_warehouse_liquid_and_lid_handling("warehouse_liquid_and_lid_handling"),
|
||||||
|
}
|
||||||
|
self.warehouse_locations = {
|
||||||
|
"io_warehouse_left": Coordinate(0.0, 650.0, 0.0),
|
||||||
|
"io_warehouse_right": Coordinate(2550.0, 650.0, 0.0),
|
||||||
|
"solutions": Coordinate(1915.0, 900.0, 0.0),
|
||||||
|
"liquid_and_lid_handling": Coordinate(1330.0, 490.0, 0.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
for warehouse_name, warehouse in self.warehouses.items():
|
||||||
|
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
|
||||||
54
unilabos/resources/bioyond/warehouses.py
Normal file
54
unilabos/resources/bioyond/warehouses.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
from unilabos.resources.warehouse import WareHouse, warehouse_factory
|
||||||
|
|
||||||
|
|
||||||
|
def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 4x1x4仓库"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=1,
|
||||||
|
num_items_y=4,
|
||||||
|
num_items_z=4,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def bioyond_warehouse_1x4x2(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 4x1x2仓库"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=1,
|
||||||
|
num_items_y=4,
|
||||||
|
num_items_z=2,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
removed_positions=None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def bioyond_warehouse_liquid_and_lid_handling(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond开关盖加液模块台面"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=2,
|
||||||
|
num_items_y=5,
|
||||||
|
num_items_z=1,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
removed_positions=None
|
||||||
|
)
|
||||||
@@ -4,6 +4,7 @@ import json
|
|||||||
from typing import Union, Any, Dict
|
from typing import Union, Any, Dict
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import networkx as nx
|
import networkx as nx
|
||||||
|
from pylabrobot.resources import ResourceHolder
|
||||||
from unilabos_msgs.msg import Resource
|
from unilabos_msgs.msg import Resource
|
||||||
|
|
||||||
from unilabos.resources.container import RegularContainer
|
from unilabos.resources.container import RegularContainer
|
||||||
@@ -480,7 +481,54 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None, w
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
def initialize_resource(resource_config: dict) -> list[dict]:
|
def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict = {}, deck: Any = None) -> list[dict]:
|
||||||
|
"""
|
||||||
|
将 bioyond 物料格式转换为 ulab 物料格式
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bioyond_materials: bioyond 系统的物料查询结果列表
|
||||||
|
type_mapping: 物料类型映射字典,格式 {bioyond_type: plr_class_name}
|
||||||
|
location_id_mapping: 库位 ID 到名称的映射字典,格式 {location_id: location_name}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
pylabrobot 格式的物料列表
|
||||||
|
"""
|
||||||
|
plr_materials = []
|
||||||
|
|
||||||
|
for material in bioyond_materials:
|
||||||
|
className = type_mapping.get(material.get("typeName"), "RegularContainer") if type_mapping else "RegularContainer"
|
||||||
|
|
||||||
|
plr_material: ResourcePLR = initialize_resource({"name": material["name"], "class": className}, resource_type=ResourcePLR)
|
||||||
|
plr_material.code = material.get("code", "") and material.get("barCode", "") or ""
|
||||||
|
|
||||||
|
# 处理子物料(detail)
|
||||||
|
if material.get("detail") and len(material["detail"]) > 0:
|
||||||
|
child_ids = []
|
||||||
|
for detail in material["detail"]:
|
||||||
|
number = (detail.get("z", 0) - 1) * plr_material.num_items_x * plr_material.num_items_y + \
|
||||||
|
(detail.get("x", 0) - 1) * plr_material.num_items_x + \
|
||||||
|
(detail.get("y", 0) - 1)
|
||||||
|
bottle = plr_material[number]
|
||||||
|
bottle.code = detail.get("code", "")
|
||||||
|
bottle.tracker.liquids = [(detail["name"], float(detail.get("quantity", 0)) if detail.get("quantity") else 0)]
|
||||||
|
|
||||||
|
plr_materials.append(plr_material)
|
||||||
|
|
||||||
|
if deck and hasattr(deck, "warehouses"):
|
||||||
|
for loc in material.get("locations", []):
|
||||||
|
if hasattr(deck, "warehouses") and loc.get("whName") in deck.warehouses:
|
||||||
|
warehouse = deck.warehouses[loc["whName"]]
|
||||||
|
idx = (loc.get("y", 0) - 1) * warehouse.num_items_x * warehouse.num_items_y + \
|
||||||
|
(loc.get("x", 0) - 1) * warehouse.num_items_x + \
|
||||||
|
(loc.get("z", 0) - 1)
|
||||||
|
if 0 <= idx < warehouse.capacity:
|
||||||
|
if warehouse[idx] is None or isinstance(warehouse[idx], ResourceHolder):
|
||||||
|
warehouse[idx] = plr_material
|
||||||
|
|
||||||
|
return plr_materials
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_resource(resource_config: dict, resource_type: Any = None) -> Union[list[dict], ResourcePLR]:
|
||||||
"""Initializes a resource based on its configuration.
|
"""Initializes a resource based on its configuration.
|
||||||
|
|
||||||
If the config is detailed, then do nothing;
|
If the config is detailed, then do nothing;
|
||||||
@@ -512,11 +560,14 @@ def initialize_resource(resource_config: dict) -> list[dict]:
|
|||||||
|
|
||||||
if resource_class_config["type"] == "pylabrobot":
|
if resource_class_config["type"] == "pylabrobot":
|
||||||
resource_plr = RESOURCE(name=resource_config["name"])
|
resource_plr = RESOURCE(name=resource_config["name"])
|
||||||
|
if resource_type != ResourcePLR:
|
||||||
r = resource_plr_to_ulab(resource_plr=resource_plr, parent_name=resource_config.get("parent", None))
|
r = resource_plr_to_ulab(resource_plr=resource_plr, parent_name=resource_config.get("parent", None))
|
||||||
# r = resource_plr_to_ulab(resource_plr=resource_plr)
|
# r = resource_plr_to_ulab(resource_plr=resource_plr)
|
||||||
if resource_config.get("position") is not None:
|
if resource_config.get("position") is not None:
|
||||||
r["position"] = resource_config["position"]
|
r["position"] = resource_config["position"]
|
||||||
r = tree_to_list([r])
|
r = tree_to_list([r])
|
||||||
|
else:
|
||||||
|
r = resource_plr
|
||||||
elif resource_class_config["type"] == "unilabos":
|
elif resource_class_config["type"] == "unilabos":
|
||||||
res_instance: RegularContainer = RESOURCE(id=resource_config["name"])
|
res_instance: RegularContainer = RESOURCE(id=resource_config["name"])
|
||||||
res_instance.ulr_resource = convert_to_ros_msg(Resource, {k:v for k,v in resource_config.items() if k != "class"})
|
res_instance.ulr_resource = convert_to_ros_msg(Resource, {k:v for k,v in resource_config.items() if k != "class"})
|
||||||
|
|||||||
357
unilabos/resources/itemized_carrier.py
Normal file
357
unilabos/resources/itemized_carrier.py
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
"""
|
||||||
|
自动化液体处理工作站物料类定义 - 简化版
|
||||||
|
Automated Liquid Handling Station Resource Classes - Simplified Version
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
from pylabrobot.resources.coordinate import Coordinate
|
||||||
|
from pylabrobot.resources.container import Container
|
||||||
|
from pylabrobot.resources.resource_holder import ResourceHolder
|
||||||
|
from pylabrobot.resources import Resource as ResourcePLR
|
||||||
|
|
||||||
|
|
||||||
|
class Bottle(Container):
|
||||||
|
"""瓶子类 - 简化版,不追踪瓶盖"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
diameter: float,
|
||||||
|
height: float,
|
||||||
|
max_volume: float,
|
||||||
|
size_x: float = 0.0,
|
||||||
|
size_y: float = 0.0,
|
||||||
|
size_z: float = 0.0,
|
||||||
|
barcode: Optional[str] = "",
|
||||||
|
category: str = "container",
|
||||||
|
model: Optional[str] = None,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
size_x=diameter,
|
||||||
|
size_y=diameter,
|
||||||
|
size_z=height,
|
||||||
|
max_volume=max_volume,
|
||||||
|
category=category,
|
||||||
|
model=model,
|
||||||
|
)
|
||||||
|
self.diameter = diameter
|
||||||
|
self.height = height
|
||||||
|
self.barcode = barcode
|
||||||
|
|
||||||
|
def serialize(self) -> dict:
|
||||||
|
return {
|
||||||
|
**super().serialize(),
|
||||||
|
"diameter": self.diameter,
|
||||||
|
"height": self.height,
|
||||||
|
"barcode": self.barcode,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
from string import ascii_uppercase as LETTERS
|
||||||
|
from typing import Dict, List, Optional, Type, TypeVar, Union, Sequence, Tuple
|
||||||
|
|
||||||
|
import pylabrobot
|
||||||
|
from pylabrobot.resources.resource_holder import ResourceHolder
|
||||||
|
|
||||||
|
T = TypeVar("T", bound=ResourceHolder)
|
||||||
|
|
||||||
|
S = TypeVar("S", bound=ResourceHolder)
|
||||||
|
|
||||||
|
|
||||||
|
class ItemizedCarrier(ResourcePLR):
|
||||||
|
"""Base class for all carriers."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
size_x: float,
|
||||||
|
size_y: float,
|
||||||
|
size_z: float,
|
||||||
|
num_items_x: int = 0,
|
||||||
|
num_items_y: int = 0,
|
||||||
|
num_items_z: int = 0,
|
||||||
|
sites: Optional[Dict[Union[int, str], Optional[ResourcePLR]]] = None,
|
||||||
|
category: Optional[str] = "carrier",
|
||||||
|
model: Optional[str] = None,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
size_x=size_x,
|
||||||
|
size_y=size_y,
|
||||||
|
size_z=size_z,
|
||||||
|
category=category,
|
||||||
|
model=model,
|
||||||
|
)
|
||||||
|
self.num_items = len(sites)
|
||||||
|
self.num_items_x, self.num_items_y, self.num_items_z = num_items_x, num_items_y, num_items_z
|
||||||
|
if isinstance(sites, dict):
|
||||||
|
sites = sites or {}
|
||||||
|
self.sites: List[Optional[ResourcePLR]] = list(sites.values())
|
||||||
|
self._ordering = sites
|
||||||
|
self.child_locations: Dict[str, Coordinate] = {}
|
||||||
|
self.child_size: Dict[str, dict] = {}
|
||||||
|
for spot, resource in sites.items():
|
||||||
|
if resource is not None and getattr(resource, "location", None) is None:
|
||||||
|
raise ValueError(f"resource {resource} has no location")
|
||||||
|
if resource is not None:
|
||||||
|
self.child_locations[spot] = resource.location
|
||||||
|
self.child_size[spot] = {"width": resource._size_x, "height": resource._size_y, "depth": resource._size_z}
|
||||||
|
else:
|
||||||
|
self.child_locations[spot] = Coordinate.zero()
|
||||||
|
self.child_size[spot] = {"width": 0, "height": 0, "depth": 0}
|
||||||
|
elif isinstance(sites, list):
|
||||||
|
# deserialize时走这里;还需要根据 self.sites 索引children
|
||||||
|
self.child_locations = {site["label"]: Coordinate(**site["position"]) for site in sites}
|
||||||
|
self.child_size = {site["label"]: site["size"] for site in sites}
|
||||||
|
self.sites = [site["occupied_by"] for site in sites]
|
||||||
|
self._ordering = {site["label"]: site["position"] for site in sites}
|
||||||
|
else:
|
||||||
|
print("sites:", sites)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def capacity(self):
|
||||||
|
"""The number of sites on this carrier."""
|
||||||
|
return len(self.sites)
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
"""Return the number of sites on this carrier."""
|
||||||
|
return len(self.sites)
|
||||||
|
|
||||||
|
def assign_child_resource(
|
||||||
|
self,
|
||||||
|
resource: ResourcePLR,
|
||||||
|
location: Optional[Coordinate],
|
||||||
|
reassign: bool = True,
|
||||||
|
spot: Optional[int] = None,
|
||||||
|
):
|
||||||
|
idx = spot
|
||||||
|
# 如果只给 location,根据坐标和 deserialize 后的 self.sites(持有names)来寻找 resource 该摆放的位置
|
||||||
|
if spot is not None:
|
||||||
|
idx = spot
|
||||||
|
else:
|
||||||
|
for i, site in enumerate(self.sites):
|
||||||
|
site_location = list(self.child_locations.values())[i]
|
||||||
|
if type(site) == str and site == resource.name:
|
||||||
|
idx = i
|
||||||
|
break
|
||||||
|
if site_location == location:
|
||||||
|
idx = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if not reassign and self.sites[idx] is not None:
|
||||||
|
raise ValueError(f"a site with index {idx} already exists")
|
||||||
|
super().assign_child_resource(resource, location=location, reassign=reassign)
|
||||||
|
self.sites[idx] = resource
|
||||||
|
|
||||||
|
def assign_resource_to_site(self, resource: ResourcePLR, spot: int):
|
||||||
|
if self.sites[spot] is not None and not isinstance(self.sites[spot], ResourceHolder):
|
||||||
|
raise ValueError(f"spot {spot} already has a resource, {resource}")
|
||||||
|
self.assign_child_resource(resource, location=self.child_locations.get(str(spot)), spot=spot)
|
||||||
|
|
||||||
|
def unassign_child_resource(self, resource: ResourcePLR):
|
||||||
|
found = False
|
||||||
|
for spot, res in enumerate(self.sites):
|
||||||
|
if res == resource:
|
||||||
|
self.sites[spot] = None
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
if not found:
|
||||||
|
raise ValueError(f"Resource {resource} is not assigned to this carrier")
|
||||||
|
if hasattr(resource, "unassign"):
|
||||||
|
resource.unassign()
|
||||||
|
|
||||||
|
def __getitem__(
|
||||||
|
self,
|
||||||
|
identifier: Union[str, int, Sequence[int], Sequence[str], slice, range],
|
||||||
|
) -> Union[List[T], T]:
|
||||||
|
"""Get the items with the given identifier.
|
||||||
|
|
||||||
|
This is a convenience method for getting the items with the given identifier. It is equivalent
|
||||||
|
to :meth:`get_items`, but adds support for slicing and supports single items in the same
|
||||||
|
functional call. Note that the return type will always be a list, even if a single item is
|
||||||
|
requested.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Getting the items with identifiers "A1" through "E1":
|
||||||
|
|
||||||
|
>>> items["A1:E1"]
|
||||||
|
|
||||||
|
[<Item A1>, <Item B1>, <Item C1>, <Item D1>, <Item E1>]
|
||||||
|
|
||||||
|
Getting the items with identifiers 0 through 4 (note that this is the same as above):
|
||||||
|
|
||||||
|
>>> items[range(5)]
|
||||||
|
|
||||||
|
[<Item A1>, <Item B1>, <Item C1>, <Item D1>, <Item E1>]
|
||||||
|
|
||||||
|
Getting items with a slice (note that this is the same as above):
|
||||||
|
|
||||||
|
>>> items[0:5]
|
||||||
|
|
||||||
|
[<Item A1>, <Item B1>, <Item C1>, <Item D1>, <Item E1>]
|
||||||
|
|
||||||
|
Getting a single item:
|
||||||
|
|
||||||
|
>>> items[0]
|
||||||
|
|
||||||
|
[<Item A1>]
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(identifier, str):
|
||||||
|
if ":" in identifier: # multiple # TODO: deprecate this, use `"A1":"E1"` instead (slice)
|
||||||
|
return self.get_items(identifier)
|
||||||
|
|
||||||
|
return self.get_item(identifier) # single
|
||||||
|
|
||||||
|
if isinstance(identifier, int):
|
||||||
|
return self.get_item(identifier)
|
||||||
|
|
||||||
|
if isinstance(identifier, (slice, range)):
|
||||||
|
start, stop = identifier.start, identifier.stop
|
||||||
|
if isinstance(identifier.start, str):
|
||||||
|
start = list(self._ordering.keys()).index(identifier.start)
|
||||||
|
elif identifier.start is None:
|
||||||
|
start = 0
|
||||||
|
if isinstance(identifier.stop, str):
|
||||||
|
stop = list(self._ordering.keys()).index(identifier.stop)
|
||||||
|
elif identifier.stop is None:
|
||||||
|
stop = self.num_items
|
||||||
|
identifier = list(range(start, stop, identifier.step or 1))
|
||||||
|
return self.get_items(identifier)
|
||||||
|
|
||||||
|
if isinstance(identifier, (list, tuple)):
|
||||||
|
return self.get_items(identifier)
|
||||||
|
|
||||||
|
raise TypeError(f"Invalid identifier type: {type(identifier)}")
|
||||||
|
|
||||||
|
def get_item(self, identifier: Union[str, int, Tuple[int, int]]) -> T:
|
||||||
|
"""Get the item with the given identifier.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
identifier: The identifier of the item. Either a string, an integer, or a tuple. If an
|
||||||
|
integer, it is the index of the item in the list of items (counted from 0, top to bottom, left
|
||||||
|
to right). If a string, it uses transposed MS Excel style notation, e.g. "A1" for the first
|
||||||
|
item, "B1" for the item below that, etc. If a tuple, it is (row, column).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
IndexError: If the identifier is out of range. The range is 0 to self.num_items-1 (inclusive).
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(identifier, tuple):
|
||||||
|
row, column = identifier
|
||||||
|
identifier = LETTERS[row] + str(column + 1) # standard transposed-Excel style notation
|
||||||
|
if isinstance(identifier, str):
|
||||||
|
try:
|
||||||
|
identifier = list(self._ordering.keys()).index(identifier)
|
||||||
|
except ValueError as e:
|
||||||
|
raise IndexError(
|
||||||
|
f"Item with identifier '{identifier}' does not exist on " f"resource '{self.name}'."
|
||||||
|
) from e
|
||||||
|
|
||||||
|
if not 0 <= identifier < self.capacity:
|
||||||
|
raise IndexError(
|
||||||
|
f"Item with identifier '{identifier}' does not exist on " f"resource '{self.name}'."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Cast child to item type. Children will always be `T`, but the type checker doesn't know that.
|
||||||
|
return self.sites[identifier]
|
||||||
|
|
||||||
|
def get_items(self, identifiers: Union[str, Sequence[int], Sequence[str]]) -> List[T]:
|
||||||
|
"""Get the items with the given identifier.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
identifier: Deprecated. Use `identifiers` instead. # TODO(deprecate-ordered-items)
|
||||||
|
identifiers: The identifiers of the items. Either a string range or a list of integers. If a
|
||||||
|
string, it uses transposed MS Excel style notation. Regions of items can be specified using
|
||||||
|
a colon, e.g. "A1:H1" for the first column. If a list of integers, it is the indices of the
|
||||||
|
items in the list of items (counted from 0, top to bottom, left to right).
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Getting the items with identifiers "A1" through "E1":
|
||||||
|
|
||||||
|
>>> items.get_items("A1:E1")
|
||||||
|
|
||||||
|
[<Item A1>, <Item B1>, <Item C1>, <Item D1>, <Item E1>]
|
||||||
|
|
||||||
|
Getting the items with identifiers 0 through 4:
|
||||||
|
|
||||||
|
>>> items.get_items(range(5))
|
||||||
|
|
||||||
|
[<Item A1>, <Item B1>, <Item C1>, <Item D1>, <Item E1>]
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(identifiers, str):
|
||||||
|
identifiers = pylabrobot.utils.expand_string_range(identifiers)
|
||||||
|
return [self.get_item(i) for i in identifiers]
|
||||||
|
|
||||||
|
def __setitem__(self, idx: Union[int, str], resource: Optional[ResourcePLR]):
|
||||||
|
"""Assign a resource to this carrier."""
|
||||||
|
if resource is None: # setting to None
|
||||||
|
assigned_resource = self[idx]
|
||||||
|
if assigned_resource is not None:
|
||||||
|
self.unassign_child_resource(assigned_resource)
|
||||||
|
else:
|
||||||
|
idx = list(self._ordering.keys()).index(idx) if isinstance(idx, str) else idx
|
||||||
|
self.assign_resource_to_site(resource, spot=idx)
|
||||||
|
|
||||||
|
def __delitem__(self, idx: int):
|
||||||
|
"""Unassign a resource from this carrier."""
|
||||||
|
assigned_resource = self[idx]
|
||||||
|
if assigned_resource is not None:
|
||||||
|
self.unassign_child_resource(assigned_resource)
|
||||||
|
|
||||||
|
def get_resources(self) -> List[ResourcePLR]:
|
||||||
|
"""Get all resources assigned to this carrier."""
|
||||||
|
return [resource for resource in self.sites.values() if resource is not None]
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return super().__eq__(other) and self.sites == other.sites
|
||||||
|
|
||||||
|
def get_free_sites(self) -> List[int]:
|
||||||
|
return [spot for spot, resource in self.sites.items() if resource is None]
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
return {
|
||||||
|
**super().serialize(),
|
||||||
|
"num_items_x": self.num_items_x,
|
||||||
|
"num_items_y": self.num_items_y,
|
||||||
|
"num_items_z": self.num_items_z,
|
||||||
|
"sites": [{
|
||||||
|
"label": str(identifier),
|
||||||
|
"visible": True if self[identifier] is not None else False,
|
||||||
|
"occupied_by": self[identifier].name
|
||||||
|
if isinstance(self[identifier], ResourcePLR) and not isinstance(self[identifier], ResourceHolder) else
|
||||||
|
self[identifier] if isinstance(self[identifier], str) else None,
|
||||||
|
"position": {"x": location.x, "y": location.y, "z": location.z},
|
||||||
|
"size": self.child_size[identifier],
|
||||||
|
"content_type": ["bottle", "container", "tube", "bottle_carrier", "tip_rack"]
|
||||||
|
} for identifier, location in self.child_locations.items()]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BottleCarrier(ItemizedCarrier):
|
||||||
|
"""瓶载架 - 直接继承自 TubeCarrier"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
size_x: float,
|
||||||
|
size_y: float,
|
||||||
|
size_z: float,
|
||||||
|
sites: Optional[Dict[Union[int, str], ResourceHolder]] = None,
|
||||||
|
category: str = "bottle_carrier",
|
||||||
|
model: Optional[str] = None,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
size_x=size_x,
|
||||||
|
size_y=size_y,
|
||||||
|
size_z=size_z,
|
||||||
|
sites=sites,
|
||||||
|
category=category,
|
||||||
|
model=model,
|
||||||
|
)
|
||||||
104
unilabos/resources/warehouse.py
Normal file
104
unilabos/resources/warehouse.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
from typing import Dict, Optional, List, Union
|
||||||
|
from pylabrobot.resources import Coordinate
|
||||||
|
from pylabrobot.resources.carrier import ResourceHolder, create_homogeneous_resources
|
||||||
|
|
||||||
|
from unilabos.resources.itemized_carrier import ItemizedCarrier, ResourcePLR
|
||||||
|
|
||||||
|
|
||||||
|
def warehouse_factory(
|
||||||
|
name: str,
|
||||||
|
num_items_x: int = 4,
|
||||||
|
num_items_y: int = 1,
|
||||||
|
num_items_z: int = 4,
|
||||||
|
dx: float = 137.0,
|
||||||
|
dy: float = 96.0,
|
||||||
|
dz: float = 120.0,
|
||||||
|
item_dx: float = 10.0,
|
||||||
|
item_dy: float = 10.0,
|
||||||
|
item_dz: float = 10.0,
|
||||||
|
removed_positions: Optional[List[int]] = None,
|
||||||
|
empty: bool = False,
|
||||||
|
category: str = "warehouse",
|
||||||
|
model: Optional[str] = None,
|
||||||
|
):
|
||||||
|
# 创建16个板架位 (4层 x 4位置)
|
||||||
|
locations = []
|
||||||
|
for layer in range(num_items_z): # 4层
|
||||||
|
for row in range(num_items_y): # 4行
|
||||||
|
for col in range(num_items_x): # 1列 (每层4x1=4个位置)
|
||||||
|
# 计算位置
|
||||||
|
x = dx + col * item_dx
|
||||||
|
y = dy + (num_items_y - row - 1) * item_dy
|
||||||
|
z = dz + (num_items_z - layer - 1) * item_dz
|
||||||
|
locations.append(Coordinate(x, y, z))
|
||||||
|
if removed_positions:
|
||||||
|
locations = [loc for i, loc in enumerate(locations) if i not in removed_positions]
|
||||||
|
sites = create_homogeneous_resources(
|
||||||
|
klass=ResourceHolder,
|
||||||
|
locations=locations,
|
||||||
|
resource_size_x=127.0,
|
||||||
|
resource_size_y=86.0,
|
||||||
|
name_prefix=name,
|
||||||
|
)
|
||||||
|
|
||||||
|
return WareHouse(
|
||||||
|
name=name,
|
||||||
|
size_x=dx + item_dx * num_items_x,
|
||||||
|
size_y=dy + item_dy * num_items_y,
|
||||||
|
size_z=dz + item_dz * num_items_z,
|
||||||
|
num_items_x = num_items_x,
|
||||||
|
num_items_y = num_items_y,
|
||||||
|
num_items_z = num_items_z,
|
||||||
|
# ordered_items=ordered_items,
|
||||||
|
# ordering=ordering,
|
||||||
|
sites=sites,
|
||||||
|
category=category,
|
||||||
|
model=model,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WareHouse(ItemizedCarrier):
|
||||||
|
"""堆栈载体类 - 可容纳16个板位的载体(4层x4行x1列)"""
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
size_x: float,
|
||||||
|
size_y: float,
|
||||||
|
size_z: float,
|
||||||
|
num_items_x: int,
|
||||||
|
num_items_y: int,
|
||||||
|
num_items_z: int,
|
||||||
|
sites: Optional[Dict[Union[int, str], Optional[ResourcePLR]]] = None,
|
||||||
|
category: str = "warehouse",
|
||||||
|
model: Optional[str] = None,
|
||||||
|
):
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
size_x=size_x,
|
||||||
|
size_y=size_y,
|
||||||
|
size_z=size_z,
|
||||||
|
# ordered_items=ordered_items,
|
||||||
|
# ordering=ordering,
|
||||||
|
num_items_x=num_items_x,
|
||||||
|
num_items_y=num_items_y,
|
||||||
|
num_items_z=num_items_z,
|
||||||
|
sites=sites,
|
||||||
|
category=category,
|
||||||
|
model=model,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_site_by_layer_position(self, row: int, col: int, layer: int) -> ResourceHolder:
|
||||||
|
if not (0 <= layer < 4 and 0 <= row < 4 and 0 <= col < 1):
|
||||||
|
raise ValueError("无效的位置: layer={}, row={}, col={}".format(layer, row, col))
|
||||||
|
|
||||||
|
site_index = layer * 4 + row * 1 + col
|
||||||
|
return self.sites[site_index]
|
||||||
|
|
||||||
|
def add_rack_to_position(self, row: int, col: int, layer: int, rack) -> None:
|
||||||
|
site = self.get_site_by_layer_position(row, col, layer)
|
||||||
|
site.assign_child_resource(rack)
|
||||||
|
|
||||||
|
def get_rack_at_position(self, row: int, col: int, layer: int):
|
||||||
|
site = self.get_site_by_layer_position(row, col, layer)
|
||||||
|
return site.resource
|
||||||
@@ -296,14 +296,14 @@ class WorkstationNodeCreator(DeviceClassCreator[T]):
|
|||||||
try:
|
try:
|
||||||
# 创建实例,额外补充一个给protocol node的字段,后面考虑取消
|
# 创建实例,额外补充一个给protocol node的字段,后面考虑取消
|
||||||
data["children"] = self.children
|
data["children"] = self.children
|
||||||
station_resource_dict = data.get("station_resource")
|
deck_dict = data.get("deck")
|
||||||
if station_resource_dict:
|
if deck_dict:
|
||||||
from pylabrobot.resources import Deck, Resource
|
from pylabrobot.resources import Deck, Resource
|
||||||
plrc = PyLabRobotCreator(Deck, self.children, self.resource_tracker)
|
plrc = PyLabRobotCreator(Deck, self.children, self.resource_tracker)
|
||||||
station_resource = plrc.create_instance(station_resource_dict)
|
deck = plrc.create_instance(deck_dict)
|
||||||
data["station_resource"] = station_resource
|
data["deck"] = deck
|
||||||
else:
|
else:
|
||||||
data["station_resource"] = None
|
data["deck"] = None
|
||||||
self.device_instance = super(WorkstationNodeCreator, self).create_instance(data)
|
self.device_instance = super(WorkstationNodeCreator, self).create_instance(data)
|
||||||
self.post_create()
|
self.post_create()
|
||||||
return self.device_instance
|
return self.device_instance
|
||||||
|
|||||||
Reference in New Issue
Block a user