mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2026-02-09 00:15:10 +00:00
Compare commits
24 Commits
f8ef6e0686
...
workstatio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5805f94e9a | ||
|
|
3adcc41ce8 | ||
|
|
243922caf4 | ||
|
|
079ec9d1b4 | ||
|
|
54cfaf15f3 | ||
|
|
1c9d2ee98a | ||
|
|
3fe8f4ca44 | ||
|
|
2476821dcc | ||
|
|
7b426ed5ae | ||
|
|
9bbae96447 | ||
|
|
10aabb7592 | ||
|
|
a5397ffe12 | ||
|
|
196e0f7e2b | ||
|
|
a632fd495e | ||
|
|
a8cc02a126 | ||
|
|
ad2e1432c6 | ||
|
|
c3b9583eac | ||
|
|
5c47cd0c8a | ||
|
|
63ab1af45d | ||
|
|
a8419dc0c3 | ||
|
|
34f05f2e25 | ||
|
|
0dc2488f02 | ||
|
|
f13156e792 | ||
|
|
13fd1ac572 |
@@ -127,16 +127,16 @@ add_action_files(
|
||||
```bash
|
||||
mamba remove --force ros-humble-unilabos-msgs
|
||||
mamba config set safety_checks disabled # 如果没有提升版本号,会触发md5与网络上md5不一致,是正常现象,因此通过本指令关闭md5检查
|
||||
mamba install xxx.conda2 --offline
|
||||
mamba install xxx.conda --offline
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
**Q: 构建失败怎么办?**
|
||||
**Q: 构建失败怎么办?**
|
||||
A: 检查 Actions 日志中的错误信息,通常是语法错误或依赖问题。修复后重新推送代码即可自动触发新的构建。
|
||||
|
||||
**Q: 如何测试特定平台?**
|
||||
**Q: 如何测试特定平台?**
|
||||
A: 在手动触发构建时,在平台选择中只填写你需要的平台,如 `linux-64` 或 `win-64`。
|
||||
|
||||
**Q: 构建包在哪里下载?**
|
||||
**Q: 构建包在哪里下载?**
|
||||
A: 在 Actions 页面的构建结果中,查找 "Artifacts" 部分,每个平台都有对应的构建包可供下载。
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"HydrogenateProtocol",
|
||||
"RecrystallizeProtocol"
|
||||
],
|
||||
"station_resource": {
|
||||
"deck": {
|
||||
"data": {
|
||||
"_resource_child_name": "deck",
|
||||
"_resource_type": "pylabrobot.resources.opentrons.deck:OTDeck"
|
||||
|
||||
60
test/experiments/dispensing_station_bioyond.json
Normal file
60
test/experiments/dispensing_station_bioyond.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "dispensing_station_bioyond",
|
||||
"name": "dispensing_station_bioyond",
|
||||
"children": [
|
||||
"Bioyond_Dispensing_Deck"
|
||||
],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "dispensing_station.bioyond",
|
||||
"config": {
|
||||
"config": {
|
||||
"api_key": "DE9BDDA0",
|
||||
"api_host": "http://192.168.1.200:44388"
|
||||
},
|
||||
"deck": {
|
||||
"data": {
|
||||
"_resource_child_name": "Bioyond_Dispensing_Deck",
|
||||
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerPreparationStation_Deck"
|
||||
}
|
||||
},
|
||||
"station_config": {
|
||||
"station_type": "dispensing_station",
|
||||
"enable_dispensing_station": true,
|
||||
"enable_reaction_station": false,
|
||||
"station_name": "DispensingStation_001",
|
||||
"description": "Bioyond配液工作站"
|
||||
},
|
||||
"protocol_type": []
|
||||
},
|
||||
"data": {}
|
||||
},
|
||||
{
|
||||
"id": "Bioyond_Dispensing_Deck",
|
||||
"name": "Bioyond_Dispensing_Deck",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "dispensing_station_bioyond",
|
||||
"type": "deck",
|
||||
"class": "BIOYOND_PolymerPreparationStation_Deck",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "BIOYOND_PolymerPreparationStation_Deck",
|
||||
"setup": true,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
}
|
||||
},
|
||||
"data": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
69
test/experiments/reaction_station_bioyond.json
Normal file
69
test/experiments/reaction_station_bioyond.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "reaction_station_bioyond",
|
||||
"name": "reaction_station_bioyond",
|
||||
"parent": null,
|
||||
"children": [
|
||||
"Bioyond_Deck"
|
||||
],
|
||||
"type": "device",
|
||||
"class": "reaction_station.bioyond",
|
||||
"config": {
|
||||
"bioyond_config": {
|
||||
"api_key": "DE9BDDA0",
|
||||
"api_host": "http://192.168.1.200:44402",
|
||||
"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": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
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])
|
||||
@@ -25,6 +25,7 @@ class HTTPClient:
|
||||
remote_addr: 远程服务器地址,如果不提供则从配置中获取
|
||||
auth: 授权信息
|
||||
"""
|
||||
self.initialized = False
|
||||
self.remote_addr = remote_addr or HTTPConfig.remote_addr
|
||||
if auth is not None:
|
||||
self.auth = auth
|
||||
@@ -66,16 +67,25 @@ class HTTPClient:
|
||||
|
||||
Args:
|
||||
resources: 要添加的资源列表
|
||||
database_process_later: 后台处理资源
|
||||
Returns:
|
||||
Response: API响应对象
|
||||
"""
|
||||
response = requests.post(
|
||||
f"{self.remote_addr}/lab/material",
|
||||
json={"nodes": resources},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=100,
|
||||
)
|
||||
if not self.initialized:
|
||||
self.initialized = True
|
||||
info(f"首次添加资源,当前远程地址: {self.remote_addr}")
|
||||
response = requests.post(
|
||||
f"{self.remote_addr}/lab/material",
|
||||
json={"nodes": resources},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=100,
|
||||
)
|
||||
else:
|
||||
response = requests.put(
|
||||
f"{self.remote_addr}/lab/material",
|
||||
json={"nodes": resources},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=100,
|
||||
)
|
||||
if response.status_code == 200:
|
||||
res = response.json()
|
||||
if "code" in res and res["code"] != 0:
|
||||
@@ -131,14 +141,29 @@ class HTTPClient:
|
||||
Returns:
|
||||
Response: API响应对象
|
||||
"""
|
||||
return self.resource_add(resources)
|
||||
response = requests.patch(
|
||||
f"{self.remote_addr}/lab/resource/batch_update/?edge_format=1",
|
||||
json=resources,
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=100,
|
||||
)
|
||||
return response
|
||||
if not self.initialized:
|
||||
self.initialized = True
|
||||
info(f"首次添加资源,当前远程地址: {self.remote_addr}")
|
||||
response = requests.post(
|
||||
f"{self.remote_addr}/lab/material",
|
||||
json={"nodes": resources},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=100,
|
||||
)
|
||||
else:
|
||||
response = requests.put(
|
||||
f"{self.remote_addr}/lab/material",
|
||||
json={"nodes": resources},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=100,
|
||||
)
|
||||
if response.status_code == 200:
|
||||
res = response.json()
|
||||
if "code" in res and res["code"] != 0:
|
||||
logger.error(f"添加物料失败: {response.text}")
|
||||
if response.status_code != 200:
|
||||
logger.error(f"添加物料失败: {response.text}")
|
||||
return response.json()
|
||||
|
||||
def upload_file(self, file_path: str, scene: str = "models") -> requests.Response:
|
||||
"""
|
||||
|
||||
@@ -1093,7 +1093,7 @@ class WebSocketClient(BaseCommunicationClient):
|
||||
},
|
||||
}
|
||||
self.message_processor.send_message(message)
|
||||
logger.trace(f"[WebSocketClient] Device status published: {device_id}.{property_name}")
|
||||
logger.debug(f"[WebSocketClient] Device status published: {device_id}.{property_name}")
|
||||
|
||||
def publish_job_status(
|
||||
self, feedback_data: dict, item: QueueItem, status: str, return_info: Optional[dict] = None
|
||||
|
||||
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
BIN
unilabos/devices/workstation/bioyond_cell/2025092702.xlsx
Normal file
BIN
unilabos/devices/workstation/bioyond_cell/2025092702.xlsx
Normal file
Binary file not shown.
BIN
unilabos/devices/workstation/bioyond_cell/2025101301.xlsx
Normal file
BIN
unilabos/devices/workstation/bioyond_cell/2025101301.xlsx
Normal file
Binary file not shown.
49
unilabos/devices/workstation/bioyond_cell/benyao_test.py
Normal file
49
unilabos/devices/workstation/bioyond_cell/benyao_test.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime
|
||||
def test_benyao_api():
|
||||
# 配置信息
|
||||
ip_addr = "192.168.1.200"
|
||||
port = 44386
|
||||
#url = f"http://{ip_addr}:{port}/api/lims/scheduler/scheduler-status"
|
||||
#url = f"http://{ip_addr}:{port}/api/lims/order/order-list-status"
|
||||
url = f"http://{ip_addr}:{port}/api/lims/storage/stock-material"
|
||||
apiKey = "8A819E5C" # 请替换为实际apiKey
|
||||
|
||||
# 构造请求体
|
||||
request_data = {
|
||||
"apiKey": apiKey,
|
||||
"requestTime": datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ"), # 示例:2025-08-15T10:00:00.000Z
|
||||
"data": {
|
||||
"typeMode": 1,
|
||||
"includeDetail": True
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#request_data = {
|
||||
# "apiKey": apiKey,
|
||||
# "requestTime": datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ"), # 示例:2025-08-15T10:00:00.000Z
|
||||
# "data":
|
||||
#}
|
||||
|
||||
|
||||
print(request_data)
|
||||
# 发送POST请求
|
||||
try:
|
||||
response = requests.post(url, json=request_data, timeout=10)
|
||||
response.raise_for_status() # 检查HTTP状态码
|
||||
|
||||
# 解析响应
|
||||
result = response.json()
|
||||
print("响应状态码:", response.status_code)
|
||||
print("响应内容:")
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print("请求失败:", e)
|
||||
except json.JSONDecodeError as e:
|
||||
print("JSON解析失败:", e)
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_benyao_api()
|
||||
@@ -0,0 +1,374 @@
|
||||
"""
|
||||
Bioyond物料管理实现
|
||||
Bioyond Material Management Implementation
|
||||
|
||||
基于Bioyond系统的物料管理,支持从Bioyond系统同步物料到UniLab工作站
|
||||
"""
|
||||
from typing import Dict, Any, List, Optional, Union
|
||||
import json
|
||||
import asyncio
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from pylabrobot.resources import (
|
||||
Resource as PLRResource,
|
||||
Container,
|
||||
Deck,
|
||||
Coordinate as PLRCoordinate,
|
||||
)
|
||||
|
||||
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
|
||||
from unilabos.utils.log import logger
|
||||
from unilabos.resources.graphio import (
|
||||
resource_plr_to_ulab,
|
||||
resource_ulab_to_plr,
|
||||
resource_bioyond_to_ulab,
|
||||
resource_bioyond_container_to_ulab,
|
||||
resource_ulab_to_bioyond
|
||||
)
|
||||
from .workstation_material_management import MaterialManagementBase
|
||||
|
||||
|
||||
class BioyondMaterialManagement(MaterialManagementBase):
|
||||
"""Bioyond物料管理类
|
||||
|
||||
实现从Bioyond系统同步物料到UniLab工作站的功能:
|
||||
1. 从Bioyond系统获取物料数据
|
||||
2. 转换为UniLab格式
|
||||
3. 同步到PyLabRobot Deck
|
||||
4. 支持双向同步
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device_id: str,
|
||||
deck_config: Dict[str, Any],
|
||||
resource_tracker: DeviceNodeResourceTracker,
|
||||
children_config: Dict[str, Dict[str, Any]] = None,
|
||||
bioyond_config: Dict[str, Any] = None
|
||||
):
|
||||
self.bioyond_config = bioyond_config or {}
|
||||
self.bioyond_api_client = None
|
||||
self.sync_interval = self.bioyond_config.get("sync_interval", 30) # 同步间隔(秒)
|
||||
|
||||
# 初始化父类
|
||||
super().__init__(device_id, deck_config, resource_tracker, children_config)
|
||||
|
||||
# 初始化Bioyond API客户端
|
||||
self._initialize_bioyond_client()
|
||||
|
||||
# 启动同步任务
|
||||
self._start_sync_task()
|
||||
|
||||
def _initialize_bioyond_client(self):
|
||||
"""初始化Bioyond API客户端"""
|
||||
try:
|
||||
# 这里应该根据实际的Bioyond API实现
|
||||
# 暂时使用模拟客户端
|
||||
self.bioyond_api_client = BioyondAPIClient(self.bioyond_config)
|
||||
logger.info(f"Bioyond API客户端初始化成功")
|
||||
except Exception as e:
|
||||
logger.error(f"Bioyond API客户端初始化失败: {e}")
|
||||
self.bioyond_api_client = None
|
||||
|
||||
def _start_sync_task(self):
|
||||
"""启动同步任务"""
|
||||
if self.bioyond_api_client:
|
||||
# 创建异步同步任务
|
||||
asyncio.create_task(self._periodic_sync())
|
||||
logger.info(f"Bioyond同步任务已启动,间隔: {self.sync_interval}秒")
|
||||
|
||||
async def _periodic_sync(self):
|
||||
"""定期同步任务"""
|
||||
while True:
|
||||
try:
|
||||
await self.sync_from_bioyond()
|
||||
await asyncio.sleep(self.sync_interval)
|
||||
except Exception as e:
|
||||
logger.error(f"Bioyond同步任务出错: {e}")
|
||||
await asyncio.sleep(self.sync_interval)
|
||||
|
||||
async def sync_from_bioyond(self) -> bool:
|
||||
"""从Bioyond系统同步物料"""
|
||||
try:
|
||||
if not self.bioyond_api_client:
|
||||
logger.warning("Bioyond API客户端未初始化")
|
||||
return False
|
||||
|
||||
# 1. 从Bioyond获取物料数据
|
||||
bioyond_data = await self.bioyond_api_client.get_materials()
|
||||
if not bioyond_data:
|
||||
logger.warning("从Bioyond获取物料数据为空")
|
||||
return False
|
||||
|
||||
# 2. 转换为UniLab格式
|
||||
if isinstance(bioyond_data, dict) and "data" in bioyond_data:
|
||||
# 容器格式数据
|
||||
unilab_resources = resource_bioyond_container_to_ulab(bioyond_data)
|
||||
else:
|
||||
# 物料列表格式数据
|
||||
unilab_resources = resource_bioyond_to_ulab(bioyond_data)
|
||||
|
||||
# 3. 转换为PLR格式并分配到Deck
|
||||
await self._assign_resources_to_deck(unilab_resources)
|
||||
|
||||
logger.info(f"从Bioyond同步了 {len(unilab_resources)} 个资源")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"从Bioyond同步物料失败: {e}")
|
||||
return False
|
||||
|
||||
async def sync_to_bioyond(self, plr_resource: PLRResource) -> bool:
|
||||
"""将本地物料变更同步到Bioyond系统"""
|
||||
try:
|
||||
if not self.bioyond_api_client:
|
||||
logger.warning("Bioyond API客户端未初始化")
|
||||
return False
|
||||
|
||||
# 1. 转换为UniLab格式
|
||||
unilab_resource = resource_plr_to_ulab(plr_resource)
|
||||
|
||||
# 2. 转换为Bioyond格式
|
||||
bioyond_materials = resource_ulab_to_bioyond([unilab_resource])
|
||||
|
||||
# 3. 发送到Bioyond系统
|
||||
success = await self.bioyond_api_client.update_materials(bioyond_materials)
|
||||
|
||||
if success:
|
||||
logger.info(f"成功同步物料 {plr_resource.name} 到Bioyond")
|
||||
else:
|
||||
logger.warning(f"同步物料 {plr_resource.name} 到Bioyond失败")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"同步物料到Bioyond失败: {e}")
|
||||
return False
|
||||
|
||||
async def _assign_resources_to_deck(self, unilab_resources: List[Dict[str, Any]]):
|
||||
"""将UniLab资源分配到Deck"""
|
||||
try:
|
||||
# 转换为PLR格式
|
||||
from unilabos.resources.graphio import list_to_nested_dict
|
||||
nested_resources = list_to_nested_dict(unilab_resources)
|
||||
plr_resources = resource_ulab_to_plr(nested_resources)
|
||||
|
||||
# 分配资源到Deck
|
||||
if hasattr(plr_resources, 'children'):
|
||||
resources_to_assign = plr_resources.children
|
||||
elif isinstance(plr_resources, list):
|
||||
resources_to_assign = plr_resources
|
||||
else:
|
||||
resources_to_assign = [plr_resources]
|
||||
|
||||
for resource in resources_to_assign:
|
||||
try:
|
||||
# 获取资源位置
|
||||
if hasattr(resource, 'location') and resource.location:
|
||||
location = PLRCoordinate(resource.location.x, resource.location.y, resource.location.z)
|
||||
else:
|
||||
location = PLRCoordinate(0, 0, 0)
|
||||
|
||||
# 分配资源到Deck
|
||||
self.plr_deck.assign_child_resource(resource, location)
|
||||
|
||||
# 注册到resource tracker
|
||||
self.resource_tracker.add_resource(resource)
|
||||
|
||||
# 保存资源引用
|
||||
self.plr_resources[resource.name] = resource
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"分配资源 {resource.name} 到Deck失败: {e}")
|
||||
|
||||
logger.info(f"成功分配了 {len(resources_to_assign)} 个资源到Deck")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"分配资源到Deck失败: {e}")
|
||||
|
||||
def _create_resource_by_type(
|
||||
self,
|
||||
resource_id: str,
|
||||
resource_type: str,
|
||||
config: Dict[str, Any],
|
||||
data: Dict[str, Any],
|
||||
location: PLRCoordinate
|
||||
) -> Optional[PLRResource]:
|
||||
"""根据类型创建Bioyond相关资源"""
|
||||
try:
|
||||
# 这里可以根据需要实现特定的Bioyond资源类型
|
||||
# 目前使用通用的容器类型
|
||||
if resource_type in ["container", "plate", "well"]:
|
||||
return self._create_generic_container(resource_id, resource_type, config, data, location)
|
||||
else:
|
||||
logger.warning(f"未知的Bioyond资源类型: {resource_type}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建Bioyond资源失败 {resource_id} ({resource_type}): {e}")
|
||||
return None
|
||||
|
||||
def _create_generic_container(
|
||||
self,
|
||||
resource_id: str,
|
||||
resource_type: str,
|
||||
config: Dict[str, Any],
|
||||
data: Dict[str, Any],
|
||||
location: PLRCoordinate
|
||||
) -> Optional[PLRResource]:
|
||||
"""创建通用容器资源"""
|
||||
try:
|
||||
from pylabrobot.resources import Plate, Well
|
||||
|
||||
if resource_type == "plate":
|
||||
return Plate(
|
||||
name=resource_id,
|
||||
size_x=config.get("size_x", 127.76),
|
||||
size_y=config.get("size_y", 85.48),
|
||||
size_z=config.get("size_z", 14.35),
|
||||
location=location,
|
||||
category="plate"
|
||||
)
|
||||
elif resource_type == "well":
|
||||
return Well(
|
||||
name=resource_id,
|
||||
size_x=config.get("size_x", 9.0),
|
||||
size_y=config.get("size_y", 9.0),
|
||||
size_z=config.get("size_z", 10.0),
|
||||
location=location,
|
||||
category="well"
|
||||
)
|
||||
else:
|
||||
return Container(
|
||||
name=resource_id,
|
||||
size_x=config.get("size_x", 50.0),
|
||||
size_y=config.get("size_y", 50.0),
|
||||
size_z=config.get("size_z", 10.0),
|
||||
location=location,
|
||||
category="container"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建通用容器失败 {resource_id}: {e}")
|
||||
return None
|
||||
|
||||
def get_bioyond_materials(self) -> List[Dict[str, Any]]:
|
||||
"""获取当前Bioyond物料列表"""
|
||||
try:
|
||||
# 将当前PLR资源转换为Bioyond格式
|
||||
bioyond_materials = []
|
||||
for resource in self.plr_resources.values():
|
||||
unilab_resource = resource_plr_to_ulab(resource)
|
||||
bioyond_materials.extend(resource_ulab_to_bioyond([unilab_resource]))
|
||||
return bioyond_materials
|
||||
except Exception as e:
|
||||
logger.error(f"获取Bioyond物料列表失败: {e}")
|
||||
return []
|
||||
|
||||
def update_material_from_bioyond(self, material_id: str, bioyond_data: Dict[str, Any]) -> bool:
|
||||
"""从Bioyond数据更新指定物料"""
|
||||
try:
|
||||
# 查找现有物料
|
||||
material = self.find_material_by_id(material_id)
|
||||
if not material:
|
||||
logger.warning(f"未找到物料: {material_id}")
|
||||
return False
|
||||
|
||||
# 转换Bioyond数据为UniLab格式
|
||||
unilab_resources = resource_bioyond_to_ulab([bioyond_data])
|
||||
if not unilab_resources:
|
||||
logger.warning(f"转换Bioyond数据失败: {material_id}")
|
||||
return False
|
||||
|
||||
# 更新物料属性
|
||||
unilab_resource = unilab_resources[0]
|
||||
material.name = unilab_resource.get("name", material.name)
|
||||
|
||||
# 更新位置
|
||||
position = unilab_resource.get("position", {})
|
||||
if position:
|
||||
material.location = PLRCoordinate(
|
||||
position.get("x", 0),
|
||||
position.get("y", 0),
|
||||
position.get("z", 0)
|
||||
)
|
||||
|
||||
logger.info(f"成功更新物料: {material_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"更新物料失败 {material_id}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
class BioyondAPIClient:
|
||||
"""Bioyond API客户端(模拟实现)
|
||||
|
||||
实际使用时需要根据Bioyond系统的API接口实现
|
||||
"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
self.config = config
|
||||
self.base_url = config.get("base_url", "http://localhost:8080")
|
||||
self.api_key = config.get("api_key", "")
|
||||
self.timeout = config.get("timeout", 30)
|
||||
|
||||
async def get_materials(self) -> Optional[Union[Dict[str, Any], List[Dict[str, Any]]]]:
|
||||
"""从Bioyond系统获取物料数据"""
|
||||
try:
|
||||
# 这里应该实现实际的API调用
|
||||
# 暂时返回模拟数据
|
||||
logger.info("从Bioyond API获取物料数据")
|
||||
|
||||
# 模拟API调用延迟
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# 返回模拟数据(实际应该从API获取)
|
||||
return {
|
||||
"data": [],
|
||||
"code": 1,
|
||||
"message": "success",
|
||||
"timestamp": 1234567890
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Bioyond API调用失败: {e}")
|
||||
return None
|
||||
|
||||
async def update_materials(self, materials: List[Dict[str, Any]]) -> bool:
|
||||
"""更新Bioyond系统中的物料数据"""
|
||||
try:
|
||||
# 这里应该实现实际的API调用
|
||||
logger.info(f"更新Bioyond系统中的 {len(materials)} 个物料")
|
||||
|
||||
# 模拟API调用延迟
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# 模拟成功响应
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"更新Bioyond物料失败: {e}")
|
||||
return False
|
||||
|
||||
async def get_material_by_id(self, material_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""根据ID获取单个物料"""
|
||||
try:
|
||||
# 这里应该实现实际的API调用
|
||||
logger.info(f"从Bioyond API获取物料: {material_id}")
|
||||
|
||||
# 模拟API调用延迟
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# 返回模拟数据
|
||||
return {
|
||||
"id": material_id,
|
||||
"name": f"material_{material_id}",
|
||||
"type": "container",
|
||||
"quantity": 1.0,
|
||||
"unit": "个"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取Bioyond物料失败 {material_id}: {e}")
|
||||
return None
|
||||
796
unilabos/devices/workstation/bioyond_cell/bioyond_workstation.py
Normal file
796
unilabos/devices/workstation/bioyond_cell/bioyond_workstation.py
Normal file
@@ -0,0 +1,796 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime, timezone
|
||||
import requests
|
||||
from pathlib import Path
|
||||
import pandas as pd
|
||||
import time
|
||||
from datetime import datetime, timezone, timedelta
|
||||
import re
|
||||
import threading
|
||||
|
||||
from urllib3 import response
|
||||
from unilabos.devices.workstation.workstation_base import WorkstationBase
|
||||
from unilabos.devices.workstation.workstation_http_service import WorkstationHTTPService
|
||||
from unilabos.utils.log import logger
|
||||
from pylabrobot.resources.deck import Deck
|
||||
|
||||
def _iso_utc_now_ms() -> str:
|
||||
# 文档要求:到毫秒 + Z,例如 2025-08-15T05:43:22.814Z
|
||||
dt = datetime.now()
|
||||
return dt.strftime("%Y-%m-%dT%H:%M:%S.") + f"{int(dt.microsecond/1000):03d}Z"
|
||||
|
||||
|
||||
class BioyondWorkstation(WorkstationBase):
|
||||
"""
|
||||
集成 Bioyond LIMS 的工作站示例,
|
||||
覆盖:入库(2.17/2.18) → 新建实验(2.14) → 启动调度(2.7) →
|
||||
运行中推送:物料变更(2.24)、步骤完成(2.21)、订单完成(2.23) →
|
||||
查询实验(2.5/2.6) → 3-2-1 转运(2.32) → 样品/废料取出(2.28)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bioyond_config: Optional[Dict[str, Any]] = None,
|
||||
station_resource: Optional[Dict[str, Any]] = None,
|
||||
debug_mode: bool = False,
|
||||
*args, **kwargs,
|
||||
):
|
||||
default_config = {
|
||||
#"base_url": "http://192.168.1.200:44388",
|
||||
"base_url": "http://61.169.57.196:44422",
|
||||
"api_key": "8A819E5C",
|
||||
"timeout": 30,
|
||||
"report_token": "CHANGE_ME_TOKEN"
|
||||
}
|
||||
self.bioyond_config = {**default_config, **(bioyond_config or {})}
|
||||
|
||||
self.http_service_started = False
|
||||
self.debug_mode = debug_mode
|
||||
super().__init__(deck=Deck, station_resource=station_resource, *args, **kwargs)
|
||||
logger.info(f"Bioyond工作站初始化完成 (debug_mode={self.debug_mode})")
|
||||
|
||||
# 实例化并在后台线程启动 HTTP 报送服务
|
||||
# self.order_status = {}
|
||||
# try:
|
||||
# t = threading.Thread(target=self._start_http_service_bg, daemon=True, name="unilab_http")
|
||||
# t.start()
|
||||
|
||||
# except Exception as e:
|
||||
# logger.error(f"unilab-server后台启动报送服务失败: {e}")
|
||||
|
||||
# @property
|
||||
# def device_id(self) -> str:
|
||||
# try:
|
||||
# return getattr(self, "_ros_node").device_id # 兼容 ROS 场景
|
||||
# except Exception:
|
||||
# return "bioyond_workstation"
|
||||
|
||||
# def _start_http_service_bg(self, host: str = "192.168.1.104", port: int = 8080) -> None:
|
||||
# logger.info("进入 _start_http_service_bg 函数")
|
||||
# try:
|
||||
# self.service = WorkstationHTTPService(self, host=host, port=port)
|
||||
# logger.info("WorkstationHTTPService 实例化完成")
|
||||
# self.service.start()
|
||||
# self.http_service_started = True
|
||||
# logger.info(f"unilab_HTTP 服务成功启动: {host}:{port}")
|
||||
|
||||
# 一直挂着,直到进程退出
|
||||
# while True:
|
||||
# time.sleep(1)
|
||||
|
||||
# except Exception as e:
|
||||
# self.http_service_started = False
|
||||
# logger.error(f"启动unilab_HTTP服务失败: {e}", exc_info=True)
|
||||
|
||||
# -------------------- 基础HTTP封装 --------------------
|
||||
def _url(self, path: str) -> str:
|
||||
return f"{self.bioyond_config['base_url'].rstrip('/')}/{path.lstrip('/')}"
|
||||
def _post_lims(self, path: str, data: Optional[Any] = None) -> Dict[str, Any]:
|
||||
"""LIMS API:大多数接口用 {apiKey/requestTime,data} 包装"""
|
||||
payload = {
|
||||
"apiKey": self.bioyond_config["api_key"],
|
||||
"requestTime": _iso_utc_now_ms()
|
||||
}
|
||||
|
||||
if data is not None:
|
||||
payload["data"] = data
|
||||
|
||||
if self.debug_mode:
|
||||
# 模拟返回,不发真实请求
|
||||
logger.info(f"[DEBUG] POST {path} with payload={payload}")
|
||||
return {"debug": True, "url": self._url(path), "payload": payload, "status": "ok"}
|
||||
try:
|
||||
r = requests.post(
|
||||
self._url(path),
|
||||
json=payload,
|
||||
timeout=self.bioyond_config.get("timeout", 30),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
r.raise_for_status()
|
||||
#print(r.json())
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
logger.error(f"POST {path} 失败: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
# --- 修正:_post_report / _post_report_raw 同样走 debug_mode ---
|
||||
def _post_report(self, path: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
payload = {
|
||||
"token": self.bioyond_config.get("report_token", ""),
|
||||
"request_time": _iso_utc_now_ms(),
|
||||
"data": data
|
||||
}
|
||||
if self.debug_mode:
|
||||
logger.info(f"[DEBUG] POST {path} with payload={payload}")
|
||||
return {"debug": True, "url": self._url(path), "payload": payload, "status": "ok"}
|
||||
try:
|
||||
r = requests.post(self._url(path), json=payload,
|
||||
timeout=self.bioyond_config.get("timeout", 30),
|
||||
headers={"Content-Type": "application/json"})
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
logger.error(f"POST {path} 失败: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
def _post_report_raw(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if self.debug_mode:
|
||||
logger.info(f"[DEBUG] POST {path} with body={body}")
|
||||
return {"debug": True, "url": self._url(path), "payload": body, "status": "ok"}
|
||||
try:
|
||||
r = requests.post(self._url(path), json=body,
|
||||
timeout=self.bioyond_config.get("timeout", 30),
|
||||
headers={"Content-Type": "application/json"})
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
logger.error(f"POST {path} 失败: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
# -------------------- 单点接口封装 --------------------
|
||||
# 2.17 入库物料(单个)
|
||||
def storage_inbound(self, material_id: str, location_id: str) -> Dict[str, Any]:
|
||||
return self._post_lims("/api/lims/storage/inbound", {
|
||||
"materialId": material_id,
|
||||
"locationId": location_id
|
||||
})
|
||||
|
||||
# 2.18 批量入库(多个)
|
||||
def storage_batch_inbound(self, items: List[Dict[str, str]]) -> Dict[str, Any]:
|
||||
"""
|
||||
items = [{"materialId": "...", "locationId": "..."}, ...]
|
||||
"""
|
||||
return self._post_lims("/api/lims/storage/batch-inbound", items)
|
||||
|
||||
# 3.30 自动化上料(Excel -> JSON -> POST /api/lims/order/auto-feeding4to3)
|
||||
def auto_feeding4to3_from_xlsx(self, xlsx_path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
根据固定模板解析 Excel:
|
||||
- 四号手套箱加样头面 (2-13行, 3-7列)
|
||||
- 四号手套箱原液瓶面 (15-23行, 3-9列)
|
||||
- 三号手套箱人工堆栈 (26-40行, 3-7列)
|
||||
"""
|
||||
path = Path(xlsx_path)
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"未找到 Excel 文件:{path}")
|
||||
|
||||
try:
|
||||
df = pd.read_excel(path, sheet_name=0, header=None, engine="openpyxl")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"读取 Excel 失败:{e}")
|
||||
|
||||
items: List[Dict[str, Any]] = []
|
||||
|
||||
# 四号手套箱 - 加样头面(2-13行, 3-7列)
|
||||
for _, row in df.iloc[1:13, 2:7].iterrows():
|
||||
item = {
|
||||
"sourceWHName": "四号手套箱堆栈",
|
||||
"posX": int(row[2]),
|
||||
"posY": int(row[3]),
|
||||
"posZ": int(row[4]),
|
||||
"materialName": str(row[5]).strip() if pd.notna(row[5]) else "",
|
||||
"quantity": float(row[6]) if pd.notna(row[6]) else 0.0,
|
||||
}
|
||||
if item["materialName"]:
|
||||
items.append(item)
|
||||
|
||||
# 四号手套箱 - 原液瓶面(15-23行, 3-9列)
|
||||
for _, row in df.iloc[14:23, 2:9].iterrows():
|
||||
item = {
|
||||
"sourceWHName": "四号手套箱堆栈",
|
||||
"posX": int(row[2]),
|
||||
"posY": int(row[3]),
|
||||
"posZ": int(row[4]),
|
||||
"materialName": str(row[5]).strip() if pd.notna(row[5]) else "",
|
||||
"quantity": float(row[6]) if pd.notna(row[6]) else 0.0,
|
||||
"materialType": str(row[7]).strip() if pd.notna(row[7]) else "",
|
||||
"targetWH": str(row[8]).strip() if pd.notna(row[8]) else "",
|
||||
}
|
||||
if item["materialName"]:
|
||||
items.append(item)
|
||||
|
||||
# 三号手套箱人工堆栈(26-40行, 3-7列)
|
||||
for _, row in df.iloc[25:40, 2:7].iterrows():
|
||||
item = {
|
||||
"sourceWHName": "三号手套箱人工堆栈",
|
||||
"posX": int(row[2]),
|
||||
"posY": int(row[3]),
|
||||
"posZ": int(row[4]),
|
||||
"materialType": str(row[5]).strip() if pd.notna(row[5]) else "",
|
||||
"materialId": str(row[6]).strip() if pd.notna(row[6]) else "",
|
||||
"quantity": 1 # 默认数量1
|
||||
}
|
||||
if item["materialId"] or item["materialType"]:
|
||||
items.append(item)
|
||||
|
||||
return self._post_lims("/api/lims/order/auto-feeding4to3", items)
|
||||
|
||||
|
||||
|
||||
def auto_batch_outbound_from_xlsx(self, xlsx_path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
3.31 自动化下料(Excel -> JSON -> POST /api/lims/storage/auto-batch-out-bound)
|
||||
"""
|
||||
path = Path(xlsx_path)
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"未找到 Excel 文件:{path}")
|
||||
|
||||
try:
|
||||
df = pd.read_excel(path, sheet_name=0, engine="openpyxl")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"读取 Excel 失败:{e}")
|
||||
|
||||
def pick(names: List[str]) -> Optional[str]:
|
||||
for n in names:
|
||||
if n in df.columns:
|
||||
return n
|
||||
return None
|
||||
|
||||
c_loc = pick(["locationId", "库位ID", "库位Id", "库位id"])
|
||||
c_wh = pick(["warehouseId", "仓库ID", "仓库Id", "仓库id"])
|
||||
c_qty = pick(["数量", "quantity"])
|
||||
c_x = pick(["x", "X", "posX", "坐标X"])
|
||||
c_y = pick(["y", "Y", "posY", "坐标Y"])
|
||||
c_z = pick(["z", "Z", "posZ", "坐标Z"])
|
||||
|
||||
required = [c_loc, c_wh, c_qty, c_x, c_y, c_z]
|
||||
if any(c is None for c in required):
|
||||
raise KeyError("Excel 缺少必要列:locationId/warehouseId/数量/x/y/z(支持多别名,至少要能匹配到)。")
|
||||
|
||||
def as_int(v, d=0):
|
||||
try:
|
||||
if pd.isna(v): return d
|
||||
return int(v)
|
||||
except Exception:
|
||||
try:
|
||||
return int(float(v))
|
||||
except Exception:
|
||||
return d
|
||||
|
||||
def as_float(v, d=0.0):
|
||||
try:
|
||||
if pd.isna(v): return d
|
||||
return float(v)
|
||||
except Exception:
|
||||
return d
|
||||
|
||||
def as_str(v, d=""):
|
||||
if v is None or (isinstance(v, float) and pd.isna(v)): return d
|
||||
s = str(v).strip()
|
||||
return s if s else d
|
||||
|
||||
items: List[Dict[str, Any]] = []
|
||||
for _, row in df.iterrows():
|
||||
items.append({
|
||||
"locationId": as_str(row[c_loc]),
|
||||
"warehouseId": as_str(row[c_wh]),
|
||||
"quantity": as_float(row[c_qty]),
|
||||
"x": as_int(row[c_x]),
|
||||
"y": as_int(row[c_y]),
|
||||
"z": as_int(row[c_z]),
|
||||
})
|
||||
|
||||
return self._post_lims("/api/lims/storage/auto-batch-out-bound", items)
|
||||
|
||||
# 2.14 新建实验
|
||||
def create_orders(self, xlsx_path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
从 Excel 解析并创建实验(2.14)
|
||||
约定:
|
||||
- batchId = Excel 文件名(不含扩展名)
|
||||
- 物料列:所有以 "(g)" 结尾(不再读取“总质量(g)”列)
|
||||
- totalMass 自动计算为所有物料质量之和
|
||||
- createTime 缺失或为空时自动填充为当前日期(YYYY/M/D)
|
||||
"""
|
||||
path = Path(xlsx_path)
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"未找到 Excel 文件:{path}")
|
||||
|
||||
try:
|
||||
df = pd.read_excel(path, sheet_name=0, engine="openpyxl")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"读取 Excel 失败:{e}")
|
||||
|
||||
# 列名容错:返回可选列名,找不到则返回 None
|
||||
def _pick(col_names: List[str]) -> Optional[str]:
|
||||
for c in col_names:
|
||||
if c in df.columns:
|
||||
return c
|
||||
return None
|
||||
|
||||
col_order_name = _pick(["配方ID", "orderName", "订单编号"])
|
||||
col_create_time = _pick(["创建日期", "createTime"])
|
||||
col_bottle_type = _pick(["配液瓶类型", "bottleType"])
|
||||
col_mix_time = _pick(["混匀时间(s)", "mixTime"])
|
||||
col_load = _pick(["扣电组装分液体积", "loadSheddingInfo"])
|
||||
col_pouch = _pick(["软包组装分液体积", "pouchCellInfo"])
|
||||
col_cond = _pick(["电导测试分液体积", "conductivityInfo"])
|
||||
col_cond_cnt = _pick(["电导测试分液瓶数", "conductivityBottleCount"])
|
||||
|
||||
# 物料列:所有以 (g) 结尾
|
||||
material_cols = [c for c in df.columns if isinstance(c, str) and c.endswith("(g)")]
|
||||
if not material_cols:
|
||||
raise KeyError("未发现任何以“(g)”结尾的物料列,请检查表头。")
|
||||
|
||||
batch_id = path.stem
|
||||
|
||||
def _to_ymd_slash(v) -> str:
|
||||
# 统一为 "YYYY/M/D";为空或解析失败则用当前日期
|
||||
if v is None or (isinstance(v, float) and pd.isna(v)) or str(v).strip() == "":
|
||||
ts = datetime.now()
|
||||
else:
|
||||
try:
|
||||
ts = pd.to_datetime(v)
|
||||
except Exception:
|
||||
ts = datetime.now()
|
||||
return f"{ts.year}/{ts.month}/{ts.day}"
|
||||
|
||||
def _as_int(val, default=0) -> int:
|
||||
try:
|
||||
if pd.isna(val):
|
||||
return default
|
||||
return int(val)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
def _as_str(val, default="") -> str:
|
||||
if val is None or (isinstance(val, float) and pd.isna(val)):
|
||||
return default
|
||||
s = str(val).strip()
|
||||
return s if s else default
|
||||
|
||||
orders: List[Dict[str, Any]] = []
|
||||
|
||||
for idx, row in df.iterrows():
|
||||
mats: List[Dict[str, Any]] = []
|
||||
total_mass = 0.0
|
||||
|
||||
for mcol in material_cols:
|
||||
val = row.get(mcol, None)
|
||||
if val is None or (isinstance(val, float) and pd.isna(val)):
|
||||
continue
|
||||
try:
|
||||
mass = float(val)
|
||||
except Exception:
|
||||
continue
|
||||
if mass > 0:
|
||||
mats.append({"name": mcol.replace("(g)", ""), "mass": mass})
|
||||
total_mass += mass
|
||||
|
||||
order_data = {
|
||||
"batchId": batch_id,
|
||||
"orderName": _as_str(row[col_order_name], default=f"{batch_id}_order_{idx+1}") if col_order_name else f"{batch_id}_order_{idx+1}",
|
||||
"createTime": _to_ymd_slash(row[col_create_time]) if col_create_time else _to_ymd_slash(None),
|
||||
"bottleType": _as_str(row[col_bottle_type], default="配液小瓶") if col_bottle_type else "配液小瓶",
|
||||
"mixTime": _as_int(row[col_mix_time]) if col_mix_time else 0,
|
||||
"loadSheddingInfo": _as_int(row[col_load]) if col_load else 0,
|
||||
"pouchCellInfo": _as_int(row[col_pouch]) if col_pouch else 0,
|
||||
"conductivityInfo": _as_int(row[col_cond]) if col_cond else 0,
|
||||
"conductivityBottleCount": _as_int(row[col_cond_cnt]) if col_cond_cnt else 0,
|
||||
"materialInfos": mats,
|
||||
"totalMass": round(total_mass, 4) # 自动汇总
|
||||
}
|
||||
orders.append(order_data)
|
||||
|
||||
# print(orders)
|
||||
while True:
|
||||
time.sleep(5)
|
||||
response = self._post_lims("/api/lims/order/orders", orders)
|
||||
if response.get("data", []):
|
||||
break
|
||||
logger.info(f"等待配液实验创建完成")
|
||||
|
||||
|
||||
|
||||
# self.order_status[response["data"]["orderCode"]] = "running"
|
||||
|
||||
# while True:
|
||||
# time.sleep(5)
|
||||
# if self.order_status.get(response["data"]["orderCode"], None) == "finished":
|
||||
# logger.info(f"配液实验已完成 ,即将执行 3-2-1 转运")
|
||||
# break
|
||||
# logger.info(f"等待配液实验完成")
|
||||
|
||||
# self.transfer_3_to_2_to_1()
|
||||
# self.wait_for_transfer_task()
|
||||
# logger.info(f"3-2-1 转运完成,返回结果")
|
||||
# return r321
|
||||
return response
|
||||
|
||||
# 2.7 启动调度
|
||||
def scheduler_start(self) -> Dict[str, Any]:
|
||||
response = self._post_lims("/api/lims/scheduler/start")
|
||||
print(response)
|
||||
return response
|
||||
# 3.10 停止调度
|
||||
def scheduler_stop(self) -> Dict[str, Any]:
|
||||
"""
|
||||
停止调度 (3.10)
|
||||
请求体只包含 apiKey 和 requestTime
|
||||
"""
|
||||
return self._post_lims("/api/lims/scheduler/stop")
|
||||
# 2.9 继续调度
|
||||
def scheduler_continue(self) -> Dict[str, Any]:
|
||||
"""
|
||||
继续调度 (2.9)
|
||||
请求体只包含 apiKey 和 requestTime
|
||||
"""
|
||||
return self._post_lims("/api/lims/scheduler/continue")
|
||||
|
||||
|
||||
|
||||
# 2.24 物料变更推送
|
||||
def report_material_change(self, material_obj: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
material_obj 按 2.24 的裸对象格式(包含 id/typeName/locations/detail 等)
|
||||
"""
|
||||
return self._post_report_raw("/report/material_change", material_obj)
|
||||
|
||||
# 2.21 步骤完成推送(BS → LIMS)
|
||||
def report_step_finish(self,
|
||||
order_code: str,
|
||||
order_name: str,
|
||||
step_name: str,
|
||||
step_id: str,
|
||||
sample_id: str,
|
||||
start_time: str,
|
||||
end_time: str,
|
||||
execution_status: str = "completed") -> Dict[str, Any]:
|
||||
data = {
|
||||
"orderCode": order_code,
|
||||
"orderName": order_name,
|
||||
"stepName": step_name,
|
||||
"stepId": step_id,
|
||||
"sampleId": sample_id,
|
||||
"startTime": start_time,
|
||||
"endTime": end_time,
|
||||
"executionStatus": execution_status
|
||||
}
|
||||
return self._post_report("/report/step_finish", data)
|
||||
|
||||
# 2.23 订单完成推送(BS → LIMS)
|
||||
def report_order_finish(self,
|
||||
order_code: str,
|
||||
order_name: str,
|
||||
start_time: str,
|
||||
end_time: str,
|
||||
status: str = "30", # 30 完成 / -11 异常停止 / -12 人工停止
|
||||
workflow_status: str = "Finished",
|
||||
completion_time: Optional[str] = None,
|
||||
used_materials: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
|
||||
data = {
|
||||
"orderCode": order_code,
|
||||
"orderName": order_name,
|
||||
"startTime": start_time,
|
||||
"endTime": end_time,
|
||||
"status": status,
|
||||
"workflowStatus": workflow_status,
|
||||
"completionTime": completion_time or end_time,
|
||||
"usedMaterials": used_materials or []
|
||||
}
|
||||
return self._post_report("/report/order_finish", data)
|
||||
|
||||
# 2.5 批量查询实验报告(用于轮询是否完成)
|
||||
def order_list(self,
|
||||
status: Optional[str] = None,
|
||||
begin_time: Optional[str] = None,
|
||||
end_time: Optional[str] = None,
|
||||
filter_text: Optional[str] = None,
|
||||
skip: int = 0, page: int = 10) -> Dict[str, Any]:
|
||||
data: Dict[str, Any] = {"skipCount": skip, "pageCount": page}
|
||||
if status is not None: # 80 成功 / 90 失败 / 100 执行中
|
||||
data["status"] = status
|
||||
if begin_time:
|
||||
data["timeType"] = "CreationTime"
|
||||
data["beginTime"] = begin_time
|
||||
if end_time:
|
||||
data["endTime"] = end_time
|
||||
if filter_text:
|
||||
data["filter"] = filter_text
|
||||
return self._post_lims("/api/lims/order/order-list", data)
|
||||
|
||||
# 2.6 实验报告查询(根据任务ID拿详情)
|
||||
def order_report(self, order_id: str) -> Dict[str, Any]:
|
||||
return self._post_lims("/api/lims/order/order-report", order_id)
|
||||
|
||||
# 2.32 3-2-1 物料转运
|
||||
def transfer_3_to_2_to_1(self,
|
||||
# source_wh_id: Optional[str] = None,
|
||||
source_wh_id: Optional[str] = '3a19debc-84b4-0359-e2d4-b3beea49348b',
|
||||
source_x: int = 1, source_y: int = 1, source_z: int = 1) -> Dict[str, Any]:
|
||||
payload: Dict[str, Any] = {
|
||||
"sourcePosX": source_x, "sourcePosY": source_y, "sourcePosZ": source_z
|
||||
}
|
||||
if source_wh_id:
|
||||
payload["sourceWHID"] = source_wh_id
|
||||
return self._post_lims("/api/lims/order/transfer-task3To2To1", payload)
|
||||
|
||||
# 2.28 样品/废料取出
|
||||
def take_out(self,
|
||||
order_id: str,
|
||||
preintake_ids: Optional[List[str]] = None,
|
||||
material_ids: Optional[List[str]] = None) -> Dict[str, Any]:
|
||||
data = {
|
||||
"orderId": order_id,
|
||||
"preintakeIds": preintake_ids or [],
|
||||
"materialIds": material_ids or []
|
||||
}
|
||||
return self._post_lims("/api/lims/order/take-out", data)
|
||||
|
||||
# --------(可选)占位方法:文档未定义的“1号站内部流程 / 1-2转运”--------
|
||||
def start_station1_internal_flow(self, **kwargs) -> None:
|
||||
logger.info("启动1号站内部流程(占位,按现场系统填充具体指令)")
|
||||
|
||||
|
||||
# 3.x 1→2 物料转运
|
||||
def transfer_1_to_2(self) -> Dict[str, Any]:
|
||||
"""
|
||||
1→2 物料转运
|
||||
URL: /api/lims/order/transfer-task1To2
|
||||
只需要 apiKey 和 requestTime
|
||||
"""
|
||||
return self._post_lims("/api/lims/order/transfer-task1To2")
|
||||
|
||||
|
||||
# -------------------- 整体编排 --------------------
|
||||
def run_full_workflow(self,
|
||||
inbound_items: List[Dict[str, str]],
|
||||
orders: List[Dict[str, Any]],
|
||||
poll_filter_code: Optional[str] = None,
|
||||
poll_timeout_s: int = 600,
|
||||
poll_interval_s: int = 5,
|
||||
transfer_source: Optional[Dict[str, Any]] = None,
|
||||
takeout_order_id: Optional[str] = None) -> None:
|
||||
"""
|
||||
一键串联:
|
||||
1) 入库 3-4 个物料 → 2) 新建实验 → 3) 启动调度
|
||||
运行中(如需):4) 物料变更推送 5) 步骤完成推送 6) 订单完成推送
|
||||
完成后:查询实验(2.5/2.6)→ 7) 3-2-1 转运 → 8) 1号站内部流程
|
||||
→ 9) 1-2 转运 → 10) 样品/废料取出
|
||||
"""
|
||||
# 1. 入库(多于1个就用批量接口 2.18)
|
||||
if len(inbound_items) == 1:
|
||||
r = self.storage_inbound(inbound_items[0]["materialId"], inbound_items[0]["locationId"])
|
||||
logger.info(f"单个入库结果: {r}")
|
||||
else:
|
||||
r = self.storage_batch_inbound(inbound_items)
|
||||
logger.info(f"批量入库结果: {r}")
|
||||
|
||||
# 2. 新建实验(2.14)
|
||||
r = self.create_orders(orders)
|
||||
logger.info(f"新建实验结果: {r}")
|
||||
|
||||
# 3. 启动调度(2.7)
|
||||
r = self.scheduler_start()
|
||||
logger.info(f"启动调度结果: {r}")
|
||||
|
||||
# —— 运行中各类推送(2.24 / 2.21 / 2.23),通常由实际任务驱动,这里提供调用方式 —— #
|
||||
# self.report_material_change({...})
|
||||
# self.report_step_finish(order_code="BSO...", order_name="配液分液", step_name="xxx", step_id="...", sample_id="...",
|
||||
# start_time=_iso_utc_now_ms(), end_time=_iso_utc_now_ms(), execution_status="completed")
|
||||
# self.report_order_finish(order_code="BSO...", order_name="配液分液", start_time="...", end_time=_iso_utc_now_ms())
|
||||
|
||||
# 完成后才能转运:用 2.5 批量查询配合 filter=任务编码 轮询到 status=80(成功)
|
||||
if poll_filter_code:
|
||||
import time
|
||||
deadline = time.time() + poll_timeout_s
|
||||
while time.time() < deadline:
|
||||
res = self.order_list(status="80", filter_text=poll_filter_code, page=5)
|
||||
if isinstance(res, dict) and res.get("data", {}).get("items"):
|
||||
logger.info(f"实验 {poll_filter_code} 已完成:{res['data']['items'][0]}")
|
||||
break
|
||||
time.sleep(poll_interval_s)
|
||||
else:
|
||||
logger.warning(f"等待实验 {poll_filter_code} 完成超时(未到 status=80)")
|
||||
|
||||
# 7. 启动 3-2-1 转运(2.32)
|
||||
if transfer_source:
|
||||
r = self.transfer_3_to_2_to_1(
|
||||
source_wh_id=transfer_source.get("sourceWHID"),
|
||||
source_x=transfer_source.get("sourcePosX", 1),
|
||||
source_y=transfer_source.get("sourcePosY", 1),
|
||||
source_z=transfer_source.get("sourcePosZ", 1),
|
||||
)
|
||||
logger.info(f"3-2-1 转运结果: {r}")
|
||||
|
||||
# 8. 1号站内部流程(占位)
|
||||
self.start_station1_internal_flow()
|
||||
|
||||
# 9. 1→2 转运(占位)
|
||||
self.transfer_1_to_2()
|
||||
|
||||
# 10. 样品/废料取出(2.28)
|
||||
if takeout_order_id:
|
||||
r = self.take_out(order_id=takeout_order_id)
|
||||
logger.info(f"样品/废料取出结果: {r}")
|
||||
|
||||
# 2.5 批量查询实验报告
|
||||
def order_list_v2(self,
|
||||
timeType: str = "string",
|
||||
beginTime: str = "",
|
||||
endTime: str = "",
|
||||
status: str = "",
|
||||
filter: str = "",
|
||||
skipCount: int = 0,
|
||||
pageCount: int = 1,
|
||||
sorting: str = "") -> Dict[str, Any]:
|
||||
"""
|
||||
批量查询实验报告的详细信息 (2.5)
|
||||
URL: /api/lims/order/order-list
|
||||
参数默认值和接口文档保持一致
|
||||
"""
|
||||
data: Dict[str, Any] = {
|
||||
"timeType": timeType,
|
||||
"beginTime": beginTime,
|
||||
"endTime": endTime,
|
||||
"status": status,
|
||||
"filter": filter,
|
||||
"skipCount": skipCount,
|
||||
"pageCount": pageCount,
|
||||
"sorting": sorting
|
||||
}
|
||||
return self._post_lims("/api/lims/order/order-list", data)
|
||||
|
||||
|
||||
def wait_for_transfer_task(self, timeout: int = 3000, interval: int = 5, filter_text: Optional[str] = None) -> bool:
|
||||
"""
|
||||
轮询查询物料转移任务是否成功完成 (status=80)
|
||||
- timeout: 最大等待秒数 (默认600秒)
|
||||
- interval: 轮询间隔秒数 (默认3秒)
|
||||
返回 True 表示找到并成功完成,False 表示超时未找到
|
||||
"""
|
||||
now = datetime.now()
|
||||
beginTime = now.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
endTime = (now + timedelta(minutes=5)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
print(beginTime, endTime)
|
||||
|
||||
deadline = time.time() + timeout
|
||||
|
||||
while time.time() < deadline:
|
||||
result = self.order_list_v2(
|
||||
timeType="string",
|
||||
beginTime=beginTime,
|
||||
endTime=endTime,
|
||||
status="",
|
||||
filter=filter_text,
|
||||
skipCount=0,
|
||||
pageCount=1,
|
||||
sorting=""
|
||||
)
|
||||
print(result)
|
||||
|
||||
items = result.get("data", {}).get("items", [])
|
||||
for item in items:
|
||||
name = item.get("name", "")
|
||||
status = item.get("status")
|
||||
# 改成用 filter_text 判断
|
||||
if (not filter_text or filter_text in name) and status == 80:
|
||||
logger.info(f"硬件转移动作完成: {name}, status={status}")
|
||||
return True
|
||||
|
||||
logger.info(f"等待中: {name}, status={status}")
|
||||
time.sleep(interval)
|
||||
|
||||
logger.warning("超时未找到成功的物料转移任务")
|
||||
return False
|
||||
|
||||
|
||||
def Bioystation_scheduler_start_task(self) -> bool:
|
||||
logger.info("开始调度")
|
||||
self.scheduler_start()
|
||||
logger.info("调度已启动")
|
||||
|
||||
def Bioystation_scheduler_stop_task(self) -> bool:
|
||||
logger.info("停止调度")
|
||||
self.scheduler_stop()
|
||||
logger.info("调度已停止")
|
||||
|
||||
def Bioystation_scheduler_continue_task(self) -> bool:
|
||||
logger.info("继续调度")
|
||||
self.scheduler_continue()
|
||||
logger.info("调度已继续")
|
||||
|
||||
# 3.30 上料:读取模板 Excel 自动解析并 POST
|
||||
def Bioystation_feeding4to3_from_xlsx_task(self) -> bool:
|
||||
logger.info("4号箱自动上料开始")
|
||||
r1 = self.auto_feeding4to3_from_xlsx(r"D:\Uni-lab\Uni-Lab-OS\unilabos\devices\workstation\bioyond_cell\样品导入模板.xlsx")
|
||||
self.wait_for_transfer_task(filter_text="物料转移任务")
|
||||
logger.info("4号箱向3号箱转运物料转移任务已完成")
|
||||
return True
|
||||
|
||||
# # 新建实验
|
||||
def Bioystation_start_experiment_task(self) -> bool:
|
||||
logger.info("3号箱内实验开始")
|
||||
response = self.create_orders(r"D:\Uni-lab\Uni-Lab-OS\unilabos\devices\workstation\bioyond_cell\2025101301.xlsx")
|
||||
logger.info(response)
|
||||
data_list = response.get("data", [])
|
||||
order_name = data_list[0].get("orderName", "")
|
||||
self.wait_for_transfer_task(filter_text=order_name)
|
||||
logger.info("3号站内实验完成")
|
||||
return True
|
||||
|
||||
def Bioystation_3_to_2_task(self) -> bool:
|
||||
self.transfer_3_to_2_to_1()
|
||||
self.wait_for_transfer_task(filter_text="物料转移任务")
|
||||
logger.info("3号站向2号站向1号站转移任务完成")
|
||||
return True
|
||||
|
||||
def Bioystation_1_to_2_task(self) -> bool:
|
||||
self.transfer_1_to_2()
|
||||
self.wait_for_transfer_task(filter_text="物料转移任务")
|
||||
logger.info("1号站向2号站转移任务完成")
|
||||
logger.info("全流程结束")
|
||||
return True
|
||||
|
||||
def test_benyao_workstation(self, num1, num2):
|
||||
num1 = int(num1)
|
||||
num2 = int(num2)
|
||||
for i in range(num1):
|
||||
print(f"num1 = {num1}")
|
||||
for j in range(num2):
|
||||
print(f"num1 = {num2}")
|
||||
|
||||
# --------------------------------
|
||||
if __name__ == "__main__":
|
||||
ws = BioyondWorkstation()
|
||||
#ws.scheduler_stop()
|
||||
#ws.Bioystation_scheduler_start_task()
|
||||
ws.scheduler_start()
|
||||
# ws.scheduler_start()
|
||||
# logger.info("调度启动完成")
|
||||
|
||||
# ws.scheduler_continue()
|
||||
# 3.30 上料:读取模板 Excel 自动解析并 POST
|
||||
# r1 = ws.auto_feeding4to3_from_xlsx(r"C:\ML\GitHub\Uni-Lab-OS\unilabos\devices\workstation\bioyond_cell\样品导入模板 (8).xlsx")
|
||||
# ws.wait_for_transfer_task(filter_text="物料转移任务")
|
||||
# logger.info("4号箱向3号箱转运物料转移任务已完成")
|
||||
|
||||
# ws.scheduler_start()
|
||||
# print(r1["payload"]["data"]) # 调试模式下可直接看到要发的 JSON items
|
||||
|
||||
# # 新建实验
|
||||
# response = ws.create_orders("C:/ML/GitHub/Uni-Lab-OS/unilabos/devices/workstation/bioyond_cell/2025092701.xlsx")
|
||||
# logger.info(response)
|
||||
# data_list = response.get("data", [])
|
||||
# order_name = data_list[0].get("orderName", "")
|
||||
|
||||
# ws.wait_for_transfer_task(filter_text=order_name)
|
||||
# ws.wait_for_transfer_task(filter_text='DP20250927001')
|
||||
# logger.info("3号站内实验完成")
|
||||
# # ws.scheduler_start()
|
||||
# # print(res)
|
||||
# ws.transfer_3_to_2_to_1()
|
||||
# ws.wait_for_transfer_task(filter_text="物料转移任务")
|
||||
# logger.info("3号站向2号站向1号站转移任务完成")
|
||||
# r321 = self.wait_for_transfer_task()
|
||||
#1号站启动
|
||||
# ws.transfer_1_to_2()
|
||||
#s.wait_for_transfer_task(filter_text="物料转移任务")
|
||||
#ogger.info("1号站向2号站转移任务完成")
|
||||
#ogger.info("全流程结束")
|
||||
|
||||
# 3.31 下料:同理
|
||||
# r2 = ws.auto_batch_outbound_from_xlsx(r"C:/path/样品导入模板 (8).xlsx")
|
||||
# print(r2["payload"]["data"])
|
||||
@@ -0,0 +1,772 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime, timezone
|
||||
import requests
|
||||
from pathlib import Path
|
||||
import pandas as pd
|
||||
import time
|
||||
from datetime import datetime, timezone, timedelta
|
||||
import re
|
||||
import threading
|
||||
from unilabos.devices.workstation.workstation_base import WorkstationBase
|
||||
from unilabos.devices.workstation.workstation_http_service import WorkstationHTTPService
|
||||
from unilabos.utils.log import logger
|
||||
from pylabrobot.resources.deck import Deck
|
||||
|
||||
|
||||
def _iso_utc_now_ms() -> str:
|
||||
# 文档要求:到毫秒 + Z,例如 2025-08-15T05:43:22.814Z
|
||||
dt = datetime.now(timezone.utc)
|
||||
return dt.strftime("%Y-%m-%dT%H:%M:%S.") + f"{int(dt.microsecond/1000):03d}Z"
|
||||
|
||||
|
||||
class BioyondWorkstation(WorkstationBase):
|
||||
"""
|
||||
集成 Bioyond LIMS 的工作站示例,
|
||||
覆盖:入库(2.17/2.18) → 新建实验(2.14) → 启动调度(2.7) →
|
||||
运行中推送:物料变更(2.24)、步骤完成(2.21)、订单完成(2.23) →
|
||||
查询实验(2.5/2.6) → 3-2-1 转运(2.32) → 样品/废料取出(2.28)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bioyond_config: Optional[Dict[str, Any]] = None,
|
||||
station_resource: Optional[Dict[str, Any]] = None,
|
||||
debug_mode: bool = False, # 增加调试模式开关
|
||||
*args, **kwargs,
|
||||
):
|
||||
self.bioyond_config = bioyond_config or {
|
||||
#"base_url": "http://192.168.1.200:44386",
|
||||
#"base_url": "http://172.16.11.219:44388",
|
||||
"base_url": "http://61.169.57.196:44422",
|
||||
"api_key": "8A819E5C",
|
||||
"timeout": 30,
|
||||
"report_token": "CHANGE_ME_TOKEN"
|
||||
}
|
||||
|
||||
|
||||
self.debug_mode = debug_mode
|
||||
super().__init__(deck=Deck, station_resource=station_resource, *args, **kwargs)
|
||||
logger.info(f"Bioyond工作站初始化完成 (debug_mode={self.debug_mode})")
|
||||
|
||||
# 实例化并在后台线程启动 HTTP 报送服务
|
||||
self.order_status = {}
|
||||
try:
|
||||
t = threading.Thread(target=self._start_http_service_bg, daemon=True, name="unilab_http")
|
||||
t.start()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"unilab-server后台启动报送服务失败: {e}")
|
||||
|
||||
@property
|
||||
def device_id(self) -> str:
|
||||
try:
|
||||
return getattr(self, "_ros_node").device_id # 兼容 ROS 场景
|
||||
except Exception:
|
||||
return "bioyond_workstation"
|
||||
|
||||
def _start_http_service_bg(self, host: str = "192.168.1.104", port: int = 7000) -> None:
|
||||
try:
|
||||
self.service = WorkstationHTTPService(self, host=host, port=port)
|
||||
self.service.start()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"启动HTTP服务失败: {e}")
|
||||
|
||||
# -------------------- 基础HTTP封装 --------------------
|
||||
def _url(self, path: str) -> str:
|
||||
return f"{self.bioyond_config['base_url'].rstrip('/')}/{path.lstrip('/')}"
|
||||
|
||||
def _post_lims(self, path: str, data: Optional[Any] = None) -> Dict[str, Any]:
|
||||
"""LIMS API:大多数接口用 {apiKey/requestTime,data} 包装"""
|
||||
payload = {
|
||||
"apiKey": self.bioyond_config["api_key"],
|
||||
"requestTime": _iso_utc_now_ms()
|
||||
}
|
||||
if data is not None:
|
||||
payload["data"] = data
|
||||
|
||||
if self.debug_mode:
|
||||
# 模拟返回,不发真实请求
|
||||
logger.info(f"[DEBUG] POST {path} with payload={payload}")
|
||||
return {"debug": True, "url": self._url(path), "payload": payload, "status": "ok"}
|
||||
|
||||
try:
|
||||
r = requests.post(
|
||||
self._url(path),
|
||||
json=payload,
|
||||
timeout=self.bioyond_config.get("timeout", 30),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
logger.error(f"POST {path} 失败: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
# --- 修正:_post_report / _post_report_raw 同样走 debug_mode ---
|
||||
def _post_report(self, path: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
payload = {
|
||||
"token": self.bioyond_config.get("report_token", ""),
|
||||
"request_time": _iso_utc_now_ms(),
|
||||
"data": data
|
||||
}
|
||||
if self.debug_mode:
|
||||
logger.info(f"[DEBUG] POST {path} with payload={payload}")
|
||||
return {"debug": True, "url": self._url(path), "payload": payload, "status": "ok"}
|
||||
try:
|
||||
r = requests.post(self._url(path), json=payload,
|
||||
timeout=self.bioyond_config.get("timeout", 30),
|
||||
headers={"Content-Type": "application/json"})
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
logger.error(f"POST {path} 失败: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
def _post_report_raw(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if self.debug_mode:
|
||||
logger.info(f"[DEBUG] POST {path} with body={body}")
|
||||
return {"debug": True, "url": self._url(path), "payload": body, "status": "ok"}
|
||||
try:
|
||||
r = requests.post(self._url(path), json=body,
|
||||
timeout=self.bioyond_config.get("timeout", 30),
|
||||
headers={"Content-Type": "application/json"})
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
logger.error(f"POST {path} 失败: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
# -------------------- 单点接口封装 --------------------
|
||||
# 2.17 入库物料(单个)
|
||||
def storage_inbound(self, material_id: str, location_id: str) -> Dict[str, Any]:
|
||||
return self._post_lims("/api/lims/storage/inbound", {
|
||||
"materialId": material_id,
|
||||
"locationId": location_id
|
||||
})
|
||||
|
||||
# 2.18 批量入库(多个)
|
||||
def storage_batch_inbound(self, items: List[Dict[str, str]]) -> Dict[str, Any]:
|
||||
"""
|
||||
items = [{"materialId": "...", "locationId": "..."}, ...]
|
||||
"""
|
||||
return self._post_lims("/api/lims/storage/batch-inbound", items)
|
||||
|
||||
# 3.30 自动化上料(Excel -> JSON -> POST /api/lims/order/auto-feeding4to3)
|
||||
def auto_feeding4to3_from_xlsx(self, xlsx_path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
根据固定模板解析 Excel:
|
||||
- 四号手套箱加样头面 (2-13行, 3-7列)
|
||||
- 四号手套箱原液瓶面 (15-23行, 3-9列)
|
||||
- 三号手套箱人工堆栈 (26-40行, 3-7列)
|
||||
"""
|
||||
path = Path(xlsx_path)
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"未找到 Excel 文件:{path}")
|
||||
|
||||
try:
|
||||
df = pd.read_excel(path, sheet_name=0, header=None, engine="openpyxl")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"读取 Excel 失败:{e}")
|
||||
|
||||
items: List[Dict[str, Any]] = []
|
||||
|
||||
# 四号手套箱 - 加样头面(2-13行, 3-7列)
|
||||
for _, row in df.iloc[1:13, 2:7].iterrows():
|
||||
item = {
|
||||
"sourceWHName": "四号手套箱堆栈",
|
||||
"posX": int(row[2]),
|
||||
"posY": int(row[3]),
|
||||
"posZ": int(row[4]),
|
||||
"materialName": str(row[5]).strip() if pd.notna(row[5]) else "",
|
||||
"quantity": float(row[6]) if pd.notna(row[6]) else 0.0,
|
||||
}
|
||||
if item["materialName"]:
|
||||
items.append(item)
|
||||
|
||||
# 四号手套箱 - 原液瓶面(15-23行, 3-9列)
|
||||
for _, row in df.iloc[14:23, 2:9].iterrows():
|
||||
item = {
|
||||
"sourceWHName": "四号手套箱堆栈",
|
||||
"posX": int(row[2]),
|
||||
"posY": int(row[3]),
|
||||
"posZ": int(row[4]),
|
||||
"materialName": str(row[5]).strip() if pd.notna(row[5]) else "",
|
||||
"quantity": float(row[6]) if pd.notna(row[6]) else 0.0,
|
||||
"materialType": str(row[7]).strip() if pd.notna(row[7]) else "",
|
||||
"targetWH": str(row[8]).strip() if pd.notna(row[8]) else "",
|
||||
}
|
||||
if item["materialName"]:
|
||||
items.append(item)
|
||||
|
||||
# 三号手套箱人工堆栈(26-40行, 3-7列)
|
||||
for _, row in df.iloc[25:40, 2:7].iterrows():
|
||||
item = {
|
||||
"sourceWHName": "三号手套箱人工堆栈",
|
||||
"posX": int(row[2]),
|
||||
"posY": int(row[3]),
|
||||
"posZ": int(row[4]),
|
||||
"materialType": str(row[5]).strip() if pd.notna(row[5]) else "",
|
||||
"materialId": str(row[6]).strip() if pd.notna(row[6]) else "",
|
||||
"quantity": 1 # 默认数量1
|
||||
}
|
||||
if item["materialId"] or item["materialType"]:
|
||||
items.append(item)
|
||||
print("items", items)
|
||||
return self._post_lims("/api/lims/order/auto-feeding4to3", items)
|
||||
|
||||
|
||||
|
||||
def auto_batch_outbound_from_xlsx(self, xlsx_path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
3.31 自动化下料(Excel -> JSON -> POST /api/lims/storage/auto-batch-out-bound)
|
||||
"""
|
||||
path = Path(xlsx_path)
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"未找到 Excel 文件:{path}")
|
||||
|
||||
try:
|
||||
df = pd.read_excel(path, sheet_name=0, engine="openpyxl")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"读取 Excel 失败:{e}")
|
||||
|
||||
def pick(names: List[str]) -> Optional[str]:
|
||||
for n in names:
|
||||
if n in df.columns:
|
||||
return n
|
||||
return None
|
||||
|
||||
c_loc = pick(["locationId", "库位ID", "库位Id", "库位id"])
|
||||
c_wh = pick(["warehouseId", "仓库ID", "仓库Id", "仓库id"])
|
||||
c_qty = pick(["数量", "quantity"])
|
||||
c_x = pick(["x", "X", "posX", "坐标X"])
|
||||
c_y = pick(["y", "Y", "posY", "坐标Y"])
|
||||
c_z = pick(["z", "Z", "posZ", "坐标Z"])
|
||||
|
||||
required = [c_loc, c_wh, c_qty, c_x, c_y, c_z]
|
||||
if any(c is None for c in required):
|
||||
raise KeyError("Excel 缺少必要列:locationId/warehouseId/数量/x/y/z(支持多别名,至少要能匹配到)。")
|
||||
|
||||
def as_int(v, d=0):
|
||||
try:
|
||||
if pd.isna(v): return d
|
||||
return int(v)
|
||||
except Exception:
|
||||
try:
|
||||
return int(float(v))
|
||||
except Exception:
|
||||
return d
|
||||
|
||||
def as_float(v, d=0.0):
|
||||
try:
|
||||
if pd.isna(v): return d
|
||||
return float(v)
|
||||
except Exception:
|
||||
return d
|
||||
|
||||
def as_str(v, d=""):
|
||||
if v is None or (isinstance(v, float) and pd.isna(v)): return d
|
||||
s = str(v).strip()
|
||||
return s if s else d
|
||||
|
||||
items: List[Dict[str, Any]] = []
|
||||
for _, row in df.iterrows():
|
||||
items.append({
|
||||
"locationId": as_str(row[c_loc]),
|
||||
"warehouseId": as_str(row[c_wh]),
|
||||
"quantity": as_float(row[c_qty]),
|
||||
"x": as_int(row[c_x]),
|
||||
"y": as_int(row[c_y]),
|
||||
"z": as_int(row[c_z]),
|
||||
})
|
||||
|
||||
return self._post_lims("/api/lims/storage/auto-batch-out-bound", items)
|
||||
|
||||
# 2.14 新建实验
|
||||
def create_orders(self, xlsx_path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
从 Excel 解析并创建实验(2.14)
|
||||
约定:
|
||||
- batchId = Excel 文件名(不含扩展名)
|
||||
- 物料列:所有以 "(g)" 结尾(不再读取“总质量(g)”列)
|
||||
- totalMass 自动计算为所有物料质量之和
|
||||
- createTime 缺失或为空时自动填充为当前日期(YYYY/M/D)
|
||||
"""
|
||||
path = Path(xlsx_path)
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"未找到 Excel 文件:{path}")
|
||||
|
||||
try:
|
||||
df = pd.read_excel(path, sheet_name=0, engine="openpyxl")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"读取 Excel 失败:{e}")
|
||||
|
||||
# 列名容错:返回可选列名,找不到则返回 None
|
||||
def _pick(col_names: List[str]) -> Optional[str]:
|
||||
for c in col_names:
|
||||
if c in df.columns:
|
||||
return c
|
||||
return None
|
||||
|
||||
col_order_name = _pick(["配方ID", "orderName", "订单编号"])
|
||||
col_create_time = _pick(["创建日期", "createTime"])
|
||||
col_bottle_type = _pick(["配液瓶类型", "bottleType"])
|
||||
col_mix_time = _pick(["混匀时间(s)", "mixTime"])
|
||||
col_load = _pick(["扣电组装分液体积", "loadSheddingInfo"])
|
||||
col_pouch = _pick(["软包组装分液体积", "pouchCellInfo"])
|
||||
col_cond = _pick(["电导测试分液体积", "conductivityInfo"])
|
||||
col_cond_cnt = _pick(["电导测试分液瓶数", "conductivityBottleCount"])
|
||||
|
||||
# 物料列:所有以 (g) 结尾
|
||||
material_cols = [c for c in df.columns if isinstance(c, str) and c.endswith("(g)")]
|
||||
if not material_cols:
|
||||
raise KeyError("未发现任何以“(g)”结尾的物料列,请检查表头。")
|
||||
|
||||
batch_id = path.stem
|
||||
|
||||
def _to_ymd_slash(v) -> str:
|
||||
# 统一为 "YYYY/M/D";为空或解析失败则用当前日期
|
||||
if v is None or (isinstance(v, float) and pd.isna(v)) or str(v).strip() == "":
|
||||
ts = datetime.now()
|
||||
else:
|
||||
try:
|
||||
ts = pd.to_datetime(v)
|
||||
except Exception:
|
||||
ts = datetime.now()
|
||||
return f"{ts.year}/{ts.month}/{ts.day}"
|
||||
|
||||
def _as_int(val, default=0) -> int:
|
||||
try:
|
||||
if pd.isna(val):
|
||||
return default
|
||||
return int(val)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
def _as_str(val, default="") -> str:
|
||||
if val is None or (isinstance(val, float) and pd.isna(val)):
|
||||
return default
|
||||
s = str(val).strip()
|
||||
return s if s else default
|
||||
|
||||
orders: List[Dict[str, Any]] = []
|
||||
|
||||
for idx, row in df.iterrows():
|
||||
mats: List[Dict[str, Any]] = []
|
||||
total_mass = 0.0
|
||||
|
||||
for mcol in material_cols:
|
||||
val = row.get(mcol, None)
|
||||
if val is None or (isinstance(val, float) and pd.isna(val)):
|
||||
continue
|
||||
try:
|
||||
mass = float(val)
|
||||
except Exception:
|
||||
continue
|
||||
if mass > 0:
|
||||
mats.append({"name": mcol.replace("(g)", ""), "mass": mass})
|
||||
total_mass += mass
|
||||
|
||||
order_data = {
|
||||
"batchId": batch_id,
|
||||
"orderName": _as_str(row[col_order_name], default=f"{batch_id}_order_{idx+1}") if col_order_name else f"{batch_id}_order_{idx+1}",
|
||||
"createTime": _to_ymd_slash(row[col_create_time]) if col_create_time else _to_ymd_slash(None),
|
||||
"bottleType": _as_str(row[col_bottle_type], default="配液小瓶") if col_bottle_type else "配液小瓶",
|
||||
"mixTime": _as_int(row[col_mix_time]) if col_mix_time else 0,
|
||||
"loadSheddingInfo": _as_int(row[col_load]) if col_load else 0,
|
||||
"pouchCellInfo": _as_int(row[col_pouch]) if col_pouch else 0,
|
||||
"conductivityInfo": _as_int(row[col_cond]) if col_cond else 0,
|
||||
"conductivityBottleCount": _as_int(row[col_cond_cnt]) if col_cond_cnt else 0,
|
||||
"materialInfos": mats,
|
||||
"totalMass": round(total_mass, 4) # 自动汇总
|
||||
}
|
||||
orders.append(order_data)
|
||||
|
||||
# print(orders)
|
||||
|
||||
response = self._post_lims("/api/lims/order/orders", orders)
|
||||
print(response["data"])
|
||||
|
||||
self.order_status[response["data"][0]["orderCode"]] = "running"
|
||||
|
||||
while True:
|
||||
time.sleep(5)
|
||||
if self.order_status.get(response["data"][0]["orderCode"], None) == "finished":
|
||||
break
|
||||
return response
|
||||
|
||||
|
||||
# 2.7 启动调度
|
||||
def scheduler_start(self) -> Dict[str, Any]:
|
||||
return self._post_lims("/api/lims/scheduler/start")
|
||||
# 3.10 停止调度
|
||||
def scheduler_stop(self) -> Dict[str, Any]:
|
||||
"""
|
||||
停止调度 (3.10)
|
||||
请求体只包含 apiKey 和 requestTime
|
||||
"""
|
||||
return self._post_lims("/api/lims/scheduler/stop")
|
||||
# 2.9 继续调度
|
||||
def scheduler_continue(self) -> Dict[str, Any]:
|
||||
"""
|
||||
继续调度 (2.9)
|
||||
请求体只包含 apiKey 和 requestTime
|
||||
"""
|
||||
return self._post_lims("/api/lims/scheduler/continue")
|
||||
|
||||
|
||||
|
||||
# 2.24 物料变更推送
|
||||
def report_material_change(self, material_obj: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
material_obj 按 2.24 的裸对象格式(包含 id/typeName/locations/detail 等)
|
||||
"""
|
||||
return self._post_report_raw("/report/material_change", material_obj)
|
||||
|
||||
# 2.21 步骤完成推送(BS → LIMS)
|
||||
def report_step_finish(self,
|
||||
order_code: str,
|
||||
order_name: str,
|
||||
step_name: str,
|
||||
step_id: str,
|
||||
sample_id: str,
|
||||
start_time: str,
|
||||
end_time: str,
|
||||
execution_status: str = "completed") -> Dict[str, Any]:
|
||||
data = {
|
||||
"orderCode": order_code,
|
||||
"orderName": order_name,
|
||||
"stepName": step_name,
|
||||
"stepId": step_id,
|
||||
"sampleId": sample_id,
|
||||
"startTime": start_time,
|
||||
"endTime": end_time,
|
||||
"executionStatus": execution_status
|
||||
}
|
||||
return self._post_report("/report/step_finish", data)
|
||||
|
||||
# 2.23 订单完成推送(BS → LIMS)
|
||||
def report_order_finish(self,
|
||||
order_code: str,
|
||||
order_name: str,
|
||||
start_time: str,
|
||||
end_time: str,
|
||||
status: str = "30", # 30 完成 / -11 异常停止 / -12 人工停止
|
||||
workflow_status: str = "Finished",
|
||||
completion_time: Optional[str] = None,
|
||||
used_materials: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
|
||||
data = {
|
||||
"orderCode": order_code,
|
||||
"orderName": order_name,
|
||||
"startTime": start_time,
|
||||
"endTime": end_time,
|
||||
"status": status,
|
||||
"workflowStatus": workflow_status,
|
||||
"completionTime": completion_time or end_time,
|
||||
"usedMaterials": used_materials or []
|
||||
}
|
||||
return self._post_report("/report/order_finish", data)
|
||||
|
||||
# 2.5 批量查询实验报告(用于轮询是否完成)
|
||||
def order_list(self,
|
||||
status: Optional[str] = None,
|
||||
begin_time: Optional[str] = None,
|
||||
end_time: Optional[str] = None,
|
||||
filter_text: Optional[str] = None,
|
||||
skip: int = 0, page: int = 10) -> Dict[str, Any]:
|
||||
data: Dict[str, Any] = {"skipCount": skip, "pageCount": page}
|
||||
if status is not None: # 80 成功 / 90 失败 / 100 执行中
|
||||
data["status"] = status
|
||||
if begin_time:
|
||||
data["timeType"] = "CreationTime"
|
||||
data["beginTime"] = begin_time
|
||||
if end_time:
|
||||
data["endTime"] = end_time
|
||||
if filter_text:
|
||||
data["filter"] = filter_text
|
||||
return self._post_lims("/api/lims/order/order-list", data)
|
||||
|
||||
# 2.6 实验报告查询(根据任务ID拿详情)
|
||||
def order_report(self, order_id: str) -> Dict[str, Any]:
|
||||
return self._post_lims("/api/lims/order/order-report", order_id)
|
||||
|
||||
# 2.32 3-2-1 物料转运
|
||||
def transfer_3_to_2_to_1(self,
|
||||
# source_wh_id: Optional[str] = None,
|
||||
source_wh_id: Optional[str] = '3a19debc-84b4-0359-e2d4-b3beea49348b',
|
||||
source_x: int = 1, source_y: int = 1, source_z: int = 1) -> Dict[str, Any]:
|
||||
payload: Dict[str, Any] = {
|
||||
"sourcePosX": source_x, "sourcePosY": source_y, "sourcePosZ": source_z
|
||||
}
|
||||
if source_wh_id:
|
||||
payload["sourceWHID"] = source_wh_id
|
||||
return self._post_lims("/api/lims/order/transfer-task3To2To1", payload)
|
||||
|
||||
# 2.28 样品/废料取出
|
||||
def take_out(self,
|
||||
order_id: str,
|
||||
preintake_ids: Optional[List[str]] = None,
|
||||
material_ids: Optional[List[str]] = None) -> Dict[str, Any]:
|
||||
data = {
|
||||
"orderId": order_id,
|
||||
"preintakeIds": preintake_ids or [],
|
||||
"materialIds": material_ids or []
|
||||
}
|
||||
return self._post_lims("/api/lims/order/take-out", data)
|
||||
|
||||
# --------(可选)占位方法:文档未定义的“1号站内部流程 / 1-2转运”--------
|
||||
def start_station1_internal_flow(self, **kwargs) -> None:
|
||||
logger.info("启动1号站内部流程(占位,按现场系统填充具体指令)")
|
||||
|
||||
|
||||
# 3.x 1→2 物料转运
|
||||
def transfer_1_to_2(self) -> Dict[str, Any]:
|
||||
"""
|
||||
1→2 物料转运
|
||||
URL: /api/lims/order/transfer-task1To2
|
||||
只需要 apiKey 和 requestTime
|
||||
"""
|
||||
return self._post_lims("/api/lims/order/transfer-task1To2")
|
||||
|
||||
|
||||
# -------------------- 整体编排 --------------------
|
||||
def run_full_workflow(self,
|
||||
inbound_items: List[Dict[str, str]],
|
||||
orders: List[Dict[str, Any]],
|
||||
poll_filter_code: Optional[str] = None,
|
||||
poll_timeout_s: int = 600,
|
||||
poll_interval_s: int = 5,
|
||||
transfer_source: Optional[Dict[str, Any]] = None,
|
||||
takeout_order_id: Optional[str] = None) -> None:
|
||||
"""
|
||||
一键串联:
|
||||
1) 入库 3-4 个物料 → 2) 新建实验 → 3) 启动调度
|
||||
运行中(如需):4) 物料变更推送 5) 步骤完成推送 6) 订单完成推送
|
||||
完成后:查询实验(2.5/2.6)→ 7) 3-2-1 转运 → 8) 1号站内部流程
|
||||
→ 9) 1-2 转运 → 10) 样品/废料取出
|
||||
"""
|
||||
# 1. 入库(多于1个就用批量接口 2.18)
|
||||
if len(inbound_items) == 1:
|
||||
r = self.storage_inbound(inbound_items[0]["materialId"], inbound_items[0]["locationId"])
|
||||
logger.info(f"单个入库结果: {r}")
|
||||
else:
|
||||
r = self.storage_batch_inbound(inbound_items)
|
||||
logger.info(f"批量入库结果: {r}")
|
||||
|
||||
# 2. 新建实验(2.14)
|
||||
r = self.create_orders(orders)
|
||||
logger.info(f"新建实验结果: {r}")
|
||||
|
||||
# 3. 启动调度(2.7)
|
||||
r = self.scheduler_start()
|
||||
logger.info(f"启动调度结果: {r}")
|
||||
|
||||
# —— 运行中各类推送(2.24 / 2.21 / 2.23),通常由实际任务驱动,这里提供调用方式 —— #
|
||||
# self.report_material_change({...})
|
||||
# self.report_step_finish(order_code="BSO...", order_name="配液分液", step_name="xxx", step_id="...", sample_id="...",
|
||||
# start_time=_iso_utc_now_ms(), end_time=_iso_utc_now_ms(), execution_status="completed")
|
||||
# self.report_order_finish(order_code="BSO...", order_name="配液分液", start_time="...", end_time=_iso_utc_now_ms())
|
||||
|
||||
# 完成后才能转运:用 2.5 批量查询配合 filter=任务编码 轮询到 status=80(成功)
|
||||
if poll_filter_code:
|
||||
import time
|
||||
deadline = time.time() + poll_timeout_s
|
||||
while time.time() < deadline:
|
||||
res = self.order_list(status="80", filter_text=poll_filter_code, page=5)
|
||||
if isinstance(res, dict) and res.get("data", {}).get("items"):
|
||||
logger.info(f"实验 {poll_filter_code} 已完成:{res['data']['items'][0]}")
|
||||
break
|
||||
time.sleep(poll_interval_s)
|
||||
else:
|
||||
logger.warning(f"等待实验 {poll_filter_code} 完成超时(未到 status=80)")
|
||||
|
||||
# 7. 启动 3-2-1 转运(2.32)
|
||||
if transfer_source:
|
||||
r = self.transfer_3_to_2_to_1(
|
||||
source_wh_id=transfer_source.get("sourceWHID"),
|
||||
source_x=transfer_source.get("sourcePosX", 1),
|
||||
source_y=transfer_source.get("sourcePosY", 1),
|
||||
source_z=transfer_source.get("sourcePosZ", 1),
|
||||
)
|
||||
logger.info(f"3-2-1 转运结果: {r}")
|
||||
|
||||
# 8. 1号站内部流程(占位)
|
||||
self.start_station1_internal_flow()
|
||||
|
||||
# 9. 1→2 转运(占位)
|
||||
self.transfer_1_to_2()
|
||||
|
||||
# 10. 样品/废料取出(2.28)
|
||||
if takeout_order_id:
|
||||
r = self.take_out(order_id=takeout_order_id)
|
||||
logger.info(f"样品/废料取出结果: {r}")
|
||||
|
||||
# 2.5 批量查询实验报告
|
||||
def order_list_v2(self,
|
||||
timeType: str = "string",
|
||||
beginTime: str = "",
|
||||
endTime: str = "",
|
||||
status: str = "",
|
||||
filter: str = "物料转移任务",
|
||||
skipCount: int = 0,
|
||||
pageCount: int = 1,
|
||||
sorting: str = "") -> Dict[str, Any]:
|
||||
"""
|
||||
批量查询实验报告的详细信息 (2.5)
|
||||
URL: /api/lims/order/order-list
|
||||
参数默认值和接口文档保持一致
|
||||
"""
|
||||
data: Dict[str, Any] = {
|
||||
"timeType": timeType,
|
||||
"beginTime": beginTime,
|
||||
"endTime": endTime,
|
||||
"status": status,
|
||||
"filter": filter,
|
||||
"skipCount": skipCount,
|
||||
"pageCount": pageCount,
|
||||
"sorting": sorting
|
||||
}
|
||||
return self._post_lims("/api/lims/order/order-list", data)
|
||||
|
||||
|
||||
def wait_for_transfer_task(self, timeout: int = 600, interval: int = 3) -> bool:
|
||||
"""
|
||||
轮询查询物料转移任务是否成功完成 (status=80)
|
||||
- timeout: 最大等待秒数 (默认600秒)
|
||||
- interval: 轮询间隔秒数 (默认3秒)
|
||||
返回 True 表示找到并成功完成,False 表示超时未找到
|
||||
"""
|
||||
now = datetime.now()
|
||||
beginTime = now.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
endTime = (now + timedelta(minutes=5)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
print(beginTime, endTime)
|
||||
|
||||
deadline = time.time() + timeout
|
||||
|
||||
while time.time() < deadline:
|
||||
result = self.order_list_v2(
|
||||
timeType="string",
|
||||
beginTime=beginTime,
|
||||
endTime=endTime,
|
||||
status="",
|
||||
filter="物料转移任务",
|
||||
skipCount=0,
|
||||
pageCount=1,
|
||||
sorting=""
|
||||
)
|
||||
print(result)
|
||||
|
||||
items = result.get("data", {}).get("items", [])
|
||||
for item in items:
|
||||
name = item.get("name", "")
|
||||
status = item.get("status")
|
||||
if name.startswith("物料转移任务") and status == 80:
|
||||
logger.info(f"硬件转移动作完成: {name}")
|
||||
return True
|
||||
|
||||
time.sleep(interval)
|
||||
|
||||
logger.warning("超时未找到成功的物料转移任务")
|
||||
return False
|
||||
|
||||
def wait_for_recent_task(self, timeout: int = 600, interval: int = 3) -> bool:
|
||||
"""
|
||||
轮询查询最近的任务是否成功完成 (status=80)
|
||||
- timeout: 最大等待秒数 (默认600秒)
|
||||
- interval: 轮询间隔秒数 (默认3秒)
|
||||
返回 True 表示找到并成功完成,False 表示超时未找到
|
||||
"""
|
||||
now = datetime.now()
|
||||
beginTime = now.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
endTime = (now + timedelta(minutes=5)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
print(beginTime, endTime)
|
||||
|
||||
deadline = time.time() + timeout
|
||||
|
||||
while time.time() < deadline:
|
||||
result = self.order_list_v2(
|
||||
timeType="string",
|
||||
beginTime=beginTime,
|
||||
endTime=endTime,
|
||||
status="",
|
||||
filter="",
|
||||
skipCount=0,
|
||||
pageCount=1,
|
||||
sorting=""
|
||||
)
|
||||
print(result)
|
||||
|
||||
items = result.get("data", {}).get("items", [])
|
||||
for item in items:
|
||||
name = item.get("name", "")
|
||||
status = item.get("status")
|
||||
if name.startswith("物料转移任务") and status == 80:
|
||||
logger.info(f"硬件转移动作完成: {name}")
|
||||
return True
|
||||
|
||||
time.sleep(interval)
|
||||
|
||||
logger.warning("超时未找到成功的任务")
|
||||
return False
|
||||
|
||||
# --------------------------------
|
||||
if __name__ == "__main__":
|
||||
ws = BioyondWorkstation()
|
||||
ws.scheduler_stop()
|
||||
ws.scheduler_start()
|
||||
#物料入库
|
||||
r1 = ws.auto_feeding4to3_from_xlsx(r"D:\Uni-lab\Uni-Lab-OS\unilabos\devices\workstation\bioyond_cell\样品导入模板.xlsx")
|
||||
# print(r1)
|
||||
# print("物料入库任务已提交0")
|
||||
# #等待任务完成
|
||||
# ws.wait_for_transfer_task()
|
||||
#
|
||||
# print("物料入库任务已完成1")
|
||||
#
|
||||
# #新建实验
|
||||
# res = ws.create_orders(r"D:\Uni-lab\Uni-Lab-OS\unilabos\devices\workstation\bioyond_cell\2025092501.xlsx")
|
||||
# print(res)
|
||||
# #等待任务完成
|
||||
# ws.wait_for_recent_task()
|
||||
# print("配液任务已完成")
|
||||
#
|
||||
# #新建3-2-1转运任务
|
||||
# r321 = ws.transfer_3_to_2_to_1()
|
||||
# print(r321)
|
||||
# #等待任务完成
|
||||
# ws.wait_for_recent_task()
|
||||
#
|
||||
# ws.transfer_1_to_2()
|
||||
# #等待任务完成
|
||||
# ws.wait_for_recent_task()
|
||||
|
||||
|
||||
#ws._start_http_service_bg()
|
||||
# ws.scheduler_stop()
|
||||
#ws.scheduler_start()
|
||||
# ws.scheduler_continue()
|
||||
# 3.30 上料:读取模板 Excel 自动解析并 POST
|
||||
# r1 = ws.auto_feeding4to3_from_xlsx(r"C:\ML\GitHub\Uni-Lab-OS\unilabos\devices\workstation\bioyond_cell\样品导入模板 (8).xlsx")
|
||||
#ws.wait_for_transfer_task()
|
||||
#print("转运物料转移任务已完成")
|
||||
|
||||
# ws.scheduler_start()
|
||||
# print(r1["payload"]["data"]) # 调试模式下可直接看到要发的 JSON items
|
||||
|
||||
# 新建实验
|
||||
# res = ws.create_orders("C:/ML/GitHub/Uni-Lab-OS/unilabos/devices/workstation/bioyond_cell/2025092501.xlsx")
|
||||
# ws.scheduler_start()
|
||||
# print(res)
|
||||
# r321 = ws.transfer_3_to_2_to_1()
|
||||
|
||||
|
||||
# 3.31 下料:同理
|
||||
# r2 = ws.auto_batch_outbound_from_xlsx(r"C:/path/样品导入模板 (8).xlsx")
|
||||
# print(r2["payload"]["data"])
|
||||
|
||||
# r321 = ws.transfer_3_to_2_to_1()
|
||||
# ws.transfer_1_to_2()
|
||||
|
||||
|
||||
@@ -0,0 +1,644 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime, timezone
|
||||
import requests
|
||||
from pathlib import Path
|
||||
import pandas as pd
|
||||
import time
|
||||
import threading
|
||||
from unilabos.devices.workstation.workstation_base import WorkstationBase
|
||||
from unilabos.devices.workstation.workstation_http_service import WorkstationHTTPService
|
||||
from unilabos.utils.log import logger
|
||||
from pylabrobot.resources.deck import Deck
|
||||
from .benyao_test import test_benyao_api
|
||||
|
||||
def _iso_utc_now_ms() -> str:
|
||||
# 文档要求:到毫秒 + Z,例如 2025-08-15T05:43:22.814Z
|
||||
dt = datetime.now(timezone.utc)
|
||||
return dt.strftime("%Y-%m-%dT%H:%M:%S.") + f"{int(dt.microsecond/1000):03d}Z"
|
||||
|
||||
|
||||
class BioyondWorkstation(WorkstationBase):
|
||||
"""
|
||||
集成 Bioyond LIMS 的工作站示例,
|
||||
覆盖:入库(2.17/2.18) → 新建实验(2.14) → 启动调度(2.7) →
|
||||
运行中推送:物料变更(2.24)、步骤完成(2.21)、订单完成(2.23) →
|
||||
查询实验(2.5/2.6) → 3-2-1 转运(2.32) → 样品/废料取出(2.28)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bioyond_config: Optional[Dict[str, Any]] = None,
|
||||
station_resource: Optional[Dict[str, Any]] = None,
|
||||
debug_mode: bool = False, # 增加调试模式开关
|
||||
*args, **kwargs,
|
||||
):
|
||||
self.bioyond_config = bioyond_config or {
|
||||
"base_url": "http://192.168.1.200:44386",
|
||||
"api_key": "8A819E5C",
|
||||
"timeout": 30,
|
||||
"report_token": "CHANGE_ME_TOKEN"
|
||||
}
|
||||
|
||||
|
||||
self.debug_mode = debug_mode
|
||||
super().__init__(deck=Deck, station_resource=station_resource, *args, **kwargs)
|
||||
logger.info(f"Bioyond工作站初始化完成 (debug_mode={self.debug_mode})")
|
||||
|
||||
# 实例化并在后台线程启动 HTTP 报送服务
|
||||
self.order_status = {}
|
||||
try:
|
||||
t = threading.Thread(target=self._start_http_service_bg, daemon=True, name="unilab_http")
|
||||
t.start()
|
||||
except Exception as e:
|
||||
logger.error(f"unilab-server后台启动报送服务失败: {e}")
|
||||
|
||||
@property
|
||||
def device_id(self) -> str:
|
||||
try:
|
||||
return getattr(self, "_ros_node").device_id # 兼容 ROS 场景
|
||||
except Exception:
|
||||
return "bioyond_workstation"
|
||||
|
||||
def _start_http_service_bg(self, host: str = "127.0.0.1", port: int = 8080) -> None:
|
||||
try:
|
||||
self.service = WorkstationHTTPService(self, host=host, port=port)
|
||||
self.service.start()
|
||||
except Exception as e:
|
||||
logger.error(f"启动HTTP服务失败: {e}")
|
||||
|
||||
# -------------------- 基础HTTP封装 --------------------
|
||||
def _url(self, path: str) -> str:
|
||||
return f"{self.bioyond_config['base_url'].rstrip('/')}/{path.lstrip('/')}"
|
||||
|
||||
def _post_lims(self, path: str, data: Optional[Any] = None) -> Dict[str, Any]:
|
||||
"""LIMS API:大多数接口用 {apiKey/requestTime,data} 包装"""
|
||||
payload = {
|
||||
"apiKey": self.bioyond_config["api_key"],
|
||||
"requestTime": _iso_utc_now_ms()
|
||||
}
|
||||
if data is not None:
|
||||
payload["data"] = data
|
||||
|
||||
if self.debug_mode:
|
||||
# 模拟返回,不发真实请求
|
||||
logger.info(f"[DEBUG] POST {path} with payload={payload}")
|
||||
return {"debug": True, "url": self._url(path), "payload": payload, "status": "ok"}
|
||||
|
||||
try:
|
||||
r = requests.post(
|
||||
self._url(path),
|
||||
json=payload,
|
||||
timeout=self.bioyond_config.get("timeout", 30),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
logger.error(f"POST {path} 失败: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
# --- 修正:_post_report / _post_report_raw 同样走 debug_mode ---
|
||||
def _post_report(self, path: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
payload = {
|
||||
"token": self.bioyond_config.get("report_token", ""),
|
||||
"request_time": _iso_utc_now_ms(),
|
||||
"data": data
|
||||
}
|
||||
if self.debug_mode:
|
||||
logger.info(f"[DEBUG] POST {path} with payload={payload}")
|
||||
return {"debug": True, "url": self._url(path), "payload": payload, "status": "ok"}
|
||||
try:
|
||||
r = requests.post(self._url(path), json=payload,
|
||||
timeout=self.bioyond_config.get("timeout", 30),
|
||||
headers={"Content-Type": "application/json"})
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
logger.error(f"POST {path} 失败: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
def _post_report_raw(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if self.debug_mode:
|
||||
logger.info(f"[DEBUG] POST {path} with body={body}")
|
||||
return {"debug": True, "url": self._url(path), "payload": body, "status": "ok"}
|
||||
try:
|
||||
r = requests.post(self._url(path), json=body,
|
||||
timeout=self.bioyond_config.get("timeout", 30),
|
||||
headers={"Content-Type": "application/json"})
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
logger.error(f"POST {path} 失败: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
# -------------------- 单点接口封装 --------------------
|
||||
# 2.17 入库物料(单个)
|
||||
def storage_inbound(self, material_id: str, location_id: str) -> Dict[str, Any]:
|
||||
return self._post_lims("/api/lims/storage/inbound", {
|
||||
"materialId": material_id,
|
||||
"locationId": location_id
|
||||
})
|
||||
|
||||
# 2.18 批量入库(多个)
|
||||
def storage_batch_inbound(self, items: List[Dict[str, str]]) -> Dict[str, Any]:
|
||||
"""
|
||||
items = [{"materialId": "...", "locationId": "..."}, ...]
|
||||
"""
|
||||
return self._post_lims("/api/lims/storage/batch-inbound", items)
|
||||
|
||||
# 3.30 自动化上料(Excel -> JSON -> POST /api/lims/order/auto-feeding4to3)
|
||||
def auto_feeding4to3_from_xlsx(self, xlsx_path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
根据固定模板解析 Excel:
|
||||
- 四号手套箱加样头面 (2-13行, 3-7列)
|
||||
- 四号手套箱原液瓶面 (15-23行, 3-9列)
|
||||
- 三号手套箱人工堆栈 (26-40行, 3-7列)
|
||||
"""
|
||||
path = Path(xlsx_path)
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"未找到 Excel 文件:{path}")
|
||||
|
||||
try:
|
||||
df = pd.read_excel(path, sheet_name=0, header=None, engine="openpyxl")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"读取 Excel 失败:{e}")
|
||||
|
||||
items: List[Dict[str, Any]] = []
|
||||
|
||||
# 四号手套箱 - 加样头面(2-13行, 3-7列)
|
||||
for _, row in df.iloc[1:13, 2:7].iterrows():
|
||||
item = {
|
||||
"sourceWHName": "四号手套箱堆栈",
|
||||
"posX": int(row[2]),
|
||||
"posY": int(row[3]),
|
||||
"posZ": int(row[4]),
|
||||
"materialName": str(row[5]).strip() if pd.notna(row[5]) else "",
|
||||
"quantity": float(row[6]) if pd.notna(row[6]) else 0.0,
|
||||
}
|
||||
if item["materialName"]:
|
||||
items.append(item)
|
||||
|
||||
# 四号手套箱 - 原液瓶面(15-23行, 3-9列)
|
||||
for _, row in df.iloc[14:23, 2:9].iterrows():
|
||||
item = {
|
||||
"sourceWHName": "四号手套箱堆栈",
|
||||
"posX": int(row[2]),
|
||||
"posY": int(row[3]),
|
||||
"posZ": int(row[4]),
|
||||
"materialName": str(row[5]).strip() if pd.notna(row[5]) else "",
|
||||
"quantity": float(row[6]) if pd.notna(row[6]) else 0.0,
|
||||
"materialType": str(row[7]).strip() if pd.notna(row[7]) else "",
|
||||
"targetWH": str(row[8]).strip() if pd.notna(row[8]) else "",
|
||||
}
|
||||
if item["materialName"]:
|
||||
items.append(item)
|
||||
|
||||
# 三号手套箱人工堆栈(26-40行, 3-7列)
|
||||
for _, row in df.iloc[25:40, 2:7].iterrows():
|
||||
item = {
|
||||
"sourceWHName": "三号手套箱人工堆栈",
|
||||
"posX": int(row[2]),
|
||||
"posY": int(row[3]),
|
||||
"posZ": int(row[4]),
|
||||
"materialType": str(row[5]).strip() if pd.notna(row[5]) else "",
|
||||
"materialId": str(row[6]).strip() if pd.notna(row[6]) else "",
|
||||
"quantity": 1 # 默认数量1
|
||||
}
|
||||
if item["materialId"] or item["materialType"]:
|
||||
items.append(item)
|
||||
|
||||
return self._post_lims("/api/lims/order/auto-feeding4to3", items)
|
||||
|
||||
|
||||
|
||||
def auto_batch_outbound_from_xlsx(self, xlsx_path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
3.31 自动化下料(Excel -> JSON -> POST /api/lims/storage/auto-batch-out-bound)
|
||||
"""
|
||||
path = Path(xlsx_path)
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"未找到 Excel 文件:{path}")
|
||||
|
||||
try:
|
||||
df = pd.read_excel(path, sheet_name=0, engine="openpyxl")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"读取 Excel 失败:{e}")
|
||||
|
||||
def pick(names: List[str]) -> Optional[str]:
|
||||
for n in names:
|
||||
if n in df.columns:
|
||||
return n
|
||||
return None
|
||||
|
||||
c_loc = pick(["locationId", "库位ID", "库位Id", "库位id"])
|
||||
c_wh = pick(["warehouseId", "仓库ID", "仓库Id", "仓库id"])
|
||||
c_qty = pick(["数量", "quantity"])
|
||||
c_x = pick(["x", "X", "posX", "坐标X"])
|
||||
c_y = pick(["y", "Y", "posY", "坐标Y"])
|
||||
c_z = pick(["z", "Z", "posZ", "坐标Z"])
|
||||
|
||||
required = [c_loc, c_wh, c_qty, c_x, c_y, c_z]
|
||||
if any(c is None for c in required):
|
||||
raise KeyError("Excel 缺少必要列:locationId/warehouseId/数量/x/y/z(支持多别名,至少要能匹配到)。")
|
||||
|
||||
def as_int(v, d=0):
|
||||
try:
|
||||
if pd.isna(v): return d
|
||||
return int(v)
|
||||
except Exception:
|
||||
try:
|
||||
return int(float(v))
|
||||
except Exception:
|
||||
return d
|
||||
|
||||
def as_float(v, d=0.0):
|
||||
try:
|
||||
if pd.isna(v): return d
|
||||
return float(v)
|
||||
except Exception:
|
||||
return d
|
||||
|
||||
def as_str(v, d=""):
|
||||
if v is None or (isinstance(v, float) and pd.isna(v)): return d
|
||||
s = str(v).strip()
|
||||
return s if s else d
|
||||
|
||||
items: List[Dict[str, Any]] = []
|
||||
for _, row in df.iterrows():
|
||||
items.append({
|
||||
"locationId": as_str(row[c_loc]),
|
||||
"warehouseId": as_str(row[c_wh]),
|
||||
"quantity": as_float(row[c_qty]),
|
||||
"x": as_int(row[c_x]),
|
||||
"y": as_int(row[c_y]),
|
||||
"z": as_int(row[c_z]),
|
||||
})
|
||||
|
||||
return self._post_lims("/api/lims/storage/auto-batch-out-bound", items)
|
||||
|
||||
# 2.14 新建实验
|
||||
def create_orders(self, xlsx_path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
从 Excel 解析并创建实验(2.14)
|
||||
约定:
|
||||
- batchId = Excel 文件名(不含扩展名)
|
||||
- 物料列:所有以 "(g)" 结尾(不再读取“总质量(g)”列)
|
||||
- totalMass 自动计算为所有物料质量之和
|
||||
- createTime 缺失或为空时自动填充为当前日期(YYYY/M/D)
|
||||
"""
|
||||
path = Path(xlsx_path)
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"未找到 Excel 文件:{path}")
|
||||
|
||||
try:
|
||||
df = pd.read_excel(path, sheet_name=0, engine="openpyxl")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"读取 Excel 失败:{e}")
|
||||
|
||||
# 列名容错:返回可选列名,找不到则返回 None
|
||||
def _pick(col_names: List[str]) -> Optional[str]:
|
||||
for c in col_names:
|
||||
if c in df.columns:
|
||||
return c
|
||||
return None
|
||||
|
||||
col_order_name = _pick(["配方ID", "orderName", "订单编号"])
|
||||
col_create_time = _pick(["创建日期", "createTime"])
|
||||
col_bottle_type = _pick(["配液瓶类型", "bottleType"])
|
||||
col_mix_time = _pick(["混匀时间(s)", "mixTime"])
|
||||
col_load = _pick(["扣电组装分液体积", "loadSheddingInfo"])
|
||||
col_pouch = _pick(["软包组装分液体积", "pouchCellInfo"])
|
||||
col_cond = _pick(["电导测试分液体积", "conductivityInfo"])
|
||||
col_cond_cnt = _pick(["电导测试分液瓶数", "conductivityBottleCount"])
|
||||
|
||||
# 物料列:所有以 (g) 结尾
|
||||
material_cols = [c for c in df.columns if isinstance(c, str) and c.endswith("(g)")]
|
||||
if not material_cols:
|
||||
raise KeyError("未发现任何以“(g)”结尾的物料列,请检查表头。")
|
||||
|
||||
batch_id = path.stem
|
||||
|
||||
def _to_ymd_slash(v) -> str:
|
||||
# 统一为 "YYYY/M/D";为空或解析失败则用当前日期
|
||||
if v is None or (isinstance(v, float) and pd.isna(v)) or str(v).strip() == "":
|
||||
ts = datetime.now()
|
||||
else:
|
||||
try:
|
||||
ts = pd.to_datetime(v)
|
||||
except Exception:
|
||||
ts = datetime.now()
|
||||
return f"{ts.year}/{ts.month}/{ts.day}"
|
||||
|
||||
def _as_int(val, default=0) -> int:
|
||||
try:
|
||||
if pd.isna(val):
|
||||
return default
|
||||
return int(val)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
def _as_str(val, default="") -> str:
|
||||
if val is None or (isinstance(val, float) and pd.isna(val)):
|
||||
return default
|
||||
s = str(val).strip()
|
||||
return s if s else default
|
||||
|
||||
orders: List[Dict[str, Any]] = []
|
||||
|
||||
for idx, row in df.iterrows():
|
||||
mats: List[Dict[str, Any]] = []
|
||||
total_mass = 0.0
|
||||
|
||||
for mcol in material_cols:
|
||||
val = row.get(mcol, None)
|
||||
if val is None or (isinstance(val, float) and pd.isna(val)):
|
||||
continue
|
||||
try:
|
||||
mass = float(val)
|
||||
except Exception:
|
||||
continue
|
||||
if mass > 0:
|
||||
mats.append({"name": mcol.replace("(g)", ""), "mass": mass})
|
||||
total_mass += mass
|
||||
|
||||
order_data = {
|
||||
"batchId": batch_id,
|
||||
"orderName": _as_str(row[col_order_name], default=f"{batch_id}_order_{idx+1}") if col_order_name else f"{batch_id}_order_{idx+1}",
|
||||
"createTime": _to_ymd_slash(row[col_create_time]) if col_create_time else _to_ymd_slash(None),
|
||||
"bottleType": _as_str(row[col_bottle_type], default="配液小瓶") if col_bottle_type else "配液小瓶",
|
||||
"mixTime": _as_int(row[col_mix_time]) if col_mix_time else 0,
|
||||
"loadSheddingInfo": _as_int(row[col_load]) if col_load else 0,
|
||||
"pouchCellInfo": _as_int(row[col_pouch]) if col_pouch else 0,
|
||||
"conductivityInfo": _as_int(row[col_cond]) if col_cond else 0,
|
||||
"conductivityBottleCount": _as_int(row[col_cond_cnt]) if col_cond_cnt else 0,
|
||||
"materialInfos": mats,
|
||||
"totalMass": round(total_mass, 4) # 自动汇总
|
||||
}
|
||||
orders.append(order_data)
|
||||
|
||||
# print(orders)
|
||||
|
||||
response = self._post_lims("/api/lims/order/orders", orders)
|
||||
self.order_status[response["data"]["orderCode"]] = "running"
|
||||
|
||||
while True:
|
||||
time.sleep(5)
|
||||
if self.order_status.get(response["data"]["orderCode"], None) == "finished":
|
||||
break
|
||||
return response
|
||||
|
||||
|
||||
# 2.7 启动调度
|
||||
def scheduler_start(self) -> Dict[str, Any]:
|
||||
return self._post_lims("/api/lims/scheduler/start")
|
||||
# 3.10 停止调度
|
||||
def scheduler_stop(self) -> Dict[str, Any]:
|
||||
"""
|
||||
停止调度 (3.10)
|
||||
请求体只包含 apiKey 和 requestTime
|
||||
"""
|
||||
return self._post_lims("/api/lims/scheduler/stop")
|
||||
|
||||
|
||||
# 2.24 物料变更推送
|
||||
def report_material_change(self, material_obj: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
material_obj 按 2.24 的裸对象格式(包含 id/typeName/locations/detail 等)
|
||||
"""
|
||||
return self._post_report_raw("/report/material_change", material_obj)
|
||||
|
||||
# 2.21 步骤完成推送(BS → LIMS)
|
||||
def report_step_finish(self,
|
||||
order_code: str,
|
||||
order_name: str,
|
||||
step_name: str,
|
||||
step_id: str,
|
||||
sample_id: str,
|
||||
start_time: str,
|
||||
end_time: str,
|
||||
execution_status: str = "completed") -> Dict[str, Any]:
|
||||
data = {
|
||||
"orderCode": order_code,
|
||||
"orderName": order_name,
|
||||
"stepName": step_name,
|
||||
"stepId": step_id,
|
||||
"sampleId": sample_id,
|
||||
"startTime": start_time,
|
||||
"endTime": end_time,
|
||||
"executionStatus": execution_status
|
||||
}
|
||||
return self._post_report("/report/step_finish", data)
|
||||
|
||||
# 2.23 订单完成推送(BS → LIMS)
|
||||
def report_order_finish(self,
|
||||
order_code: str,
|
||||
order_name: str,
|
||||
start_time: str,
|
||||
end_time: str,
|
||||
status: str = "30", # 30 完成 / -11 异常停止 / -12 人工停止
|
||||
workflow_status: str = "Finished",
|
||||
completion_time: Optional[str] = None,
|
||||
used_materials: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
|
||||
data = {
|
||||
"orderCode": order_code,
|
||||
"orderName": order_name,
|
||||
"startTime": start_time,
|
||||
"endTime": end_time,
|
||||
"status": status,
|
||||
"workflowStatus": workflow_status,
|
||||
"completionTime": completion_time or end_time,
|
||||
"usedMaterials": used_materials or []
|
||||
}
|
||||
return self._post_report("/report/order_finish", data)
|
||||
|
||||
# 2.5 批量查询实验报告(用于轮询是否完成)
|
||||
def order_list(self,
|
||||
status: Optional[str] = None,
|
||||
begin_time: Optional[str] = None,
|
||||
end_time: Optional[str] = None,
|
||||
filter_text: Optional[str] = None,
|
||||
skip: int = 0, page: int = 10) -> Dict[str, Any]:
|
||||
data: Dict[str, Any] = {"skipCount": skip, "pageCount": page}
|
||||
if status is not None: # 80 成功 / 90 失败 / 100 执行中
|
||||
data["status"] = status
|
||||
if begin_time:
|
||||
data["timeType"] = "CreationTime"
|
||||
data["beginTime"] = begin_time
|
||||
if end_time:
|
||||
data["endTime"] = end_time
|
||||
if filter_text:
|
||||
data["filter"] = filter_text
|
||||
return self._post_lims("/api/lims/order/order-list", data)
|
||||
|
||||
# 2.6 实验报告查询(根据任务ID拿详情)
|
||||
def order_report(self, order_id: str) -> Dict[str, Any]:
|
||||
return self._post_lims("/api/lims/order/order-report", order_id)
|
||||
|
||||
# 2.32 3-2-1 物料转运
|
||||
def transfer_3_to_2_to_1(self,
|
||||
# source_wh_id: Optional[str] = None,
|
||||
source_wh_id: Optional[str] = '3a19debc-84b4-0359-e2d4-b3beea49348b',
|
||||
source_x: int = 1, source_y: int = 1, source_z: int = 1) -> Dict[str, Any]:
|
||||
payload: Dict[str, Any] = {
|
||||
"sourcePosX": source_x, "sourcePosY": source_y, "sourcePosZ": source_z
|
||||
}
|
||||
if source_wh_id:
|
||||
payload["sourceWHID"] = source_wh_id
|
||||
return self._post_lims("/api/lims/order/transfer-task3To2To1", payload)
|
||||
|
||||
# 2.28 样品/废料取出
|
||||
def take_out(self,
|
||||
order_id: str,
|
||||
preintake_ids: Optional[List[str]] = None,
|
||||
material_ids: Optional[List[str]] = None) -> Dict[str, Any]:
|
||||
data = {
|
||||
"orderId": order_id,
|
||||
"preintakeIds": preintake_ids or [],
|
||||
"materialIds": material_ids or []
|
||||
}
|
||||
return self._post_lims("/api/lims/order/take-out", data)
|
||||
|
||||
# --------(可选)占位方法:文档未定义的“1号站内部流程 / 1-2转运”--------
|
||||
def start_station1_internal_flow(self, **kwargs) -> None:
|
||||
logger.info("启动1号站内部流程(占位,按现场系统填充具体指令)")
|
||||
|
||||
|
||||
# 3.x 1→2 物料转运
|
||||
def transfer_1_to_2(self) -> Dict[str, Any]:
|
||||
"""
|
||||
1→2 物料转运
|
||||
URL: /api/lims/order/transfer-task1To2
|
||||
只需要 apiKey 和 requestTime
|
||||
"""
|
||||
return self._post_lims("/api/lims/order/transfer-task1To2")
|
||||
|
||||
|
||||
# -------------------- 整体编排 --------------------
|
||||
def run_full_workflow(self,
|
||||
inbound_items: List[Dict[str, str]],
|
||||
orders: List[Dict[str, Any]],
|
||||
poll_filter_code: Optional[str] = None,
|
||||
poll_timeout_s: int = 600,
|
||||
poll_interval_s: int = 5,
|
||||
transfer_source: Optional[Dict[str, Any]] = None,
|
||||
takeout_order_id: Optional[str] = None) -> None:
|
||||
"""
|
||||
一键串联:
|
||||
1) 入库 3-4 个物料 → 2) 新建实验 → 3) 启动调度
|
||||
运行中(如需):4) 物料变更推送 5) 步骤完成推送 6) 订单完成推送
|
||||
完成后:查询实验(2.5/2.6)→ 7) 3-2-1 转运 → 8) 1号站内部流程
|
||||
→ 9) 1-2 转运 → 10) 样品/废料取出
|
||||
"""
|
||||
# 1. 入库(多于1个就用批量接口 2.18)
|
||||
if len(inbound_items) == 1:
|
||||
r = self.storage_inbound(inbound_items[0]["materialId"], inbound_items[0]["locationId"])
|
||||
logger.info(f"单个入库结果: {r}")
|
||||
else:
|
||||
r = self.storage_batch_inbound(inbound_items)
|
||||
logger.info(f"批量入库结果: {r}")
|
||||
|
||||
# 2. 新建实验(2.14)
|
||||
r = self.create_orders(orders)
|
||||
logger.info(f"新建实验结果: {r}")
|
||||
|
||||
# 3. 启动调度(2.7)
|
||||
r = self.scheduler_start()
|
||||
logger.info(f"启动调度结果: {r}")
|
||||
|
||||
# —— 运行中各类推送(2.24 / 2.21 / 2.23),通常由实际任务驱动,这里提供调用方式 —— #
|
||||
# self.report_material_change({...})
|
||||
# self.report_step_finish(order_code="BSO...", order_name="配液分液", step_name="xxx", step_id="...", sample_id="...",
|
||||
# start_time=_iso_utc_now_ms(), end_time=_iso_utc_now_ms(), execution_status="completed")
|
||||
# self.report_order_finish(order_code="BSO...", order_name="配液分液", start_time="...", end_time=_iso_utc_now_ms())
|
||||
|
||||
# 完成后才能转运:用 2.5 批量查询配合 filter=任务编码 轮询到 status=80(成功)
|
||||
if poll_filter_code:
|
||||
import time
|
||||
deadline = time.time() + poll_timeout_s
|
||||
while time.time() < deadline:
|
||||
res = self.order_list(status="80", filter_text=poll_filter_code, page=5)
|
||||
if isinstance(res, dict) and res.get("data", {}).get("items"):
|
||||
logger.info(f"实验 {poll_filter_code} 已完成:{res['data']['items'][0]}")
|
||||
break
|
||||
time.sleep(poll_interval_s)
|
||||
else:
|
||||
logger.warning(f"等待实验 {poll_filter_code} 完成超时(未到 status=80)")
|
||||
|
||||
# 7. 启动 3-2-1 转运(2.32)
|
||||
if transfer_source:
|
||||
r = self.transfer_3_to_2_to_1(
|
||||
source_wh_id=transfer_source.get("sourceWHID"),
|
||||
source_x=transfer_source.get("sourcePosX", 1),
|
||||
source_y=transfer_source.get("sourcePosY", 1),
|
||||
source_z=transfer_source.get("sourcePosZ", 1),
|
||||
)
|
||||
logger.info(f"3-2-1 转运结果: {r}")
|
||||
|
||||
# 8. 1号站内部流程(占位)
|
||||
self.start_station1_internal_flow()
|
||||
|
||||
# 9. 1→2 转运(占位)
|
||||
self.transfer_1_to_2()
|
||||
|
||||
# 10. 样品/废料取出(2.28)
|
||||
if takeout_order_id:
|
||||
r = self.take_out(order_id=takeout_order_id)
|
||||
logger.info(f"样品/废料取出结果: {r}")
|
||||
|
||||
|
||||
# 套接字服务端收到“步骤完成”时调用
|
||||
def process_step_finish_report(self, report_request):
|
||||
order_code = report_request.data.get("orderCode")
|
||||
if order_code:
|
||||
self.order_status[order_code] = "step_finished"
|
||||
logger.info(f"[REPORT] 订单 {order_code} 步骤完成")
|
||||
return {"ack": True}
|
||||
|
||||
def process_order_finish_report(self, report_request, used_materials=None):
|
||||
order_code = report_request.data.get("orderCode")
|
||||
if order_code:
|
||||
self.order_status[order_code] = "finished"
|
||||
logger.info(f"[REPORT] 订单 {order_code} 已完成,状态改为 finished")
|
||||
return {"ack": True, "usedMaterials": used_materials or []}
|
||||
|
||||
# 收到“通量完成”时调用
|
||||
def process_sample_finish_report(self, report_request):
|
||||
order_code = report_request.data.get("orderCode")
|
||||
if order_code:
|
||||
self.order_status[order_code] = "sample_finished"
|
||||
logger.info(f"[REPORT] 订单 {order_code} 通量完成")
|
||||
return {"ack": True}
|
||||
|
||||
def test_benyao_workstation(self, num1, num2):
|
||||
num1 = int(num1)
|
||||
num2 = int(num2)
|
||||
for i in range(num1):
|
||||
print(f"num1 = {num1}")
|
||||
for j in range(num2):
|
||||
print(f"num1 = {num2}")
|
||||
test_benyao_api()
|
||||
|
||||
|
||||
# --------------------------------
|
||||
if __name__ == "__main__":
|
||||
ws = BioyondWorkstation()
|
||||
# ws.scheduler_stop()
|
||||
# 3.30 上料:读取模板 Excel 自动解析并 POST
|
||||
# r1 = ws.auto_feeding4to3_from_xlsx(r"C:\ML\GitHub\Uni-Lab-OS\unilabos\devices\workstation\bioyond_cell\样品导入模板 (8).xlsx")
|
||||
# ws.scheduler_start()
|
||||
# print(r1["payload"]["data"]) # 调试模式下可直接看到要发的 JSON items
|
||||
|
||||
# 新建实验
|
||||
# res = ws.create_orders("C:/ML/GitHub/Uni-Lab-OS/unilabos/devices/workstation/bioyond_cell/2025092501.xlsx")
|
||||
# print(res)
|
||||
|
||||
# 3.31 下料:同理
|
||||
# r2 = ws.auto_batch_outbound_from_xlsx(r"C:/path/样品导入模板 (8).xlsx")
|
||||
# print(r2["payload"]["data"])
|
||||
|
||||
# r321 = ws.transfer_3_to_2_to_1()
|
||||
# ws.transfer_1_to_2()
|
||||
|
||||
|
||||
706
unilabos/devices/workstation/bioyond_cell/cellconfig3c.json
Normal file
706
unilabos/devices/workstation/bioyond_cell/cellconfig3c.json
Normal file
@@ -0,0 +1,706 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "bioyond_workstation",
|
||||
"name": "配液分液工站",
|
||||
"children": [
|
||||
],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "bioyondworkstation_device",
|
||||
"config": {
|
||||
"protocol_type": [],
|
||||
"station_resource": {}
|
||||
|
||||
},
|
||||
"data": {}
|
||||
},
|
||||
{
|
||||
"id": "BatteryStation",
|
||||
"name": "扣电工作站",
|
||||
"children": [
|
||||
"coin_cell_deck"
|
||||
],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "bettery_station_registry",
|
||||
"position": {
|
||||
"x": 600,
|
||||
"y": 400,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"debug_mode": false,
|
||||
"_comment": "protocol_type接外部工站固定写法字段,一般为空,station_resource写法也固定",
|
||||
"protocol_type": [],
|
||||
"station_resource": {
|
||||
"data": {
|
||||
"_resource_child_name": "coin_cell_deck",
|
||||
"_resource_type": "unilabos.devices.workstation.coin_cell_assembly.button_battery_station:CoincellDeck"
|
||||
}
|
||||
},
|
||||
|
||||
"address": "192.168.1.20",
|
||||
"port": 502
|
||||
},
|
||||
"data": {}
|
||||
},
|
||||
{
|
||||
"id": "coin_cell_deck",
|
||||
"name": "coin_cell_deck",
|
||||
"sample_id": null,
|
||||
"children": [
|
||||
"\u7535\u6c60\u6599\u76d8"
|
||||
],
|
||||
"parent": null,
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "CoincellDeck",
|
||||
"size_x": 1000,
|
||||
"size_y": 1000,
|
||||
"size_z": 900,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "coin_cell_deck",
|
||||
"barcode": null
|
||||
},
|
||||
"data": {}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8",
|
||||
"name": "\u7535\u6c60\u6599\u76d8",
|
||||
"sample_id": null,
|
||||
"children": [
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_0_0",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_0_1",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_0_2",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_0_3",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_1_0",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_1_1",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_1_2",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_1_3",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_2_0",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_2_1",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_2_2",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_2_3",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_3_0",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_3_1",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_3_2",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_3_3"
|
||||
],
|
||||
"parent": "coin_cell_deck",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialPlate",
|
||||
"size_x": 120.8,
|
||||
"size_y": 160.5,
|
||||
"size_z": 10.0,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_plate",
|
||||
"model": null,
|
||||
"barcode": null,
|
||||
"ordering": {
|
||||
"A1": "\u7535\u6c60\u6599\u76d8_materialhole_0_0",
|
||||
"B1": "\u7535\u6c60\u6599\u76d8_materialhole_0_1",
|
||||
"C1": "\u7535\u6c60\u6599\u76d8_materialhole_0_2",
|
||||
"D1": "\u7535\u6c60\u6599\u76d8_materialhole_0_3",
|
||||
"A2": "\u7535\u6c60\u6599\u76d8_materialhole_1_0",
|
||||
"B2": "\u7535\u6c60\u6599\u76d8_materialhole_1_1",
|
||||
"C2": "\u7535\u6c60\u6599\u76d8_materialhole_1_2",
|
||||
"D2": "\u7535\u6c60\u6599\u76d8_materialhole_1_3",
|
||||
"A3": "\u7535\u6c60\u6599\u76d8_materialhole_2_0",
|
||||
"B3": "\u7535\u6c60\u6599\u76d8_materialhole_2_1",
|
||||
"C3": "\u7535\u6c60\u6599\u76d8_materialhole_2_2",
|
||||
"D3": "\u7535\u6c60\u6599\u76d8_materialhole_2_3",
|
||||
"A4": "\u7535\u6c60\u6599\u76d8_materialhole_3_0",
|
||||
"B4": "\u7535\u6c60\u6599\u76d8_materialhole_3_1",
|
||||
"C4": "\u7535\u6c60\u6599\u76d8_materialhole_3_2",
|
||||
"D4": "\u7535\u6c60\u6599\u76d8_materialhole_3_3"
|
||||
}
|
||||
},
|
||||
"data": {}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_0_0",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_0_0",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 12.4,
|
||||
"y": 104.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_0_1",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_0_1",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 12.4,
|
||||
"y": 80.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_0_2",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_0_2",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 12.4,
|
||||
"y": 56.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_0_3",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_0_3",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 12.4,
|
||||
"y": 32.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_1_0",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_1_0",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 36.4,
|
||||
"y": 104.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_1_1",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_1_1",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 36.4,
|
||||
"y": 80.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_1_2",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_1_2",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 36.4,
|
||||
"y": 56.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_1_3",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_1_3",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 36.4,
|
||||
"y": 32.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_2_0",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_2_0",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 60.4,
|
||||
"y": 104.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_2_1",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_2_1",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 60.4,
|
||||
"y": 80.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_2_2",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_2_2",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 60.4,
|
||||
"y": 56.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_2_3",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_2_3",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 60.4,
|
||||
"y": 32.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_3_0",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_3_0",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 84.4,
|
||||
"y": 104.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_3_1",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_3_1",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 84.4,
|
||||
"y": 80.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_3_2",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_3_2",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 84.4,
|
||||
"y": 56.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_3_3",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_3_3",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 84.4,
|
||||
"y": 32.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
20
unilabos/devices/workstation/bioyond_cell/cellconfig3ca.json
Normal file
20
unilabos/devices/workstation/bioyond_cell/cellconfig3ca.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "bioyond_workstation",
|
||||
"name": "配液分液工站",
|
||||
"children": [
|
||||
],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "bioyondworkstation_device",
|
||||
"config": {
|
||||
"protocol_type": [],
|
||||
"station_resource": {}
|
||||
|
||||
},
|
||||
"data": {}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
BIN
unilabos/devices/workstation/bioyond_cell/样品导入模板.xlsx
Normal file
BIN
unilabos/devices/workstation/bioyond_cell/样品导入模板.xlsx
Normal file
Binary file not shown.
374
unilabos/devices/workstation/bioyond_material_management.py
Normal file
374
unilabos/devices/workstation/bioyond_material_management.py
Normal file
@@ -0,0 +1,374 @@
|
||||
"""
|
||||
Bioyond物料管理实现
|
||||
Bioyond Material Management Implementation
|
||||
|
||||
基于Bioyond系统的物料管理,支持从Bioyond系统同步物料到UniLab工作站
|
||||
"""
|
||||
from typing import Dict, Any, List, Optional, Union
|
||||
import json
|
||||
import asyncio
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from pylabrobot.resources import (
|
||||
Resource as PLRResource,
|
||||
Container,
|
||||
Deck,
|
||||
Coordinate as PLRCoordinate,
|
||||
)
|
||||
|
||||
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
|
||||
from unilabos.utils.log import logger
|
||||
from unilabos.resources.graphio import (
|
||||
resource_plr_to_ulab,
|
||||
resource_ulab_to_plr,
|
||||
resource_bioyond_to_ulab,
|
||||
resource_bioyond_container_to_ulab,
|
||||
resource_ulab_to_bioyond
|
||||
)
|
||||
from .workstation_material_management import MaterialManagementBase
|
||||
|
||||
|
||||
class BioyondMaterialManagement(MaterialManagementBase):
|
||||
"""Bioyond物料管理类
|
||||
|
||||
实现从Bioyond系统同步物料到UniLab工作站的功能:
|
||||
1. 从Bioyond系统获取物料数据
|
||||
2. 转换为UniLab格式
|
||||
3. 同步到PyLabRobot Deck
|
||||
4. 支持双向同步
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device_id: str,
|
||||
deck_config: Dict[str, Any],
|
||||
resource_tracker: DeviceNodeResourceTracker,
|
||||
children_config: Dict[str, Dict[str, Any]] = None,
|
||||
bioyond_config: Dict[str, Any] = None
|
||||
):
|
||||
self.bioyond_config = bioyond_config or {}
|
||||
self.bioyond_api_client = None
|
||||
self.sync_interval = self.bioyond_config.get("sync_interval", 30) # 同步间隔(秒)
|
||||
|
||||
# 初始化父类
|
||||
super().__init__(device_id, deck_config, resource_tracker, children_config)
|
||||
|
||||
# 初始化Bioyond API客户端
|
||||
self._initialize_bioyond_client()
|
||||
|
||||
# 启动同步任务
|
||||
self._start_sync_task()
|
||||
|
||||
def _initialize_bioyond_client(self):
|
||||
"""初始化Bioyond API客户端"""
|
||||
try:
|
||||
# 这里应该根据实际的Bioyond API实现
|
||||
# 暂时使用模拟客户端
|
||||
self.bioyond_api_client = BioyondAPIClient(self.bioyond_config)
|
||||
logger.info(f"Bioyond API客户端初始化成功")
|
||||
except Exception as e:
|
||||
logger.error(f"Bioyond API客户端初始化失败: {e}")
|
||||
self.bioyond_api_client = None
|
||||
|
||||
def _start_sync_task(self):
|
||||
"""启动同步任务"""
|
||||
if self.bioyond_api_client:
|
||||
# 创建异步同步任务
|
||||
asyncio.create_task(self._periodic_sync())
|
||||
logger.info(f"Bioyond同步任务已启动,间隔: {self.sync_interval}秒")
|
||||
|
||||
async def _periodic_sync(self):
|
||||
"""定期同步任务"""
|
||||
while True:
|
||||
try:
|
||||
await self.sync_from_bioyond()
|
||||
await asyncio.sleep(self.sync_interval)
|
||||
except Exception as e:
|
||||
logger.error(f"Bioyond同步任务出错: {e}")
|
||||
await asyncio.sleep(self.sync_interval)
|
||||
|
||||
async def sync_from_bioyond(self) -> bool:
|
||||
"""从Bioyond系统同步物料"""
|
||||
try:
|
||||
if not self.bioyond_api_client:
|
||||
logger.warning("Bioyond API客户端未初始化")
|
||||
return False
|
||||
|
||||
# 1. 从Bioyond获取物料数据
|
||||
bioyond_data = await self.bioyond_api_client.get_materials()
|
||||
if not bioyond_data:
|
||||
logger.warning("从Bioyond获取物料数据为空")
|
||||
return False
|
||||
|
||||
# 2. 转换为UniLab格式
|
||||
if isinstance(bioyond_data, dict) and "data" in bioyond_data:
|
||||
# 容器格式数据
|
||||
unilab_resources = resource_bioyond_container_to_ulab(bioyond_data)
|
||||
else:
|
||||
# 物料列表格式数据
|
||||
unilab_resources = resource_bioyond_to_ulab(bioyond_data)
|
||||
|
||||
# 3. 转换为PLR格式并分配到Deck
|
||||
await self._assign_resources_to_deck(unilab_resources)
|
||||
|
||||
logger.info(f"从Bioyond同步了 {len(unilab_resources)} 个资源")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"从Bioyond同步物料失败: {e}")
|
||||
return False
|
||||
|
||||
async def sync_to_bioyond(self, plr_resource: PLRResource) -> bool:
|
||||
"""将本地物料变更同步到Bioyond系统"""
|
||||
try:
|
||||
if not self.bioyond_api_client:
|
||||
logger.warning("Bioyond API客户端未初始化")
|
||||
return False
|
||||
|
||||
# 1. 转换为UniLab格式
|
||||
unilab_resource = resource_plr_to_ulab(plr_resource)
|
||||
|
||||
# 2. 转换为Bioyond格式
|
||||
bioyond_materials = resource_ulab_to_bioyond([unilab_resource])
|
||||
|
||||
# 3. 发送到Bioyond系统
|
||||
success = await self.bioyond_api_client.update_materials(bioyond_materials)
|
||||
|
||||
if success:
|
||||
logger.info(f"成功同步物料 {plr_resource.name} 到Bioyond")
|
||||
else:
|
||||
logger.warning(f"同步物料 {plr_resource.name} 到Bioyond失败")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"同步物料到Bioyond失败: {e}")
|
||||
return False
|
||||
|
||||
async def _assign_resources_to_deck(self, unilab_resources: List[Dict[str, Any]]):
|
||||
"""将UniLab资源分配到Deck"""
|
||||
try:
|
||||
# 转换为PLR格式
|
||||
from unilabos.resources.graphio import list_to_nested_dict
|
||||
nested_resources = list_to_nested_dict(unilab_resources)
|
||||
plr_resources = resource_ulab_to_plr(nested_resources)
|
||||
|
||||
# 分配资源到Deck
|
||||
if hasattr(plr_resources, 'children'):
|
||||
resources_to_assign = plr_resources.children
|
||||
elif isinstance(plr_resources, list):
|
||||
resources_to_assign = plr_resources
|
||||
else:
|
||||
resources_to_assign = [plr_resources]
|
||||
|
||||
for resource in resources_to_assign:
|
||||
try:
|
||||
# 获取资源位置
|
||||
if hasattr(resource, 'location') and resource.location:
|
||||
location = PLRCoordinate(resource.location.x, resource.location.y, resource.location.z)
|
||||
else:
|
||||
location = PLRCoordinate(0, 0, 0)
|
||||
|
||||
# 分配资源到Deck
|
||||
self.plr_deck.assign_child_resource(resource, location)
|
||||
|
||||
# 注册到resource tracker
|
||||
self.resource_tracker.add_resource(resource)
|
||||
|
||||
# 保存资源引用
|
||||
self.plr_resources[resource.name] = resource
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"分配资源 {resource.name} 到Deck失败: {e}")
|
||||
|
||||
logger.info(f"成功分配了 {len(resources_to_assign)} 个资源到Deck")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"分配资源到Deck失败: {e}")
|
||||
|
||||
def _create_resource_by_type(
|
||||
self,
|
||||
resource_id: str,
|
||||
resource_type: str,
|
||||
config: Dict[str, Any],
|
||||
data: Dict[str, Any],
|
||||
location: PLRCoordinate
|
||||
) -> Optional[PLRResource]:
|
||||
"""根据类型创建Bioyond相关资源"""
|
||||
try:
|
||||
# 这里可以根据需要实现特定的Bioyond资源类型
|
||||
# 目前使用通用的容器类型
|
||||
if resource_type in ["container", "plate", "well"]:
|
||||
return self._create_generic_container(resource_id, resource_type, config, data, location)
|
||||
else:
|
||||
logger.warning(f"未知的Bioyond资源类型: {resource_type}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建Bioyond资源失败 {resource_id} ({resource_type}): {e}")
|
||||
return None
|
||||
|
||||
def _create_generic_container(
|
||||
self,
|
||||
resource_id: str,
|
||||
resource_type: str,
|
||||
config: Dict[str, Any],
|
||||
data: Dict[str, Any],
|
||||
location: PLRCoordinate
|
||||
) -> Optional[PLRResource]:
|
||||
"""创建通用容器资源"""
|
||||
try:
|
||||
from pylabrobot.resources import Plate, Well
|
||||
|
||||
if resource_type == "plate":
|
||||
return Plate(
|
||||
name=resource_id,
|
||||
size_x=config.get("size_x", 127.76),
|
||||
size_y=config.get("size_y", 85.48),
|
||||
size_z=config.get("size_z", 14.35),
|
||||
location=location,
|
||||
category="plate"
|
||||
)
|
||||
elif resource_type == "well":
|
||||
return Well(
|
||||
name=resource_id,
|
||||
size_x=config.get("size_x", 9.0),
|
||||
size_y=config.get("size_y", 9.0),
|
||||
size_z=config.get("size_z", 10.0),
|
||||
location=location,
|
||||
category="well"
|
||||
)
|
||||
else:
|
||||
return Container(
|
||||
name=resource_id,
|
||||
size_x=config.get("size_x", 50.0),
|
||||
size_y=config.get("size_y", 50.0),
|
||||
size_z=config.get("size_z", 10.0),
|
||||
location=location,
|
||||
category="container"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建通用容器失败 {resource_id}: {e}")
|
||||
return None
|
||||
|
||||
def get_bioyond_materials(self) -> List[Dict[str, Any]]:
|
||||
"""获取当前Bioyond物料列表"""
|
||||
try:
|
||||
# 将当前PLR资源转换为Bioyond格式
|
||||
bioyond_materials = []
|
||||
for resource in self.plr_resources.values():
|
||||
unilab_resource = resource_plr_to_ulab(resource)
|
||||
bioyond_materials.extend(resource_ulab_to_bioyond([unilab_resource]))
|
||||
return bioyond_materials
|
||||
except Exception as e:
|
||||
logger.error(f"获取Bioyond物料列表失败: {e}")
|
||||
return []
|
||||
|
||||
def update_material_from_bioyond(self, material_id: str, bioyond_data: Dict[str, Any]) -> bool:
|
||||
"""从Bioyond数据更新指定物料"""
|
||||
try:
|
||||
# 查找现有物料
|
||||
material = self.find_material_by_id(material_id)
|
||||
if not material:
|
||||
logger.warning(f"未找到物料: {material_id}")
|
||||
return False
|
||||
|
||||
# 转换Bioyond数据为UniLab格式
|
||||
unilab_resources = resource_bioyond_to_ulab([bioyond_data])
|
||||
if not unilab_resources:
|
||||
logger.warning(f"转换Bioyond数据失败: {material_id}")
|
||||
return False
|
||||
|
||||
# 更新物料属性
|
||||
unilab_resource = unilab_resources[0]
|
||||
material.name = unilab_resource.get("name", material.name)
|
||||
|
||||
# 更新位置
|
||||
position = unilab_resource.get("position", {})
|
||||
if position:
|
||||
material.location = PLRCoordinate(
|
||||
position.get("x", 0),
|
||||
position.get("y", 0),
|
||||
position.get("z", 0)
|
||||
)
|
||||
|
||||
logger.info(f"成功更新物料: {material_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"更新物料失败 {material_id}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
class BioyondAPIClient:
|
||||
"""Bioyond API客户端(模拟实现)
|
||||
|
||||
实际使用时需要根据Bioyond系统的API接口实现
|
||||
"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
self.config = config
|
||||
self.base_url = config.get("base_url", "http://localhost:8080")
|
||||
self.api_key = config.get("api_key", "")
|
||||
self.timeout = config.get("timeout", 30)
|
||||
|
||||
async def get_materials(self) -> Optional[Union[Dict[str, Any], List[Dict[str, Any]]]]:
|
||||
"""从Bioyond系统获取物料数据"""
|
||||
try:
|
||||
# 这里应该实现实际的API调用
|
||||
# 暂时返回模拟数据
|
||||
logger.info("从Bioyond API获取物料数据")
|
||||
|
||||
# 模拟API调用延迟
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# 返回模拟数据(实际应该从API获取)
|
||||
return {
|
||||
"data": [],
|
||||
"code": 1,
|
||||
"message": "success",
|
||||
"timestamp": 1234567890
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Bioyond API调用失败: {e}")
|
||||
return None
|
||||
|
||||
async def update_materials(self, materials: List[Dict[str, Any]]) -> bool:
|
||||
"""更新Bioyond系统中的物料数据"""
|
||||
try:
|
||||
# 这里应该实现实际的API调用
|
||||
logger.info(f"更新Bioyond系统中的 {len(materials)} 个物料")
|
||||
|
||||
# 模拟API调用延迟
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# 模拟成功响应
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"更新Bioyond物料失败: {e}")
|
||||
return False
|
||||
|
||||
async def get_material_by_id(self, material_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""根据ID获取单个物料"""
|
||||
try:
|
||||
# 这里应该实现实际的API调用
|
||||
logger.info(f"从Bioyond API获取物料: {material_id}")
|
||||
|
||||
# 模拟API调用延迟
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# 返回模拟数据
|
||||
return {
|
||||
"id": material_id,
|
||||
"name": f"material_{material_id}",
|
||||
"type": "container",
|
||||
"quantity": 1.0,
|
||||
"unit": "个"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取Bioyond物料失败 {material_id}: {e}")
|
||||
return None
|
||||
1951
unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py
Normal file
1951
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}")
|
||||
2379
unilabos/devices/workstation/bioyond_studio/station.py
Normal file
2379
unilabos/devices/workstation/bioyond_studio/station.py
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1332
unilabos/devices/workstation/coin_cell_assembly/celljson.json
Normal file
1332
unilabos/devices/workstation/coin_cell_assembly/celljson.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
Name,DataType,InitValue,Comment,Attribute,DeviceType,Address
|
||||
COIL_SYS_START_CMD,BOOL,,<EFBFBD>豸<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,coil,8010
|
||||
COIL_SYS_STOP_CMD,BOOL,,<EFBFBD>豸ֹͣ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,coil,8020
|
||||
COIL_SYS_RESET_CMD,BOOL,,<EFBFBD>豸<EFBFBD><EFBFBD>λ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,coil,8030
|
||||
COIL_SYS_HAND_CMD,BOOL,,<EFBFBD>豸<EFBFBD>ֶ<EFBFBD>ģʽ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,coil,8040
|
||||
COIL_SYS_AUTO_CMD,BOOL,,<EFBFBD>豸<EFBFBD>Զ<EFBFBD>ģʽ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,coil,8050
|
||||
COIL_SYS_INIT_CMD,BOOL,,<EFBFBD>豸<EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD>ģʽ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,coil,8060
|
||||
COIL_UNILAB_SEND_MSG_SUCC_CMD,BOOL,,UNILAB<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>䷽<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,coil,8700
|
||||
COIL_UNILAB_REC_MSG_SUCC_CMD,BOOL,,UNILAB<EFBFBD><EFBFBD><EFBFBD>ܲ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,coil,8710
|
||||
COIL_SYS_START_STATUS,BOOL,,<EFBFBD>豸<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,coil,8210
|
||||
COIL_SYS_STOP_STATUS,BOOL,,<EFBFBD>豸ֹͣ<EFBFBD><EFBFBD>,,coil,8220
|
||||
COIL_SYS_RESET_STATUS,BOOL,,<EFBFBD>豸<EFBFBD><EFBFBD>λ<EFBFBD><EFBFBD>,,coil,8230
|
||||
COIL_SYS_HAND_STATUS,BOOL,,<EFBFBD>豸<EFBFBD>ֶ<EFBFBD>ģʽ,,coil,8240
|
||||
COIL_SYS_AUTO_STATUS,BOOL,,<EFBFBD>豸<EFBFBD>Զ<EFBFBD>ģʽ,,coil,8250
|
||||
COIL_SYS_INIT_STATUS,BOOL,,<EFBFBD>豸<EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,coil,8260
|
||||
COIL_REQUEST_REC_MSG_STATUS,BOOL,,<EFBFBD>豸<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>䷽,,coil,8510
|
||||
COIL_REQUEST_SEND_MSG_STATUS,BOOL,,<EFBFBD>豸<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ͳ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,coil,8500
|
||||
REG_MSG_ELECTROLYTE_USE_NUM,INT16,,<EFBFBD><EFBFBD>ƿ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Һʹ<EFBFBD>ô<EFBFBD><EFBFBD><EFBFBD>,,hold_register,11000
|
||||
REG_MSG_ELECTROLYTE_NUM,INT16,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Һʹ<EFBFBD><EFBFBD>ƿ<EFBFBD><EFBFBD>,,hold_register,11002
|
||||
REG_MSG_ELECTROLYTE_VOLUME,INT16,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Һ<EFBFBD><EFBFBD>ȡ<EFBFBD><EFBFBD>,,hold_register,11004
|
||||
REG_MSG_ASSEMBLY_TYPE,INT16,,<EFBFBD><EFBFBD>װ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƭ<EFBFBD>ѵ<EFBFBD><EFBFBD><EFBFBD>ʽ,,hold_register,11006
|
||||
REG_MSG_ASSEMBLY_PRESSURE,INT16,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>װѹ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,hold_register,11008
|
||||
REG_DATA_ASSEMBLY_COIN_CELL_NUM,INT16,,<EFBFBD><EFBFBD>ǰ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>װ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,hold_register,10000
|
||||
REG_DATA_OPEN_CIRCUIT_VOLTAGE,FLOAT32,,<EFBFBD><EFBFBD>ǰ<EFBFBD><EFBFBD><EFBFBD>ص<EFBFBD>ѹ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,hold_register,10002
|
||||
REG_DATA_AXIS_X_POS,FLOAT32,,<EFBFBD><EFBFBD>ҺX<EFBFBD>ᵱǰλ<EFBFBD><EFBFBD>,,hold_register,10004
|
||||
REG_DATA_AXIS_Y_POS,FLOAT32,,<EFBFBD><EFBFBD>ҺZ<EFBFBD>ᵱǰλ<EFBFBD><EFBFBD>,,hold_register,10006
|
||||
REG_DATA_AXIS_Z_POS,FLOAT32,,<EFBFBD><EFBFBD>ҺY<EFBFBD>ᵱǰλ<EFBFBD><EFBFBD>,,hold_register,10008
|
||||
REG_DATA_POLE_WEIGHT,FLOAT32,,<EFBFBD><EFBFBD>ǰ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƭ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,hold_register,10010
|
||||
REG_DATA_ASSEMBLY_PER_TIME,FLOAT32,,<EFBFBD><EFBFBD>ǰ<EFBFBD><EFBFBD><EFBFBD>ŵ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>װʱ<EFBFBD><EFBFBD>,,hold_register,10012
|
||||
REG_DATA_ASSEMBLY_PRESSURE,INT16,,<EFBFBD><EFBFBD>ǰ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>װѹ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,hold_register,10014
|
||||
REG_DATA_ELECTROLYTE_VOLUME,INT16,,<EFBFBD><EFBFBD>ǰ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Һ<EFBFBD><EFBFBD>ע<EFBFBD><EFBFBD>,,hold_register,10016
|
||||
REG_DATA_COIN_NUM,INT16,,<EFBFBD><EFBFBD>ǰ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,hold_register,10018
|
||||
REG_DATA_ELECTROLYTE_CODE,STRING,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Һ<EFBFBD><EFBFBD>ά<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>к<EFBFBD>,,hold_register,10020
|
||||
REG_DATA_COIN_CELL_CODE,STRING,,<EFBFBD><EFBFBD><EFBFBD>ض<EFBFBD>ά<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>к<EFBFBD>,,hold_register,10030
|
||||
REG_DATA_STACK_VISON_CODE,STRING,,<EFBFBD><EFBFBD><EFBFBD>϶ѵ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͼƬ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,hold_register,12004
|
||||
REG_DATA_GLOVE_BOX_PRESSURE,FLOAT32,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѹ<EFBFBD><EFBFBD>,,hold_register,10050
|
||||
REG_DATA_GLOVE_BOX_WATER_CONTENT,FLOAT32,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ˮ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,hold_register,10052
|
||||
REG_DATA_GLOVE_BOX_O2_CONTENT,FLOAT32,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,hold_register,10054
|
||||
UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM,BOOL,,Unilabȷ<EFBFBD><EFBFBD><EFBFBD>ѷ<EFBFBD><EFBFBD>͵<EFBFBD><EFBFBD><EFBFBD>Һƿ<EFBFBD><EFBFBD><EFBFBD>ź<EFBFBD>,,coil,8720
|
||||
UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM,BOOL,,Unilab<EFBFBD>ɽ<EFBFBD><EFBFBD>ܵ<EFBFBD><EFBFBD><EFBFBD>Һƿ<EFBFBD><EFBFBD>,,coil,8520
|
||||
REG_MSG_ELECTROLYTE_NUM_USED,INT16,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Һ<EFBFBD><EFBFBD>װ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ƽ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,hold_register,496
|
||||
REG_DATA_ELECTROLYTE_USE_NUM,INT16,,<EFBFBD><EFBFBD>ǰ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>װƽ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,hold_register,10000
|
||||
UNILAB_SEND_FINISHED_CMD,BOOL,,Unilab<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>յ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ź<EFBFBD>,,coil,8730
|
||||
UNILAB_RECE_FINISHED_CMD,BOOL,,<EFBFBD><EFBFBD>֪unilab<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ź<EFBFBD>,,coil,8530
|
||||
|
@@ -0,0 +1,46 @@
|
||||
Name,DataType,InitValue,Comment,Attribute,DeviceType,Address,
|
||||
COIL_SYS_START_CMD,BOOL,,,,coil,8010,
|
||||
COIL_SYS_STOP_CMD,BOOL,,,,coil,8020,
|
||||
COIL_SYS_RESET_CMD,BOOL,,,,coil,8030,
|
||||
COIL_SYS_HAND_CMD,BOOL,,,,coil,8040,
|
||||
COIL_SYS_AUTO_CMD,BOOL,,,,coil,8050,
|
||||
COIL_SYS_INIT_CMD,BOOL,,,,coil,8060,
|
||||
COIL_UNILAB_SEND_MSG_SUCC_CMD,BOOL,,,,coil,8700,
|
||||
COIL_UNILAB_REC_MSG_SUCC_CMD,BOOL,,,,coil,8710,unilab_rec_msg_succ_cmd
|
||||
COIL_SYS_START_STATUS,BOOL,,,,coil,8210,
|
||||
COIL_SYS_STOP_STATUS,BOOL,,,,coil,8220,
|
||||
COIL_SYS_RESET_STATUS,BOOL,,,,coil,8230,
|
||||
COIL_SYS_HAND_STATUS,BOOL,,,,coil,8240,
|
||||
COIL_SYS_AUTO_STATUS,BOOL,,,,coil,8250,
|
||||
COIL_SYS_INIT_STATUS,BOOL,,,,coil,8260,
|
||||
COIL_REQUEST_REC_MSG_STATUS,BOOL,,,,coil,8500,
|
||||
COIL_REQUEST_SEND_MSG_STATUS,BOOL,,,,coil,8510,request_send_msg_status
|
||||
REG_MSG_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,11000,
|
||||
REG_MSG_ELECTROLYTE_NUM,INT16,,,,hold_register,11002,unilab_send_msg_electrolyte_num
|
||||
REG_MSG_ELECTROLYTE_VOLUME,INT16,,,,hold_register,11004,unilab_send_msg_electrolyte_vol
|
||||
REG_MSG_ASSEMBLY_TYPE,INT16,,,,hold_register,11006,unilab_send_msg_assembly_type
|
||||
REG_MSG_ASSEMBLY_PRESSURE,INT16,,,,hold_register,11008,unilab_send_msg_assembly_pressure
|
||||
REG_DATA_ASSEMBLY_COIN_CELL_NUM,INT16,,,,hold_register,10000,data_assembly_coin_cell_num
|
||||
REG_DATA_OPEN_CIRCUIT_VOLTAGE,FLOAT32,,,,hold_register,10002,data_open_circuit_voltage
|
||||
REG_DATA_AXIS_X_POS,FLOAT32,,,,hold_register,10004,
|
||||
REG_DATA_AXIS_Y_POS,FLOAT32,,,,hold_register,10006,
|
||||
REG_DATA_AXIS_Z_POS,FLOAT32,,,,hold_register,10008,
|
||||
REG_DATA_POLE_WEIGHT,FLOAT32,,,,hold_register,10010,data_pole_weight
|
||||
REG_DATA_ASSEMBLY_PER_TIME,FLOAT32,,,,hold_register,10012,data_assembly_time
|
||||
REG_DATA_ASSEMBLY_PRESSURE,INT16,,,,hold_register,10014,data_assembly_pressure
|
||||
REG_DATA_ELECTROLYTE_VOLUME,INT16,,,,hold_register,10016,data_electrolyte_volume
|
||||
REG_DATA_COIN_NUM,INT16,,,,hold_register,10018,data_coin_num
|
||||
REG_DATA_ELECTROLYTE_CODE,STRING,,,,hold_register,10020,data_electrolyte_code()
|
||||
REG_DATA_COIN_CELL_CODE,STRING,,,,hold_register,10030,data_coin_cell_code()
|
||||
REG_DATA_STACK_VISON_CODE,STRING,,,,hold_register,12004,data_stack_vision_code()
|
||||
REG_DATA_GLOVE_BOX_PRESSURE,FLOAT32,,,,hold_register,10050,data_glove_box_pressure
|
||||
REG_DATA_GLOVE_BOX_WATER_CONTENT,FLOAT32,,,,hold_register,10052,data_glove_box_water_content
|
||||
REG_DATA_GLOVE_BOX_O2_CONTENT,FLOAT32,,,,hold_register,10054,data_glove_box_o2_content
|
||||
UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,8720,
|
||||
UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,8520,
|
||||
REG_MSG_ELECTROLYTE_NUM_USED,INT16,,,,hold_register,496,
|
||||
REG_DATA_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,10000,
|
||||
UNILAB_SEND_FINISHED_CMD,BOOL,,,,coil,8730,
|
||||
UNILAB_RECE_FINISHED_CMD,BOOL,,,,coil,8530,
|
||||
REG_DATA_ASSEMBLY_TYPE,INT16,,,,hold_register,10018,ASSEMBLY_TYPE7or8
|
||||
COIL_ALUMINUM_FOIL,BOOL,,,,coil,8340,
|
||||
|
1925
unilabos/devices/workstation/coin_cell_assembly/new_cellconfig.json
Normal file
1925
unilabos/devices/workstation/coin_cell_assembly/new_cellconfig.json
Normal file
File diff suppressed because it is too large
Load Diff
1925
unilabos/devices/workstation/coin_cell_assembly/new_cellconfig2.json
Normal file
1925
unilabos/devices/workstation/coin_cell_assembly/new_cellconfig2.json
Normal file
File diff suppressed because it is too large
Load Diff
1925
unilabos/devices/workstation/coin_cell_assembly/new_cellconfig3.json
Normal file
1925
unilabos/devices/workstation/coin_cell_assembly/new_cellconfig3.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,691 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "BatteryStation",
|
||||
"name": "扣电工作站",
|
||||
"children": [
|
||||
"coin_cell_deck"
|
||||
],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "bettery_station_registry",
|
||||
"position": {
|
||||
"x": 600,
|
||||
"y": 400,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"debug_mode": false,
|
||||
"_comment": "protocol_type接外部工站固定写法字段,一般为空,station_resource写法也固定",
|
||||
"protocol_type": [],
|
||||
"station_resource": {
|
||||
"data": {
|
||||
"_resource_child_name": "coin_cell_deck",
|
||||
"_resource_type": "unilabos.devices.workstation.coin_cell_assembly.button_battery_station:CoincellDeck"
|
||||
}
|
||||
},
|
||||
|
||||
"address": "192.168.1.20",
|
||||
"port": 502
|
||||
},
|
||||
"data": {}
|
||||
},
|
||||
{
|
||||
"id": "coin_cell_deck",
|
||||
"name": "coin_cell_deck",
|
||||
"sample_id": null,
|
||||
"children": [
|
||||
"\u7535\u6c60\u6599\u76d8"
|
||||
],
|
||||
"parent": null,
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "CoincellDeck",
|
||||
"size_x": 1000,
|
||||
"size_y": 1000,
|
||||
"size_z": 900,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "coin_cell_deck",
|
||||
"barcode": null
|
||||
},
|
||||
"data": {}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8",
|
||||
"name": "\u7535\u6c60\u6599\u76d8",
|
||||
"sample_id": null,
|
||||
"children": [
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_0_0",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_0_1",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_0_2",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_0_3",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_1_0",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_1_1",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_1_2",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_1_3",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_2_0",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_2_1",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_2_2",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_2_3",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_3_0",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_3_1",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_3_2",
|
||||
"\u7535\u6c60\u6599\u76d8_materialhole_3_3"
|
||||
],
|
||||
"parent": "coin_cell_deck",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialPlate",
|
||||
"size_x": 120.8,
|
||||
"size_y": 160.5,
|
||||
"size_z": 10.0,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_plate",
|
||||
"model": null,
|
||||
"barcode": null,
|
||||
"ordering": {
|
||||
"A1": "\u7535\u6c60\u6599\u76d8_materialhole_0_0",
|
||||
"B1": "\u7535\u6c60\u6599\u76d8_materialhole_0_1",
|
||||
"C1": "\u7535\u6c60\u6599\u76d8_materialhole_0_2",
|
||||
"D1": "\u7535\u6c60\u6599\u76d8_materialhole_0_3",
|
||||
"A2": "\u7535\u6c60\u6599\u76d8_materialhole_1_0",
|
||||
"B2": "\u7535\u6c60\u6599\u76d8_materialhole_1_1",
|
||||
"C2": "\u7535\u6c60\u6599\u76d8_materialhole_1_2",
|
||||
"D2": "\u7535\u6c60\u6599\u76d8_materialhole_1_3",
|
||||
"A3": "\u7535\u6c60\u6599\u76d8_materialhole_2_0",
|
||||
"B3": "\u7535\u6c60\u6599\u76d8_materialhole_2_1",
|
||||
"C3": "\u7535\u6c60\u6599\u76d8_materialhole_2_2",
|
||||
"D3": "\u7535\u6c60\u6599\u76d8_materialhole_2_3",
|
||||
"A4": "\u7535\u6c60\u6599\u76d8_materialhole_3_0",
|
||||
"B4": "\u7535\u6c60\u6599\u76d8_materialhole_3_1",
|
||||
"C4": "\u7535\u6c60\u6599\u76d8_materialhole_3_2",
|
||||
"D4": "\u7535\u6c60\u6599\u76d8_materialhole_3_3"
|
||||
}
|
||||
},
|
||||
"data": {}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_0_0",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_0_0",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 12.4,
|
||||
"y": 104.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_0_1",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_0_1",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 12.4,
|
||||
"y": 80.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_0_2",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_0_2",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 12.4,
|
||||
"y": 56.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_0_3",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_0_3",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 12.4,
|
||||
"y": 32.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_1_0",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_1_0",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 36.4,
|
||||
"y": 104.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_1_1",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_1_1",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 36.4,
|
||||
"y": 80.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_1_2",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_1_2",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 36.4,
|
||||
"y": 56.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_1_3",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_1_3",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 36.4,
|
||||
"y": 32.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_2_0",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_2_0",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 60.4,
|
||||
"y": 104.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_2_1",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_2_1",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 60.4,
|
||||
"y": 80.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_2_2",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_2_2",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 60.4,
|
||||
"y": 56.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_2_3",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_2_3",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 60.4,
|
||||
"y": 32.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_3_0",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_3_0",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 84.4,
|
||||
"y": 104.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_3_1",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_3_1",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 84.4,
|
||||
"y": 80.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_3_2",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_3_2",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 84.4,
|
||||
"y": 56.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_3_3",
|
||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_3_3",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
||||
"type": "container",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 84.4,
|
||||
"y": 32.25,
|
||||
"z": 10.0
|
||||
},
|
||||
"config": {
|
||||
"type": "MaterialHole",
|
||||
"size_x": 16,
|
||||
"size_y": 16,
|
||||
"size_z": 16,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "material_hole",
|
||||
"model": null,
|
||||
"barcode": null
|
||||
},
|
||||
"data": {
|
||||
"diameter": 20,
|
||||
"depth": 10,
|
||||
"max_sheets": 1,
|
||||
"info": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
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
6674
unilabos/devices/workstation/coin_cell_assembly/work_station.yaml
Normal file
6674
unilabos/devices/workstation/coin_cell_assembly/work_station.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -158,6 +158,10 @@ class WorkstationBase(ABC):
|
||||
# PLR 物料系统
|
||||
self.deck: Optional[Deck] = None
|
||||
self.plr_resources: Dict[str, PLRResource] = {}
|
||||
|
||||
# 资源同步器(可选)
|
||||
# self.resource_synchronizer = ResourceSynchronizer(self) # 要在driver中自行初始化,只有workstation用
|
||||
|
||||
# 硬件接口
|
||||
self.hardware_interface: Union[Any, str] = None
|
||||
|
||||
@@ -170,19 +174,67 @@ class WorkstationBase(ABC):
|
||||
# 支持的工作流(静态预定义)
|
||||
self.supported_workflows: Dict[str, WorkflowInfo] = {}
|
||||
|
||||
def post_init(self, ros_node: ROS2WorkstationNode) -> None:
|
||||
# 初始化物料系统
|
||||
self._ros_node = ros_node
|
||||
self._initialize_material_system()
|
||||
|
||||
# 注册支持的工作流
|
||||
# self._register_supported_workflows()
|
||||
|
||||
# logger.info(f"工作站 {device_id} 初始化完成(简化版)")
|
||||
|
||||
def _initialize_material_system(self):
|
||||
"""初始化物料系统 - 使用 graphio 转换"""
|
||||
pass
|
||||
try:
|
||||
from unilabos.resources.graphio import resource_ulab_to_plr
|
||||
|
||||
# # 1. 合并 deck_config 和 children 创建完整的资源树
|
||||
# complete_resource_config = self._create_complete_resource_config()
|
||||
|
||||
# # 2. 使用 graphio 转换为 PLR 资源
|
||||
# self.deck = resource_ulab_to_plr(complete_resource_config, plr_model=True)
|
||||
|
||||
# # 3. 建立资源映射
|
||||
# self._build_resource_mappings(self.deck)
|
||||
|
||||
# # 4. 如果有资源同步器,执行初始同步
|
||||
# if self.resource_synchronizer:
|
||||
# # 这里可以异步执行,暂时跳过
|
||||
# pass
|
||||
|
||||
# logger.info(f"工作站 {self.device_id} 物料系统初始化成功,创建了 {len(self.plr_resources)} 个资源")
|
||||
pass
|
||||
except Exception as e:
|
||||
# logger.error(f"工作站 {self.device_id} 物料系统初始化失败: {e}")
|
||||
raise
|
||||
|
||||
def _create_complete_resource_config(self) -> Dict[str, Any]:
|
||||
"""创建完整的资源配置 - 合并 deck_config 和 children"""
|
||||
# 创建主 deck 配置
|
||||
return {}
|
||||
deck_resource = {
|
||||
"id": f"{self.device_id}_deck",
|
||||
"name": f"{self.device_id}_deck",
|
||||
"type": "deck",
|
||||
"position": {"x": 0, "y": 0, "z": 0},
|
||||
"config": {
|
||||
"size_x": self.deck_config.get("size_x", 1000.0),
|
||||
"size_y": self.deck_config.get("size_y", 1000.0),
|
||||
"size_z": self.deck_config.get("size_z", 100.0),
|
||||
**{k: v for k, v in self.deck_config.items() if k not in ["size_x", "size_y", "size_z"]},
|
||||
},
|
||||
"data": {},
|
||||
"children": [],
|
||||
"parent": None,
|
||||
}
|
||||
|
||||
# 添加子资源
|
||||
if self._children:
|
||||
children_list = []
|
||||
for child_id, child_config in self._children.items():
|
||||
child_resource = self._normalize_child_resource(child_id, child_config, deck_resource["id"])
|
||||
children_list.append(child_resource)
|
||||
deck_resource["children"] = children_list
|
||||
|
||||
return deck_resource
|
||||
|
||||
def _normalize_child_resource(self, resource_id: str, config: Dict[str, Any], parent_id: str) -> Dict[str, Any]:
|
||||
"""标准化子资源配置"""
|
||||
@@ -232,12 +284,12 @@ class WorkstationBase(ABC):
|
||||
def set_hardware_interface(self, hardware_interface: Union[Any, str]):
|
||||
"""设置硬件接口"""
|
||||
self.hardware_interface = hardware_interface
|
||||
logger.info(f"工作站 {self._ros_node.device_id} 硬件接口设置: {type(hardware_interface).__name__}")
|
||||
logger.info(f"工作站 {self.device_id} 硬件接口设置: {type(hardware_interface).__name__}")
|
||||
|
||||
def set_workstation_node(self, workstation_node: "ROS2WorkstationNode"):
|
||||
"""设置协议节点引用(用于代理模式)"""
|
||||
self._ros_node = workstation_node
|
||||
logger.info(f"工作站 {self._ros_node.device_id} 关联协议节点")
|
||||
logger.info(f"工作站 {self.device_id} 关联协议节点")
|
||||
|
||||
# ============ 设备操作接口 ============
|
||||
|
||||
@@ -299,18 +351,18 @@ class WorkstationBase(ABC):
|
||||
async def sync_with_external_system(self) -> bool:
|
||||
"""与外部物料系统同步"""
|
||||
if not self.resource_synchronizer:
|
||||
logger.info(f"工作站 {self._ros_node.device_id} 没有配置资源同步器")
|
||||
logger.info(f"工作站 {self.device_id} 没有配置资源同步器")
|
||||
return True
|
||||
|
||||
try:
|
||||
success = await self.resource_synchronizer.sync_from_external()
|
||||
if success:
|
||||
logger.info(f"工作站 {self._ros_node.device_id} 外部同步成功")
|
||||
logger.info(f"工作站 {self.device_id} 外部同步成功")
|
||||
else:
|
||||
logger.warning(f"工作站 {self._ros_node.device_id} 外部同步失败")
|
||||
logger.warning(f"工作站 {self.device_id} 外部同步失败")
|
||||
return success
|
||||
except Exception as e:
|
||||
logger.error(f"工作站 {self._ros_node.device_id} 外部同步异常: {e}")
|
||||
logger.error(f"工作站 {self.device_id} 外部同步异常: {e}")
|
||||
return False
|
||||
|
||||
# ============ 简化的工作流控制 ============
|
||||
@@ -328,23 +380,23 @@ class WorkstationBase(ABC):
|
||||
|
||||
if success:
|
||||
self.current_workflow_status = WorkflowStatus.RUNNING
|
||||
logger.info(f"工作站 {self._ros_node.device_id} 工作流 {workflow_name} 启动成功")
|
||||
logger.info(f"工作站 {self.device_id} 工作流 {workflow_name} 启动成功")
|
||||
else:
|
||||
self.current_workflow_status = WorkflowStatus.ERROR
|
||||
logger.error(f"工作站 {self._ros_node.device_id} 工作流 {workflow_name} 启动失败")
|
||||
logger.error(f"工作站 {self.device_id} 工作流 {workflow_name} 启动失败")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
self.current_workflow_status = WorkflowStatus.ERROR
|
||||
logger.error(f"工作站 {self._ros_node.device_id} 执行工作流失败: {e}")
|
||||
logger.error(f"工作站 {self.device_id} 执行工作流失败: {e}")
|
||||
return False
|
||||
|
||||
def stop_workflow(self, emergency: bool = False) -> bool:
|
||||
"""停止工作流"""
|
||||
try:
|
||||
if self.current_workflow_status in [WorkflowStatus.IDLE, WorkflowStatus.STOPPED]:
|
||||
logger.warning(f"工作站 {self._ros_node.device_id} 没有正在运行的工作流")
|
||||
logger.warning(f"工作站 {self.device_id} 没有正在运行的工作流")
|
||||
return True
|
||||
|
||||
self.current_workflow_status = WorkflowStatus.STOPPING
|
||||
@@ -354,16 +406,16 @@ class WorkstationBase(ABC):
|
||||
|
||||
if success:
|
||||
self.current_workflow_status = WorkflowStatus.STOPPED
|
||||
logger.info(f"工作站 {self._ros_node.device_id} 工作流停止成功 (紧急: {emergency})")
|
||||
logger.info(f"工作站 {self.device_id} 工作流停止成功 (紧急: {emergency})")
|
||||
else:
|
||||
self.current_workflow_status = WorkflowStatus.ERROR
|
||||
logger.error(f"工作站 {self._ros_node.device_id} 工作流停止失败")
|
||||
logger.error(f"工作站 {self.device_id} 工作流停止失败")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
self.current_workflow_status = WorkflowStatus.ERROR
|
||||
logger.error(f"工作站 {self._ros_node.device_id} 停止工作流失败: {e}")
|
||||
logger.error(f"工作站 {self.device_id} 停止工作流失败: {e}")
|
||||
return False
|
||||
|
||||
# ============ 状态属性 ============
|
||||
@@ -389,7 +441,49 @@ class WorkstationBase(ABC):
|
||||
return 0.0
|
||||
return time.time() - self.workflow_start_time
|
||||
|
||||
# ============ 抽象方法 - 子类必须实现 ============
|
||||
|
||||
class ProtocolNode(WorkstationBase):
|
||||
def __init__(self, station_resource: Optional[PLRResource], *args, **kwargs):
|
||||
super().__init__(station_resource, *args, **kwargs)
|
||||
# @abstractmethod
|
||||
# def _register_supported_workflows(self):
|
||||
# """注册支持的工作流 - 子类必须实现"""
|
||||
# pass
|
||||
|
||||
# @abstractmethod
|
||||
# def _execute_workflow_impl(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
|
||||
# """执行工作流的具体实现 - 子类必须实现"""
|
||||
# pass
|
||||
|
||||
# @abstractmethod
|
||||
# def _stop_workflow_impl(self, emergency: bool = False) -> bool:
|
||||
# """停止工作流的具体实现 - 子类必须实现"""
|
||||
# pass
|
||||
|
||||
class WorkstationExample(WorkstationBase):
|
||||
"""工作站示例实现"""
|
||||
|
||||
def _register_supported_workflows(self):
|
||||
"""注册支持的工作流"""
|
||||
self.supported_workflows["example_workflow"] = WorkflowInfo(
|
||||
name="example_workflow",
|
||||
description="这是一个示例工作流",
|
||||
estimated_duration=300.0,
|
||||
required_materials=["sample_plate"],
|
||||
output_product="processed_plate",
|
||||
parameters_schema={"param1": "string", "param2": "integer"},
|
||||
)
|
||||
|
||||
def _execute_workflow_impl(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
|
||||
"""执行工作流的具体实现"""
|
||||
if workflow_name not in self.supported_workflows:
|
||||
logger.error(f"工作站 {self.device_id} 不支持工作流: {workflow_name}")
|
||||
return False
|
||||
|
||||
# 这里添加实际的工作流逻辑
|
||||
logger.info(f"工作站 {self.device_id} 正在执行工作流: {workflow_name} with parameters {parameters}")
|
||||
return True
|
||||
|
||||
def _stop_workflow_impl(self, emergency: bool = False) -> bool:
|
||||
"""停止工作流的具体实现"""
|
||||
# 这里添加实际的停止逻辑
|
||||
logger.info(f"工作站 {self.device_id} 正在停止工作流 (紧急: {emergency})")
|
||||
return True
|
||||
812
unilabos/registry/devices/bioyond_workstation.yaml
Normal file
812
unilabos/registry/devices/bioyond_workstation.yaml
Normal file
@@ -0,0 +1,812 @@
|
||||
bioyondworkstation_device:
|
||||
category:
|
||||
- bioyond_workstation
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-Bioystation_1_to_2_task:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: Bioystation_1_to_2_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-Bioystation_3_to_2_task:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: Bioystation_3_to_2_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-Bioystation_feeding4to3_from_xlsx_task:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: Bioystation_feeding4to3_from_xlsx_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-Bioystation_scheduler_continue_task:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: Bioystation_scheduler_continue_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-Bioystation_scheduler_start_task:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: Bioystation_scheduler_start_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-Bioystation_scheduler_stop_task:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: Bioystation_scheduler_stop_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-Bioystation_start_experiment_task:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: Bioystation_start_experiment_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-auto_batch_outbound_from_xlsx:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
xlsx_path: null
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
xlsx_path:
|
||||
type: string
|
||||
required:
|
||||
- xlsx_path
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: auto_batch_outbound_from_xlsx参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-auto_feeding4to3_from_xlsx:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
xlsx_path: null
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
xlsx_path:
|
||||
type: string
|
||||
required:
|
||||
- xlsx_path
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: auto_feeding4to3_from_xlsx参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-create_orders:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
xlsx_path: null
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
xlsx_path:
|
||||
type: string
|
||||
required:
|
||||
- xlsx_path
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: create_orders参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-order_list:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
begin_time: null
|
||||
end_time: null
|
||||
filter_text: null
|
||||
page: 10
|
||||
skip: 0
|
||||
status: null
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
begin_time:
|
||||
type: string
|
||||
end_time:
|
||||
type: string
|
||||
filter_text:
|
||||
type: string
|
||||
page:
|
||||
default: 10
|
||||
type: integer
|
||||
skip:
|
||||
default: 0
|
||||
type: integer
|
||||
status:
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: order_list参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-order_list_v2:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
beginTime: ''
|
||||
endTime: ''
|
||||
filter: ''
|
||||
pageCount: 1
|
||||
skipCount: 0
|
||||
sorting: ''
|
||||
status: ''
|
||||
timeType: string
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
beginTime:
|
||||
default: ''
|
||||
type: string
|
||||
endTime:
|
||||
default: ''
|
||||
type: string
|
||||
filter:
|
||||
default: ''
|
||||
type: string
|
||||
pageCount:
|
||||
default: 1
|
||||
type: integer
|
||||
skipCount:
|
||||
default: 0
|
||||
type: integer
|
||||
sorting:
|
||||
default: ''
|
||||
type: string
|
||||
status:
|
||||
default: ''
|
||||
type: string
|
||||
timeType:
|
||||
default: string
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: order_list_v2参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-order_report:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
order_id: null
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
order_id:
|
||||
type: string
|
||||
required:
|
||||
- order_id
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: order_report参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-report_material_change:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
material_obj: null
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
material_obj:
|
||||
type: object
|
||||
required:
|
||||
- material_obj
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: report_material_change参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-report_order_finish:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
completion_time: null
|
||||
end_time: null
|
||||
order_code: null
|
||||
order_name: null
|
||||
start_time: null
|
||||
status: '30'
|
||||
used_materials: null
|
||||
workflow_status: Finished
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
completion_time:
|
||||
type: string
|
||||
end_time:
|
||||
type: string
|
||||
order_code:
|
||||
type: string
|
||||
order_name:
|
||||
type: string
|
||||
start_time:
|
||||
type: string
|
||||
status:
|
||||
default: '30'
|
||||
type: string
|
||||
used_materials:
|
||||
type: string
|
||||
workflow_status:
|
||||
default: Finished
|
||||
type: string
|
||||
required:
|
||||
- order_code
|
||||
- order_name
|
||||
- start_time
|
||||
- end_time
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: report_order_finish参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-report_step_finish:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
end_time: null
|
||||
execution_status: completed
|
||||
order_code: null
|
||||
order_name: null
|
||||
sample_id: null
|
||||
start_time: null
|
||||
step_id: null
|
||||
step_name: null
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
end_time:
|
||||
type: string
|
||||
execution_status:
|
||||
default: completed
|
||||
type: string
|
||||
order_code:
|
||||
type: string
|
||||
order_name:
|
||||
type: string
|
||||
sample_id:
|
||||
type: string
|
||||
start_time:
|
||||
type: string
|
||||
step_id:
|
||||
type: string
|
||||
step_name:
|
||||
type: string
|
||||
required:
|
||||
- order_code
|
||||
- order_name
|
||||
- step_name
|
||||
- step_id
|
||||
- sample_id
|
||||
- start_time
|
||||
- end_time
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: report_step_finish参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-run_full_workflow:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
inbound_items: null
|
||||
orders: null
|
||||
poll_filter_code: null
|
||||
poll_interval_s: 5
|
||||
poll_timeout_s: 600
|
||||
takeout_order_id: null
|
||||
transfer_source: null
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
inbound_items:
|
||||
items:
|
||||
type: object
|
||||
type: array
|
||||
orders:
|
||||
items:
|
||||
type: object
|
||||
type: array
|
||||
poll_filter_code:
|
||||
type: string
|
||||
poll_interval_s:
|
||||
default: 5
|
||||
type: integer
|
||||
poll_timeout_s:
|
||||
default: 600
|
||||
type: integer
|
||||
takeout_order_id:
|
||||
type: string
|
||||
transfer_source:
|
||||
type: string
|
||||
required:
|
||||
- inbound_items
|
||||
- orders
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: run_full_workflow参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-scheduler_continue:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: scheduler_continue参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-scheduler_start:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: scheduler_start参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-scheduler_stop:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: scheduler_stop参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-start_station1_internal_flow:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: start_station1_internal_flow参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-storage_batch_inbound:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
items: null
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- items
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: storage_batch_inbound参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-storage_inbound:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
location_id: null
|
||||
material_id: null
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
location_id:
|
||||
type: string
|
||||
material_id:
|
||||
type: string
|
||||
required:
|
||||
- material_id
|
||||
- location_id
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: storage_inbound参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-take_out:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
material_ids: null
|
||||
order_id: null
|
||||
preintake_ids: null
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
material_ids:
|
||||
type: string
|
||||
order_id:
|
||||
type: string
|
||||
preintake_ids:
|
||||
type: string
|
||||
required:
|
||||
- order_id
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: take_out参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-test_benyao_workstation:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
num1: null
|
||||
num2: null
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
num1:
|
||||
type: string
|
||||
num2:
|
||||
type: string
|
||||
required:
|
||||
- num1
|
||||
- num2
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: test_benyao_workstation参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-transfer_1_to_2:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: transfer_1_to_2参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-transfer_3_to_2_to_1:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
source_wh_id: 3a19debc-84b4-0359-e2d4-b3beea49348b
|
||||
source_x: 1
|
||||
source_y: 1
|
||||
source_z: 1
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
source_wh_id:
|
||||
default: 3a19debc-84b4-0359-e2d4-b3beea49348b
|
||||
type: string
|
||||
source_x:
|
||||
default: 1
|
||||
type: integer
|
||||
source_y:
|
||||
default: 1
|
||||
type: integer
|
||||
source_z:
|
||||
default: 1
|
||||
type: integer
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: transfer_3_to_2_to_1参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-wait_for_transfer_task:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
filter_text: null
|
||||
interval: 5
|
||||
timeout: 3000
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
filter_text:
|
||||
type: string
|
||||
interval:
|
||||
default: 5
|
||||
type: integer
|
||||
timeout:
|
||||
default: 3000
|
||||
type: integer
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: wait_for_transfer_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
module: unilabos.devices.workstation.bioyond_cell.bioyond_workstation:BioyondWorkstation
|
||||
status_types: {}
|
||||
type: python
|
||||
config_info: []
|
||||
description: 宜宾配液分液工站
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
bioyond_config:
|
||||
type: string
|
||||
debug_mode:
|
||||
default: false
|
||||
type: boolean
|
||||
station_resource:
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
data:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
registry_type: device
|
||||
version: 1.0.0
|
||||
506
unilabos/registry/devices/dispensing_station_bioyond.yaml
Normal file
506
unilabos/registry/devices/dispensing_station_bioyond.yaml
Normal file
@@ -0,0 +1,506 @@
|
||||
dispensing_station.bioyond:
|
||||
category:
|
||||
- work_station
|
||||
- dispensing_station_bioyond
|
||||
class:
|
||||
action_value_mappings:
|
||||
bioyond_sync:
|
||||
feedback: {}
|
||||
goal:
|
||||
force_sync: force_sync
|
||||
sync_type: sync_type
|
||||
goal_default:
|
||||
force_sync: false
|
||||
sync_type: full
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: 从Bioyond系统同步物料
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
force_sync:
|
||||
description: 是否强制同步
|
||||
type: boolean
|
||||
sync_type:
|
||||
description: 同步类型
|
||||
enum:
|
||||
- full
|
||||
- incremental
|
||||
type: string
|
||||
required:
|
||||
- sync_type
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: bioyond_sync参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
bioyond_update:
|
||||
feedback: {}
|
||||
goal:
|
||||
material_ids: material_ids
|
||||
sync_all: sync_all
|
||||
goal_default:
|
||||
material_ids: []
|
||||
sync_all: true
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: 将本地物料变更同步到Bioyond
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
material_ids:
|
||||
description: 要同步的物料ID列表
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
sync_all:
|
||||
description: 是否同步所有物料
|
||||
type: boolean
|
||||
required:
|
||||
- sync_all
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: bioyond_update参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
create_90_10_vial_feeding_task:
|
||||
feedback: {}
|
||||
goal:
|
||||
delay_time: delay_time
|
||||
order_name: order_name
|
||||
percent_10_1_assign_material_name: percent_10_1_assign_material_name
|
||||
percent_10_1_liquid_material_name: percent_10_1_liquid_material_name
|
||||
percent_10_1_target_weigh: percent_10_1_target_weigh
|
||||
percent_10_1_volume: percent_10_1_volume
|
||||
percent_10_2_assign_material_name: percent_10_2_assign_material_name
|
||||
percent_10_2_liquid_material_name: percent_10_2_liquid_material_name
|
||||
percent_10_2_target_weigh: percent_10_2_target_weigh
|
||||
percent_10_2_volume: percent_10_2_volume
|
||||
percent_90_1_assign_material_name: percent_90_1_assign_material_name
|
||||
percent_90_1_target_weigh: percent_90_1_target_weigh
|
||||
percent_90_2_assign_material_name: percent_90_2_assign_material_name
|
||||
percent_90_2_target_weigh: percent_90_2_target_weigh
|
||||
percent_90_3_assign_material_name: percent_90_3_assign_material_name
|
||||
percent_90_3_target_weigh: percent_90_3_target_weigh
|
||||
speed: speed
|
||||
temperature: temperature
|
||||
goal_default:
|
||||
delay_time: '600'
|
||||
order_name: ''
|
||||
percent_10_1_assign_material_name: ''
|
||||
percent_10_1_liquid_material_name: ''
|
||||
percent_10_1_target_weigh: ''
|
||||
percent_10_1_volume: ''
|
||||
percent_10_2_assign_material_name: ''
|
||||
percent_10_2_liquid_material_name: ''
|
||||
percent_10_2_target_weigh: ''
|
||||
percent_10_2_volume: ''
|
||||
percent_90_1_assign_material_name: ''
|
||||
percent_90_1_target_weigh: ''
|
||||
percent_90_2_assign_material_name: ''
|
||||
percent_90_2_target_weigh: ''
|
||||
percent_90_3_assign_material_name: ''
|
||||
percent_90_3_target_weigh: ''
|
||||
speed: '400'
|
||||
temperature: '20'
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: 创建90%/10%小瓶投料任务
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
delay_time:
|
||||
default: '600'
|
||||
description: 延迟时间(s)
|
||||
type: string
|
||||
order_name:
|
||||
description: 任务名称
|
||||
type: string
|
||||
percent_10_1_assign_material_name:
|
||||
description: 10%组分1物料名称
|
||||
type: string
|
||||
percent_10_1_liquid_material_name:
|
||||
description: 10%组分1液体物料名称
|
||||
type: string
|
||||
percent_10_1_target_weigh:
|
||||
description: 10%组分1目标重量(g)
|
||||
type: string
|
||||
percent_10_1_volume:
|
||||
description: 10%组分1液体体积(mL)
|
||||
type: string
|
||||
percent_10_2_assign_material_name:
|
||||
description: 10%组分2物料名称
|
||||
type: string
|
||||
percent_10_2_liquid_material_name:
|
||||
description: 10%组分2液体物料名称
|
||||
type: string
|
||||
percent_10_2_target_weigh:
|
||||
description: 10%组分2目标重量(g)
|
||||
type: string
|
||||
percent_10_2_volume:
|
||||
description: 10%组分2液体体积(mL)
|
||||
type: string
|
||||
percent_90_1_assign_material_name:
|
||||
description: 90%组分1物料名称
|
||||
type: string
|
||||
percent_90_1_target_weigh:
|
||||
description: 90%组分1目标重量(g)
|
||||
type: string
|
||||
percent_90_2_assign_material_name:
|
||||
description: 90%组分2物料名称
|
||||
type: string
|
||||
percent_90_2_target_weigh:
|
||||
description: 90%组分2目标重量(g)
|
||||
type: string
|
||||
percent_90_3_assign_material_name:
|
||||
description: 90%组分3物料名称
|
||||
type: string
|
||||
percent_90_3_target_weigh:
|
||||
description: 90%组分3目标重量(g)
|
||||
type: string
|
||||
speed:
|
||||
default: '400'
|
||||
description: 搅拌速度(rpm)
|
||||
type: string
|
||||
temperature:
|
||||
default: '20'
|
||||
description: 温度(°C)
|
||||
type: string
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: create_90_10_vial_feeding_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
create_batch_90_10_vial_feeding_task:
|
||||
feedback: {}
|
||||
goal:
|
||||
batch_data: batch_data
|
||||
goal_default:
|
||||
batch_data: '{}'
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: 创建批量90%10%小瓶投料任务
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
batch_data:
|
||||
description: 批量90%10%小瓶投料任务数据(JSON格式),包含batch_name、tasks列表和global_settings
|
||||
type: string
|
||||
required:
|
||||
- batch_data
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: create_batch_90_10_vial_feeding_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
create_batch_diamine_solution_task:
|
||||
feedback: {}
|
||||
goal:
|
||||
batch_data: batch_data
|
||||
goal_default:
|
||||
batch_data: '{}'
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: 创建批量二胺溶液配制任务
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
batch_data:
|
||||
description: 批量二胺溶液配制任务数据(JSON格式),包含batch_name、tasks列表和global_settings
|
||||
type: string
|
||||
required:
|
||||
- batch_data
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: create_batch_diamine_solution_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
create_diamine_solution_task:
|
||||
feedback: {}
|
||||
goal:
|
||||
delay_time: delay_time
|
||||
hold_m_name: hold_m_name
|
||||
liquid_material_name: liquid_material_name
|
||||
material_name: material_name
|
||||
order_name: order_name
|
||||
speed: speed
|
||||
target_weigh: target_weigh
|
||||
temperature: temperature
|
||||
volume: volume
|
||||
goal_default:
|
||||
delay_time: '600'
|
||||
hold_m_name: ''
|
||||
liquid_material_name: NMP
|
||||
material_name: ''
|
||||
order_name: ''
|
||||
speed: '400'
|
||||
target_weigh: ''
|
||||
temperature: '20'
|
||||
volume: ''
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: 创建二胺溶液配制任务
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
delay_time:
|
||||
default: '600'
|
||||
description: 延迟时间(s)
|
||||
type: string
|
||||
hold_m_name:
|
||||
description: 库位名称(如ODA-1)
|
||||
type: string
|
||||
liquid_material_name:
|
||||
default: NMP
|
||||
description: 液体物料名称
|
||||
type: string
|
||||
material_name:
|
||||
description: 固体物料名称
|
||||
type: string
|
||||
order_name:
|
||||
description: 任务名称
|
||||
type: string
|
||||
speed:
|
||||
default: '400'
|
||||
description: 搅拌速度(rpm)
|
||||
type: string
|
||||
target_weigh:
|
||||
description: 固体目标重量(g)
|
||||
type: string
|
||||
temperature:
|
||||
default: '20'
|
||||
description: 温度(°C)
|
||||
type: string
|
||||
volume:
|
||||
description: 液体体积(mL)
|
||||
type: string
|
||||
required:
|
||||
- material_name
|
||||
- target_weigh
|
||||
- volume
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: create_diamine_solution_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
create_resource:
|
||||
feedback: {}
|
||||
goal:
|
||||
resource_config: resource_config
|
||||
resource_type: resource_type
|
||||
goal_default:
|
||||
resource_config: {}
|
||||
resource_type: ''
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: 创建资源操作
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
resource_config:
|
||||
description: 资源配置
|
||||
type: object
|
||||
resource_type:
|
||||
description: 资源类型
|
||||
type: string
|
||||
required:
|
||||
- resource_type
|
||||
- resource_config
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: create_resource参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
dispensing_material_inbound:
|
||||
feedback: {}
|
||||
goal:
|
||||
location: location
|
||||
material_id: material_id
|
||||
goal_default:
|
||||
location: ''
|
||||
material_id: ''
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: 配液站物料入库操作
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
location:
|
||||
description: 存储位置
|
||||
type: string
|
||||
material_id:
|
||||
description: 物料ID
|
||||
type: string
|
||||
required:
|
||||
- material_id
|
||||
- location
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: dispensing_material_inbound参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
dispensing_material_outbound:
|
||||
feedback: {}
|
||||
goal:
|
||||
material_id: material_id
|
||||
quantity: quantity
|
||||
goal_default:
|
||||
material_id: ''
|
||||
quantity: 0.0
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: 配液站物料出库操作
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
material_id:
|
||||
description: 物料ID
|
||||
type: string
|
||||
quantity:
|
||||
description: 出库数量
|
||||
type: number
|
||||
required:
|
||||
- material_id
|
||||
- quantity
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: dispensing_material_outbound参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
sample_waste_removal:
|
||||
feedback: {}
|
||||
goal:
|
||||
sample_id: sample_id
|
||||
waste_type: waste_type
|
||||
goal_default:
|
||||
sample_id: ''
|
||||
waste_type: general
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: 样品废料移除操作
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
sample_id:
|
||||
description: 样品ID
|
||||
type: string
|
||||
waste_type:
|
||||
description: 废料类型
|
||||
enum:
|
||||
- general
|
||||
- hazardous
|
||||
- organic
|
||||
- inorganic
|
||||
type: string
|
||||
required:
|
||||
- sample_id
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: sample_waste_removal参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
module: unilabos.devices.workstation.bioyond_studio.station:BioyondWorkstation
|
||||
protocol_type: []
|
||||
status_types:
|
||||
bioyond_status: dict
|
||||
enable_dispensing_station: bool
|
||||
enable_reaction_station: bool
|
||||
station_type: str
|
||||
type: python
|
||||
config_info: []
|
||||
description: Bioyond配液站 - 专门用于物料配制和管理的工作站
|
||||
handles: []
|
||||
icon: 配液站.webp
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
bioyond_config:
|
||||
description: Bioyond API配置
|
||||
properties:
|
||||
api_host:
|
||||
description: Bioyond API主机地址
|
||||
type: string
|
||||
api_key:
|
||||
description: Bioyond API密钥
|
||||
type: string
|
||||
material_type_mappings:
|
||||
description: 物料类型映射配置
|
||||
type: object
|
||||
workflow_mappings:
|
||||
description: 工作流映射配置
|
||||
type: object
|
||||
type: object
|
||||
deck:
|
||||
description: Deck配置
|
||||
type: object
|
||||
station_config:
|
||||
description: 配液站配置
|
||||
properties:
|
||||
description:
|
||||
description: 配液站描述
|
||||
type: string
|
||||
enable_dispensing_station:
|
||||
default: true
|
||||
description: 启用配液站功能
|
||||
type: boolean
|
||||
enable_reaction_station:
|
||||
default: false
|
||||
description: 禁用反应站功能
|
||||
type: boolean
|
||||
station_name:
|
||||
description: 配液站名称
|
||||
type: string
|
||||
station_type:
|
||||
default: dispensing_station
|
||||
description: 站点类型 - 配液站
|
||||
enum:
|
||||
- dispensing_station
|
||||
type: string
|
||||
type: object
|
||||
required: []
|
||||
type: object
|
||||
data:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
version: 1.0.0
|
||||
@@ -7304,151 +7304,6 @@ liquid_handler.prcxi:
|
||||
title: LiquidHandlerRemove
|
||||
type: object
|
||||
type: LiquidHandlerRemove
|
||||
set_group:
|
||||
feedback: {}
|
||||
goal:
|
||||
group_name: group_name
|
||||
volumes: volumes
|
||||
wells: wells
|
||||
goal_default:
|
||||
group_name: ''
|
||||
volumes:
|
||||
- 0.0
|
||||
wells:
|
||||
- category: ''
|
||||
children: []
|
||||
config: ''
|
||||
data: ''
|
||||
id: ''
|
||||
name: ''
|
||||
parent: ''
|
||||
pose:
|
||||
orientation:
|
||||
w: 1.0
|
||||
x: 0.0
|
||||
y: 0.0
|
||||
z: 0.0
|
||||
position:
|
||||
x: 0.0
|
||||
y: 0.0
|
||||
z: 0.0
|
||||
sample_id: ''
|
||||
type: ''
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties: {}
|
||||
required: []
|
||||
title: LiquidHandlerSetGroup_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
group_name:
|
||||
type: string
|
||||
volumes:
|
||||
items:
|
||||
type: number
|
||||
type: array
|
||||
wells:
|
||||
items:
|
||||
properties:
|
||||
category:
|
||||
type: string
|
||||
children:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
config:
|
||||
type: string
|
||||
data:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
parent:
|
||||
type: string
|
||||
pose:
|
||||
properties:
|
||||
orientation:
|
||||
properties:
|
||||
w:
|
||||
type: number
|
||||
x:
|
||||
type: number
|
||||
y:
|
||||
type: number
|
||||
z:
|
||||
type: number
|
||||
required:
|
||||
- x
|
||||
- y
|
||||
- z
|
||||
- w
|
||||
title: orientation
|
||||
type: object
|
||||
position:
|
||||
properties:
|
||||
x:
|
||||
type: number
|
||||
y:
|
||||
type: number
|
||||
z:
|
||||
type: number
|
||||
required:
|
||||
- x
|
||||
- y
|
||||
- z
|
||||
title: position
|
||||
type: object
|
||||
required:
|
||||
- position
|
||||
- orientation
|
||||
title: pose
|
||||
type: object
|
||||
sample_id:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- sample_id
|
||||
- children
|
||||
- parent
|
||||
- type
|
||||
- category
|
||||
- pose
|
||||
- config
|
||||
- data
|
||||
title: wells
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- group_name
|
||||
- wells
|
||||
- volumes
|
||||
title: LiquidHandlerSetGroup_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: LiquidHandlerSetGroup_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: LiquidHandlerSetGroup
|
||||
type: object
|
||||
type: LiquidHandlerSetGroup
|
||||
set_liquid:
|
||||
feedback: {}
|
||||
goal:
|
||||
@@ -7824,56 +7679,6 @@ liquid_handler.prcxi:
|
||||
title: Transfer
|
||||
type: object
|
||||
type: Transfer
|
||||
transfer_group:
|
||||
feedback: {}
|
||||
goal:
|
||||
source_group_name: source_group_name
|
||||
target_group_name: target_group_name
|
||||
unit_volume: unit_volume
|
||||
goal_default:
|
||||
source_group_name: ''
|
||||
target_group_name: ''
|
||||
unit_volume: 0.0
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties: {}
|
||||
required: []
|
||||
title: LiquidHandlerTransferGroup_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
source_group_name:
|
||||
type: string
|
||||
target_group_name:
|
||||
type: string
|
||||
unit_volume:
|
||||
type: number
|
||||
required:
|
||||
- source_group_name
|
||||
- target_group_name
|
||||
- unit_volume
|
||||
title: LiquidHandlerTransferGroup_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: LiquidHandlerTransferGroup_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: LiquidHandlerTransferGroup
|
||||
type: object
|
||||
type: LiquidHandlerTransferGroup
|
||||
transfer_liquid:
|
||||
feedback: {}
|
||||
goal:
|
||||
|
||||
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
|
||||
384
unilabos/registry/devices/reaction_station_bioyond.yaml
Normal file
384
unilabos/registry/devices/reaction_station_bioyond.yaml
Normal file
@@ -0,0 +1,384 @@
|
||||
reaction_station.bioyond:
|
||||
category:
|
||||
- work_station
|
||||
- reaction_station_bioyond
|
||||
class:
|
||||
action_value_mappings:
|
||||
bioyond_sync:
|
||||
feedback: {}
|
||||
goal:
|
||||
force_sync: force_sync
|
||||
sync_type: sync_type
|
||||
goal_default:
|
||||
force_sync: false
|
||||
sync_type: full
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: 从Bioyond系统同步物料
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
force_sync:
|
||||
description: 是否强制同步
|
||||
type: boolean
|
||||
sync_type:
|
||||
description: 同步类型
|
||||
enum:
|
||||
- full
|
||||
- incremental
|
||||
type: string
|
||||
required:
|
||||
- sync_type
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: bioyond_sync参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
bioyond_update:
|
||||
feedback: {}
|
||||
goal:
|
||||
material_ids: material_ids
|
||||
sync_all: sync_all
|
||||
goal_default:
|
||||
material_ids: []
|
||||
sync_all: true
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: 将本地物料变更同步到Bioyond
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
material_ids:
|
||||
description: 要同步的物料ID列表
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
sync_all:
|
||||
description: 是否同步所有物料
|
||||
type: boolean
|
||||
required:
|
||||
- sync_all
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: bioyond_update参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
reaction_station_drip_back:
|
||||
feedback: {}
|
||||
goal:
|
||||
assign_material_name: assign_material_name
|
||||
time: time
|
||||
torque_variation: torque_variation
|
||||
volume: volume
|
||||
goal_default:
|
||||
assign_material_name: ''
|
||||
time: ''
|
||||
torque_variation: ''
|
||||
volume: ''
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: 反应站滴回操作
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
assign_material_name:
|
||||
description: 溶剂名称
|
||||
type: string
|
||||
time:
|
||||
description: 观察时间(单位min)
|
||||
type: string
|
||||
torque_variation:
|
||||
description: 是否观察1否2是
|
||||
type: string
|
||||
volume:
|
||||
description: 投料体积
|
||||
type: string
|
||||
required:
|
||||
- volume
|
||||
- assign_material_name
|
||||
- time
|
||||
- torque_variation
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: reaction_station_drip_back参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
reaction_station_liquid_feed:
|
||||
feedback: {}
|
||||
goal:
|
||||
assign_material_name: assign_material_name
|
||||
time: time
|
||||
titration_type: titration_type
|
||||
torque_variation: torque_variation
|
||||
volume: volume
|
||||
goal_default:
|
||||
assign_material_name: ''
|
||||
time: ''
|
||||
titration_type: ''
|
||||
torque_variation: ''
|
||||
volume: ''
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: 反应站液体进料操作
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
assign_material_name:
|
||||
description: 溶剂名称
|
||||
type: string
|
||||
time:
|
||||
description: 观察时间(单位min)
|
||||
type: string
|
||||
titration_type:
|
||||
description: 滴定类型1否2是
|
||||
type: string
|
||||
torque_variation:
|
||||
description: 是否观察1否2是
|
||||
type: string
|
||||
volume:
|
||||
description: 投料体积
|
||||
type: string
|
||||
required:
|
||||
- titration_type
|
||||
- volume
|
||||
- assign_material_name
|
||||
- time
|
||||
- torque_variation
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: reaction_station_liquid_feed参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
reaction_station_process_execute:
|
||||
feedback: {}
|
||||
goal:
|
||||
task_name: task_name
|
||||
workflow_name: workflow_name
|
||||
goal_default:
|
||||
task_name: ''
|
||||
workflow_name: ''
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: 反应站流程执行
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
task_name:
|
||||
description: 任务名称
|
||||
type: string
|
||||
workflow_name:
|
||||
description: 工作流名称
|
||||
type: string
|
||||
required:
|
||||
- workflow_name
|
||||
- task_name
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: reaction_station_process_execute参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
reaction_station_reactor_taken_out:
|
||||
feedback: {}
|
||||
goal:
|
||||
order_id: order_id
|
||||
preintake_id: preintake_id
|
||||
goal_default:
|
||||
order_id: ''
|
||||
preintake_id: ''
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: 反应站反应器取出操作 - 通过订单ID和预取样ID进行精确控制
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
order_id:
|
||||
description: 订单ID,用于标识要取出的订单
|
||||
type: string
|
||||
preintake_id:
|
||||
description: 预取样ID,用于标识具体的取样任务
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
code:
|
||||
description: 操作结果代码(1表示成功,0表示失败)
|
||||
type: integer
|
||||
return_info:
|
||||
description: 操作结果详细信息
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: reaction_station_reactor_taken_out参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
reaction_station_solid_feed_vial:
|
||||
feedback: {}
|
||||
goal:
|
||||
assign_material_name: assign_material_name
|
||||
material_id: material_id
|
||||
time: time
|
||||
torque_variation: torque_variation
|
||||
goal_default:
|
||||
assign_material_name: ''
|
||||
material_id: ''
|
||||
time: ''
|
||||
torque_variation: ''
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: 反应站固体进料操作
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
assign_material_name:
|
||||
description: 固体名称_粉末加样模块-投料
|
||||
type: string
|
||||
material_id:
|
||||
description: 固体投料类型_粉末加样模块-投料
|
||||
type: string
|
||||
time:
|
||||
description: 观察时间_反应模块-观察搅拌结果
|
||||
type: string
|
||||
torque_variation:
|
||||
description: 是否观察1否2是_反应模块-观察搅拌结果
|
||||
type: string
|
||||
required:
|
||||
- assign_material_name
|
||||
- material_id
|
||||
- time
|
||||
- torque_variation
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: reaction_station_solid_feed_vial参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
reaction_station_take_in:
|
||||
feedback: {}
|
||||
goal:
|
||||
assign_material_name: assign_material_name
|
||||
cutoff: cutoff
|
||||
temperature: temperature
|
||||
goal_default:
|
||||
assign_material_name: ''
|
||||
cutoff: ''
|
||||
temperature: ''
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: 反应站取入操作
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
assign_material_name:
|
||||
description: 物料名称
|
||||
type: string
|
||||
cutoff:
|
||||
description: 截止参数
|
||||
type: string
|
||||
temperature:
|
||||
description: 温度
|
||||
type: string
|
||||
required:
|
||||
- cutoff
|
||||
- temperature
|
||||
- assign_material_name
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: reaction_station_take_in参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
module: unilabos.devices.workstation.bioyond_studio.station:BioyondWorkstation
|
||||
protocol_type: []
|
||||
status_types:
|
||||
bioyond_status: dict
|
||||
enable_dispensing_station: bool
|
||||
enable_reaction_station: bool
|
||||
station_type: str
|
||||
type: python
|
||||
config_info: []
|
||||
description: Bioyond反应站 - 专门用于化学反应操作的工作站
|
||||
handles: []
|
||||
icon: 反应站.webp
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
bioyond_config:
|
||||
description: Bioyond API配置
|
||||
properties:
|
||||
api_host:
|
||||
description: Bioyond API主机地址
|
||||
type: string
|
||||
api_key:
|
||||
description: Bioyond API密钥
|
||||
type: string
|
||||
material_type_mappings:
|
||||
description: 物料类型映射配置
|
||||
type: object
|
||||
workflow_mappings:
|
||||
description: 工作流映射配置
|
||||
type: object
|
||||
type: object
|
||||
deck:
|
||||
description: Deck配置
|
||||
type: object
|
||||
station_config:
|
||||
description: 反应站配置
|
||||
properties:
|
||||
description:
|
||||
description: 反应站描述
|
||||
type: string
|
||||
enable_dispensing_station:
|
||||
default: false
|
||||
description: 禁用配液站功能
|
||||
type: boolean
|
||||
enable_reaction_station:
|
||||
default: true
|
||||
description: 启用反应站功能
|
||||
type: boolean
|
||||
station_name:
|
||||
description: 反应站名称
|
||||
type: string
|
||||
station_type:
|
||||
default: reaction_station
|
||||
description: 站点类型 - 反应站
|
||||
enum:
|
||||
- reaction_station
|
||||
type: string
|
||||
type: object
|
||||
required: []
|
||||
type: object
|
||||
data:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
version: 1.0.0
|
||||
@@ -1,3 +1,517 @@
|
||||
bettery_station_registry:
|
||||
category:
|
||||
- work_station
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-change_hole_sheet_to_2:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
hole: null
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
hole:
|
||||
type: object
|
||||
required:
|
||||
- hole
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: change_hole_sheet_to_2参数
|
||||
type: object
|
||||
type: UniLabJsonCommandAsync
|
||||
auto-fill_plate:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: fill_plate参数
|
||||
type: object
|
||||
type: UniLabJsonCommandAsync
|
||||
auto-fun_wuliao_test:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: fun_wuliao_test参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-func_allpack_cmd:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
assembly_pressure: 4200
|
||||
assembly_type: 7
|
||||
elec_num: null
|
||||
elec_use_num: null
|
||||
elec_vol: 50
|
||||
file_path: D:\coin_cell_data
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
assembly_pressure:
|
||||
default: 4200
|
||||
type: integer
|
||||
assembly_type:
|
||||
default: 7
|
||||
type: integer
|
||||
elec_num:
|
||||
type: string
|
||||
elec_use_num:
|
||||
type: string
|
||||
elec_vol:
|
||||
default: 50
|
||||
type: integer
|
||||
file_path:
|
||||
default: D:\coin_cell_data
|
||||
type: string
|
||||
required:
|
||||
- elec_num
|
||||
- elec_use_num
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: func_allpack_cmd参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-func_get_csv_export_status:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: func_get_csv_export_status参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-func_pack_device_auto:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: func_pack_device_auto参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-func_pack_device_init:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: func_pack_device_init参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-func_pack_device_start:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: func_pack_device_start参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-func_pack_device_stop:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: func_pack_device_stop参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-func_pack_get_msg_cmd:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
file_path: D:\coin_cell_data
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
file_path:
|
||||
default: D:\coin_cell_data
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: func_pack_get_msg_cmd参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-func_pack_send_bottle_num:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
bottle_num: null
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
bottle_num:
|
||||
type: string
|
||||
required:
|
||||
- bottle_num
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: func_pack_send_bottle_num参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-func_pack_send_finished_cmd:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: func_pack_send_finished_cmd参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-func_pack_send_msg_cmd:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
assembly_pressure: null
|
||||
assembly_type: null
|
||||
elec_use_num: null
|
||||
elec_vol: null
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
assembly_pressure:
|
||||
type: string
|
||||
assembly_type:
|
||||
type: string
|
||||
elec_use_num:
|
||||
type: string
|
||||
elec_vol:
|
||||
type: string
|
||||
required:
|
||||
- elec_use_num
|
||||
- elec_vol
|
||||
- assembly_type
|
||||
- assembly_pressure
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: func_pack_send_msg_cmd参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-func_read_data_and_output:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
file_path: D:\coin_cell_data
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
file_path:
|
||||
default: D:\coin_cell_data
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: func_read_data_and_output参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-func_stop_read_data:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: func_stop_read_data参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-modify_deck_name:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
resource_name: null
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
resource_name:
|
||||
type: string
|
||||
required:
|
||||
- resource_name
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: modify_deck_name参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-post_init:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
ros_node: null
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
ros_node:
|
||||
type: object
|
||||
required:
|
||||
- ros_node
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: post_init参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
module: unilabos.devices.workstation.coin_cell_assembly.coin_cell_assembly:CoinCellAssemblyWorkstation
|
||||
status_types:
|
||||
data_assembly_coin_cell_num: int
|
||||
data_assembly_pressure: int
|
||||
data_assembly_time: float
|
||||
data_axis_x_pos: float
|
||||
data_axis_y_pos: float
|
||||
data_axis_z_pos: float
|
||||
data_coin_cell_code: str
|
||||
data_coin_num: int
|
||||
data_electrolyte_code: str
|
||||
data_electrolyte_volume: int
|
||||
data_glove_box_o2_content: float
|
||||
data_glove_box_pressure: float
|
||||
data_glove_box_water_content: float
|
||||
data_open_circuit_voltage: float
|
||||
data_pole_weight: float
|
||||
request_rec_msg_status: bool
|
||||
request_send_msg_status: bool
|
||||
sys_mode: str
|
||||
sys_status: str
|
||||
type: python
|
||||
config_info: []
|
||||
description: ''
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
address:
|
||||
default: 192.168.1.20
|
||||
type: string
|
||||
debug_mode:
|
||||
default: true
|
||||
type: boolean
|
||||
port:
|
||||
default: '502'
|
||||
type: string
|
||||
station_resource:
|
||||
type: object
|
||||
required:
|
||||
- station_resource
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
data_assembly_coin_cell_num:
|
||||
type: integer
|
||||
data_assembly_pressure:
|
||||
type: integer
|
||||
data_assembly_time:
|
||||
type: number
|
||||
data_axis_x_pos:
|
||||
type: number
|
||||
data_axis_y_pos:
|
||||
type: number
|
||||
data_axis_z_pos:
|
||||
type: number
|
||||
data_coin_cell_code:
|
||||
type: string
|
||||
data_coin_num:
|
||||
type: integer
|
||||
data_electrolyte_code:
|
||||
type: string
|
||||
data_electrolyte_volume:
|
||||
type: integer
|
||||
data_glove_box_o2_content:
|
||||
type: number
|
||||
data_glove_box_pressure:
|
||||
type: number
|
||||
data_glove_box_water_content:
|
||||
type: number
|
||||
data_open_circuit_voltage:
|
||||
type: number
|
||||
data_pole_weight:
|
||||
type: number
|
||||
request_rec_msg_status:
|
||||
type: boolean
|
||||
request_send_msg_status:
|
||||
type: boolean
|
||||
sys_mode:
|
||||
type: string
|
||||
sys_status:
|
||||
type: string
|
||||
required:
|
||||
- sys_status
|
||||
- sys_mode
|
||||
- request_rec_msg_status
|
||||
- request_send_msg_status
|
||||
- data_assembly_coin_cell_num
|
||||
- data_assembly_time
|
||||
- data_open_circuit_voltage
|
||||
- data_axis_x_pos
|
||||
- data_axis_y_pos
|
||||
- data_axis_z_pos
|
||||
- data_pole_weight
|
||||
- data_assembly_pressure
|
||||
- data_electrolyte_volume
|
||||
- data_coin_num
|
||||
- data_coin_cell_code
|
||||
- data_electrolyte_code
|
||||
- data_glove_box_pressure
|
||||
- data_glove_box_o2_content
|
||||
- data_glove_box_water_content
|
||||
type: object
|
||||
version: 1.0.0
|
||||
workstation:
|
||||
category:
|
||||
- work_station
|
||||
@@ -6024,9 +6538,97 @@ workstation:
|
||||
title: WashSolid
|
||||
type: object
|
||||
type: WashSolid
|
||||
module: unilabos.devices.workstation.workstation_base:ProtocolNode
|
||||
auto-create_ros_action_server:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
action_name: null
|
||||
action_value_mapping: null
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: create_ros_action_server的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
action_name:
|
||||
type: string
|
||||
action_value_mapping:
|
||||
type: string
|
||||
required:
|
||||
- action_name
|
||||
- action_value_mapping
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: create_ros_action_server参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-execute_single_action:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
action_kwargs: null
|
||||
action_name: null
|
||||
device_id: null
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: execute_single_action的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
action_kwargs:
|
||||
type: string
|
||||
action_name:
|
||||
type: string
|
||||
device_id:
|
||||
type: string
|
||||
required:
|
||||
- device_id
|
||||
- action_name
|
||||
- action_kwargs
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: execute_single_action参数
|
||||
type: object
|
||||
type: UniLabJsonCommandAsync
|
||||
auto-initialize_device:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
device_config: null
|
||||
device_id: null
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: initialize_device的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
device_config:
|
||||
type: string
|
||||
device_id:
|
||||
type: string
|
||||
required:
|
||||
- device_id
|
||||
- device_config
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: initialize_device参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
module: unilabos.ros.nodes.presets.workstation:ROS2WorkstationNode
|
||||
status_types: {}
|
||||
type: python
|
||||
type: ros2
|
||||
config_info: []
|
||||
description: Workstation
|
||||
handles: []
|
||||
@@ -6034,8 +6636,58 @@ workstation:
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
station_resource:
|
||||
action_value_mappings:
|
||||
type: object
|
||||
children:
|
||||
type: object
|
||||
device_id:
|
||||
type: string
|
||||
driver_instance:
|
||||
type: string
|
||||
hardware_interface:
|
||||
type: object
|
||||
print_publish:
|
||||
default: true
|
||||
type: string
|
||||
protocol_type:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
resource_tracker:
|
||||
type: string
|
||||
status_types:
|
||||
type: object
|
||||
required:
|
||||
- protocol_type
|
||||
- children
|
||||
- driver_instance
|
||||
- device_id
|
||||
- status_types
|
||||
- action_value_mappings
|
||||
- hardware_interface
|
||||
type: object
|
||||
data:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
version: 1.0.0
|
||||
workstation.example:
|
||||
category:
|
||||
- work_station
|
||||
class:
|
||||
action_value_mappings: {}
|
||||
module: unilabos.devices.workstation.workstation_base:WorkstationExample
|
||||
status_types: {}
|
||||
type: python
|
||||
config_info: []
|
||||
description: ''
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
station_resource:
|
||||
type: object
|
||||
required:
|
||||
- station_resource
|
||||
type: object
|
||||
|
||||
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
|
||||
import numpy as np
|
||||
import networkx as nx
|
||||
from pylabrobot.resources import ResourceHolder
|
||||
from unilabos_msgs.msg import Resource
|
||||
|
||||
from unilabos.resources.container import RegularContainer
|
||||
@@ -218,14 +219,20 @@ def dict_to_tree(nodes: dict, devices_only: bool = False) -> list[dict]:
|
||||
# 将节点转换为字典,以便通过 ID 快速查找
|
||||
nodes_list = [node for node in nodes.values() if node.get("type") == "device" or not devices_only]
|
||||
id_list = [node["id"] for node in nodes_list]
|
||||
is_root = {node["id"]: True for node in nodes_list}
|
||||
|
||||
# 初始化每个节点的 children 为包含节点字典的列表
|
||||
for node in nodes_list:
|
||||
node["children"] = [nodes[child_id] for child_id in node.get("children", [])]
|
||||
for child_id in node.get("children", []):
|
||||
if child_id in is_root:
|
||||
is_root[child_id] = False
|
||||
|
||||
# 找到根节点并返回
|
||||
root_nodes = [
|
||||
node for node in nodes_list if len(nodes_list) == 1 or node.get("parent", node.get("parent_name")) in [None, "", "None", np.nan] or node.get("parent", node.get("parent_name")) not in id_list
|
||||
node
|
||||
for node in nodes_list
|
||||
if is_root.get(node["id"], False) or len(nodes_list) == 1
|
||||
]
|
||||
|
||||
# 如果存在多个根节点,返回所有根节点
|
||||
@@ -235,6 +242,7 @@ def dict_to_tree(nodes: dict, devices_only: bool = False) -> list[dict]:
|
||||
def dict_to_nested_dict(nodes: dict, devices_only: bool = False) -> dict:
|
||||
# 将节点转换为字典,以便通过 ID 快速查找
|
||||
nodes_list = [node for node in nodes.values() if node.get("type") == "device" or not devices_only]
|
||||
is_root = {node["id"]: True for node in nodes_list}
|
||||
|
||||
# 初始化每个节点的 children 为包含节点字典的列表
|
||||
for node in nodes_list:
|
||||
@@ -243,14 +251,17 @@ def dict_to_nested_dict(nodes: dict, devices_only: bool = False) -> dict:
|
||||
for child_id in node.get("children", [])
|
||||
if nodes[child_id].get("type") == "device" or not devices_only
|
||||
}
|
||||
if len(node["children"]) > 0 and node["type"].lower() == "device" and devices_only:
|
||||
for child_id in node.get("children", []):
|
||||
if child_id in is_root:
|
||||
is_root[child_id] = False
|
||||
if len(node["children"]) > 0 and node["type"].lower() == "device":
|
||||
node["config"]["children"] = node["children"]
|
||||
|
||||
# 找到根节点并返回
|
||||
root_nodes = {
|
||||
node["id"]: node
|
||||
for node in nodes_list
|
||||
if node.get("parent", node.get("parent_name")) in [None, "", "None", np.nan] or len(nodes_list) == 1
|
||||
if is_root.get(node["id"], False) or len(nodes_list) == 1
|
||||
}
|
||||
|
||||
# 如果存在多个根节点,返回所有根节点
|
||||
@@ -470,7 +481,54 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None, w
|
||||
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.
|
||||
|
||||
If the config is detailed, then do nothing;
|
||||
@@ -502,11 +560,14 @@ def initialize_resource(resource_config: dict) -> list[dict]:
|
||||
|
||||
if resource_class_config["type"] == "pylabrobot":
|
||||
resource_plr = RESOURCE(name=resource_config["name"])
|
||||
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)
|
||||
if resource_config.get("position") is not None:
|
||||
r["position"] = resource_config["position"]
|
||||
r = tree_to_list([r])
|
||||
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)
|
||||
if resource_config.get("position") is not None:
|
||||
r["position"] = resource_config["position"]
|
||||
r = tree_to_list([r])
|
||||
else:
|
||||
r = resource_plr
|
||||
elif resource_class_config["type"] == "unilabos":
|
||||
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"})
|
||||
|
||||
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:
|
||||
# 创建实例,额外补充一个给protocol node的字段,后面考虑取消
|
||||
data["children"] = self.children
|
||||
station_resource_dict = data.get("station_resource")
|
||||
if station_resource_dict:
|
||||
deck_dict = data.get("deck")
|
||||
if deck_dict:
|
||||
from pylabrobot.resources import Deck, Resource
|
||||
plrc = PyLabRobotCreator(Deck, self.children, self.resource_tracker)
|
||||
station_resource = plrc.create_instance(station_resource_dict)
|
||||
data["station_resource"] = station_resource
|
||||
deck = plrc.create_instance(deck_dict)
|
||||
data["deck"] = deck
|
||||
else:
|
||||
data["station_resource"] = None
|
||||
data["deck"] = None
|
||||
self.device_instance = super(WorkstationNodeCreator, self).create_instance(data)
|
||||
self.post_create()
|
||||
return self.device_instance
|
||||
|
||||
@@ -68,12 +68,10 @@ set(action_files
|
||||
"action/LiquidHandlerSetTipRack.action"
|
||||
"action/LiquidHandlerStamp.action"
|
||||
"action/LiquidHandlerTransfer.action"
|
||||
"action/LiquidHandlerSetGroup.action"
|
||||
"action/LiquidHandlerTransferBiomek.action"
|
||||
"action/LiquidHandlerIncubateBiomek.action"
|
||||
"action/LiquidHandlerMoveBiomek.action"
|
||||
"action/LiquidHandlerOscillateBiomek.action"
|
||||
"action/LiquidHandlerTransferGroup.action"
|
||||
"action/LiquidHandlerAdd.action"
|
||||
"action/LiquidHandlerMix.action"
|
||||
"action/LiquidHandlerMoveTo.action"
|
||||
@@ -105,10 +103,14 @@ set(action_files
|
||||
"action/PostProcessGrab.action"
|
||||
"action/PostProcessTriggerClean.action"
|
||||
"action/PostProcessTriggerPostPro.action"
|
||||
|
||||
|
||||
"action/ReactionStationDripBack.action"
|
||||
"action/ReactionStationLiquidFeed.action"
|
||||
"action/ReactionStationLiquidFeedBeaker.action"
|
||||
"action/ReactionStationLiquidFeedSolvents.action"
|
||||
"action/ReactionStationLiquidFeedTitration.action"
|
||||
"action/ReactionStationLiquidFeedVialsNonTitration.action"
|
||||
"action/ReactionStationProExecu.action"
|
||||
"action/ReactionStationReactorTakenOut.action"
|
||||
"action/ReactionStationReaTackIn.action"
|
||||
"action/ReactionStationSolidFeedVial.action"
|
||||
)
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# Goal - 滴回去
|
||||
string volume # 投料体积
|
||||
string assign_material_name # 溶剂名称
|
||||
string time # 观察时间(单位min)
|
||||
string torque_variation #是否观察1否2是
|
||||
# Goal - 滴回去操作参数
|
||||
string volume # 投料体积
|
||||
string assign_material_name # 溶剂名称
|
||||
string time # 观察时间(单位min)
|
||||
string torque_variation # 是否观察1否2是
|
||||
---
|
||||
# Result - 操作结果
|
||||
string return_info # 结果消息
|
||||
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
string return_info # 结果消息
|
||||
int32 code # 操作结果代码(1表示成功,0表示失败)
|
||||
---
|
||||
# Feedback - 实时反馈
|
||||
# Feedback - 实时反馈
|
||||
string feedback # 操作过程中的反馈信息
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
# Goal - 液体投料
|
||||
string titration_type # 滴定类型1否2是
|
||||
string volume # 投料体积
|
||||
string assign_material_name # 溶剂名称
|
||||
string time # 观察时间(单位min)
|
||||
string torque_variation #是否观察1否2是
|
||||
---
|
||||
# Result - 操作结果
|
||||
string return_info # 结果消息
|
||||
---
|
||||
# Feedback - 实时反馈
|
||||
15
unilabos_msgs/action/ReactionStationLiquidFeedBeaker.action
Normal file
15
unilabos_msgs/action/ReactionStationLiquidFeedBeaker.action
Normal file
@@ -0,0 +1,15 @@
|
||||
# Goal - 液体投料-烧杯操作参数
|
||||
string volume # 投料体积
|
||||
string assign_material_name # 溶剂名称
|
||||
string titration_type # 滴定类型1否2是
|
||||
string time # 观察时间(单位min)
|
||||
string torque_variation # 是否观察1否2是
|
||||
string temperature # 温度设置
|
||||
---
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
string return_info # 结果消息
|
||||
int32 code # 操作结果代码(1表示成功,0表示失败)
|
||||
---
|
||||
# Feedback - 实时反馈
|
||||
string feedback # 操作过程中的反馈信息
|
||||
@@ -0,0 +1,15 @@
|
||||
# Goal - 液体投料-溶剂操作参数
|
||||
string volume # 投料体积
|
||||
string assign_material_name # 溶剂名称
|
||||
string titration_type # 滴定类型1否2是
|
||||
string time # 观察时间(单位min)
|
||||
string torque_variation # 是否观察1否2是
|
||||
string temperature # 温度设置
|
||||
---
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
string return_info # 结果消息
|
||||
int32 code # 操作结果代码(1表示成功,0表示失败)
|
||||
---
|
||||
# Feedback - 实时反馈
|
||||
string feedback # 操作过程中的反馈信息
|
||||
@@ -0,0 +1,15 @@
|
||||
# Goal - 液体投料滴定操作参数
|
||||
string volume_formula # 投料体积公式
|
||||
string assign_material_name # 溶剂名称
|
||||
string titration_type # 滴定类型1否2是
|
||||
string time # 观察时间(单位min)
|
||||
string torque_variation # 是否观察1否2是
|
||||
string temperature # 温度设置
|
||||
---
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
string return_info # 结果消息
|
||||
int32 code # 操作结果代码(1表示成功,0表示失败)
|
||||
---
|
||||
# Feedback - 实时反馈
|
||||
string feedback # 操作过程中的反馈信息
|
||||
@@ -0,0 +1,15 @@
|
||||
# Goal - 液体投料-小瓶非滴定操作参数
|
||||
string volume_formula # 投料体积公式
|
||||
string assign_material_name # 溶剂名称
|
||||
string titration_type # 滴定类型1否2是
|
||||
string time # 观察时间(单位min)
|
||||
string torque_variation # 是否观察1否2是
|
||||
string temperature # 温度设置
|
||||
---
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
string return_info # 结果消息
|
||||
int32 code # 操作结果代码(1表示成功,0表示失败)
|
||||
---
|
||||
# Feedback - 实时反馈
|
||||
string feedback # 操作过程中的反馈信息
|
||||
@@ -1,8 +1,11 @@
|
||||
# Goal - 合并工作流+执行
|
||||
string workflow_name # 工作流名称
|
||||
string task_name # 任务名称
|
||||
# Goal - 合并工作流+执行参数
|
||||
string workflow_name # 工作流名称
|
||||
string task_name # 任务名称
|
||||
---
|
||||
# Result - 操作结果
|
||||
string return_info # 结果消息
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
string return_info # 结果消息
|
||||
int32 code # 操作结果代码(1表示成功,0表示失败)
|
||||
---
|
||||
# Feedback - 实时反馈
|
||||
# Feedback - 实时反馈
|
||||
string feedback # 操作过程中的反馈信息
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
# Goal - 通量-配置
|
||||
string cutoff # 黏度_通量-配置
|
||||
string temperature # 温度_通量-配置
|
||||
string assign_material_name # 分液类型_通量-配置
|
||||
# Goal - 反应器放入操作参数
|
||||
string cutoff # 黏度设置
|
||||
string temperature # 温度设置
|
||||
string assign_material_name # 分液类型
|
||||
---
|
||||
# Result - 操作结果
|
||||
string return_info # 结果消息
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
string return_info # 结果消息
|
||||
int32 code # 操作结果代码(1表示成功,0表示失败)
|
||||
---
|
||||
# Feedback - 实时反馈
|
||||
# Feedback - 实时反馈
|
||||
string feedback # 操作过程中的反馈信息
|
||||
|
||||
12
unilabos_msgs/action/ReactionStationReactorTakenOut.action
Normal file
12
unilabos_msgs/action/ReactionStationReactorTakenOut.action
Normal file
@@ -0,0 +1,12 @@
|
||||
# Goal - 反应器取出操作参数
|
||||
# 反应器取出操作不需要任何参数
|
||||
---
|
||||
# Result - 操作结果
|
||||
# 反应器取出操作的结果
|
||||
bool success # 要求必须包含success,以便回传执行结果
|
||||
string return_info # 要求必须包含return_info,以便回传执行结果
|
||||
int32 code # 操作结果代码(1表示成功,0表示失败)
|
||||
---
|
||||
# Feedback - 实时反馈
|
||||
# 反应器取出操作的反馈
|
||||
string feedback # 操作过程中的反馈信息
|
||||
@@ -1,10 +1,13 @@
|
||||
# Goal - 固体投料-小瓶
|
||||
string assign_material_name # 固体名称_粉末加样模块-投料
|
||||
string material_id # 固体投料类型_粉末加样模块-投料
|
||||
string time # 观察时间_反应模块-观察搅拌结果
|
||||
string torque_variation #是否观察1否2是_反应模块-观察搅拌结果
|
||||
# Goal - 固体投料-小瓶操作参数
|
||||
string assign_material_name # 固体名称
|
||||
string material_id # 固体投料类型
|
||||
string time # 观察时间(单位min)
|
||||
string torque_variation # 是否观察1否2是
|
||||
---
|
||||
# Result - 操作结果
|
||||
string return_info # 结果消息
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
string return_info # 结果消息
|
||||
int32 code # 操作结果代码(1表示成功,0表示失败)
|
||||
---
|
||||
# Feedback - 实时反馈
|
||||
# Feedback - 实时反馈
|
||||
string feedback # 操作过程中的反馈信息
|
||||
|
||||
Reference in New Issue
Block a user