mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-18 21:41:16 +00:00
Compare commits
1 Commits
workstatio
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39cc280c91 |
14473
bioyond_yihua_YB.json
14473
bioyond_yihua_YB.json
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,54 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"id": "BatteryStation",
|
|
||||||
"name": "扣电工作站",
|
|
||||||
"parent": null,
|
|
||||||
"children": [
|
|
||||||
"coin_cell_deck"
|
|
||||||
],
|
|
||||||
"type": "device",
|
|
||||||
"class":"coincellassemblyworkstation_device",
|
|
||||||
"position": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"deck": {
|
|
||||||
"data": {
|
|
||||||
"_resource_child_name": "YB_YH_Deck",
|
|
||||||
"_resource_type": "unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials:CoincellDeck"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"debug_mode": true,
|
|
||||||
"protocol_type": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "YB_YH_Deck",
|
|
||||||
"name": "YB_YH_Deck",
|
|
||||||
"children": [],
|
|
||||||
"parent": "BatteryStation",
|
|
||||||
"type": "deck",
|
|
||||||
"class": "CoincellDeck",
|
|
||||||
"position": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "CoincellDeck",
|
|
||||||
"setup": true,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"data": {}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"links": []
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
|
|
||||||
{
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"id": "bioyond_cell_workstation",
|
|
||||||
"name": "配液分液工站",
|
|
||||||
"parent": null,
|
|
||||||
"children": [
|
|
||||||
"YB_Bioyond_Deck"
|
|
||||||
],
|
|
||||||
"type": "device",
|
|
||||||
"class": "bioyond_cell",
|
|
||||||
"config": {
|
|
||||||
"deck": {
|
|
||||||
"data": {
|
|
||||||
"_resource_child_name": "YB_Bioyond_Deck",
|
|
||||||
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_YB_Deck"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"protocol_type": []
|
|
||||||
},
|
|
||||||
"data": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "YB_Bioyond_Deck",
|
|
||||||
"name": "YB_Bioyond_Deck",
|
|
||||||
"children": [],
|
|
||||||
"parent": "bioyond_cell_workstation",
|
|
||||||
"type": "deck",
|
|
||||||
"class": "BIOYOND_YB_Deck",
|
|
||||||
"position": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "BIOYOND_YB_Deck",
|
|
||||||
"setup": true,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"data": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "BatteryStation",
|
|
||||||
"name": "扣电工作站",
|
|
||||||
"parent": null,
|
|
||||||
"children": [
|
|
||||||
"coin_cell_deck"
|
|
||||||
],
|
|
||||||
"type": "device",
|
|
||||||
"class":"coincellassemblyworkstation_device",
|
|
||||||
"config": {
|
|
||||||
"deck": {
|
|
||||||
"data": {
|
|
||||||
"_resource_child_name": "YB_YH_Deck",
|
|
||||||
"_resource_type": "unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials:CoincellDeck"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"protocol_type": []
|
|
||||||
},
|
|
||||||
"position": {
|
|
||||||
"size": {"height": 1450, "width": 1450, "depth": 2100},
|
|
||||||
"position": {
|
|
||||||
"x": -1500,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "YB_YH_Deck",
|
|
||||||
"name": "YB_YH_Deck",
|
|
||||||
"children": [],
|
|
||||||
"parent": "BatteryStation",
|
|
||||||
"type": "deck",
|
|
||||||
"class": "CoincellDeck",
|
|
||||||
"config": {
|
|
||||||
"type": "CoincellDeck",
|
|
||||||
"setup": true,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"data": {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "3a1d377b-299d-d0f2-ced9-48257f60dfad",
|
|
||||||
"typeName": "加样头(大)",
|
|
||||||
"code": "0005-00145",
|
|
||||||
"barCode": "",
|
|
||||||
"name": "LiDFOB",
|
|
||||||
"quantity": 9999.0,
|
|
||||||
"lockQuantity": 0.0,
|
|
||||||
"unit": "个",
|
|
||||||
"status": 1,
|
|
||||||
"isUse": false,
|
|
||||||
"locations": [
|
|
||||||
{
|
|
||||||
"id": "3a19da56-1379-ff7c-1745-07e200b44ce2",
|
|
||||||
"whid": "3a19da56-1378-613b-29f2-871e1a287aa5",
|
|
||||||
"whName": "粉末加样头堆栈",
|
|
||||||
"code": "0005-0001",
|
|
||||||
"x": 1,
|
|
||||||
"y": 1,
|
|
||||||
"z": 1,
|
|
||||||
"quantity": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"detail": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "3a1d377b-6a81-6a7e-147c-f89f6463656d",
|
|
||||||
"typeName": "液",
|
|
||||||
"code": "0006-00141",
|
|
||||||
"barCode": "",
|
|
||||||
"name": "EMC",
|
|
||||||
"quantity": 99999.0,
|
|
||||||
"lockQuantity": 0.0,
|
|
||||||
"unit": "g",
|
|
||||||
"status": 1,
|
|
||||||
"isUse": false,
|
|
||||||
"locations": [
|
|
||||||
{
|
|
||||||
"id": "3a1baa20-a7b1-c665-8b9c-d8099d07d2f6",
|
|
||||||
"whid": "3a1baa20-a7b0-5c19-8844-5de8924d4e78",
|
|
||||||
"whName": "4号手套箱内部堆栈",
|
|
||||||
"code": "0015-0001",
|
|
||||||
"x": 1,
|
|
||||||
"y": 1,
|
|
||||||
"z": 1,
|
|
||||||
"quantity": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"detail": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
{
|
|
||||||
"typeId": "3a190c8b-3284-af78-d29f-9a69463ad047",
|
|
||||||
"code": "",
|
|
||||||
"barCode": "",
|
|
||||||
"name": "test",
|
|
||||||
"unit": "",
|
|
||||||
"parameters": "{}",
|
|
||||||
"quantity": "",
|
|
||||||
"details": [
|
|
||||||
{
|
|
||||||
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb",
|
|
||||||
"code": "",
|
|
||||||
"name": "配液瓶(小)11",
|
|
||||||
"quantity": "1",
|
|
||||||
"x": 1,
|
|
||||||
"y": 1,
|
|
||||||
"z": 1,
|
|
||||||
"unit": "",
|
|
||||||
"parameters": "{}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb",
|
|
||||||
"code": "",
|
|
||||||
"name": "配液瓶(小)21",
|
|
||||||
"quantity": "1",
|
|
||||||
"x": 2,
|
|
||||||
"y": 1,
|
|
||||||
"z": 1,
|
|
||||||
"unit": "",
|
|
||||||
"parameters": "{}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb",
|
|
||||||
"code": "",
|
|
||||||
"name": "配液瓶(小)12",
|
|
||||||
"quantity": "1",
|
|
||||||
"x": 1,
|
|
||||||
"y": 2,
|
|
||||||
"z": 1,
|
|
||||||
"unit": "",
|
|
||||||
"parameters": "{}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb",
|
|
||||||
"code": "",
|
|
||||||
"name": "配液瓶(小)22",
|
|
||||||
"quantity": "1",
|
|
||||||
"x": 2,
|
|
||||||
"y": 2,
|
|
||||||
"z": 1,
|
|
||||||
"unit": "",
|
|
||||||
"parameters": "{}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb",
|
|
||||||
"code": "",
|
|
||||||
"name": "配液瓶(小)13",
|
|
||||||
"quantity": "1",
|
|
||||||
"x": 1,
|
|
||||||
"y": 3,
|
|
||||||
"z": 1,
|
|
||||||
"unit": "",
|
|
||||||
"parameters": "{}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb",
|
|
||||||
"code": "",
|
|
||||||
"name": "配液瓶(小)23",
|
|
||||||
"quantity": "1",
|
|
||||||
"x": 2,
|
|
||||||
"y": 3,
|
|
||||||
"z": 1,
|
|
||||||
"unit": "",
|
|
||||||
"parameters": "{}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb",
|
|
||||||
"code": "",
|
|
||||||
"name": "配液瓶(小)14",
|
|
||||||
"quantity": "1",
|
|
||||||
"x": 1,
|
|
||||||
"y": 4,
|
|
||||||
"z": 1,
|
|
||||||
"unit": "",
|
|
||||||
"parameters": "{}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb",
|
|
||||||
"code": "",
|
|
||||||
"name": "配液瓶(小)24",
|
|
||||||
"quantity": "1",
|
|
||||||
"x": 2,
|
|
||||||
"y": 4,
|
|
||||||
"z": 1,
|
|
||||||
"unit": "",
|
|
||||||
"parameters": "{}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "3a1d4c14-a9fb-d7dc-9e96-7a3ad6e50219",
|
|
||||||
"typeName": "配液瓶(小)板",
|
|
||||||
"code": "0001-00093",
|
|
||||||
"barCode": "",
|
|
||||||
"name": "test",
|
|
||||||
"quantity": 2.0,
|
|
||||||
"lockQuantity": 0.0,
|
|
||||||
"unit": "块",
|
|
||||||
"status": 1,
|
|
||||||
"isUse": false,
|
|
||||||
"locations": [
|
|
||||||
{
|
|
||||||
"id": "3a19deae-2c7a-36f5-5e41-02c5b66feaea",
|
|
||||||
"whid": "3a19deae-2c79-05a3-9c76-8e6760424841",
|
|
||||||
"whName": "手动堆栈",
|
|
||||||
"code": "1",
|
|
||||||
"x": 1,
|
|
||||||
"y": 1,
|
|
||||||
"z": 1,
|
|
||||||
"quantity": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"detail": [
|
|
||||||
{
|
|
||||||
"id": "3a1d4c14-a9fc-1daa-71fa-146cb1ccb930",
|
|
||||||
"detailMaterialId": "3a1d4c14-a9fc-4f38-4c48-68486c391c42",
|
|
||||||
"code": "0001-00093 - 05",
|
|
||||||
"name": "配液瓶(小)",
|
|
||||||
"quantity": "1",
|
|
||||||
"lockQuantity": "0",
|
|
||||||
"unit": "个",
|
|
||||||
"x": 1,
|
|
||||||
"y": 3,
|
|
||||||
"z": 1,
|
|
||||||
"associateId": null,
|
|
||||||
"typeName": "配液瓶(小)",
|
|
||||||
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "3a1d4c14-a9fc-3659-ea61-cd587da9e131",
|
|
||||||
"detailMaterialId": "3a1d4c14-a9fc-018f-93e5-c49343d37758",
|
|
||||||
"code": "0001-00093 - 08",
|
|
||||||
"name": "配液瓶(小)",
|
|
||||||
"quantity": "1",
|
|
||||||
"lockQuantity": "0",
|
|
||||||
"unit": "个",
|
|
||||||
"x": 2,
|
|
||||||
"y": 4,
|
|
||||||
"z": 1,
|
|
||||||
"associateId": null,
|
|
||||||
"typeName": "配液瓶(小)",
|
|
||||||
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "3a1d4c14-a9fc-3f94-de83-979d2646e313",
|
|
||||||
"detailMaterialId": "3a1d4c14-a9fc-9987-c0ef-4b7cbad49e6b",
|
|
||||||
"code": "0001-00093 - 01",
|
|
||||||
"name": "配液瓶(小)",
|
|
||||||
"quantity": "1",
|
|
||||||
"lockQuantity": "0",
|
|
||||||
"unit": "个",
|
|
||||||
"x": 1,
|
|
||||||
"y": 1,
|
|
||||||
"z": 1,
|
|
||||||
"associateId": null,
|
|
||||||
"typeName": "配液瓶(小)",
|
|
||||||
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "3a1d4c14-a9fc-8c35-6b25-913b11dbaf4e",
|
|
||||||
"detailMaterialId": "3a1d4c14-a9fc-9a83-865b-0c26ea5e8cc4",
|
|
||||||
"code": "0001-00093 - 03",
|
|
||||||
"name": "配液瓶(小)",
|
|
||||||
"quantity": "1",
|
|
||||||
"lockQuantity": "0",
|
|
||||||
"unit": "个",
|
|
||||||
"x": 1,
|
|
||||||
"y": 2,
|
|
||||||
"z": 1,
|
|
||||||
"associateId": null,
|
|
||||||
"typeName": "配液瓶(小)",
|
|
||||||
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "3a1d4c14-a9fc-b41f-e968-64953bfddccd",
|
|
||||||
"detailMaterialId": "3a1d4c14-a9fc-daf7-9d64-e5ec8d3ae0e2",
|
|
||||||
"code": "0001-00093 - 07",
|
|
||||||
"name": "配液瓶(小)",
|
|
||||||
"quantity": "1",
|
|
||||||
"lockQuantity": "0",
|
|
||||||
"unit": "个",
|
|
||||||
"x": 1,
|
|
||||||
"y": 4,
|
|
||||||
"z": 1,
|
|
||||||
"associateId": null,
|
|
||||||
"typeName": "配液瓶(小)",
|
|
||||||
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "3a1d4c14-a9fc-c20f-c26e-b1bb2cdc3bca",
|
|
||||||
"detailMaterialId": "3a1d4c14-a9fc-673b-ac83-aaaf71287f1f",
|
|
||||||
"code": "0001-00093 - 06",
|
|
||||||
"name": "配液瓶(小)",
|
|
||||||
"quantity": "1",
|
|
||||||
"lockQuantity": "0",
|
|
||||||
"unit": "个",
|
|
||||||
"x": 2,
|
|
||||||
"y": 3,
|
|
||||||
"z": 1,
|
|
||||||
"associateId": null,
|
|
||||||
"typeName": "配液瓶(小)",
|
|
||||||
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "3a1d4c14-a9fc-cf21-059c-fde361d82b6f",
|
|
||||||
"detailMaterialId": "3a1d4c14-a9fc-25b1-e736-6b0d8dac0fae",
|
|
||||||
"code": "0001-00093 - 02",
|
|
||||||
"name": "配液瓶(小)",
|
|
||||||
"quantity": "1",
|
|
||||||
"lockQuantity": "0",
|
|
||||||
"unit": "个",
|
|
||||||
"x": 2,
|
|
||||||
"y": 1,
|
|
||||||
"z": 1,
|
|
||||||
"associateId": null,
|
|
||||||
"typeName": "配液瓶(小)",
|
|
||||||
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "3a1d4c14-a9fc-d732-2b93-9b2bd2bf581b",
|
|
||||||
"detailMaterialId": "3a1d4c14-a9fc-7f5d-b6b6-8bcb2e15f320",
|
|
||||||
"code": "0001-00093 - 04",
|
|
||||||
"name": "配液瓶(小)",
|
|
||||||
"quantity": "1",
|
|
||||||
"lockQuantity": "0",
|
|
||||||
"unit": "个",
|
|
||||||
"x": 2,
|
|
||||||
"y": 2,
|
|
||||||
"z": 1,
|
|
||||||
"associateId": null,
|
|
||||||
"typeName": "配液瓶(小)",
|
|
||||||
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from unilabos.resources.bioyond.bottle_carriers import BIOYOND_Electrolyte_6VialCarrier, BIOYOND_Electrolyte_1BottleCarrier
|
from unilabos.resources.bioyond.bottle_carriers import BIOYOND_Electrolyte_6VialCarrier, BIOYOND_Electrolyte_1BottleCarrier
|
||||||
from unilabos.resources.bioyond.bottles import YB_Solid_Vial, YB_Solution_Beaker, YB_Reagent_Bottle
|
from unilabos.resources.bioyond.bottles import BIOYOND_PolymerStation_Solid_Vial, BIOYOND_PolymerStation_Solution_Beaker, BIOYOND_PolymerStation_Reagent_Bottle
|
||||||
|
|
||||||
|
|
||||||
def test_bottle_carrier() -> "BottleCarrier":
|
def test_bottle_carrier() -> "BottleCarrier":
|
||||||
@@ -16,9 +16,9 @@ def test_bottle_carrier() -> "BottleCarrier":
|
|||||||
print(f"1烧杯载架: {beaker_carrier.name}, 位置数: {len(beaker_carrier.sites)}")
|
print(f"1烧杯载架: {beaker_carrier.name}, 位置数: {len(beaker_carrier.sites)}")
|
||||||
|
|
||||||
# 创建瓶子和烧杯
|
# 创建瓶子和烧杯
|
||||||
powder_bottle = YB_Solid_Vial("powder_bottle_01")
|
powder_bottle = BIOYOND_PolymerStation_Solid_Vial("powder_bottle_01")
|
||||||
solution_beaker = YB_Solution_Beaker("solution_beaker_01")
|
solution_beaker = BIOYOND_PolymerStation_Solution_Beaker("solution_beaker_01")
|
||||||
reagent_bottle = YB_Reagent_Bottle("reagent_bottle_01")
|
reagent_bottle = BIOYOND_PolymerStation_Reagent_Bottle("reagent_bottle_01")
|
||||||
|
|
||||||
print(f"\n创建的物料:")
|
print(f"\n创建的物料:")
|
||||||
print(f"粉末瓶: {powder_bottle.name} - {powder_bottle.diameter}mm x {powder_bottle.height}mm, {powder_bottle.max_volume}μL")
|
print(f"粉末瓶: {powder_bottle.name} - {powder_bottle.diameter}mm x {powder_bottle.height}mm, {powder_bottle.max_volume}μL")
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ lab_registry.setup()
|
|||||||
|
|
||||||
|
|
||||||
type_mapping = {
|
type_mapping = {
|
||||||
"烧杯": ("YB_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"),
|
"烧杯": ("BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"),
|
||||||
"试剂瓶": ("YB_1BottleCarrier", ""),
|
"试剂瓶": ("BIOYOND_PolymerStation_1BottleCarrier", ""),
|
||||||
"样品板": ("YB_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"),
|
"样品板": ("BIOYOND_PolymerStation_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"),
|
||||||
"分装板": ("YB_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"),
|
"分装板": ("BIOYOND_PolymerStation_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"),
|
||||||
"样品瓶": ("YB_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"),
|
"样品瓶": ("BIOYOND_PolymerStation_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"),
|
||||||
"90%分装小瓶": ("YB_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"),
|
"90%分装小瓶": ("BIOYOND_PolymerStation_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"),
|
||||||
"10%分装小瓶": ("YB_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"),
|
"10%分装小瓶": ("BIOYOND_PolymerStation_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
from ast import If
|
|
||||||
import pytest
|
import pytest
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -9,16 +8,18 @@ from unilabos.ros.nodes.resource_tracker import ResourceTreeSet
|
|||||||
from unilabos.registry.registry import lab_registry
|
from unilabos.registry.registry import lab_registry
|
||||||
|
|
||||||
from unilabos.resources.bioyond.decks import BIOYOND_PolymerReactionStation_Deck
|
from unilabos.resources.bioyond.decks import BIOYOND_PolymerReactionStation_Deck
|
||||||
from unilabos.resources.bioyond.decks import YB_Deck
|
|
||||||
|
|
||||||
lab_registry.setup()
|
lab_registry.setup()
|
||||||
|
|
||||||
|
|
||||||
type_mapping = {
|
type_mapping = {
|
||||||
"加样头(大)": ("YB_jia_yang_tou_da", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
"烧杯": ("BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"),
|
||||||
"液": ("YB_1BottleCarrier", "3a190ca1-2add-2b23-f8e1-bbd348b7f790"),
|
"试剂瓶": ("BIOYOND_PolymerStation_1BottleCarrier", ""),
|
||||||
"配液瓶(小)板": ("YB_peiyepingxiaoban", "3a190c8b-3284-af78-d29f-9a69463ad047"),
|
"样品板": ("BIOYOND_PolymerStation_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"),
|
||||||
"配液瓶(小)": ("YB_pei_ye_xiao_Bottler", "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"),
|
"分装板": ("BIOYOND_PolymerStation_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"),
|
||||||
|
"样品瓶": ("BIOYOND_PolymerStation_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"),
|
||||||
|
"90%分装小瓶": ("BIOYOND_PolymerStation_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"),
|
||||||
|
"10%分装小瓶": ("BIOYOND_PolymerStation_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -56,20 +57,12 @@ def bioyond_materials_liquidhandling_2() -> list[dict]:
|
|||||||
"bioyond_materials_reaction",
|
"bioyond_materials_reaction",
|
||||||
"bioyond_materials_liquidhandling_1",
|
"bioyond_materials_liquidhandling_1",
|
||||||
])
|
])
|
||||||
def test_resourcetreeset_from_plr() -> list[dict]:
|
def test_resourcetreeset_from_plr(materials_fixture, request) -> list[dict]:
|
||||||
# 直接加载 bioyond_materials_reaction.json 文件
|
materials = request.getfixturevalue(materials_fixture)
|
||||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
deck = BIOYOND_PolymerReactionStation_Deck("test_deck")
|
||||||
json_path = os.path.join(current_dir, "test.json")
|
|
||||||
with open(json_path, "r", encoding="utf-8") as f:
|
|
||||||
materials = json.load(f)
|
|
||||||
deck = YB_Deck("test_deck")
|
|
||||||
output = resource_bioyond_to_plr(materials, type_mapping=type_mapping, deck=deck)
|
output = resource_bioyond_to_plr(materials, type_mapping=type_mapping, deck=deck)
|
||||||
print(output)
|
print(deck.summary())
|
||||||
# print(deck.summary())
|
|
||||||
|
|
||||||
r = ResourceTreeSet.from_plr_resources([deck])
|
r = ResourceTreeSet.from_plr_resources([deck])
|
||||||
print(r.dump())
|
print(r.dump())
|
||||||
# json.dump(deck.serialize(), open("test.json", "w", encoding="utf-8"), indent=4)
|
# json.dump(deck.serialize(), open("test.json", "w", encoding="utf-8"), indent=4)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
test_resourcetreeset_from_plr()
|
|
||||||
|
|||||||
@@ -367,37 +367,10 @@ def main():
|
|||||||
graph, resource_tree_set, resource_links = read_node_link_json(request_startup_json)
|
graph, resource_tree_set, resource_links = read_node_link_json(request_startup_json)
|
||||||
else:
|
else:
|
||||||
if not os.path.isfile(file_path):
|
if not os.path.isfile(file_path):
|
||||||
# 尝试从 main.py 向上两级目录查找
|
|
||||||
temp_file_path = os.path.abspath(str(os.path.join(__file__, "..", "..", file_path)))
|
temp_file_path = os.path.abspath(str(os.path.join(__file__, "..", "..", file_path)))
|
||||||
if os.path.isfile(temp_file_path):
|
if os.path.isfile(temp_file_path):
|
||||||
print_status(f"使用相对路径{temp_file_path}", "info")
|
print_status(f"使用相对路径{temp_file_path}", "info")
|
||||||
file_path = temp_file_path
|
file_path = temp_file_path
|
||||||
else:
|
|
||||||
# 尝试在 working_dir 中查找
|
|
||||||
working_dir_file_path = os.path.join(working_dir, file_path)
|
|
||||||
if os.path.isfile(working_dir_file_path):
|
|
||||||
print_status(f"在工作目录中找到文件: {working_dir_file_path}", "info")
|
|
||||||
file_path = working_dir_file_path
|
|
||||||
else:
|
|
||||||
# 尝试使用文件名在 working_dir 中查找
|
|
||||||
file_name = os.path.basename(file_path)
|
|
||||||
working_dir_file_path = os.path.join(working_dir, file_name)
|
|
||||||
if os.path.isfile(working_dir_file_path):
|
|
||||||
print_status(f"在工作目录中找到文件: {working_dir_file_path}", "info")
|
|
||||||
file_path = working_dir_file_path
|
|
||||||
# 最终检查文件是否存在
|
|
||||||
if not os.path.isfile(file_path):
|
|
||||||
print_status(
|
|
||||||
f"无法找到设备加载文件: {file_path}\n"
|
|
||||||
f"已尝试在以下位置查找:\n"
|
|
||||||
f" 1. 原始路径: {args_dict.get('graph', BasicConfig.startup_json_path)}\n"
|
|
||||||
f" 2. 相对路径: {os.path.abspath(str(os.path.join(__file__, '..', '..', args_dict.get('graph', BasicConfig.startup_json_path) or '')))}\n"
|
|
||||||
f" 3. 工作目录: {os.path.join(working_dir, args_dict.get('graph', BasicConfig.startup_json_path) or '')}\n"
|
|
||||||
f" 4. 工作目录(仅文件名): {os.path.join(working_dir, os.path.basename(args_dict.get('graph', BasicConfig.startup_json_path) or ''))}\n"
|
|
||||||
f"请使用 -g 参数指定正确的文件路径,或在工作目录 {working_dir} 中放置文件",
|
|
||||||
"error"
|
|
||||||
)
|
|
||||||
os._exit(1)
|
|
||||||
if file_path.endswith(".json"):
|
if file_path.endswith(".json"):
|
||||||
graph, resource_tree_set, resource_links = read_node_link_json(file_path)
|
graph, resource_tree_set, resource_links = read_node_link_json(file_path)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -421,7 +421,7 @@ class MessageProcessor:
|
|||||||
ssl_context = ssl_module.create_default_context()
|
ssl_context = ssl_module.create_default_context()
|
||||||
|
|
||||||
ws_logger = logging.getLogger("websockets.client")
|
ws_logger = logging.getLogger("websockets.client")
|
||||||
# 日志级别已在 unilabos.utils.log 中统一配置为 WARNING
|
ws_logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
async with websockets.connect(
|
async with websockets.connect(
|
||||||
self.websocket_url,
|
self.websocket_url,
|
||||||
@@ -1240,7 +1240,7 @@ class WebSocketClient(BaseCommunicationClient):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
self.message_processor.send_message(message)
|
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(
|
def publish_job_status(
|
||||||
self, feedback_data: dict, item: QueueItem, status: str, return_info: Optional[dict] = None
|
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": []
|
||||||
|
}
|
||||||
@@ -13,8 +13,6 @@
|
|||||||
- 状态类型: working/stop/finish/protect/pause/false/unknown
|
- 状态类型: working/stop/finish/protect/pause/false/unknown
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import socket
|
import socket
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
import json
|
import json
|
||||||
@@ -23,6 +21,7 @@ from dataclasses import dataclass
|
|||||||
from typing import Any, Dict, List, Optional, TypedDict
|
from typing import Any, Dict, List, Optional, TypedDict
|
||||||
|
|
||||||
from pylabrobot.resources import ResourceHolder, Coordinate, create_ordered_items_2d, Deck, Plate
|
from pylabrobot.resources import ResourceHolder, Coordinate, create_ordered_items_2d, Deck, Plate
|
||||||
|
|
||||||
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode
|
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode
|
||||||
from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
|
from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
|
||||||
|
|
||||||
@@ -57,6 +56,13 @@ class BatteryTestPositionState(TypedDict):
|
|||||||
status: str # 通道状态
|
status: str # 通道状态
|
||||||
color: str # 状态对应颜色
|
color: str # 状态对应颜色
|
||||||
|
|
||||||
|
# 额外的inquire协议字段
|
||||||
|
relativetime: float # 相对时间 (s)
|
||||||
|
open_or_close: int # 0=关闭, 1=打开
|
||||||
|
step_type: str # 步骤类型
|
||||||
|
cycle_id: int # 循环ID
|
||||||
|
step_id: int # 步骤ID
|
||||||
|
log_code: str # 日志代码
|
||||||
|
|
||||||
|
|
||||||
class BatteryTestPosition(ResourceHolder):
|
class BatteryTestPosition(ResourceHolder):
|
||||||
@@ -136,9 +142,9 @@ class NewareBatteryTestSystem:
|
|||||||
devtype: str = None,
|
devtype: str = None,
|
||||||
timeout: int = None,
|
timeout: int = None,
|
||||||
|
|
||||||
size_x: float = 50,
|
size_x: float = 500.0,
|
||||||
size_y: float = 50,
|
size_y: float = 500.0,
|
||||||
size_z: float = 20,
|
size_z: float = 2000.0,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
初始化新威电池测试系统
|
初始化新威电池测试系统
|
||||||
@@ -156,12 +162,6 @@ class NewareBatteryTestSystem:
|
|||||||
self.machine_id = machine_id
|
self.machine_id = machine_id
|
||||||
self.devtype = devtype or self.DEVTYPE
|
self.devtype = devtype or self.DEVTYPE
|
||||||
self.timeout = timeout or self.TIMEOUT
|
self.timeout = timeout or self.TIMEOUT
|
||||||
|
|
||||||
# 存储设备物理尺寸
|
|
||||||
self.size_x = size_x
|
|
||||||
self.size_y = size_y
|
|
||||||
self.size_z = size_z
|
|
||||||
|
|
||||||
self._last_status_update = None
|
self._last_status_update = None
|
||||||
self._cached_status = {}
|
self._cached_status = {}
|
||||||
self._ros_node: Optional[ROS2WorkstationNode] = None # ROS节点引用,由框架设置
|
self._ros_node: Optional[ROS2WorkstationNode] = None # ROS节点引用,由框架设置
|
||||||
@@ -192,9 +192,8 @@ class NewareBatteryTestSystem:
|
|||||||
def _setup_material_management(self):
|
def _setup_material_management(self):
|
||||||
"""设置物料管理系统"""
|
"""设置物料管理系统"""
|
||||||
# 第1盘:5行8列网格 (A1-E8) - 5行对应subdevid 1-5,8列对应chlid 1-8
|
# 第1盘:5行8列网格 (A1-E8) - 5行对应subdevid 1-5,8列对应chlid 1-8
|
||||||
# 先给物料设置一个最大的Deck,并设置其在空间中的位置
|
# 先给物料设置一个最大的Deck
|
||||||
|
deck_main = Deck("ADeckName", 200, 200, 200)
|
||||||
deck_main = Deck("ADeckName", 2000, 1800, 100, origin=Coordinate(2000,2000,0))
|
|
||||||
|
|
||||||
plate1_resources: Dict[str, BatteryTestPosition] = create_ordered_items_2d(
|
plate1_resources: Dict[str, BatteryTestPosition] = create_ordered_items_2d(
|
||||||
BatteryTestPosition,
|
BatteryTestPosition,
|
||||||
@@ -203,8 +202,8 @@ class NewareBatteryTestSystem:
|
|||||||
dx=10,
|
dx=10,
|
||||||
dy=10,
|
dy=10,
|
||||||
dz=0,
|
dz=0,
|
||||||
item_dx=65,
|
item_dx=45,
|
||||||
item_dy=65
|
item_dy=45
|
||||||
)
|
)
|
||||||
plate1 = Plate("P1", 400, 300, 50, ordered_items=plate1_resources)
|
plate1 = Plate("P1", 400, 300, 50, ordered_items=plate1_resources)
|
||||||
deck_main.assign_child_resource(plate1, location=Coordinate(0, 0, 0))
|
deck_main.assign_child_resource(plate1, location=Coordinate(0, 0, 0))
|
||||||
@@ -233,15 +232,11 @@ class NewareBatteryTestSystem:
|
|||||||
num_items_y=5, # 5行(对应subdevid 6-10,即A-E)
|
num_items_y=5, # 5行(对应subdevid 6-10,即A-E)
|
||||||
dx=10,
|
dx=10,
|
||||||
dy=10,
|
dy=10,
|
||||||
dz=0,
|
dz=100, # Z轴偏移100mm
|
||||||
item_dx=65,
|
item_dx=65,
|
||||||
item_dy=65
|
item_dy=65
|
||||||
)
|
)
|
||||||
|
|
||||||
plate2 = Plate("P2", 400, 300, 50, ordered_items=plate2_resources)
|
|
||||||
deck_main.assign_child_resource(plate2, location=Coordinate(0, 350, 0))
|
|
||||||
|
|
||||||
|
|
||||||
# 为第2盘资源添加P2_前缀
|
# 为第2盘资源添加P2_前缀
|
||||||
self.station_resources_plate2 = {}
|
self.station_resources_plate2 = {}
|
||||||
for name, resource in plate2_resources.items():
|
for name, resource in plate2_resources.items():
|
||||||
@@ -311,132 +306,55 @@ class NewareBatteryTestSystem:
|
|||||||
|
|
||||||
def _update_plate_resources(self, subunits: Dict):
|
def _update_plate_resources(self, subunits: Dict):
|
||||||
"""更新两盘电池资源的状态"""
|
"""更新两盘电池资源的状态"""
|
||||||
# 第1盘:subdevid 1-5 映射到 8列5行网格 (列0-7, 行0-4)
|
# 第1盘:subdevid 1-5 映射到 P1_A1-P1_E8 (5行8列)
|
||||||
for subdev_id in range(1, 6): # subdevid 1-5
|
for subdev_id in range(1, 6): # subdevid 1-5
|
||||||
status_row = subunits.get(subdev_id, {})
|
status_row = subunits.get(subdev_id, {})
|
||||||
|
|
||||||
for chl_id in range(1, 9): # chlid 1-8
|
for chl_id in range(1, 9): # chlid 1-8
|
||||||
try:
|
try:
|
||||||
# 根据用户描述:第一个是(0,0),最后一个是(7,4)
|
# 计算在5×8网格中的位置
|
||||||
# 说明是8列5行,列从0开始,行从0开始
|
row_idx = (subdev_id - 1) # 0-4 (对应A-E)
|
||||||
col_idx = (chl_id - 1) # 0-7 (chlid 1-8 -> 列0-7)
|
col_idx = (chl_id - 1) # 0-7 (对应1-8)
|
||||||
row_idx = (subdev_id - 1) # 0-4 (subdevid 1-5 -> 行0-4)
|
resource_name = f"P1_{self.LETTERS[row_idx]}{col_idx + 1}"
|
||||||
|
|
||||||
# 尝试多种可能的资源命名格式
|
|
||||||
possible_names = [
|
|
||||||
f"P1_batterytestposition_{col_idx}_{row_idx}", # 用户提到的格式
|
|
||||||
f"P1_{self.LETTERS[row_idx]}{col_idx + 1}", # 原有的A1-E8格式
|
|
||||||
f"P1_{self.LETTERS[row_idx].lower()}{col_idx + 1}", # 小写字母格式
|
|
||||||
]
|
|
||||||
|
|
||||||
r = None
|
|
||||||
resource_name = None
|
|
||||||
for name in possible_names:
|
|
||||||
if name in self.station_resources:
|
|
||||||
r = self.station_resources[name]
|
|
||||||
resource_name = name
|
|
||||||
break
|
|
||||||
|
|
||||||
|
r = self.station_resources.get(resource_name)
|
||||||
if r:
|
if r:
|
||||||
status_channel = status_row.get(chl_id, {})
|
status_channel = status_row.get(chl_id, {})
|
||||||
metrics = status_channel.get("metrics", {})
|
|
||||||
# 构建BatteryTestPosition状态数据(移除capacity和energy)
|
|
||||||
channel_state = {
|
channel_state = {
|
||||||
# 基本测量数据
|
|
||||||
"voltage": metrics.get("voltage_V", 0.0),
|
|
||||||
"current": metrics.get("current_A", 0.0),
|
|
||||||
"time": metrics.get("totaltime_s", 0.0),
|
|
||||||
|
|
||||||
# 状态信息
|
|
||||||
"status": status_channel.get("state", "unknown"),
|
"status": status_channel.get("state", "unknown"),
|
||||||
"color": status_channel.get("color", self.STATUS_COLOR["unknown"]),
|
"color": status_channel.get("color", self.STATUS_COLOR["unknown"]),
|
||||||
|
"voltage": status_channel.get("voltage_V", 0.0),
|
||||||
# 通道名称标识
|
"current": status_channel.get("current_A", 0.0),
|
||||||
"Channel_Name": f"{self.machine_id}-{subdev_id}-{chl_id}",
|
"time": status_channel.get("totaltime_s", 0.0),
|
||||||
|
|
||||||
}
|
}
|
||||||
r.load_state(channel_state)
|
r.load_state(channel_state)
|
||||||
|
except (KeyError, IndexError):
|
||||||
# 调试信息
|
|
||||||
if self._ros_node and hasattr(self._ros_node, 'lab_logger'):
|
|
||||||
self._ros_node.lab_logger().debug(
|
|
||||||
f"更新P1资源状态: {resource_name} <- subdev{subdev_id}/chl{chl_id} "
|
|
||||||
f"状态:{channel_state['status']}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# 如果找不到资源,记录调试信息
|
|
||||||
if self._ros_node and hasattr(self._ros_node, 'lab_logger'):
|
|
||||||
self._ros_node.lab_logger().debug(
|
|
||||||
f"P1未找到资源: subdev{subdev_id}/chl{chl_id} -> 尝试的名称: {possible_names}"
|
|
||||||
)
|
|
||||||
except (KeyError, IndexError) as e:
|
|
||||||
if self._ros_node and hasattr(self._ros_node, 'lab_logger'):
|
|
||||||
self._ros_node.lab_logger().debug(f"P1映射错误: subdev{subdev_id}/chl{chl_id} - {e}")
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 第2盘:subdevid 6-10 映射到 8列5行网格 (列0-7, 行0-4)
|
# 第2盘:subdevid 6-10 映射到 P2_A1-P2_E8 (5行8列)
|
||||||
for subdev_id in range(6, 11): # subdevid 6-10
|
for subdev_id in range(6, 11): # subdevid 6-10
|
||||||
status_row = subunits.get(subdev_id, {})
|
status_row = subunits.get(subdev_id, {})
|
||||||
|
|
||||||
for chl_id in range(1, 9): # chlid 1-8
|
for chl_id in range(1, 9): # chlid 1-8
|
||||||
try:
|
try:
|
||||||
col_idx = (chl_id - 1) # 0-7 (chlid 1-8 -> 列0-7)
|
# 计算在5×8网格中的位置
|
||||||
row_idx = (subdev_id - 6) # 0-4 (subdevid 6-10 -> 行0-4)
|
row_idx = (subdev_id - 6) # 0-4 (subdevid 6->0, 7->1, ..., 10->4) (对应A-E)
|
||||||
|
col_idx = (chl_id - 1) # 0-7 (对应1-8)
|
||||||
# 尝试多种可能的资源命名格式
|
resource_name = f"P2_{self.LETTERS[row_idx]}{col_idx + 1}"
|
||||||
possible_names = [
|
|
||||||
f"P2_batterytestposition_{col_idx}_{row_idx}", # 用户提到的格式
|
|
||||||
f"P2_{self.LETTERS[row_idx]}{col_idx + 1}", # 原有的A1-E8格式
|
|
||||||
f"P2_{self.LETTERS[row_idx].lower()}{col_idx + 1}", # 小写字母格式
|
|
||||||
]
|
|
||||||
|
|
||||||
r = None
|
|
||||||
resource_name = None
|
|
||||||
for name in possible_names:
|
|
||||||
if name in self.station_resources:
|
|
||||||
r = self.station_resources[name]
|
|
||||||
resource_name = name
|
|
||||||
break
|
|
||||||
|
|
||||||
|
r = self.station_resources.get(resource_name)
|
||||||
if r:
|
if r:
|
||||||
status_channel = status_row.get(chl_id, {})
|
status_channel = status_row.get(chl_id, {})
|
||||||
metrics = status_channel.get("metrics", {})
|
|
||||||
# 构建BatteryTestPosition状态数据(移除capacity和energy)
|
|
||||||
channel_state = {
|
channel_state = {
|
||||||
# 基本测量数据
|
|
||||||
"voltage": metrics.get("voltage_V", 0.0),
|
|
||||||
"current": metrics.get("current_A", 0.0),
|
|
||||||
"time": metrics.get("totaltime_s", 0.0),
|
|
||||||
|
|
||||||
# 状态信息
|
|
||||||
"status": status_channel.get("state", "unknown"),
|
"status": status_channel.get("state", "unknown"),
|
||||||
"color": status_channel.get("color", self.STATUS_COLOR["unknown"]),
|
"color": status_channel.get("color", self.STATUS_COLOR["unknown"]),
|
||||||
|
"voltage": status_channel.get("voltage_V", 0.0),
|
||||||
# 通道名称标识
|
"current": status_channel.get("current_A", 0.0),
|
||||||
"Channel_Name": f"{self.machine_id}-{subdev_id}-{chl_id}",
|
"time": status_channel.get("totaltime_s", 0.0),
|
||||||
|
|
||||||
}
|
}
|
||||||
r.load_state(channel_state)
|
r.load_state(channel_state)
|
||||||
|
except (KeyError, IndexError):
|
||||||
# 调试信息
|
|
||||||
if self._ros_node and hasattr(self._ros_node, 'lab_logger'):
|
|
||||||
self._ros_node.lab_logger().debug(
|
|
||||||
f"更新P2资源状态: {resource_name} <- subdev{subdev_id}/chl{chl_id} "
|
|
||||||
f"状态:{channel_state['status']}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# 如果找不到资源,记录调试信息
|
|
||||||
if self._ros_node and hasattr(self._ros_node, 'lab_logger'):
|
|
||||||
self._ros_node.lab_logger().debug(
|
|
||||||
f"P2未找到资源: subdev{subdev_id}/chl{chl_id} -> 尝试的名称: {possible_names}"
|
|
||||||
)
|
|
||||||
except (KeyError, IndexError) as e:
|
|
||||||
if self._ros_node and hasattr(self._ros_node, 'lab_logger'):
|
|
||||||
self._ros_node.lab_logger().debug(f"P2映射错误: subdev{subdev_id}/chl{chl_id} - {e}")
|
|
||||||
continue
|
continue
|
||||||
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
|
|
||||||
"resources": list(self.station_resources.values())
|
|
||||||
})
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def connection_info(self) -> Dict[str, str]:
|
def connection_info(self) -> Dict[str, str]:
|
||||||
@@ -572,45 +490,6 @@ class NewareBatteryTestSystem:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def debug_resource_names(self) -> dict:
|
|
||||||
"""
|
|
||||||
调试方法:显示所有资源的实际名称(ROS2动作)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: ROS2动作结果格式,包含所有资源名称信息
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
debug_info = {
|
|
||||||
"total_resources": len(self.station_resources),
|
|
||||||
"plate1_resources": len(self.station_resources_plate1),
|
|
||||||
"plate2_resources": len(self.station_resources_plate2),
|
|
||||||
"plate1_names": list(self.station_resources_plate1.keys())[:10], # 显示前10个
|
|
||||||
"plate2_names": list(self.station_resources_plate2.keys())[:10], # 显示前10个
|
|
||||||
"all_resource_names": list(self.station_resources.keys())[:20], # 显示前20个
|
|
||||||
}
|
|
||||||
|
|
||||||
# 检查是否有用户提到的命名格式
|
|
||||||
batterytestposition_names = [name for name in self.station_resources.keys()
|
|
||||||
if "batterytestposition" in name]
|
|
||||||
debug_info["batterytestposition_names"] = batterytestposition_names[:10]
|
|
||||||
|
|
||||||
success_msg = f"资源调试信息获取成功,共{debug_info['total_resources']}个资源"
|
|
||||||
if self._ros_node:
|
|
||||||
self._ros_node.lab_logger().info(success_msg)
|
|
||||||
self._ros_node.lab_logger().info(f"调试信息: {debug_info}")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"return_info": success_msg,
|
|
||||||
"success": True,
|
|
||||||
"debug_data": debug_info
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = f"获取资源调试信息失败: {str(e)}"
|
|
||||||
if self._ros_node:
|
|
||||||
self._ros_node.lab_logger().error(error_msg)
|
|
||||||
return {"return_info": error_msg, "success": False}
|
|
||||||
|
|
||||||
# ========================
|
# ========================
|
||||||
# 辅助方法
|
# 辅助方法
|
||||||
# ========================
|
# ========================
|
||||||
@@ -659,228 +538,6 @@ class NewareBatteryTestSystem:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" 获取状态失败: {e}")
|
print(f" 获取状态失败: {e}")
|
||||||
|
|
||||||
# ========================
|
|
||||||
# CSV批量提交功能(新增)
|
|
||||||
# ========================
|
|
||||||
|
|
||||||
def _ensure_local_import_path(self):
|
|
||||||
"""确保本地模块导入路径"""
|
|
||||||
base_dir = os.path.dirname(__file__)
|
|
||||||
if base_dir not in sys.path:
|
|
||||||
sys.path.insert(0, base_dir)
|
|
||||||
|
|
||||||
def _canon(self, bs: str) -> str:
|
|
||||||
"""规范化电池体系名称"""
|
|
||||||
return str(bs).strip().replace('-', '_').upper()
|
|
||||||
|
|
||||||
def _compute_values(self, row):
|
|
||||||
"""
|
|
||||||
计算活性物质质量和容量
|
|
||||||
|
|
||||||
Args:
|
|
||||||
row: DataFrame行数据
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple: (活性物质质量mg, 容量mAh)
|
|
||||||
"""
|
|
||||||
pw = float(row['Pole_Weight'])
|
|
||||||
cm = float(row['集流体质量'])
|
|
||||||
am = row['活性物质含量']
|
|
||||||
if isinstance(am, str) and am.endswith('%'):
|
|
||||||
amv = float(am.rstrip('%')) / 100.0
|
|
||||||
else:
|
|
||||||
amv = float(am)
|
|
||||||
act_mass = (pw - cm) * amv
|
|
||||||
sc = float(row['克容量mah/g'])
|
|
||||||
cap = act_mass * sc / 1000.0
|
|
||||||
return round(act_mass, 2), round(cap, 3)
|
|
||||||
|
|
||||||
def _get_xml_builder(self, gen_mod, key: str):
|
|
||||||
"""
|
|
||||||
获取对应电池体系的XML生成函数
|
|
||||||
|
|
||||||
Args:
|
|
||||||
gen_mod: generate_xml_content模块
|
|
||||||
key: 电池体系标识
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
callable: XML生成函数
|
|
||||||
"""
|
|
||||||
fmap = {
|
|
||||||
'LB6': gen_mod.xml_LB6,
|
|
||||||
'GR_LI': gen_mod.xml_Gr_Li,
|
|
||||||
'LFP_LI': gen_mod.xml_LFP_Li,
|
|
||||||
'LFP_GR': gen_mod.xml_LFP_Gr,
|
|
||||||
'811_LI_002': gen_mod.xml_811_Li_002,
|
|
||||||
'811_LI_005': gen_mod.xml_811_Li_005,
|
|
||||||
'SIGR_LI_STEP': gen_mod.xml_SiGr_Li_Step,
|
|
||||||
'SIGR_LI': gen_mod.xml_SiGr_Li_Step,
|
|
||||||
'811_SIGR': gen_mod.xml_811_SiGr,
|
|
||||||
}
|
|
||||||
if key not in fmap:
|
|
||||||
raise ValueError(f"未定义电池体系映射: {key}")
|
|
||||||
return fmap[key]
|
|
||||||
|
|
||||||
def _save_xml(self, xml: str, path: str):
|
|
||||||
"""
|
|
||||||
保存XML文件
|
|
||||||
|
|
||||||
Args:
|
|
||||||
xml: XML内容
|
|
||||||
path: 文件路径
|
|
||||||
"""
|
|
||||||
with open(path, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(xml)
|
|
||||||
|
|
||||||
def submit_from_csv(self, csv_path: str, output_dir: str = ".") -> dict:
|
|
||||||
"""
|
|
||||||
从CSV文件批量提交Neware测试任务(设备动作)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
csv_path (str): 输入CSV文件路径
|
|
||||||
output_dir (str): 输出目录,用于存储XML文件和备份,默认当前目录
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: 执行结果 {"return_info": str, "success": bool, "submitted_count": int}
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# 确保可以导入本地模块
|
|
||||||
self._ensure_local_import_path()
|
|
||||||
import pandas as pd
|
|
||||||
import generate_xml_content as gen_mod
|
|
||||||
from neware_driver import start_test
|
|
||||||
|
|
||||||
if self._ros_node:
|
|
||||||
self._ros_node.lab_logger().info(f"开始从CSV文件提交任务: {csv_path}")
|
|
||||||
|
|
||||||
# 读取CSV文件
|
|
||||||
if not os.path.exists(csv_path):
|
|
||||||
error_msg = f"CSV文件不存在: {csv_path}"
|
|
||||||
if self._ros_node:
|
|
||||||
self._ros_node.lab_logger().error(error_msg)
|
|
||||||
return {"return_info": error_msg, "success": False, "submitted_count": 0}
|
|
||||||
|
|
||||||
df = pd.read_csv(csv_path, encoding='gbk')
|
|
||||||
|
|
||||||
# 验证必需列
|
|
||||||
required = [
|
|
||||||
'Battery_Code', 'Pole_Weight', '集流体质量', '活性物质含量',
|
|
||||||
'克容量mah/g', '电池体系', '设备号', '排号', '通道号'
|
|
||||||
]
|
|
||||||
missing = [c for c in required if c not in df.columns]
|
|
||||||
if missing:
|
|
||||||
error_msg = f"CSV缺少必需列: {missing}"
|
|
||||||
if self._ros_node:
|
|
||||||
self._ros_node.lab_logger().error(error_msg)
|
|
||||||
return {"return_info": error_msg, "success": False, "submitted_count": 0}
|
|
||||||
|
|
||||||
# 创建输出目录
|
|
||||||
xml_dir = os.path.join(output_dir, 'xml_dir')
|
|
||||||
backup_dir = os.path.join(output_dir, 'backup_dir')
|
|
||||||
os.makedirs(xml_dir, exist_ok=True)
|
|
||||||
os.makedirs(backup_dir, exist_ok=True)
|
|
||||||
|
|
||||||
if self._ros_node:
|
|
||||||
self._ros_node.lab_logger().info(
|
|
||||||
f"输出目录: XML={xml_dir}, 备份={backup_dir}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 逐行处理CSV数据
|
|
||||||
submitted_count = 0
|
|
||||||
results = []
|
|
||||||
|
|
||||||
for idx, row in df.iterrows():
|
|
||||||
try:
|
|
||||||
coin_id = str(row['Battery_Code'])
|
|
||||||
|
|
||||||
# 计算活性物质质量和容量
|
|
||||||
act_mass, cap_mAh = self._compute_values(row)
|
|
||||||
|
|
||||||
if cap_mAh < 0:
|
|
||||||
error_msg = (
|
|
||||||
f"容量为负数: Battery_Code={coin_id}, "
|
|
||||||
f"活性物质质量mg={act_mass}, 容量mah={cap_mAh}"
|
|
||||||
)
|
|
||||||
if self._ros_node:
|
|
||||||
self._ros_node.lab_logger().warning(error_msg)
|
|
||||||
results.append(f"行{idx+1} 失败: {error_msg}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 获取电池体系对应的XML生成函数
|
|
||||||
key = self._canon(row['电池体系'])
|
|
||||||
builder = self._get_xml_builder(gen_mod, key)
|
|
||||||
|
|
||||||
# 生成XML内容
|
|
||||||
xml_content = builder(act_mass, cap_mAh)
|
|
||||||
|
|
||||||
# 获取设备信息
|
|
||||||
devid = int(row['设备号'])
|
|
||||||
subdevid = int(row['排号'])
|
|
||||||
chlid = int(row['通道号'])
|
|
||||||
|
|
||||||
# 保存XML文件
|
|
||||||
recipe_path = os.path.join(
|
|
||||||
xml_dir,
|
|
||||||
f"{coin_id}_{devid}_{subdevid}_{chlid}.xml"
|
|
||||||
)
|
|
||||||
self._save_xml(xml_content, recipe_path)
|
|
||||||
|
|
||||||
# 提交测试任务
|
|
||||||
resp = start_test(
|
|
||||||
ip=self.ip,
|
|
||||||
port=self.port,
|
|
||||||
devid=devid,
|
|
||||||
subdevid=subdevid,
|
|
||||||
chlid=chlid,
|
|
||||||
CoinID=coin_id,
|
|
||||||
recipe_path=recipe_path,
|
|
||||||
backup_dir=backup_dir
|
|
||||||
)
|
|
||||||
|
|
||||||
submitted_count += 1
|
|
||||||
results.append(f"行{idx+1} {coin_id}: {resp}")
|
|
||||||
|
|
||||||
if self._ros_node:
|
|
||||||
self._ros_node.lab_logger().info(
|
|
||||||
f"已提交 {coin_id} (设备{devid}-{subdevid}-{chlid}): {resp}"
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = f"行{idx+1} 处理失败: {str(e)}"
|
|
||||||
results.append(error_msg)
|
|
||||||
if self._ros_node:
|
|
||||||
self._ros_node.lab_logger().error(error_msg)
|
|
||||||
|
|
||||||
# 汇总结果
|
|
||||||
success_msg = (
|
|
||||||
f"批量提交完成: 成功{submitted_count}个,共{len(df)}行。"
|
|
||||||
f"\n详细结果:\n" + "\n".join(results)
|
|
||||||
)
|
|
||||||
|
|
||||||
if self._ros_node:
|
|
||||||
self._ros_node.lab_logger().info(
|
|
||||||
f"批量提交完成: 成功{submitted_count}/{len(df)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"return_info": success_msg,
|
|
||||||
"success": True,
|
|
||||||
"submitted_count": submitted_count,
|
|
||||||
"total_count": len(df),
|
|
||||||
"results": results
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = f"批量提交失败: {str(e)}"
|
|
||||||
if self._ros_node:
|
|
||||||
self._ros_node.lab_logger().error(error_msg)
|
|
||||||
return {
|
|
||||||
"return_info": error_msg,
|
|
||||||
"success": False,
|
|
||||||
"submitted_count": 0
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_device_summary(self) -> dict:
|
def get_device_summary(self) -> dict:
|
||||||
"""
|
"""
|
||||||
获取设备级别的摘要统计(设备动作)
|
获取设备级别的摘要统计(设备动作)
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
import serial
|
|
||||||
import time
|
|
||||||
import csv
|
|
||||||
import threading
|
|
||||||
import os
|
|
||||||
from collections import deque
|
|
||||||
from typing import Dict, Any, Optional
|
|
||||||
from pylabrobot.resources import Deck
|
|
||||||
|
|
||||||
from unilabos.devices.workstation.workstation_base import WorkstationBase
|
|
||||||
|
|
||||||
|
|
||||||
class ElectrolysisWaterPlatform(WorkstationBase):
|
|
||||||
"""
|
|
||||||
电解水平台工作站
|
|
||||||
基于 WorkstationBase 的电解水实验平台,支持串口通信和数据采集
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
deck: Deck,
|
|
||||||
port: str = "COM10",
|
|
||||||
baudrate: int = 115200,
|
|
||||||
csv_path: Optional[str] = None,
|
|
||||||
timeout: float = 0.2,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
super().__init__(deck, **kwargs)
|
|
||||||
|
|
||||||
# ========== 配置 ==========
|
|
||||||
self.port = port
|
|
||||||
self.baudrate = baudrate
|
|
||||||
# 如果没有指定路径,默认保存在代码文件所在目录
|
|
||||||
if csv_path is None:
|
|
||||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
self.csv_path = os.path.join(current_dir, "stm32_data.csv")
|
|
||||||
else:
|
|
||||||
self.csv_path = csv_path
|
|
||||||
self.ser_timeout = timeout
|
|
||||||
self.chunk_read = 128
|
|
||||||
|
|
||||||
# 串口对象
|
|
||||||
self.ser: Optional[serial.Serial] = None
|
|
||||||
self.stop_flag = False
|
|
||||||
|
|
||||||
# 线程对象
|
|
||||||
self.rx_thread: Optional[threading.Thread] = None
|
|
||||||
self.tx_thread: Optional[threading.Thread] = None
|
|
||||||
|
|
||||||
# ==== 接收(下位机->上位机):固定 1+13+1 = 15 字节 ====
|
|
||||||
self.RX_HEAD = 0x3E
|
|
||||||
self.RX_TAIL = 0x3E
|
|
||||||
self.RX_FRAME_LEN = 1 + 13 + 1 # 15
|
|
||||||
|
|
||||||
# ==== 发送(上位机->下位机):固定 1+9+1 = 11 字节 ====
|
|
||||||
self.TX_HEAD = 0x3E
|
|
||||||
self.TX_TAIL = 0xE3 # 协议图中标注 E3 作为帧尾
|
|
||||||
self.TX_FRAME_LEN = 1 + 9 + 1 # 11
|
|
||||||
|
|
||||||
def open_serial(self, port: Optional[str] = None, baudrate: Optional[int] = None, timeout: Optional[float] = None) -> Optional[serial.Serial]:
|
|
||||||
"""打开串口"""
|
|
||||||
port = port or self.port
|
|
||||||
baudrate = baudrate or self.baudrate
|
|
||||||
timeout = timeout or self.ser_timeout
|
|
||||||
try:
|
|
||||||
ser = serial.Serial(port, baudrate, timeout=timeout)
|
|
||||||
print(f"[OK] 串口 {port} 已打开,波特率 {baudrate}")
|
|
||||||
ser.reset_input_buffer()
|
|
||||||
ser.reset_output_buffer()
|
|
||||||
self.ser = ser
|
|
||||||
return ser
|
|
||||||
except serial.SerialException as e:
|
|
||||||
print(f"[ERR] 无法打开串口 {port}: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def close_serial(self):
|
|
||||||
"""关闭串口"""
|
|
||||||
if self.ser and self.ser.is_open:
|
|
||||||
self.ser.close()
|
|
||||||
print("[INFO] 串口已关闭")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def u16_be(h: int, l: int) -> int:
|
|
||||||
"""将两个字节组合成16位无符号整数(大端序)"""
|
|
||||||
return ((h & 0xFF) << 8) | (l & 0xFF)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def split_u16_be(val: int) -> tuple:
|
|
||||||
"""返回 (高字节, 低字节),输入会夹到 0..65535"""
|
|
||||||
v = int(max(0, min(65535, int(val))))
|
|
||||||
return (v >> 8) & 0xFF, v & 0xFF
|
|
||||||
|
|
||||||
# ================== 接收:固定15字节 ==================
|
|
||||||
def parse_rx_payload(self, dat13: bytes) -> Optional[Dict[str, Any]]:
|
|
||||||
"""解析 13 字节数据区(下位机发送到上位机)"""
|
|
||||||
if len(dat13) != 13:
|
|
||||||
return None
|
|
||||||
current_mA = self.u16_be(dat13[0], dat13[1])
|
|
||||||
voltage_mV = self.u16_be(dat13[2], dat13[3])
|
|
||||||
temperature_raw = self.u16_be(dat13[4], dat13[5])
|
|
||||||
tds_ppm = self.u16_be(dat13[6], dat13[7])
|
|
||||||
gas_sccm = self.u16_be(dat13[8], dat13[9])
|
|
||||||
liquid_mL = self.u16_be(dat13[10], dat13[11])
|
|
||||||
ph_raw = dat13[12] & 0xFF
|
|
||||||
|
|
||||||
return {
|
|
||||||
"Current_mA": current_mA,
|
|
||||||
"Voltage_mV": voltage_mV,
|
|
||||||
"Temperature_C": round(temperature_raw / 100.0, 2),
|
|
||||||
"TDS_ppm": tds_ppm,
|
|
||||||
"GasFlow_sccm": gas_sccm,
|
|
||||||
"LiquidFlow_mL": liquid_mL,
|
|
||||||
"pH": round(ph_raw / 10.0, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
def try_parse_rx_frame(self, frame15: bytes) -> Optional[Dict[str, Any]]:
|
|
||||||
"""尝试解析接收帧"""
|
|
||||||
if len(frame15) != self.RX_FRAME_LEN:
|
|
||||||
return None
|
|
||||||
if frame15[0] != self.RX_HEAD or frame15[-1] != self.RX_TAIL:
|
|
||||||
return None
|
|
||||||
return self.parse_rx_payload(frame15[1:-1])
|
|
||||||
|
|
||||||
def rx_thread_fn(self):
|
|
||||||
"""接收线程函数"""
|
|
||||||
headers = ["Timestamp", "Current_mA", "Voltage_mV",
|
|
||||||
"Temperature_C", "TDS_ppm", "GasFlow_sccm", "LiquidFlow_mL", "pH"]
|
|
||||||
|
|
||||||
new_file = not os.path.exists(self.csv_path)
|
|
||||||
f = open(self.csv_path, mode='a', newline='', encoding='utf-8')
|
|
||||||
writer = csv.writer(f)
|
|
||||||
if new_file:
|
|
||||||
writer.writerow(headers)
|
|
||||||
f.flush()
|
|
||||||
|
|
||||||
buf = deque(maxlen=8192)
|
|
||||||
print(f"[RX] 开始接收(帧长 {self.RX_FRAME_LEN} 字节);写入:{self.csv_path}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
while not self.stop_flag and self.ser and self.ser.is_open:
|
|
||||||
chunk = self.ser.read(self.chunk_read)
|
|
||||||
if chunk:
|
|
||||||
buf.extend(chunk)
|
|
||||||
while True:
|
|
||||||
# 找帧头
|
|
||||||
try:
|
|
||||||
start = next(i for i, b in enumerate(buf) if b == self.RX_HEAD)
|
|
||||||
except StopIteration:
|
|
||||||
buf.clear()
|
|
||||||
break
|
|
||||||
if start > 0:
|
|
||||||
for _ in range(start):
|
|
||||||
buf.popleft()
|
|
||||||
if len(buf) < self.RX_FRAME_LEN:
|
|
||||||
break
|
|
||||||
candidate = bytes([buf[i] for i in range(self.RX_FRAME_LEN)])
|
|
||||||
if candidate[-1] == self.RX_TAIL:
|
|
||||||
parsed = self.try_parse_rx_frame(candidate)
|
|
||||||
for _ in range(self.RX_FRAME_LEN):
|
|
||||||
buf.popleft()
|
|
||||||
if parsed:
|
|
||||||
ts = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
row = [ts,
|
|
||||||
parsed["Current_mA"], parsed["Voltage_mV"],
|
|
||||||
parsed["Temperature_C"], parsed["TDS_ppm"],
|
|
||||||
parsed["GasFlow_sccm"], parsed["LiquidFlow_mL"],
|
|
||||||
parsed["pH"]]
|
|
||||||
writer.writerow(row)
|
|
||||||
f.flush()
|
|
||||||
# 若不想打印可注释下一行
|
|
||||||
# print(f"[{ts}] I={parsed['Current_mA']} mA, V={parsed['Voltage_mV']} mV, "
|
|
||||||
# f"T={parsed['Temperature_C']} °C, TDS={parsed['TDS_ppm']}, "
|
|
||||||
# f"Gas={parsed['GasFlow_sccm']} sccm, Liq={parsed['LiquidFlow_mL']} mL, pH={parsed['pH']}")
|
|
||||||
else:
|
|
||||||
# 头不变,尾不对,丢1字节继续对齐
|
|
||||||
buf.popleft()
|
|
||||||
else:
|
|
||||||
time.sleep(0.01)
|
|
||||||
finally:
|
|
||||||
f.close()
|
|
||||||
print("[RX] 接收线程退出,CSV 已关闭")
|
|
||||||
|
|
||||||
# ================== 发送:固定11字节 ==================
|
|
||||||
def build_tx_frame(self, mode: int, current_ma: int, voltage_mv: int, temp_c: float, ki: float, pump_percent: float) -> bytes:
|
|
||||||
"""
|
|
||||||
发送帧:HEAD + [mode, I_hi, I_lo, V_hi, V_lo, T_hi, T_lo, Ki_byte, Pump_byte] + TAIL
|
|
||||||
- mode: 0=恒压, 1=恒流
|
|
||||||
- current_ma: mA (0..65535)
|
|
||||||
- voltage_mv: mV (0..65535)
|
|
||||||
- temp_c: ℃,将 *100 后拆分为高/低字节
|
|
||||||
- ki: 0.0..20.0 -> byte = round(ki * 10) 夹到 0..200
|
|
||||||
- pump_percent: 0..100 -> byte = round(pump * 2) 夹到 0..200
|
|
||||||
"""
|
|
||||||
mode_b = 1 if int(mode) == 1 else 0
|
|
||||||
|
|
||||||
i_hi, i_lo = self.split_u16_be(current_ma)
|
|
||||||
v_hi, v_lo = self.split_u16_be(voltage_mv)
|
|
||||||
|
|
||||||
t100 = int(round(float(temp_c) * 100.0))
|
|
||||||
t_hi, t_lo = self.split_u16_be(t100)
|
|
||||||
|
|
||||||
ki_b = int(max(0, min(200, round(float(ki) * 10))))
|
|
||||||
pump_b = int(max(0, min(200, round(float(pump_percent) * 2))))
|
|
||||||
|
|
||||||
return bytes((
|
|
||||||
self.TX_HEAD,
|
|
||||||
mode_b,
|
|
||||||
i_hi, i_lo,
|
|
||||||
v_hi, v_lo,
|
|
||||||
t_hi, t_lo,
|
|
||||||
ki_b,
|
|
||||||
pump_b,
|
|
||||||
self.TX_TAIL
|
|
||||||
))
|
|
||||||
|
|
||||||
def tx_thread_fn(self):
|
|
||||||
"""
|
|
||||||
发送线程函数
|
|
||||||
用户输入 6 个用逗号分隔的数值:
|
|
||||||
mode,current_mA,voltage_mV,set_temp_C,Ki,pump_percent
|
|
||||||
例如: 0,1000,500,0,0,50
|
|
||||||
"""
|
|
||||||
print("\n输入 6 个值(用英文逗号分隔),顺序为:")
|
|
||||||
print("mode,current_mA,voltage_mV,set_temp_C,Ki,pump_percent")
|
|
||||||
print("示例恒压:0,500,1000,25,0,100 (stop 结束)\n")
|
|
||||||
print("示例恒流:1,1000,500,25,0,100 (stop 结束)\n")
|
|
||||||
print("示例恒流:1,2000,500,25,0,100 (stop 结束)\n")
|
|
||||||
# 1,2000,500,25,0,100
|
|
||||||
|
|
||||||
while not self.stop_flag and self.ser and self.ser.is_open:
|
|
||||||
try:
|
|
||||||
line = input(">>> ").strip()
|
|
||||||
except EOFError:
|
|
||||||
self.stop_flag = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
if line.lower() == "stop":
|
|
||||||
self.stop_flag = True
|
|
||||||
print("[SYS] 停止程序")
|
|
||||||
break
|
|
||||||
|
|
||||||
try:
|
|
||||||
parts = [p.strip() for p in line.split(",")]
|
|
||||||
if len(parts) != 6:
|
|
||||||
raise ValueError("需要 6 个逗号分隔的数值")
|
|
||||||
mode = int(parts[0])
|
|
||||||
i_ma = int(float(parts[1]))
|
|
||||||
v_mv = int(float(parts[2]))
|
|
||||||
t_c = float(parts[3])
|
|
||||||
ki = float(parts[4])
|
|
||||||
pump = float(parts[5])
|
|
||||||
|
|
||||||
frame = self.build_tx_frame(mode, i_ma, v_mv, t_c, ki, pump)
|
|
||||||
self.ser.write(frame)
|
|
||||||
print("[TX]", " ".join(f"{b:02X}" for b in frame))
|
|
||||||
except Exception as e:
|
|
||||||
print("[TX] 输入/打包失败:", e)
|
|
||||||
print("格式:mode,current_mA,voltage_mV,set_temp_C,Ki,pump_percent")
|
|
||||||
continue
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
"""启动电解水平台"""
|
|
||||||
self.ser = self.open_serial()
|
|
||||||
if self.ser:
|
|
||||||
try:
|
|
||||||
self.rx_thread = threading.Thread(target=self.rx_thread_fn, daemon=True)
|
|
||||||
self.tx_thread = threading.Thread(target=self.tx_thread_fn, daemon=True)
|
|
||||||
self.rx_thread.start()
|
|
||||||
self.tx_thread.start()
|
|
||||||
print("[INFO] 电解水平台已启动")
|
|
||||||
self.tx_thread.join() # 等待用户输入线程结束(输入 stop)
|
|
||||||
finally:
|
|
||||||
self.close_serial()
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
"""停止电解水平台"""
|
|
||||||
self.stop_flag = True
|
|
||||||
if self.rx_thread and self.rx_thread.is_alive():
|
|
||||||
self.rx_thread.join(timeout=2.0)
|
|
||||||
if self.tx_thread and self.tx_thread.is_alive():
|
|
||||||
self.tx_thread.join(timeout=2.0)
|
|
||||||
self.close_serial()
|
|
||||||
print("[INFO] 电解水平台已停止")
|
|
||||||
|
|
||||||
|
|
||||||
# ================== 主入口 ==================
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# 创建一个简单的 Deck 用于测试
|
|
||||||
from pylabrobot.resources import Deck
|
|
||||||
|
|
||||||
deck = Deck()
|
|
||||||
platform = ElectrolysisWaterPlatform(deck)
|
|
||||||
platform.start()
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
from .neware_battery_test_system import NewareBatteryTestSystem
|
|
||||||
from .neware_driver import build_start_command, start_test
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"NewareBatteryTestSystem",
|
|
||||||
"build_start_command",
|
|
||||||
"start_test",
|
|
||||||
]
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
Timestamp,Battery_Count,Assembly_Time,Open_Circuit_Voltage,Pole_Weight,Assembly_Pressure,Battery_Code,Electrolyte_Code,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʺ<EFBFBD><EFBFBD><EFBFBD>,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>mah/g,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϵ,<EFBFBD>豸<EFBFBD><EFBFBD>,<EFBFBD>ź<EFBFBD>,ͨ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
|
||||||
2025/10/29 17:32,7,5,0.11299999803304672,18.049999237060547,3593,Li000595,Si-Gr001,9.2,0.954,469,SiGr_Li,1,1,2
|
|
||||||
2025/10/30 17:49,2,5,0,13.109999895095825,4094,YS101224,NoRead88,5.2,0.92,190,SiGr_Li,2,1,1
|
|
||||||
|
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"id": "NEWARE_BATTERY_TEST_SYSTEM",
|
|
||||||
"name": "Neware Battery Test System",
|
|
||||||
"parent": null,
|
|
||||||
"type": "device",
|
|
||||||
"class": "neware_battery_test_system",
|
|
||||||
"position": {
|
|
||||||
"x": 620.0,
|
|
||||||
"y": 200.0,
|
|
||||||
"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": {
|
|
||||||
"功能说明": "新威电池测试系统,提供720通道监控和CSV批量提交功能",
|
|
||||||
"监控功能": "支持720个通道的实时状态监控、2盘电池物料管理、状态导出等",
|
|
||||||
"提交功能": "通过submit_from_csv action从CSV文件批量提交测试任务。CSV必须包含: Battery_Code, Pole_Weight, 集流体质量, 活性物质含量, 克容量mah/g, 电池体系, 设备号, 排号, 通道号"
|
|
||||||
},
|
|
||||||
"children": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"links": []
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,49 +0,0 @@
|
|||||||
import socket
|
|
||||||
END_MARKS = [b"\r\n#\r\n", b"</bts>"] # 读到任一标志即可判定完整响应
|
|
||||||
|
|
||||||
def build_start_command(devid, subdevid, chlid, CoinID,
|
|
||||||
ip_in_xml="127.0.0.1",
|
|
||||||
devtype:int=27,
|
|
||||||
recipe_path:str=f"D:\\HHM_test\\A001.xml",
|
|
||||||
backup_dir:str=f"D:\\HHM_test\\backup") -> str:
|
|
||||||
lines = [
|
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
||||||
'<bts version="1.0">',
|
|
||||||
' <cmd>start</cmd>',
|
|
||||||
' <list count="1">',
|
|
||||||
f' <start ip="{ip_in_xml}" devtype="{devtype}" devid="{devid}" subdevid="{subdevid}" chlid="{chlid}" barcode="{CoinID}">{recipe_path}</start>',
|
|
||||||
f' <backup backupdir="{backup_dir}" remotedir="" filenametype="1" customfilename="" createdirbydate="0" filetype="0" backupontime="1" backupontimeinterval="1" backupfree="0" />',
|
|
||||||
' </list>',
|
|
||||||
'</bts>',
|
|
||||||
]
|
|
||||||
# TCP 模式:请求必须以 #\r\n 结束(协议要求)
|
|
||||||
return "\r\n".join(lines) + "\r\n#\r\n"
|
|
||||||
|
|
||||||
def recv_until_marks(sock: socket.socket, timeout=60):
|
|
||||||
sock.settimeout(timeout) # 上限给足,协议允许到 30s:contentReference[oaicite:2]{index=2}
|
|
||||||
buf = bytearray()
|
|
||||||
while True:
|
|
||||||
chunk = sock.recv(8192)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
buf += chunk
|
|
||||||
# 读到结束标志就停,避免等对端断开
|
|
||||||
for m in END_MARKS:
|
|
||||||
if m in buf:
|
|
||||||
return bytes(buf)
|
|
||||||
# 保险:读到完整 XML 结束标签也停
|
|
||||||
if b"</bts>" in buf:
|
|
||||||
return bytes(buf)
|
|
||||||
return bytes(buf)
|
|
||||||
|
|
||||||
def start_test(ip="127.0.0.1", port=502, devid=3, subdevid=2, chlid=1, CoinID="A001", recipe_path=f"D:\\HHM_test\\A001.xml", backup_dir=f"D:\\HHM_test\\backup"):
|
|
||||||
xml_cmd = build_start_command(devid=devid, subdevid=subdevid, chlid=chlid, CoinID=CoinID, recipe_path=recipe_path, backup_dir=backup_dir)
|
|
||||||
#print(xml_cmd)
|
|
||||||
with socket.create_connection((ip, port), timeout=60) as s:
|
|
||||||
s.sendall(xml_cmd.encode("utf-8"))
|
|
||||||
data = recv_until_marks(s, timeout=60)
|
|
||||||
return data.decode("utf-8", errors="replace")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
resp = start_test(ip="127.0.0.1", port=502, devid=4, subdevid=10, chlid=1, CoinID="A001", recipe_path=f"D:\\HHM_test\\A001.xml", backup_dir=f"D:\\HHM_test\\backup")
|
|
||||||
print(resp)
|
|
||||||
@@ -1,282 +1,636 @@
|
|||||||
import sys
|
# -*- coding: utf-8 -*-
|
||||||
import threading
|
"""
|
||||||
|
Contains drivers for:
|
||||||
|
1. SyringePump: Runze Fluid SY-03B (ASCII)
|
||||||
|
2. EmmMotor: Emm V5.0 Closed-loop Stepper (Modbus-RTU variant)
|
||||||
|
3. XKCSensor: XKC Non-contact Level Sensor (Modbus-RTU)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import socket
|
||||||
import serial
|
import serial
|
||||||
import serial.tools.list_ports
|
|
||||||
import re
|
|
||||||
import time
|
import time
|
||||||
from typing import Optional, List, Dict, Tuple
|
import threading
|
||||||
|
import struct
|
||||||
|
import re
|
||||||
|
import traceback
|
||||||
|
import queue
|
||||||
|
from typing import Optional, Dict, List, Any
|
||||||
|
|
||||||
class ChinweDevice:
|
try:
|
||||||
"""
|
from unilabos.device_comms.universal_driver import UniversalDriver
|
||||||
ChinWe设备控制类
|
except ImportError:
|
||||||
提供串口通信、电机控制、传感器数据读取等功能
|
import logging
|
||||||
"""
|
class UniversalDriver:
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = logging.getLogger(self.__class__.__name__)
|
||||||
|
|
||||||
def __init__(self, port: str, baudrate: int = 115200, debug: bool = False):
|
def execute_command_from_outer(self, command: str):
|
||||||
"""
|
pass
|
||||||
初始化ChinWe设备
|
|
||||||
|
|
||||||
Args:
|
# ==============================================================================
|
||||||
port: 串口名称,如果为None则自动检测
|
# 1. Transport Layer (通信层)
|
||||||
baudrate: 波特率,默认115200
|
# ==============================================================================
|
||||||
|
|
||||||
|
class TransportManager:
|
||||||
"""
|
"""
|
||||||
self.debug = debug
|
统一通信管理类。
|
||||||
|
自动识别 串口 (Serial) 或 网络 (TCP) 连接。
|
||||||
|
"""
|
||||||
|
def __init__(self, port: str, baudrate: int = 9600, timeout: float = 3.0, logger=None):
|
||||||
self.port = port
|
self.port = port
|
||||||
self.baudrate = baudrate
|
self.baudrate = baudrate
|
||||||
self.serial_port: Optional[serial.Serial] = None
|
self.timeout = timeout
|
||||||
self._voltage: float = 0.0
|
self.logger = logger
|
||||||
self._ec_value: float = 0.0
|
self.lock = threading.RLock() # 线程锁,确保多设备共用一个连接时不冲突
|
||||||
self._ec_adc_value: int = 0
|
|
||||||
|
self.is_tcp = False
|
||||||
|
self.serial = None
|
||||||
|
self.socket = None
|
||||||
|
|
||||||
|
# 简单判断: 如果包含 ':' (如 192.168.1.1:8899) 或者看起来像 IP,则认为是 TCP
|
||||||
|
if ':' in self.port or (self.port.count('.') == 3 and not self.port.startswith('/')):
|
||||||
|
self.is_tcp = True
|
||||||
|
self._connect_tcp()
|
||||||
|
else:
|
||||||
|
self._connect_serial()
|
||||||
|
|
||||||
|
def _log(self, msg):
|
||||||
|
if self.logger:
|
||||||
|
pass
|
||||||
|
# self.logger.debug(f"[Transport] {msg}")
|
||||||
|
|
||||||
|
def _connect_tcp(self):
|
||||||
|
try:
|
||||||
|
if ':' in self.port:
|
||||||
|
host, p = self.port.split(':')
|
||||||
|
self.tcp_host = host
|
||||||
|
self.tcp_port = int(p)
|
||||||
|
else:
|
||||||
|
self.tcp_host = self.port
|
||||||
|
self.tcp_port = 8899 # 默认端口
|
||||||
|
|
||||||
|
# if self.logger: self.logger.info(f"Connecting TCP {self.tcp_host}:{self.tcp_port} ...")
|
||||||
|
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
self.socket.settimeout(self.timeout)
|
||||||
|
self.socket.connect((self.tcp_host, self.tcp_port))
|
||||||
|
except Exception as e:
|
||||||
|
raise ConnectionError(f"TCP connection failed: {e}")
|
||||||
|
|
||||||
|
def _connect_serial(self):
|
||||||
|
try:
|
||||||
|
# if self.logger: self.logger.info(f"Opening Serial {self.port} (Baud: {self.baudrate}) ...")
|
||||||
|
self.serial = serial.Serial(
|
||||||
|
port=self.port,
|
||||||
|
baudrate=self.baudrate,
|
||||||
|
timeout=self.timeout
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise ConnectionError(f"Serial open failed: {e}")
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""关闭连接"""
|
||||||
|
if self.is_tcp and self.socket:
|
||||||
|
try: self.socket.close()
|
||||||
|
except: pass
|
||||||
|
elif not self.is_tcp and self.serial and self.serial.is_open:
|
||||||
|
self.serial.close()
|
||||||
|
|
||||||
|
def clear_buffer(self):
|
||||||
|
"""清空缓冲区 (Thread-safe)"""
|
||||||
|
with self.lock:
|
||||||
|
if self.is_tcp:
|
||||||
|
self.socket.setblocking(False)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
if not self.socket.recv(1024): break
|
||||||
|
except: pass
|
||||||
|
finally: self.socket.settimeout(self.timeout)
|
||||||
|
else:
|
||||||
|
self.serial.reset_input_buffer()
|
||||||
|
|
||||||
|
def write(self, data: bytes):
|
||||||
|
"""发送原始字节"""
|
||||||
|
with self.lock:
|
||||||
|
if self.is_tcp:
|
||||||
|
self.socket.sendall(data)
|
||||||
|
else:
|
||||||
|
self.serial.write(data)
|
||||||
|
|
||||||
|
def read(self, size: int) -> bytes:
|
||||||
|
"""读取指定长度字节"""
|
||||||
|
if self.is_tcp:
|
||||||
|
data = b''
|
||||||
|
start = time.time()
|
||||||
|
while len(data) < size:
|
||||||
|
if time.time() - start > self.timeout: break
|
||||||
|
try:
|
||||||
|
chunk = self.socket.recv(size - len(data))
|
||||||
|
if not chunk: break
|
||||||
|
data += chunk
|
||||||
|
except socket.timeout: break
|
||||||
|
return data
|
||||||
|
else:
|
||||||
|
return self.serial.read(size)
|
||||||
|
|
||||||
|
def send_ascii_command(self, command: str) -> str:
|
||||||
|
"""
|
||||||
|
发送 ASCII 字符串命令 (如注射泵指令),读取直到 '\r'。
|
||||||
|
"""
|
||||||
|
with self.lock:
|
||||||
|
data = command.encode('ascii') if isinstance(command, str) else command
|
||||||
|
self.clear_buffer()
|
||||||
|
self.write(data)
|
||||||
|
|
||||||
|
# Read until \r
|
||||||
|
if self.is_tcp:
|
||||||
|
resp = b''
|
||||||
|
start = time.time()
|
||||||
|
while True:
|
||||||
|
if time.time() - start > self.timeout: break
|
||||||
|
try:
|
||||||
|
char = self.socket.recv(1)
|
||||||
|
if not char: break
|
||||||
|
resp += char
|
||||||
|
if char == b'\r': break
|
||||||
|
except: break
|
||||||
|
return resp.decode('ascii', errors='ignore').strip()
|
||||||
|
else:
|
||||||
|
return self.serial.read_until(b'\r').decode('ascii', errors='ignore').strip()
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# 2. Syringe Pump Driver (注射泵)
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
class SyringePump:
|
||||||
|
"""SY-03B 注射泵驱动 (ASCII协议)"""
|
||||||
|
|
||||||
|
CMD_INITIALIZE = "Z{speed},{drain_port},{output_port}R"
|
||||||
|
CMD_SWITCH_VALVE = "I{port}R"
|
||||||
|
CMD_ASPIRATE = "P{vol}R"
|
||||||
|
CMD_DISPENSE = "D{vol}R"
|
||||||
|
CMD_DISPENSE_ALL = "A0R"
|
||||||
|
CMD_STOP = "TR"
|
||||||
|
CMD_QUERY_STATUS = "Q"
|
||||||
|
CMD_QUERY_PLUNGER = "?0"
|
||||||
|
|
||||||
|
def __init__(self, device_id: int, transport: TransportManager):
|
||||||
|
if not 1 <= device_id <= 15:
|
||||||
|
pass # Allow all IDs for now
|
||||||
|
self.id = str(device_id)
|
||||||
|
self.transport = transport
|
||||||
|
|
||||||
|
def _send(self, template: str, **kwargs) -> str:
|
||||||
|
cmd = f"/{self.id}" + template.format(**kwargs) + "\r"
|
||||||
|
return self.transport.send_ascii_command(cmd)
|
||||||
|
|
||||||
|
def is_busy(self) -> bool:
|
||||||
|
"""查询繁忙状态"""
|
||||||
|
resp = self._send(self.CMD_QUERY_STATUS)
|
||||||
|
# 响应如 /0` (Ready, 0x60) 或 /0@ (Busy, 0x40)
|
||||||
|
if len(resp) >= 3:
|
||||||
|
status_byte = ord(resp[2])
|
||||||
|
# Bit 5: 1=Ready, 0=Busy
|
||||||
|
return (status_byte & 0x20) == 0
|
||||||
|
return False
|
||||||
|
|
||||||
|
def wait_until_idle(self, timeout=30):
|
||||||
|
"""阻塞等待直到空闲"""
|
||||||
|
start = time.time()
|
||||||
|
while time.time() - start < timeout:
|
||||||
|
if not self.is_busy(): return
|
||||||
|
time.sleep(0.5)
|
||||||
|
# raise TimeoutError(f"Pump {self.id} wait idle timeout")
|
||||||
|
pass
|
||||||
|
|
||||||
|
def initialize(self, drain_port=0, output_port=0, speed=10):
|
||||||
|
"""初始化"""
|
||||||
|
self._send(self.CMD_INITIALIZE, speed=speed, drain_port=drain_port, output_port=output_port)
|
||||||
|
|
||||||
|
def switch_valve(self, port: int):
|
||||||
|
"""切换阀门 (1-8)"""
|
||||||
|
self._send(self.CMD_SWITCH_VALVE, port=port)
|
||||||
|
|
||||||
|
def aspirate(self, steps: int):
|
||||||
|
"""吸液 (相对步数)"""
|
||||||
|
self._send(self.CMD_ASPIRATE, vol=steps)
|
||||||
|
|
||||||
|
def dispense(self, steps: int):
|
||||||
|
"""排液 (相对步数)"""
|
||||||
|
self._send(self.CMD_DISPENSE, vol=steps)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""停止"""
|
||||||
|
self._send(self.CMD_STOP)
|
||||||
|
|
||||||
|
def get_position(self) -> int:
|
||||||
|
"""获取柱塞位置 (步数)"""
|
||||||
|
resp = self._send(self.CMD_QUERY_PLUNGER)
|
||||||
|
m = re.search(r'\d+', resp)
|
||||||
|
return int(m.group()) if m else -1
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# 3. Stepper Motor Driver (步进电机)
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
class EmmMotor:
|
||||||
|
"""Emm V5.0 闭环步进电机驱动"""
|
||||||
|
|
||||||
|
def __init__(self, device_id: int, transport: TransportManager):
|
||||||
|
self.id = device_id
|
||||||
|
self.transport = transport
|
||||||
|
|
||||||
|
def _send(self, func_code: int, payload: list) -> bytes:
|
||||||
|
with self.transport.lock:
|
||||||
|
self.transport.clear_buffer()
|
||||||
|
# 格式: [ID] [Func] [Data...] [Check=0x6B]
|
||||||
|
body = [self.id, func_code] + payload
|
||||||
|
body.append(0x6B) # Checksum
|
||||||
|
self.transport.write(bytes(body))
|
||||||
|
|
||||||
|
# 根据指令不同,读取不同长度响应
|
||||||
|
read_len = 10 if func_code in [0x31, 0x32, 0x35, 0x24, 0x27] else 4
|
||||||
|
return self.transport.read(read_len)
|
||||||
|
|
||||||
|
def enable(self, on=True):
|
||||||
|
"""使能 (True=锁轴, False=松轴)"""
|
||||||
|
state = 1 if on else 0
|
||||||
|
self._send(0xF3, [0xAB, state, 0])
|
||||||
|
|
||||||
|
def run_speed(self, speed_rpm: int, direction=0, acc=10):
|
||||||
|
"""速度模式运行"""
|
||||||
|
sp = struct.pack('>H', int(speed_rpm))
|
||||||
|
self._send(0xF6, [direction, sp[0], sp[1], acc, 0])
|
||||||
|
|
||||||
|
def run_position(self, pulses: int, speed_rpm: int, direction=0, acc=10, absolute=False):
|
||||||
|
"""位置模式运行"""
|
||||||
|
sp = struct.pack('>H', int(speed_rpm))
|
||||||
|
pl = struct.pack('>I', int(pulses))
|
||||||
|
is_abs = 1 if absolute else 0
|
||||||
|
self._send(0xFD, [direction, sp[0], sp[1], acc, pl[0], pl[1], pl[2], pl[3], is_abs, 0])
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""停止"""
|
||||||
|
self._send(0xFE, [0x98, 0])
|
||||||
|
|
||||||
|
def set_zero(self):
|
||||||
|
"""清零位置"""
|
||||||
|
self._send(0x0A, [])
|
||||||
|
|
||||||
|
def get_position(self) -> int:
|
||||||
|
"""获取当前脉冲位置"""
|
||||||
|
resp = self._send(0x32, [])
|
||||||
|
if len(resp) >= 8:
|
||||||
|
sign = resp[2]
|
||||||
|
val = struct.unpack('>I', resp[3:7])[0]
|
||||||
|
return -val if sign == 1 else val
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# 4. Liquid Sensor Driver (液位传感器)
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
class XKCSensor:
|
||||||
|
"""XKC RS485 液位传感器 (Modbus RTU)"""
|
||||||
|
|
||||||
|
def __init__(self, device_id: int, transport: TransportManager, threshold: int = 300):
|
||||||
|
self.id = device_id
|
||||||
|
self.transport = transport
|
||||||
|
self.threshold = threshold
|
||||||
|
|
||||||
|
def _crc(self, data: bytes) -> bytes:
|
||||||
|
crc = 0xFFFF
|
||||||
|
for byte in data:
|
||||||
|
crc ^= byte
|
||||||
|
for _ in range(8):
|
||||||
|
if crc & 0x0001: crc = (crc >> 1) ^ 0xA001
|
||||||
|
else: crc >>= 1
|
||||||
|
return struct.pack('<H', crc)
|
||||||
|
|
||||||
|
def read_level(self) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
读取液位。
|
||||||
|
返回: {'level': bool, 'rssi': int}
|
||||||
|
"""
|
||||||
|
with self.transport.lock:
|
||||||
|
self.transport.clear_buffer()
|
||||||
|
# Modbus Read Registers: 01 03 00 01 00 02 CRC
|
||||||
|
payload = struct.pack('>HH', 0x0001, 0x0002)
|
||||||
|
msg = struct.pack('BB', self.id, 0x03) + payload
|
||||||
|
msg += self._crc(msg)
|
||||||
|
self.transport.write(msg)
|
||||||
|
|
||||||
|
# Read header
|
||||||
|
h = self.transport.read(3) # Addr, Func, Len
|
||||||
|
if len(h) < 3: return None
|
||||||
|
length = h[2]
|
||||||
|
|
||||||
|
# Read body + CRC
|
||||||
|
body = self.transport.read(length + 2)
|
||||||
|
if len(body) < length + 2:
|
||||||
|
# Firmware bug fix specific to some modules
|
||||||
|
if len(body) == 4 and length == 4:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
data = body[:-2]
|
||||||
|
if len(data) == 2:
|
||||||
|
rssi = data[1]
|
||||||
|
elif len(data) >= 4:
|
||||||
|
rssi = (data[2] << 8) | data[3]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
'level': rssi > self.threshold,
|
||||||
|
'rssi': rssi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# 5. Main Device Class (ChinweDevice)
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
class ChinweDevice(UniversalDriver):
|
||||||
|
"""
|
||||||
|
ChinWe 工作站主驱动
|
||||||
|
继承自 UniversalDriver,管理所有子设备(泵、电机、传感器)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, port: str = "192.168.1.200:8899", baudrate: int = 9600,
|
||||||
|
pump_ids: List[int] = None, motor_ids: List[int] = None,
|
||||||
|
sensor_id: int = 6, sensor_threshold: int = 300):
|
||||||
|
"""
|
||||||
|
初始化 ChinWe 工作站
|
||||||
|
:param port: 串口号 或 IP:Port
|
||||||
|
:param baudrate: 串口波特率
|
||||||
|
:param pump_ids: 注射泵 ID列表 (默认 [1, 2, 3])
|
||||||
|
:param motor_ids: 步进电机 ID列表 (默认 [4, 5])
|
||||||
|
:param sensor_id: 液位传感器 ID (默认 6)
|
||||||
|
"""
|
||||||
|
super().__init__()
|
||||||
|
self.port = port
|
||||||
|
self.baudrate = baudrate
|
||||||
|
self.mgr = None
|
||||||
self._is_connected = False
|
self._is_connected = False
|
||||||
|
|
||||||
|
# 默认配置
|
||||||
|
if pump_ids is None: pump_ids = [1, 2, 3]
|
||||||
|
if motor_ids is None: motor_ids = [4, 5]
|
||||||
|
|
||||||
|
# 配置信息
|
||||||
|
self.pump_ids = pump_ids
|
||||||
|
self.motor_ids = motor_ids
|
||||||
|
self.sensor_id = sensor_id
|
||||||
|
self.sensor_threshold = sensor_threshold
|
||||||
|
|
||||||
|
# 子设备实例容器
|
||||||
|
self.pumps: Dict[int, SyringePump] = {}
|
||||||
|
self.motors: Dict[int, EmmMotor] = {}
|
||||||
|
self.sensor: Optional[XKCSensor] = None
|
||||||
|
|
||||||
|
# 轮询线程控制
|
||||||
|
self._stop_event = threading.Event()
|
||||||
|
self._poll_thread = None
|
||||||
|
|
||||||
|
# 实时状态缓存
|
||||||
|
self.status_cache = {
|
||||||
|
"sensor_rssi": 0,
|
||||||
|
"sensor_level": False,
|
||||||
|
"connected": False
|
||||||
|
}
|
||||||
|
|
||||||
|
# 自动连接
|
||||||
|
if self.port:
|
||||||
self.connect()
|
self.connect()
|
||||||
|
|
||||||
|
def connect(self) -> bool:
|
||||||
|
if self._is_connected: return True
|
||||||
|
try:
|
||||||
|
self.logger.info(f"Connecting to {self.port}...")
|
||||||
|
self.mgr = TransportManager(self.port, baudrate=self.baudrate, logger=self.logger)
|
||||||
|
|
||||||
|
# 初始化所有泵
|
||||||
|
for pid in self.pump_ids:
|
||||||
|
self.pumps[pid] = SyringePump(pid, self.mgr)
|
||||||
|
|
||||||
|
# 初始化所有电机
|
||||||
|
for mid in self.motor_ids:
|
||||||
|
self.motors[mid] = EmmMotor(mid, self.mgr)
|
||||||
|
|
||||||
|
# 初始化传感器
|
||||||
|
self.sensor = XKCSensor(self.sensor_id, self.mgr, self.sensor_threshold)
|
||||||
|
|
||||||
|
self._is_connected = True
|
||||||
|
self.status_cache["connected"] = True
|
||||||
|
|
||||||
|
# 启动轮询线程
|
||||||
|
self._start_polling()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Connection failed: {e}")
|
||||||
|
self._is_connected = False
|
||||||
|
self.status_cache["connected"] = False
|
||||||
|
return False
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
self._stop_event.set()
|
||||||
|
if self._poll_thread:
|
||||||
|
self._poll_thread.join(timeout=2.0)
|
||||||
|
|
||||||
|
if self.mgr:
|
||||||
|
self.mgr.close()
|
||||||
|
|
||||||
|
self._is_connected = False
|
||||||
|
self.status_cache["connected"] = False
|
||||||
|
self.logger.info("Disconnected.")
|
||||||
|
|
||||||
|
def _start_polling(self):
|
||||||
|
"""启动传感器轮询线程"""
|
||||||
|
if self._poll_thread and self._poll_thread.is_alive():
|
||||||
|
return
|
||||||
|
|
||||||
|
self._stop_event.clear()
|
||||||
|
self._poll_thread = threading.Thread(target=self._polling_loop, daemon=True, name="ChinwePoll")
|
||||||
|
self._poll_thread.start()
|
||||||
|
|
||||||
|
def _polling_loop(self):
|
||||||
|
"""轮询主循环"""
|
||||||
|
self.logger.info("Sensor polling started.")
|
||||||
|
error_count = 0
|
||||||
|
while not self._stop_event.is_set():
|
||||||
|
if not self._is_connected or not self.sensor:
|
||||||
|
time.sleep(1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 获取传感器数据
|
||||||
|
data = self.sensor.read_level()
|
||||||
|
if data:
|
||||||
|
self.status_cache["sensor_rssi"] = data['rssi']
|
||||||
|
self.status_cache["sensor_level"] = data['level']
|
||||||
|
error_count = 0
|
||||||
|
else:
|
||||||
|
error_count += 1
|
||||||
|
|
||||||
|
# 降低轮询频率防止总线拥塞
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_count += 1
|
||||||
|
if error_count > 10: # 连续错误记录日志
|
||||||
|
# self.logger.error(f"Polling error: {e}")
|
||||||
|
error_count = 0
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# --- 对外暴露属性 (Properties) ---
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sensor_level(self) -> bool:
|
||||||
|
return self.status_cache["sensor_level"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sensor_rssi(self) -> int:
|
||||||
|
return self.status_cache["sensor_rssi"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_connected(self) -> bool:
|
def is_connected(self) -> bool:
|
||||||
"""获取连接状态"""
|
return self._is_connected
|
||||||
return self._is_connected and self.serial_port and self.serial_port.is_open
|
|
||||||
|
|
||||||
@property
|
# --- 对外功能指令 (Actions) ---
|
||||||
def voltage(self) -> float:
|
|
||||||
"""获取电源电压值"""
|
|
||||||
return self._voltage
|
|
||||||
|
|
||||||
@property
|
def pump_initialize(self, pump_id: int, drain_port=0, output_port=0, speed=10):
|
||||||
def ec_value(self) -> float:
|
"""指定泵初始化"""
|
||||||
"""获取电导率值 (ms/cm)"""
|
pump_id = int(pump_id)
|
||||||
return self._ec_value
|
if pump_id in self.pumps:
|
||||||
|
self.pumps[pump_id].initialize(drain_port, output_port, speed)
|
||||||
@property
|
self.pumps[pump_id].wait_until_idle()
|
||||||
def ec_adc_value(self) -> int:
|
|
||||||
"""获取EC ADC原始值"""
|
|
||||||
return self._ec_adc_value
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def device_status(self) -> Dict[str, any]:
|
|
||||||
"""
|
|
||||||
获取设备状态信息
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
包含设备状态的字典
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
"connected": self.is_connected,
|
|
||||||
"port": self.port,
|
|
||||||
"baudrate": self.baudrate,
|
|
||||||
"voltage": self.voltage,
|
|
||||||
"ec_value": self.ec_value,
|
|
||||||
"ec_adc_value": self.ec_adc_value
|
|
||||||
}
|
|
||||||
|
|
||||||
def connect(self, port: Optional[str] = None, baudrate: Optional[int] = None) -> bool:
|
|
||||||
"""
|
|
||||||
连接到串口设备
|
|
||||||
|
|
||||||
Args:
|
|
||||||
port: 串口名称,如果为None则使用初始化时的port或自动检测
|
|
||||||
baudrate: 波特率,如果为None则使用初始化时的baudrate
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
连接是否成功
|
|
||||||
"""
|
|
||||||
if self.is_connected:
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
target_port = port or self.port
|
|
||||||
target_baudrate = baudrate or self.baudrate
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.serial_port = serial.Serial(target_port, target_baudrate, timeout=0.5)
|
|
||||||
self._is_connected = True
|
|
||||||
self.port = target_port
|
|
||||||
self.baudrate = target_baudrate
|
|
||||||
connect_allow_times = 5
|
|
||||||
while not self.serial_port.is_open and connect_allow_times > 0:
|
|
||||||
time.sleep(0.5)
|
|
||||||
connect_allow_times -= 1
|
|
||||||
print(f"尝试连接到 {target_port} @ {target_baudrate},剩余尝试次数: {connect_allow_times}", self.debug)
|
|
||||||
raise ValueError("串口未打开,请检查设备连接")
|
|
||||||
print(f"已连接到 {target_port} @ {target_baudrate}", self.debug)
|
|
||||||
threading.Thread(target=self._read_data, daemon=True).start()
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"ChinweDevice连接失败: {e}")
|
|
||||||
self._is_connected = False
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def disconnect(self) -> bool:
|
def pump_aspirate(self, pump_id: int, volume: int, valve_port: int):
|
||||||
"""
|
"""
|
||||||
断开串口连接
|
泵吸液 (阻塞)
|
||||||
|
:param valve_port: 阀门端口 (1-8)
|
||||||
Returns:
|
|
||||||
断开是否成功
|
|
||||||
"""
|
"""
|
||||||
if self.serial_port and self.serial_port.is_open:
|
pump_id = int(pump_id)
|
||||||
try:
|
valve_port = int(valve_port)
|
||||||
self.serial_port.close()
|
if pump_id in self.pumps:
|
||||||
self._is_connected = False
|
pump = self.pumps[pump_id]
|
||||||
print("已断开串口连接")
|
# 1. 切换阀门
|
||||||
|
pump.switch_valve(valve_port)
|
||||||
|
pump.wait_until_idle()
|
||||||
|
# 2. 吸液
|
||||||
|
pump.aspirate(volume)
|
||||||
|
pump.wait_until_idle()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
|
||||||
print(f"断开连接失败: {e}")
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _send_motor_command(self, command: str) -> bool:
|
|
||||||
"""
|
|
||||||
发送电机控制命令
|
|
||||||
|
|
||||||
Args:
|
|
||||||
command: 电机命令字符串,例如 "M 1 CW 1.5"
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
发送是否成功
|
|
||||||
"""
|
|
||||||
if not self.is_connected:
|
|
||||||
print("设备未连接")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
def pump_dispense(self, pump_id: int, volume: int, valve_port: int):
|
||||||
self.serial_port.write((command + "\n").encode('utf-8'))
|
"""
|
||||||
print(f"发送命令: {command}")
|
泵排液 (阻塞)
|
||||||
|
:param valve_port: 阀门端口 (1-8)
|
||||||
|
"""
|
||||||
|
pump_id = int(pump_id)
|
||||||
|
valve_port = int(valve_port)
|
||||||
|
if pump_id in self.pumps:
|
||||||
|
pump = self.pumps[pump_id]
|
||||||
|
# 1. 切换阀门
|
||||||
|
pump.switch_valve(valve_port)
|
||||||
|
pump.wait_until_idle()
|
||||||
|
# 2. 排液
|
||||||
|
pump.dispense(volume)
|
||||||
|
pump.wait_until_idle()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
|
||||||
print(f"发送命令失败: {e}")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def rotate_motor(self, motor_id: int, turns: float, clockwise: bool = True) -> bool:
|
def pump_valve(self, pump_id: int, port: int):
|
||||||
|
"""泵切换阀门 (阻塞)"""
|
||||||
|
pump_id = int(pump_id)
|
||||||
|
port = int(port)
|
||||||
|
if pump_id in self.pumps:
|
||||||
|
pump = self.pumps[pump_id]
|
||||||
|
pump.switch_valve(port)
|
||||||
|
pump.wait_until_idle()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def motor_run_continuous(self, motor_id: int, speed: int, direction: str = "顺时针"):
|
||||||
"""
|
"""
|
||||||
使电机转动指定圈数
|
电机一直旋转 (速度模式)
|
||||||
|
:param direction: "顺时针" or "逆时针"
|
||||||
Args:
|
|
||||||
motor_id: 电机ID(1, 2, 3...)
|
|
||||||
turns: 转动圈数,支持小数
|
|
||||||
clockwise: True为顺时针,False为逆时针
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
命令发送是否成功
|
|
||||||
"""
|
"""
|
||||||
if clockwise:
|
motor_id = int(motor_id)
|
||||||
command = f"M {motor_id} CW {turns}"
|
if motor_id not in self.motors: return False
|
||||||
else:
|
|
||||||
command = f"M {motor_id} CCW {turns}"
|
|
||||||
return self._send_motor_command(command)
|
|
||||||
|
|
||||||
def set_motor_speed(self, motor_id: int, speed: float) -> bool:
|
dir_val = 0 if direction == "顺时针" else 1
|
||||||
|
self.motors[motor_id].run_speed(speed, dir_val)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def motor_rotate_quarter(self, motor_id: int, speed: int = 60, direction: str = "顺时针"):
|
||||||
"""
|
"""
|
||||||
设置电机转速(如果设备支持)
|
电机旋转1/4圈 (阻塞)
|
||||||
|
假设电机设置为 3200 脉冲/圈,1/4圈 = 800脉冲
|
||||||
Args:
|
|
||||||
motor_id: 电机ID(1, 2, 3...)
|
|
||||||
speed: 转速值
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
命令发送是否成功
|
|
||||||
"""
|
"""
|
||||||
command = f"M {motor_id} SPEED {speed}"
|
motor_id = int(motor_id)
|
||||||
return self._send_motor_command(command)
|
if motor_id not in self.motors: return False
|
||||||
|
|
||||||
def _read_data(self) -> List[str]:
|
pulses = 800
|
||||||
|
dir_val = 0 if direction == "顺时针" else 1
|
||||||
|
|
||||||
|
self.motors[motor_id].run_position(pulses, speed, dir_val, absolute=False)
|
||||||
|
|
||||||
|
# 预估时间阻塞 (单位: 分钟 -> 秒)
|
||||||
|
# Time(s) = revs / (RPM/60). revs = 0.25. time = 15 / RPM.
|
||||||
|
estimated_time = 15.0 / max(1, speed)
|
||||||
|
time.sleep(estimated_time + 0.5)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def motor_stop(self, motor_id: int):
|
||||||
|
"""电机停止"""
|
||||||
|
motor_id = int(motor_id)
|
||||||
|
if motor_id in self.motors:
|
||||||
|
self.motors[motor_id].stop()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def wait_sensor_level(self, target_state: str = "有液", timeout: int = 30) -> bool:
|
||||||
"""
|
"""
|
||||||
读取串口数据并解析
|
等待传感器达到指定电平
|
||||||
|
:param target_state: "有液" or "无液"
|
||||||
Returns:
|
|
||||||
读取到的数据行列表
|
|
||||||
"""
|
"""
|
||||||
print("开始读取串口数据...")
|
target_bool = True if target_state == "有液" else False
|
||||||
if not self.is_connected:
|
|
||||||
return []
|
|
||||||
|
|
||||||
data_lines = []
|
self.logger.info(f"Wait sensor: {target_state} ({target_bool}), timeout: {timeout}")
|
||||||
try:
|
start = time.time()
|
||||||
while self.serial_port.in_waiting:
|
while time.time() - start < timeout:
|
||||||
time.sleep(0.1) # 等待数据稳定
|
if self.sensor_level == target_bool:
|
||||||
try:
|
return True
|
||||||
line = self.serial_port.readline().decode('utf-8', errors='ignore').strip()
|
time.sleep(0.1)
|
||||||
if line:
|
self.logger.warning("Wait sensor level timeout")
|
||||||
data_lines.append(line)
|
return False
|
||||||
self._parse_sensor_data(line)
|
|
||||||
except Exception as ex:
|
|
||||||
print(f"解码数据错误: {ex}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"读取串口数据错误: {e}")
|
|
||||||
|
|
||||||
return data_lines
|
def execute_command_from_outer(self, command_dict: Dict[str, Any]) -> bool:
|
||||||
|
"""支持标准 JSON 指令调用"""
|
||||||
|
return super().execute_command_from_outer(command_dict)
|
||||||
|
|
||||||
def _parse_sensor_data(self, line: str) -> None:
|
|
||||||
"""
|
|
||||||
解析传感器数据
|
|
||||||
|
|
||||||
Args:
|
|
||||||
line: 接收到的数据行
|
|
||||||
"""
|
|
||||||
# 解析电源电压
|
|
||||||
if "电源电压" in line:
|
|
||||||
try:
|
|
||||||
val = float(line.split(":")[1].replace("V", "").strip())
|
|
||||||
self._voltage = val
|
|
||||||
if self.debug:
|
|
||||||
print(f"电源电压更新: {val}V")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 解析电导率和ADC原始值(支持两种格式)
|
|
||||||
if "电导率" in line and "ADC原始值" in line:
|
|
||||||
try:
|
|
||||||
# 支持格式如:电导率:2.50ms/cm, ADC原始值:2052
|
|
||||||
ec_match = re.search(r"电导率[::]\s*([\d\.]+)", line)
|
|
||||||
adc_match = re.search(r"ADC原始值[::]\s*(\d+)", line)
|
|
||||||
if ec_match:
|
|
||||||
ec_val = float(ec_match.group(1))
|
|
||||||
self._ec_value = ec_val
|
|
||||||
if self.debug:
|
|
||||||
print(f"电导率更新: {ec_val:.2f} ms/cm")
|
|
||||||
if adc_match:
|
|
||||||
adc_val = int(adc_match.group(1))
|
|
||||||
self._ec_adc_value = adc_val
|
|
||||||
if self.debug:
|
|
||||||
print(f"EC ADC原始值更新: {adc_val}")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
# 仅电导率,无ADC原始值
|
|
||||||
elif "电导率" in line:
|
|
||||||
try:
|
|
||||||
val = float(line.split(":")[1].replace("ms/cm", "").strip())
|
|
||||||
self._ec_value = val
|
|
||||||
if self.debug:
|
|
||||||
print(f"电导率更新: {val:.2f} ms/cm")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
# 仅ADC原始值(如有分开回传场景)
|
|
||||||
elif "ADC原始值" in line:
|
|
||||||
try:
|
|
||||||
adc_val = int(line.split(":")[1].strip())
|
|
||||||
self._ec_adc_value = adc_val
|
|
||||||
if self.debug:
|
|
||||||
print(f"EC ADC原始值更新: {adc_val}")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def spin_when_ec_ge_0():
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""测试函数"""
|
|
||||||
print("=== ChinWe设备测试 ===")
|
|
||||||
|
|
||||||
# 创建设备实例
|
|
||||||
device = ChinweDevice("/dev/tty.usbserial-A5069RR4", debug=True)
|
|
||||||
try:
|
|
||||||
# 测试5: 发送电机命令
|
|
||||||
print("\n5. 发送电机命令测试:")
|
|
||||||
print(" 5.3 使用通用函数控制电机20顺时针转2圈:")
|
|
||||||
device.rotate_motor(2, 20.0, clockwise=True)
|
|
||||||
time.sleep(0.5)
|
|
||||||
finally:
|
|
||||||
time.sleep(10)
|
|
||||||
# 测试7: 断开连接
|
|
||||||
print("\n7. 断开连接:")
|
|
||||||
device.disconnect()
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
# Test
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
dev = ChinweDevice(port="192.168.31.201:8899")
|
||||||
|
try:
|
||||||
|
if dev.is_connected:
|
||||||
|
print(f"Status: Level={dev.sensor_level}, RSSI={dev.sensor_rssi}")
|
||||||
|
|
||||||
|
# Test pump 1
|
||||||
|
# dev.pump_valve(1, 1)
|
||||||
|
# dev.pump_move(1, 1000, "aspirate")
|
||||||
|
|
||||||
|
# Test motor 4
|
||||||
|
# dev.motor_run(4, 60, 0, 2)
|
||||||
|
|
||||||
|
for _ in range(5):
|
||||||
|
print(f"Level={dev.sensor_level}, RSSI={dev.sensor_rssi}")
|
||||||
|
time.sleep(1)
|
||||||
|
finally:
|
||||||
|
dev.disconnect()
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,7 +0,0 @@
|
|||||||
material_name
|
|
||||||
LiPF6
|
|
||||||
LiDFOB
|
|
||||||
DTD
|
|
||||||
LiFSI
|
|
||||||
LiPO2F2
|
|
||||||
|
|
||||||
|
@@ -47,8 +47,8 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
print("开始初始化 BioyondV1RPC")
|
print("开始初始化 BioyondV1RPC")
|
||||||
self.config = config
|
self.config = config
|
||||||
self.api_key = config.get("api_key", "")
|
self.api_key = config["api_key"]
|
||||||
self.host = config.get("api_host", "") or config.get("base_url", "")
|
self.host = config["api_host"]
|
||||||
self._logger = SimpleLogger()
|
self._logger = SimpleLogger()
|
||||||
self.material_cache = {}
|
self.material_cache = {}
|
||||||
self._load_material_cache()
|
self._load_material_cache()
|
||||||
@@ -61,7 +61,7 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
|
|
||||||
:return: 当前时间的 ISO 8601 格式字符串
|
:return: 当前时间的 ISO 8601 格式字符串
|
||||||
"""
|
"""
|
||||||
current_time = datetime.now().isoformat(
|
current_time = datetime.now(timezone.utc).isoformat(
|
||||||
timespec='milliseconds'
|
timespec='milliseconds'
|
||||||
)
|
)
|
||||||
# 替换时区部分为 'Z'
|
# 替换时区部分为 'Z'
|
||||||
@@ -192,6 +192,23 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return []
|
return []
|
||||||
return str(response.get("data", {}))
|
return str(response.get("data", {}))
|
||||||
|
|
||||||
|
def material_type_list(self) -> list:
|
||||||
|
"""查询物料类型列表
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
list: 物料类型数组,失败返回空列表
|
||||||
|
"""
|
||||||
|
response = self.post(
|
||||||
|
url=f'{self.host}/api/lims/storage/material-type-list',
|
||||||
|
params={
|
||||||
|
"apiKey": self.api_key,
|
||||||
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
|
"data": {},
|
||||||
|
})
|
||||||
|
if not response or response['code'] != 1:
|
||||||
|
return []
|
||||||
|
return response.get("data", [])
|
||||||
|
|
||||||
def material_inbound(self, material_id: str, location_id: str) -> dict:
|
def material_inbound(self, material_id: str, location_id: str) -> dict:
|
||||||
"""
|
"""
|
||||||
描述:指定库位入库一个物料
|
描述:指定库位入库一个物料
|
||||||
@@ -212,8 +229,34 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
})
|
})
|
||||||
|
|
||||||
if not response or response['code'] != 1:
|
if not response or response['code'] != 1:
|
||||||
|
if response:
|
||||||
|
error_msg = response.get('message', '未知错误')
|
||||||
|
print(f"[ERROR] 物料入库失败: code={response.get('code')}, message={error_msg}")
|
||||||
|
else:
|
||||||
|
print(f"[ERROR] 物料入库失败: API 无响应")
|
||||||
return {}
|
return {}
|
||||||
return response.get("data", {})
|
# 入库成功时,即使没有 data 字段,也返回成功标识
|
||||||
|
return response.get("data") or {"success": True}
|
||||||
|
|
||||||
|
def batch_inbound(self, inbound_items: List[Dict[str, Any]]) -> int:
|
||||||
|
"""批量入库物料
|
||||||
|
|
||||||
|
参数:
|
||||||
|
inbound_items: 入库条目列表,每项包含 materialId/locationId/quantity 等
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
int: 成功返回1,失败返回0
|
||||||
|
"""
|
||||||
|
response = self.post(
|
||||||
|
url=f'{self.host}/api/lims/storage/batch-inbound',
|
||||||
|
params={
|
||||||
|
"apiKey": self.api_key,
|
||||||
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
|
"data": inbound_items,
|
||||||
|
})
|
||||||
|
if not response or response['code'] != 1:
|
||||||
|
return 0
|
||||||
|
return response.get("code", 0)
|
||||||
|
|
||||||
def delete_material(self, material_id: str) -> dict:
|
def delete_material(self, material_id: str) -> dict:
|
||||||
"""
|
"""
|
||||||
@@ -233,7 +276,7 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return response.get("data", {})
|
return response.get("data", {})
|
||||||
|
|
||||||
def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict:
|
def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict:
|
||||||
"""指定库位出库物料"""
|
"""指定库位出库物料(通过库位名称)"""
|
||||||
location_id = LOCATION_MAPPING.get(location_name, location_name)
|
location_id = LOCATION_MAPPING.get(location_name, location_name)
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
@@ -251,9 +294,98 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
})
|
})
|
||||||
|
|
||||||
if not response or response['code'] != 1:
|
if not response or response['code'] != 1:
|
||||||
return {}
|
return None
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
def material_outbound_by_id(self, material_id: str, location_id: str, quantity: int) -> dict:
|
||||||
|
"""指定库位出库物料(直接使用location_id)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
material_id: 物料ID
|
||||||
|
location_id: 库位ID(不是库位名称,是UUID)
|
||||||
|
quantity: 数量
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: API响应,失败返回None
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
"materialId": material_id,
|
||||||
|
"locationId": location_id,
|
||||||
|
"quantity": quantity
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.post(
|
||||||
|
url=f'{self.host}/api/lims/storage/outbound',
|
||||||
|
params={
|
||||||
|
"apiKey": self.api_key,
|
||||||
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
|
"data": params
|
||||||
|
})
|
||||||
|
|
||||||
|
if not response or response['code'] != 1:
|
||||||
|
return None
|
||||||
|
return response
|
||||||
|
|
||||||
|
def batch_outbound(self, outbound_items: List[Dict[str, Any]]) -> int:
|
||||||
|
"""批量出库物料
|
||||||
|
|
||||||
|
参数:
|
||||||
|
outbound_items: 出库条目列表,每项包含 materialId/locationId/quantity 等
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
int: 成功返回1,失败返回0
|
||||||
|
"""
|
||||||
|
response = self.post(
|
||||||
|
url=f'{self.host}/api/lims/storage/batch-outbound',
|
||||||
|
params={
|
||||||
|
"apiKey": self.api_key,
|
||||||
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
|
"data": outbound_items,
|
||||||
|
})
|
||||||
|
if not response or response['code'] != 1:
|
||||||
|
return 0
|
||||||
|
return response.get("code", 0)
|
||||||
|
|
||||||
|
def material_info(self, material_id: str) -> dict:
|
||||||
|
"""查询物料详情
|
||||||
|
|
||||||
|
参数:
|
||||||
|
material_id: 物料ID
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
dict: 物料信息字典,失败返回空字典
|
||||||
|
"""
|
||||||
|
response = self.post(
|
||||||
|
url=f'{self.host}/api/lims/storage/material-info',
|
||||||
|
params={
|
||||||
|
"apiKey": self.api_key,
|
||||||
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
|
"data": material_id,
|
||||||
|
})
|
||||||
|
if not response or response['code'] != 1:
|
||||||
|
return {}
|
||||||
|
return response.get("data", {})
|
||||||
|
|
||||||
|
def reset_location(self, location_id: str) -> int:
|
||||||
|
"""复位库位
|
||||||
|
|
||||||
|
参数:
|
||||||
|
location_id: 库位ID
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
int: 成功返回1,失败返回0
|
||||||
|
"""
|
||||||
|
response = self.post(
|
||||||
|
url=f'{self.host}/api/lims/storage/reset-location',
|
||||||
|
params={
|
||||||
|
"apiKey": self.api_key,
|
||||||
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
|
"data": location_id,
|
||||||
|
})
|
||||||
|
if not response or response['code'] != 1:
|
||||||
|
return 0
|
||||||
|
return response.get("code", 0)
|
||||||
|
|
||||||
# ==================== 工作流查询相关接口 ====================
|
# ==================== 工作流查询相关接口 ====================
|
||||||
|
|
||||||
def query_workflow(self, json_str: str) -> dict:
|
def query_workflow(self, json_str: str) -> dict:
|
||||||
@@ -297,6 +429,66 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return {}
|
return {}
|
||||||
return response.get("data", {})
|
return response.get("data", {})
|
||||||
|
|
||||||
|
def split_workflow_list(self, params: Dict[str, Any]) -> dict:
|
||||||
|
"""查询可拆分工作流列表
|
||||||
|
|
||||||
|
参数:
|
||||||
|
params: 查询条件参数
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
dict: 返回数据字典,失败返回空字典
|
||||||
|
"""
|
||||||
|
response = self.post(
|
||||||
|
url=f'{self.host}/api/lims/workflow/split-workflow-list',
|
||||||
|
params={
|
||||||
|
"apiKey": self.api_key,
|
||||||
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
|
"data": params,
|
||||||
|
})
|
||||||
|
if not response or response['code'] != 1:
|
||||||
|
return {}
|
||||||
|
return response.get("data", {})
|
||||||
|
|
||||||
|
def merge_workflow(self, data: Dict[str, Any]) -> dict:
|
||||||
|
"""合并工作流(无参数版)
|
||||||
|
|
||||||
|
参数:
|
||||||
|
data: 合并请求体,包含待合并的子工作流信息
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
dict: 合并结果,失败返回空字典
|
||||||
|
"""
|
||||||
|
response = self.post(
|
||||||
|
url=f'{self.host}/api/lims/workflow/merge-workflow',
|
||||||
|
params={
|
||||||
|
"apiKey": self.api_key,
|
||||||
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
|
"data": data,
|
||||||
|
})
|
||||||
|
if not response or response['code'] != 1:
|
||||||
|
return {}
|
||||||
|
return response.get("data", {})
|
||||||
|
|
||||||
|
def merge_workflow_with_parameters(self, data: Dict[str, Any]) -> dict:
|
||||||
|
"""合并工作流(携带参数)
|
||||||
|
|
||||||
|
参数:
|
||||||
|
data: 合并请求体,包含 name、workflows 以及 stepParameters 等
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
dict: 合并结果,失败返回空字典
|
||||||
|
"""
|
||||||
|
response = self.post(
|
||||||
|
url=f'{self.host}/api/lims/workflow/merge-workflow-with-parameters',
|
||||||
|
params={
|
||||||
|
"apiKey": self.api_key,
|
||||||
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
|
"data": data,
|
||||||
|
})
|
||||||
|
if not response or response['code'] != 1:
|
||||||
|
return {}
|
||||||
|
return response.get("data", {})
|
||||||
|
|
||||||
def validate_workflow_parameters(self, workflows: List[Dict[str, Any]]) -> Dict[str, Any]:
|
def validate_workflow_parameters(self, workflows: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||||
"""验证工作流参数格式"""
|
"""验证工作流参数格式"""
|
||||||
try:
|
try:
|
||||||
@@ -459,18 +651,15 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return {}
|
return {}
|
||||||
return response.get("data", {})
|
return response.get("data", {})
|
||||||
|
|
||||||
def order_report(self, json_str: str) -> dict:
|
def order_report(self, order_id: str) -> dict:
|
||||||
"""
|
"""查询订单报告
|
||||||
描述:查询某个任务明细
|
|
||||||
json_str 格式为JSON字符串:
|
|
||||||
'{"order_id": "order123"}'
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
data = json.loads(json_str)
|
|
||||||
order_id = data.get("order_id", "")
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
参数:
|
||||||
|
order_id: 订单ID
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
dict: 报告数据,失败返回空字典
|
||||||
|
"""
|
||||||
response = self.post(
|
response = self.post(
|
||||||
url=f'{self.host}/api/lims/order/order-report',
|
url=f'{self.host}/api/lims/order/order-report',
|
||||||
params={
|
params={
|
||||||
@@ -478,16 +667,18 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
"requestTime": self.get_current_time_iso8601(),
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
"data": order_id,
|
"data": order_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
if not response or response['code'] != 1:
|
if not response or response['code'] != 1:
|
||||||
return {}
|
return {}
|
||||||
return response.get("data", {})
|
return response.get("data", {})
|
||||||
|
|
||||||
def order_takeout(self, json_str: str) -> int:
|
def order_takeout(self, json_str: str) -> int:
|
||||||
"""
|
"""取出任务产物
|
||||||
描述:取出任务产物
|
|
||||||
json_str 格式为JSON字符串:
|
参数:
|
||||||
'{"order_id": "order123", "preintake_id": "preintake123"}'
|
json_str: JSON字符串,包含 order_id 与 preintake_id
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
int: 成功返回1,失败返回0
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = json.loads(json_str)
|
data = json.loads(json_str)
|
||||||
@@ -510,14 +701,15 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return 0
|
return 0
|
||||||
return response.get("code", 0)
|
return response.get("code", 0)
|
||||||
|
|
||||||
|
|
||||||
def sample_waste_removal(self, order_id: str) -> dict:
|
def sample_waste_removal(self, order_id: str) -> dict:
|
||||||
"""
|
"""样品/废料取出
|
||||||
样品/废料取出接口
|
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- order_id: 订单ID
|
order_id: 订单ID
|
||||||
|
|
||||||
返回: 取出结果
|
返回值:
|
||||||
|
dict: 取出结果,失败返回空字典
|
||||||
"""
|
"""
|
||||||
params = {"orderId": order_id}
|
params = {"orderId": order_id}
|
||||||
|
|
||||||
@@ -539,10 +731,13 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return response.get("data", {})
|
return response.get("data", {})
|
||||||
|
|
||||||
def cancel_order(self, json_str: str) -> bool:
|
def cancel_order(self, json_str: str) -> bool:
|
||||||
"""
|
"""取消指定任务
|
||||||
描述:取消指定任务
|
|
||||||
json_str 格式为JSON字符串:
|
参数:
|
||||||
'{"order_id": "order123"}'
|
json_str: JSON字符串,包含 order_id
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
bool: 成功返回 True,失败返回 False
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = json.loads(json_str)
|
data = json.loads(json_str)
|
||||||
@@ -562,6 +757,126 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def cancel_experiment(self, order_id: str) -> int:
|
||||||
|
"""取消指定实验
|
||||||
|
|
||||||
|
参数:
|
||||||
|
order_id: 订单ID
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
int: 成功返回1,失败返回0
|
||||||
|
"""
|
||||||
|
response = self.post(
|
||||||
|
url=f'{self.host}/api/lims/order/cancel-experiment',
|
||||||
|
params={
|
||||||
|
"apiKey": self.api_key,
|
||||||
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
|
"data": order_id,
|
||||||
|
})
|
||||||
|
if not response or response['code'] != 1:
|
||||||
|
return 0
|
||||||
|
return response.get("code", 0)
|
||||||
|
|
||||||
|
def batch_cancel_experiment(self, order_ids: List[str]) -> int:
|
||||||
|
"""批量取消实验
|
||||||
|
|
||||||
|
参数:
|
||||||
|
order_ids: 订单ID列表
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
int: 成功返回1,失败返回0
|
||||||
|
"""
|
||||||
|
response = self.post(
|
||||||
|
url=f'{self.host}/api/lims/order/batch-cancel-experiment',
|
||||||
|
params={
|
||||||
|
"apiKey": self.api_key,
|
||||||
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
|
"data": order_ids,
|
||||||
|
})
|
||||||
|
if not response or response['code'] != 1:
|
||||||
|
return 0
|
||||||
|
return response.get("code", 0)
|
||||||
|
|
||||||
|
def gantts_by_order_id(self, order_id: str) -> dict:
|
||||||
|
"""查询订单甘特图数据
|
||||||
|
|
||||||
|
参数:
|
||||||
|
order_id: 订单ID
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
dict: 甘特数据,失败返回空字典
|
||||||
|
"""
|
||||||
|
response = self.post(
|
||||||
|
url=f'{self.host}/api/lims/order/gantts-by-order-id',
|
||||||
|
params={
|
||||||
|
"apiKey": self.api_key,
|
||||||
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
|
"data": order_id,
|
||||||
|
})
|
||||||
|
if not response or response['code'] != 1:
|
||||||
|
return {}
|
||||||
|
return response.get("data", {})
|
||||||
|
|
||||||
|
def simulation_gantt_by_order_id(self, order_id: str) -> dict:
|
||||||
|
"""查询订单模拟甘特图数据
|
||||||
|
|
||||||
|
参数:
|
||||||
|
order_id: 订单ID
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
dict: 模拟甘特数据,失败返回空字典
|
||||||
|
"""
|
||||||
|
response = self.post(
|
||||||
|
url=f'{self.host}/api/lims/order/simulation-gantt-by-order-id',
|
||||||
|
params={
|
||||||
|
"apiKey": self.api_key,
|
||||||
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
|
"data": order_id,
|
||||||
|
})
|
||||||
|
if not response or response['code'] != 1:
|
||||||
|
return {}
|
||||||
|
return response.get("data", {})
|
||||||
|
|
||||||
|
def reset_order_status(self, order_id: str) -> int:
|
||||||
|
"""复位订单状态
|
||||||
|
|
||||||
|
参数:
|
||||||
|
order_id: 订单ID
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
int: 成功返回1,失败返回0
|
||||||
|
"""
|
||||||
|
response = self.post(
|
||||||
|
url=f'{self.host}/api/lims/order/reset-order-status',
|
||||||
|
params={
|
||||||
|
"apiKey": self.api_key,
|
||||||
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
|
"data": order_id,
|
||||||
|
})
|
||||||
|
if not response or response['code'] != 1:
|
||||||
|
return 0
|
||||||
|
return response.get("code", 0)
|
||||||
|
|
||||||
|
def gantt_with_simulation_by_order_id(self, order_id: str) -> dict:
|
||||||
|
"""查询订单甘特与模拟联合数据
|
||||||
|
|
||||||
|
参数:
|
||||||
|
order_id: 订单ID
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
dict: 联合数据,失败返回空字典
|
||||||
|
"""
|
||||||
|
response = self.post(
|
||||||
|
url=f'{self.host}/api/lims/order/gantt-with-simulation-by-order-id',
|
||||||
|
params={
|
||||||
|
"apiKey": self.api_key,
|
||||||
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
|
"data": order_id,
|
||||||
|
})
|
||||||
|
if not response or response['code'] != 1:
|
||||||
|
return {}
|
||||||
|
return response.get("data", {})
|
||||||
|
|
||||||
# ==================== 设备管理相关接口 ====================
|
# ==================== 设备管理相关接口 ====================
|
||||||
|
|
||||||
def device_list(self, json_str: str = "") -> list:
|
def device_list(self, json_str: str = "") -> list:
|
||||||
@@ -593,9 +908,13 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return response.get("data", [])
|
return response.get("data", [])
|
||||||
|
|
||||||
def device_operation(self, json_str: str) -> int:
|
def device_operation(self, json_str: str) -> int:
|
||||||
"""
|
"""设备操作
|
||||||
描述:操作设备
|
|
||||||
json_str 格式为JSON字符串
|
参数:
|
||||||
|
json_str: JSON字符串,包含 device_no/operationType/operationParams
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
int: 成功返回1,失败返回0
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = json.loads(json_str)
|
data = json.loads(json_str)
|
||||||
@@ -608,7 +927,7 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
response = self.post(
|
response = self.post(
|
||||||
url=f'{self.host}/api/lims/device/device-operation',
|
url=f'{self.host}/api/lims/device/execute-operation',
|
||||||
params={
|
params={
|
||||||
"apiKey": self.api_key,
|
"apiKey": self.api_key,
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
@@ -619,9 +938,30 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return 0
|
return 0
|
||||||
return response.get("code", 0)
|
return response.get("code", 0)
|
||||||
|
|
||||||
|
def reset_devices(self) -> int:
|
||||||
|
"""复位设备集合
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
int: 成功返回1,失败返回0
|
||||||
|
"""
|
||||||
|
response = self.post(
|
||||||
|
url=f'{self.host}/api/lims/device/reset-devices',
|
||||||
|
params={
|
||||||
|
"apiKey": self.api_key,
|
||||||
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
|
})
|
||||||
|
if not response or response['code'] != 1:
|
||||||
|
return 0
|
||||||
|
return response.get("code", 0)
|
||||||
|
|
||||||
# ==================== 调度器相关接口 ====================
|
# ==================== 调度器相关接口 ====================
|
||||||
|
|
||||||
def scheduler_status(self) -> dict:
|
def scheduler_status(self) -> dict:
|
||||||
|
"""查询调度器状态
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
dict: 包含 schedulerStatus/hasTask/creationTime 等
|
||||||
|
"""
|
||||||
response = self.post(
|
response = self.post(
|
||||||
url=f'{self.host}/api/lims/scheduler/scheduler-status',
|
url=f'{self.host}/api/lims/scheduler/scheduler-status',
|
||||||
params={
|
params={
|
||||||
@@ -634,7 +974,7 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return response.get("data", {})
|
return response.get("data", {})
|
||||||
|
|
||||||
def scheduler_start(self) -> int:
|
def scheduler_start(self) -> int:
|
||||||
"""描述:启动调度器"""
|
"""启动调度器"""
|
||||||
response = self.post(
|
response = self.post(
|
||||||
url=f'{self.host}/api/lims/scheduler/start',
|
url=f'{self.host}/api/lims/scheduler/start',
|
||||||
params={
|
params={
|
||||||
@@ -647,9 +987,22 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return response.get("code", 0)
|
return response.get("code", 0)
|
||||||
|
|
||||||
def scheduler_pause(self) -> int:
|
def scheduler_pause(self) -> int:
|
||||||
"""描述:暂停调度器"""
|
"""暂停调度器"""
|
||||||
response = self.post(
|
response = self.post(
|
||||||
url=f'{self.host}/api/lims/scheduler/scheduler-pause',
|
url=f'{self.host}/api/lims/scheduler/pause',
|
||||||
|
params={
|
||||||
|
"apiKey": self.api_key,
|
||||||
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
|
})
|
||||||
|
|
||||||
|
if not response or response['code'] != 1:
|
||||||
|
return 0
|
||||||
|
return response.get("code", 0)
|
||||||
|
|
||||||
|
def scheduler_smart_pause(self) -> int:
|
||||||
|
"""智能暂停调度器"""
|
||||||
|
response = self.post(
|
||||||
|
url=f'{self.host}/api/lims/scheduler/smart-pause',
|
||||||
params={
|
params={
|
||||||
"apiKey": self.api_key,
|
"apiKey": self.api_key,
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
@@ -660,8 +1013,9 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return response.get("code", 0)
|
return response.get("code", 0)
|
||||||
|
|
||||||
def scheduler_continue(self) -> int:
|
def scheduler_continue(self) -> int:
|
||||||
|
"""继续调度器"""
|
||||||
response = self.post(
|
response = self.post(
|
||||||
url=f'{self.host}/api/lims/scheduler/scheduler-continue',
|
url=f'{self.host}/api/lims/scheduler/continue',
|
||||||
params={
|
params={
|
||||||
"apiKey": self.api_key,
|
"apiKey": self.api_key,
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
@@ -672,9 +1026,9 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return response.get("code", 0)
|
return response.get("code", 0)
|
||||||
|
|
||||||
def scheduler_stop(self) -> int:
|
def scheduler_stop(self) -> int:
|
||||||
"""描述:停止调度器"""
|
"""停止调度器"""
|
||||||
response = self.post(
|
response = self.post(
|
||||||
url=f'{self.host}/api/lims/scheduler/scheduler-stop',
|
url=f'{self.host}/api/lims/scheduler/stop',
|
||||||
params={
|
params={
|
||||||
"apiKey": self.api_key,
|
"apiKey": self.api_key,
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
@@ -685,9 +1039,9 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return response.get("code", 0)
|
return response.get("code", 0)
|
||||||
|
|
||||||
def scheduler_reset(self) -> int:
|
def scheduler_reset(self) -> int:
|
||||||
"""描述:重置调度器"""
|
"""复位调度器"""
|
||||||
response = self.post(
|
response = self.post(
|
||||||
url=f'{self.host}/api/lims/scheduler/scheduler-reset',
|
url=f'{self.host}/api/lims/scheduler/reset',
|
||||||
params={
|
params={
|
||||||
"apiKey": self.api_key,
|
"apiKey": self.api_key,
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
@@ -697,6 +1051,26 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return 0
|
return 0
|
||||||
return response.get("code", 0)
|
return response.get("code", 0)
|
||||||
|
|
||||||
|
def scheduler_reply_error_handling(self, data: Dict[str, Any]) -> int:
|
||||||
|
"""调度错误处理回复
|
||||||
|
|
||||||
|
参数:
|
||||||
|
data: 错误处理参数
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
int: 成功返回1,失败返回0
|
||||||
|
"""
|
||||||
|
response = self.post(
|
||||||
|
url=f'{self.host}/api/lims/scheduler/reply-error-handling',
|
||||||
|
params={
|
||||||
|
"apiKey": self.api_key,
|
||||||
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
|
"data": data,
|
||||||
|
})
|
||||||
|
if not response or response['code'] != 1:
|
||||||
|
return 0
|
||||||
|
return response.get("code", 0)
|
||||||
|
|
||||||
# ==================== 辅助方法 ====================
|
# ==================== 辅助方法 ====================
|
||||||
|
|
||||||
def _load_material_cache(self):
|
def _load_material_cache(self):
|
||||||
@@ -705,7 +1079,7 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
print("正在加载材料列表缓存...")
|
print("正在加载材料列表缓存...")
|
||||||
|
|
||||||
# 加载所有类型的材料:耗材(0)、样品(1)、试剂(2)
|
# 加载所有类型的材料:耗材(0)、样品(1)、试剂(2)
|
||||||
material_types = [1, 2]
|
material_types = [0, 1, 2]
|
||||||
|
|
||||||
for type_mode in material_types:
|
for type_mode in material_types:
|
||||||
print(f"正在加载类型 {type_mode} 的材料...")
|
print(f"正在加载类型 {type_mode} 的材料...")
|
||||||
@@ -760,3 +1134,23 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
def get_available_materials(self):
|
def get_available_materials(self):
|
||||||
"""获取所有可用的材料名称列表"""
|
"""获取所有可用的材料名称列表"""
|
||||||
return list(self.material_cache.keys())
|
return list(self.material_cache.keys())
|
||||||
|
|
||||||
|
def get_scheduler_state(self) -> Optional[MachineState]:
|
||||||
|
"""将调度状态字符串映射为枚举值
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
Optional[MachineState]: 映射后的枚举,失败返回 None
|
||||||
|
"""
|
||||||
|
data = self.scheduler_status()
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return None
|
||||||
|
status = data.get("schedulerStatus")
|
||||||
|
mapping = {
|
||||||
|
"Init": MachineState.INITIAL,
|
||||||
|
"Stop": MachineState.STOPPED,
|
||||||
|
"Running": MachineState.RUNNING,
|
||||||
|
"Pause": MachineState.PAUSED,
|
||||||
|
"ErrorPause": MachineState.ERROR_PAUSED,
|
||||||
|
"ErrorStop": MachineState.ERROR_STOPPED,
|
||||||
|
}
|
||||||
|
return mapping.get(status)
|
||||||
|
|||||||
@@ -2,330 +2,141 @@
|
|||||||
"""
|
"""
|
||||||
配置文件 - 包含所有配置信息和映射关系
|
配置文件 - 包含所有配置信息和映射关系
|
||||||
"""
|
"""
|
||||||
import os
|
|
||||||
|
|
||||||
# ==================== API 基础配置 ====================
|
# API配置
|
||||||
# BioyondCellWorkstation 默认配置(包含所有必需参数)
|
|
||||||
API_CONFIG = {
|
API_CONFIG = {
|
||||||
# API 连接配置
|
"api_key": "",
|
||||||
# "api_host": os.getenv("BIOYOND_API_HOST", "http://172.16.1.143:44389"),#实机
|
"api_host": ""
|
||||||
"api_host": os.getenv("BIOYOND_API_HOST", "http://172.16.11.219:44388"),# 仿真机
|
}
|
||||||
"api_key": os.getenv("BIOYOND_API_KEY", "8A819E5C"),
|
|
||||||
"timeout": int(os.getenv("BIOYOND_TIMEOUT", "30")),
|
|
||||||
|
|
||||||
# 报送配置
|
# 工作流映射配置
|
||||||
"report_token": os.getenv("BIOYOND_REPORT_TOKEN", "CHANGE_ME_TOKEN"),
|
WORKFLOW_MAPPINGS = {
|
||||||
|
"reactor_taken_out": "",
|
||||||
|
"reactor_taken_in": "",
|
||||||
|
"Solid_feeding_vials": "",
|
||||||
|
"Liquid_feeding_vials(non-titration)": "",
|
||||||
|
"Liquid_feeding_solvents": "",
|
||||||
|
"Liquid_feeding(titration)": "",
|
||||||
|
"liquid_feeding_beaker": "",
|
||||||
|
"Drip_back": "",
|
||||||
|
}
|
||||||
|
|
||||||
# HTTP 服务配置
|
# 工作流名称到DisplaySectionName的映射
|
||||||
"HTTP_host": os.getenv("BIOYOND_HTTP_HOST", "172.16.11.6"), # HTTP服务监听地址,监听计算机飞连ip地址
|
WORKFLOW_TO_SECTION_MAP = {
|
||||||
"HTTP_port": int(os.getenv("BIOYOND_HTTP_PORT", "8080")),
|
'reactor_taken_in': '反应器放入',
|
||||||
"debug_mode": False,# 调试模式
|
'liquid_feeding_beaker': '液体投料-烧杯',
|
||||||
|
'Liquid_feeding_vials(non-titration)': '液体投料-小瓶(非滴定)',
|
||||||
|
'Liquid_feeding_solvents': '液体投料-溶剂',
|
||||||
|
'Solid_feeding_vials': '固体投料-小瓶',
|
||||||
|
'Liquid_feeding(titration)': '液体投料-滴定',
|
||||||
|
'reactor_taken_out': '反应器取出'
|
||||||
}
|
}
|
||||||
|
|
||||||
# 库位映射配置
|
# 库位映射配置
|
||||||
WAREHOUSE_MAPPING = {
|
WAREHOUSE_MAPPING = {
|
||||||
"粉末加样头堆栈": {
|
"粉末堆栈": {
|
||||||
"uuid": "",
|
"uuid": "",
|
||||||
"site_uuids": {
|
"site_uuids": {
|
||||||
"A01": "3a19da56-1379-ff7c-1745-07e200b44ce2",
|
# 样品板
|
||||||
"B01": "3a19da56-1379-2424-d751-fe6e94cef938",
|
"A1": "3a14198e-6929-31f0-8a22-0f98f72260df",
|
||||||
"C01": "3a19da56-1379-271c-03e3-6bdb590e395e",
|
"A2": "3a14198e-6929-4379-affa-9a2935c17f99",
|
||||||
"D01": "3a19da56-1379-277f-2b1b-0d11f7cf92c6",
|
"A3": "3a14198e-6929-56da-9a1c-7f5fbd4ae8af",
|
||||||
"E01": "3a19da56-1379-2f1c-a15b-e01db90eb39a",
|
"A4": "3a14198e-6929-5e99-2b79-80720f7cfb54",
|
||||||
"F01": "3a19da56-1379-3fa1-846b-088158ac0b3d",
|
"B1": "3a14198e-6929-f525-9a1b-1857552b28ee",
|
||||||
"G01": "3a19da56-1379-5aeb-d0cd-d3b4609d66e1",
|
"B2": "3a14198e-6929-bf98-0fd5-26e1d68bf62d",
|
||||||
"H01": "3a19da56-1379-6077-8258-bdc036870b78",
|
"B3": "3a14198e-6929-2d86-a468-602175a2b5aa",
|
||||||
"I01": "3a19da56-1379-863b-a120-f606baf04617",
|
"B4": "3a14198e-6929-1a98-ae57-e97660c489ad",
|
||||||
"J01": "3a19da56-1379-8a74-74e5-35a9b41d4fd5",
|
# 分装板
|
||||||
"K01": "3a19da56-1379-b270-b7af-f18773918abe",
|
"C1": "3a14198e-6929-46fe-841e-03dd753f1e4a",
|
||||||
"L01": "3a19da56-1379-ba54-6d78-fd770a671ffc",
|
"C2": "3a14198e-6929-1bc9-a9bd-3b7ca66e7f95",
|
||||||
"M01": "3a19da56-1379-c22d-c96f-0ceb5eb54a04",
|
"C3": "3a14198e-6929-72ac-32ce-9b50245682b8",
|
||||||
"N01": "3a19da56-1379-d64e-c6c5-c72ea4829888",
|
"C4": "3a14198e-6929-3bd8-e6c7-4a9fd93be118",
|
||||||
"O01": "3a19da56-1379-d887-1a3c-6f9cce90f90e",
|
"D1": "3a14198e-6929-8a0b-b686-6f4a2955c4e2",
|
||||||
"P01": "3a19da56-1379-e77d-0e65-7463b238a3b9",
|
"D2": "3a14198e-6929-dde1-fc78-34a84b71afdf",
|
||||||
"Q01": "3a19da56-1379-edf6-1472-802ddb628774",
|
"D3": "3a14198e-6929-a0ec-5f15-c0f9f339f963",
|
||||||
"R01": "3a19da56-1379-f281-0273-e0ef78f0fd97",
|
"D4": "3a14198e-6929-7ac8-915a-fea51cb2e884"
|
||||||
"S01": "3a19da56-1379-f924-7f68-df1fa51489f4",
|
|
||||||
"T01": "3a19da56-1379-ff7c-1745-07e200b44ce2"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"配液站内试剂仓库": {
|
"溶液堆栈": {
|
||||||
"uuid": "",
|
"uuid": "",
|
||||||
"site_uuids": {
|
"site_uuids": {
|
||||||
"A01": "3a19da43-57b5-294f-d663-154a1cc32270",
|
"A1": "3a14198e-d724-e036-afdc-2ae39a7f3383",
|
||||||
"B01": "3a19da43-57b5-7394-5f49-54efe2c9bef2",
|
"A2": "3a14198e-d724-afa4-fc82-0ac8a9016791",
|
||||||
"C01": "3a19da43-57b5-5e75-552f-8dbd0ad1075f",
|
"A3": "3a14198e-d724-ca48-bb9e-7e85751e55b6",
|
||||||
"A02": "3a19da43-57b5-8441-db94-b4d3875a4b6c",
|
"A4": "3a14198e-d724-df6d-5e32-5483b3cab583",
|
||||||
"B02": "3a19da43-57b5-3e41-c181-5119dddaf50c",
|
"B1": "3a14198e-d724-d818-6d4f-5725191a24b5",
|
||||||
"C02": "3a19da43-57b5-269b-282d-fba61fe8ce96",
|
"B2": "3a14198e-d724-be8a-5e0b-012675e195c6",
|
||||||
"A03": "3a19da43-57b5-7c1e-d02e-c40e8c33f8a1",
|
"B3": "3a14198e-d724-cc1e-5c2c-228a130f40a8",
|
||||||
"B03": "3a19da43-57b5-659f-621f-1dcf3f640363",
|
"B4": "3a14198e-d724-1e28-c885-574c3df468d0",
|
||||||
"C03": "3a19da43-57b5-855a-6e71-f398e376dee1",
|
"C1": "3a14198e-d724-b5bb-adf3-4c5a0da6fb31",
|
||||||
|
"C2": "3a14198e-d724-ab4e-48cb-817c3c146707",
|
||||||
|
"C3": "3a14198e-d724-7f18-1853-39d0c62e1d33",
|
||||||
|
"C4": "3a14198e-d724-28a2-a760-baa896f46b66",
|
||||||
|
"D1": "3a14198e-d724-d378-d266-2508a224a19f",
|
||||||
|
"D2": "3a14198e-d724-f56e-468b-0110a8feb36a",
|
||||||
|
"D3": "3a14198e-d724-0cf1-dea9-a1f40fe7e13c",
|
||||||
|
"D4": "3a14198e-d724-0ddd-9654-f9352a421de9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"试剂替换仓库": {
|
"试剂堆栈": {
|
||||||
"uuid": "",
|
"uuid": "",
|
||||||
"site_uuids": {
|
"site_uuids": {
|
||||||
"A01": "3a19da51-8f4e-30f3-ea08-4f8498e9b097",
|
"A1": "3a14198c-c2cf-8b40-af28-b467808f1c36",
|
||||||
"B01": "3a19da51-8f4e-1da7-beb0-80a4a01e67a8",
|
"A2": "3a14198c-c2d0-f3e7-871a-e470d144296f",
|
||||||
"C01": "3a19da51-8f4e-337d-2675-bfac46880b06",
|
"A3": "3a14198c-c2d0-dc7d-b8d0-e1d88cee3094",
|
||||||
"D01": "3a19da51-8f4e-e514-b92c-9c44dc5e489d",
|
"A4": "3a14198c-c2d0-2070-efc8-44e245f10c6f",
|
||||||
"E01": "3a19da51-8f4e-22d1-dd5b-9774ddc80402",
|
"B1": "3a14198c-c2d0-354f-39ad-642e1a72fcb8",
|
||||||
"F01": "3a19da51-8f4e-273a-4871-dff41c29bfd9",
|
"B2": "3a14198c-c2d0-1559-105d-0ea30682cab4",
|
||||||
"G01": "3a19da51-8f4e-b32f-454f-74bc1a665653",
|
"B3": "3a14198c-c2d0-725e-523d-34c037ac2440",
|
||||||
"H01": "3a19da51-8f4e-8c93-68c9-0b4382320f59",
|
"B4": "3a14198c-c2d0-efce-0939-69ca5a7dfd39"
|
||||||
"I01": "3a19da51-8f4e-360c-0149-291b47c6089b",
|
|
||||||
"J01": "3a19da51-8f4e-4152-9bca-8d64df8c1af0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"自动堆栈-左": {
|
|
||||||
"uuid": "",
|
|
||||||
"site_uuids": {
|
|
||||||
"A01": "3a19debc-84b5-4c1c-d3a1-26830cf273ff",
|
|
||||||
"A02": "3a19debc-84b5-033b-b31f-6b87f7c2bf52",
|
|
||||||
"B01": "3a19debc-84b5-3924-172f-719ab01b125c",
|
|
||||||
"B02": "3a19debc-84b5-aad8-70c6-b8c6bb2d8750"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"自动堆栈-右": {
|
|
||||||
"uuid": "",
|
|
||||||
"site_uuids": {
|
|
||||||
"A01": "3a19debe-5200-7df2-1dd9-7d202f158864",
|
|
||||||
"A02": "3a19debe-5200-573b-6120-8b51f50e1e50",
|
|
||||||
"B01": "3a19debe-5200-7cd8-7666-851b0a97e309",
|
|
||||||
"B02": "3a19debe-5200-e6d3-96a3-baa6e3d5e484"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"手动堆栈": {
|
|
||||||
"uuid": "",
|
|
||||||
"site_uuids": {
|
|
||||||
"A01": "3a19deae-2c7a-36f5-5e41-02c5b66feaea",
|
|
||||||
"A02": "3a19deae-2c7a-dc6d-c41e-ef285d946cfe",
|
|
||||||
"A03": "3a19deae-2c7a-5876-c454-6b7e224ca927",
|
|
||||||
"B01": "3a19deae-2c7a-2426-6d71-e9de3cb158b1",
|
|
||||||
"B02": "3a19deae-2c7a-79b0-5e44-efaafd1e4cf3",
|
|
||||||
"B03": "3a19deae-2c7a-b9eb-f4e3-e308e0cf839a",
|
|
||||||
"C01": "3a19deae-2c7a-32bc-768e-556647e292f3",
|
|
||||||
"C02": "3a19deae-2c7a-e97a-8484-f5a4599447c4",
|
|
||||||
"C03": "3a19deae-2c7a-3056-6504-10dc73fbc276",
|
|
||||||
"D01": "3a19deae-2c7a-ffad-875e-8c4cda61d440",
|
|
||||||
"D02": "3a19deae-2c7a-61be-601c-b6fb5610499a",
|
|
||||||
"D03": "3a19deae-2c7a-c0f7-05a7-e3fe2491e560",
|
|
||||||
"E01": "3a19deae-2c7a-a6f4-edd1-b436a7576363",
|
|
||||||
"E02": "3a19deae-2c7a-4367-96dd-1ca2186f4910",
|
|
||||||
"E03": "3a19deae-2c7a-b163-2219-23df15200311",
|
|
||||||
"F01": "3a19deae-2c7a-d594-fd6a-0d20de3c7c4a",
|
|
||||||
"F02": "3a19deae-2c7a-a194-ea63-8b342b8d8679",
|
|
||||||
"F03": "3a19deae-2c7a-f7c4-12bd-425799425698",
|
|
||||||
"G01": "3a19deae-2c7a-0b56-72f1-8ab86e53b955",
|
|
||||||
"G02": "3a19deae-2c7a-204e-95ed-1f1950f28343",
|
|
||||||
"G03": "3a19deae-2c7a-392b-62f1-4907c66343f8",
|
|
||||||
"H01": "3a19deae-2c7a-5602-e876-d27aca4e3201",
|
|
||||||
"H02": "3a19deae-2c7a-f15c-70e0-25b58a8c9702",
|
|
||||||
"H03": "3a19deae-2c7a-780b-8965-2e1345f7e834",
|
|
||||||
"I01": "3a19deae-2c7a-8849-e172-07de14ede928",
|
|
||||||
"I02": "3a19deae-2c7a-4772-a37f-ff99270bafc0",
|
|
||||||
"I03": "3a19deae-2c7a-cce7-6e4a-25ea4a2068c4",
|
|
||||||
"J01": "3a19deae-2c7a-1848-de92-b5d5ed054cc6",
|
|
||||||
"J02": "3a19deae-2c7a-1d45-b4f8-6f866530e205",
|
|
||||||
"J03": "3a19deae-2c7a-f237-89d9-8fe19025dee9"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"4号手套箱内部堆栈": {
|
|
||||||
"uuid": "",
|
|
||||||
"site_uuids": {
|
|
||||||
"A01": "3a1baa20-a7b1-c665-8b9c-d8099d07d2f6",
|
|
||||||
"A02": "3a1baa20-a7b1-93a7-c988-f9c8ad6c58c9",
|
|
||||||
"A03": "3a1baa20-a7b1-00ee-f751-da9b20b6c464",
|
|
||||||
"A04": "3a1baa20-a7b1-4712-c37b-0b5b658ef7b9",
|
|
||||||
"B01": "3a1baa20-a7b1-9847-fc9c-96d604cd1a8e",
|
|
||||||
"B02": "3a1baa20-a7b1-4ae9-e604-0601db06249c",
|
|
||||||
"B03": "3a1baa20-a7b1-8329-ea75-81ca559d9ce1",
|
|
||||||
"B04": "3a1baa20-a7b1-89c5-d96f-36e98a8f7268",
|
|
||||||
"C01": "3a1baa20-a7b1-32ec-39e6-8044733839d6",
|
|
||||||
"C02": "3a1baa20-a7b1-b573-e426-4c86040348b2",
|
|
||||||
"C03": "3a1baa20-a7b1-cca7-781e-0522b729bf5d",
|
|
||||||
"C04": "3a1baa20-a7b1-7c98-5fd9-5855355ae4b3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"大分液瓶堆栈": {
|
|
||||||
"uuid": "",
|
|
||||||
"site_uuids": {
|
|
||||||
"A01": "3a19da3d-4f3d-bcac-2932-7542041e10e0",
|
|
||||||
"A02": "3a19da3d-4f3d-4d75-38ac-fb58ad0687c3",
|
|
||||||
"A03": "3a19da3d-4f3d-b25e-f2b1-85342a5b7eae",
|
|
||||||
"B01": "3a19da3d-4f3d-fd3e-058a-2733a0925767",
|
|
||||||
"B02": "3a19da3d-4f3d-37bd-a944-c391ad56857f",
|
|
||||||
"B03": "3a19da3d-4f3d-e353-7862-c6d1d4bc667f",
|
|
||||||
"C01": "3a19da3d-4f3d-9519-5da7-76179c958e70",
|
|
||||||
"C02": "3a19da3d-4f3d-b586-d7ed-9ec244f6f937",
|
|
||||||
"C03": "3a19da3d-4f3d-5061-249b-35dfef732811"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"小分液瓶堆栈": {
|
|
||||||
"uuid": "",
|
|
||||||
"site_uuids": {
|
|
||||||
"C03": "3a19da40-55bf-8943-d20d-a8b3ea0d16c0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"站内Tip头盒堆栈": {
|
|
||||||
"uuid": "",
|
|
||||||
"site_uuids": {
|
|
||||||
"A01": "3a19deab-d5cc-be1e-5c37-4e9e5a664388",
|
|
||||||
"A02": "3a19deab-d5cc-b394-8141-27cb3853e8ea",
|
|
||||||
"B01": "3a19deab-d5cc-4dca-596e-ca7414d5f501",
|
|
||||||
"B02": "3a19deab-d5cc-9bc0-442b-12d9d59aa62a",
|
|
||||||
"C01": "3a19deab-d5cc-2eaf-b6a4-f0d54e4f1246",
|
|
||||||
"C02": "3a19deab-d5cc-d9f4-25df-b8018c372bc7"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"配液站内配液大板仓库(无需提前上料)": {
|
|
||||||
"uuid": "",
|
|
||||||
"site_uuids": {
|
|
||||||
"A01": "3a1a21dc-06af-3915-9cb9-80a9dc42f386"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"配液站内配液小板仓库(无需以前入料)": {
|
|
||||||
"uuid": "",
|
|
||||||
"site_uuids": {
|
|
||||||
"A01": "3a1a21de-8e8b-7938-2d06-858b36c10e31"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"移液站内大瓶板仓库(无需提前如料)": {
|
|
||||||
"uuid": "",
|
|
||||||
"site_uuids": {
|
|
||||||
"A01": "3a1a224c-c727-fa62-1f2b-0037a84b9fca"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"移液站内小瓶板仓库(无需提前入料)": {
|
|
||||||
"uuid": "",
|
|
||||||
"site_uuids": {
|
|
||||||
"A01": "3a1a224d-ed49-710c-a9c3-3fc61d479cbb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"适配器位仓库": {
|
|
||||||
"uuid": "",
|
|
||||||
"site_uuids": {
|
|
||||||
"A01": "3a1abd46-18fe-1f56-6ced-a1f7fe08e36c"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"1号2号手套箱交接堆栈": {
|
|
||||||
"uuid": "",
|
|
||||||
"site_uuids": {
|
|
||||||
"A01": "3a1baa49-7f77-35aa-60b1-e55a45d065fa"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"2号手套箱内部堆栈": {
|
|
||||||
"uuid": "",
|
|
||||||
"site_uuids": {
|
|
||||||
"A01": "3a1baa4b-393e-9f86-3921-7a18b0a8e371",
|
|
||||||
"A02": "3a1baa4b-393e-9425-928b-ee0f6f679d44",
|
|
||||||
"A03": "3a1baa4b-393e-0baf-632b-59dfdc931a3a",
|
|
||||||
"B01": "3a1baa4b-393e-f8aa-c8a9-956f3132f05c",
|
|
||||||
"B02": "3a1baa4b-393e-ef05-42f6-53f4c6e89d70",
|
|
||||||
"B03": "3a1baa4b-393e-c07b-a924-a9f0dfda9711",
|
|
||||||
"C01": "3a1baa4b-393e-4c2b-821a-16a7fe025c48",
|
|
||||||
"C02": "3a1baa4b-393e-2eaf-61a1-9063c832d5a2",
|
|
||||||
"C03": "3a1baa4b-393e-034e-8e28-8626d934a85f"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# 物料类型配置
|
# 物料类型配置
|
||||||
MATERIAL_TYPE_MAPPINGS = {
|
MATERIAL_TYPE_MAPPINGS = {
|
||||||
"100ml液体": ("YB_100ml_yeti", "d37166b3-ecaa-481e-bd84-3032b795ba07"),
|
"烧杯": ("BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"),
|
||||||
"液": ("YB_ye", "3a190ca1-2add-2b23-f8e1-bbd348b7f790"),
|
"试剂瓶": ("BIOYOND_PolymerStation_1BottleCarrier", ""),
|
||||||
"高粘液": ("YB_gaonianye", "abe8df30-563d-43d2-85e0-cabec59ddc16"),
|
"样品板": ("BIOYOND_PolymerStation_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"),
|
||||||
"加样头(大)": ("YB_jia_yang_tou_da_Carrier", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
"分装板": ("BIOYOND_PolymerStation_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"),
|
||||||
# "加样头(大)板": ("YB_jia_yang_tou_da", "a8e714ae-2a4e-4eb9-9614-e4c140ec3f16"),
|
"样品瓶": ("BIOYOND_PolymerStation_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"),
|
||||||
"5ml分液瓶板": ("YB_5ml_fenyepingban", "3a192fa4-007d-ec7b-456e-2a8be7a13f23"),
|
"90%分装小瓶": ("BIOYOND_PolymerStation_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"),
|
||||||
"5ml分液瓶": ("YB_5ml_fenyeping", "3a192c2a-ebb7-58a1-480d-8b3863bf74f4"),
|
"10%分装小瓶": ("BIOYOND_PolymerStation_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"),
|
||||||
"20ml分液瓶板": ("YB_20ml_fenyepingban", "3a192fa4-47db-3449-162a-eaf8aba57e27"),
|
|
||||||
"20ml分液瓶": ("YB_20ml_fenyeping", "3a192c2b-19e8-f0a3-035e-041ca8ca1035"),
|
|
||||||
"配液瓶(小)板": ("YB_peiyepingxiaoban", "3a190c8b-3284-af78-d29f-9a69463ad047"),
|
|
||||||
"配液瓶(小)": ("YB_pei_ye_xiao_Bottle", "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"),
|
|
||||||
"配液瓶(大)板": ("YB_peiyepingdaban", "53e50377-32dc-4781-b3c0-5ce45bc7dc27"),
|
|
||||||
"配液瓶(大)": ("YB_pei_ye_da_Bottle", "19c52ad1-51c5-494f-8854-576f4ca9c6ca"),
|
|
||||||
"适配器块": ("YB_shi_pei_qi_kuai", "efc3bb32-d504-4890-91c0-b64ed3ac80cf"),
|
|
||||||
"枪头盒": ("YB_qiang_tou_he", "3a192c2e-20f3-a44a-0334-c8301839d0b3"),
|
|
||||||
"枪头": ("YB_qiang_tou", "b6196971-1050-46da-9927-333e8dea062d"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SOLID_LIQUID_MAPPINGS = {
|
# 步骤参数配置(各工作流的步骤UUID)
|
||||||
# 固体
|
WORKFLOW_STEP_IDS = {
|
||||||
"LiDFOB": {
|
"reactor_taken_in": {
|
||||||
"typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469",
|
"config": ""
|
||||||
"code": "",
|
|
||||||
"barCode": "",
|
|
||||||
"name": "LiDFOB",
|
|
||||||
"unit": "g",
|
|
||||||
"parameters": "",
|
|
||||||
"quantity": "2",
|
|
||||||
"warningQuantity": "1",
|
|
||||||
"details": []
|
|
||||||
},
|
},
|
||||||
# "LiPF6": {
|
"liquid_feeding_beaker": {
|
||||||
# "typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469",
|
"liquid": "",
|
||||||
# "code": "",
|
"observe": ""
|
||||||
# "barCode": "",
|
},
|
||||||
# "name": "LiPF6",
|
"liquid_feeding_vials_non_titration": {
|
||||||
# "unit": "g",
|
"liquid": "",
|
||||||
# "parameters": "",
|
"observe": ""
|
||||||
# "quantity": 2,
|
},
|
||||||
# "warningQuantity": 1,
|
"liquid_feeding_solvents": {
|
||||||
# "details": []
|
"liquid": "",
|
||||||
# },
|
"observe": ""
|
||||||
# "LiFSI": {
|
},
|
||||||
# "typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469",
|
"solid_feeding_vials": {
|
||||||
# "code": "",
|
"feeding": "",
|
||||||
# "barCode": "",
|
"observe": ""
|
||||||
# "name": "LiFSI",
|
},
|
||||||
# "unit": "g",
|
"liquid_feeding_titration": {
|
||||||
# "parameters": "",
|
"liquid": "",
|
||||||
# "quantity": 2,
|
"observe": ""
|
||||||
# "warningQuantity": 1,
|
},
|
||||||
# "details": []
|
"drip_back": {
|
||||||
# },
|
"liquid": "",
|
||||||
# "DTC": {
|
"observe": ""
|
||||||
# "typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469",
|
}
|
||||||
# "code": "",
|
|
||||||
# "barCode": "",
|
|
||||||
# "name": "DTC",
|
|
||||||
# "unit": "g",
|
|
||||||
# "parameters": "",
|
|
||||||
# "quantity": 2,
|
|
||||||
# "warningQuantity": 1,
|
|
||||||
# "details": []
|
|
||||||
# },
|
|
||||||
# "LiPO2F2": {
|
|
||||||
# "typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469",
|
|
||||||
# "code": "",
|
|
||||||
# "barCode": "",
|
|
||||||
# "name": "LiPO2F2",
|
|
||||||
# "unit": "g",
|
|
||||||
# "parameters": "",
|
|
||||||
# "quantity": 2,
|
|
||||||
# "warningQuantity": 1,
|
|
||||||
# "details": []
|
|
||||||
# },
|
|
||||||
# 液体
|
|
||||||
# "SA": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
|
||||||
# "EC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
|
||||||
# "VC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
|
||||||
# "AND": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
|
||||||
# "HTCN": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
|
||||||
# "DENE": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
|
||||||
# "TMSP": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
|
||||||
# "TMSB": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
|
||||||
# "EP": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
|
||||||
# "DEC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
|
||||||
# "EMC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
|
||||||
# "SN": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
|
||||||
# "DMC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
|
||||||
# "FEC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
WORKFLOW_MAPPINGS = {}
|
|
||||||
|
|
||||||
LOCATION_MAPPING = {}
|
LOCATION_MAPPING = {}
|
||||||
|
|
||||||
|
ACTION_NAMES = {}
|
||||||
|
|
||||||
|
HTTP_SERVICE_CONFIG = {}
|
||||||
@@ -1,8 +1,25 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
|
from typing import Optional, Dict, Any, List
|
||||||
|
from typing_extensions import TypedDict
|
||||||
|
import requests
|
||||||
|
from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG
|
||||||
|
|
||||||
from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondException
|
from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondException
|
||||||
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation
|
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation
|
||||||
|
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
class ComputeExperimentDesignReturn(TypedDict):
|
||||||
|
solutions: list
|
||||||
|
titration: dict
|
||||||
|
solvents: dict
|
||||||
|
feeding_order: list
|
||||||
|
return_info: str
|
||||||
|
|
||||||
|
|
||||||
class BioyondDispensingStation(BioyondWorkstation):
|
class BioyondDispensingStation(BioyondWorkstation):
|
||||||
@@ -23,6 +40,111 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
# self._logger = SimpleLogger()
|
# self._logger = SimpleLogger()
|
||||||
# self.is_running = False
|
# self.is_running = False
|
||||||
|
|
||||||
|
# 用于跟踪任务完成状态的字典: {orderCode: {status, order_id, timestamp}}
|
||||||
|
self.order_completion_status = {}
|
||||||
|
|
||||||
|
def _post_project_api(self, endpoint: str, data: Any) -> Dict[str, Any]:
|
||||||
|
"""项目接口通用POST调用
|
||||||
|
|
||||||
|
参数:
|
||||||
|
endpoint: 接口路径(例如 /api/lims/order/brief-step-paramerers)
|
||||||
|
data: 请求体中的 data 字段内容
|
||||||
|
|
||||||
|
返回:
|
||||||
|
dict: 服务端响应,失败时返回 {code:0,message,...}
|
||||||
|
"""
|
||||||
|
request_data = {
|
||||||
|
"apiKey": API_CONFIG["api_key"],
|
||||||
|
"requestTime": self.hardware_interface.get_current_time_iso8601(),
|
||||||
|
"data": data
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
f"{self.hardware_interface.host}{endpoint}",
|
||||||
|
json=request_data,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
result = response.json()
|
||||||
|
return result if isinstance(result, dict) else {"code": 0, "message": "非JSON响应"}
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return {"code": 0, "message": "非JSON响应"}
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
return {"code": 0, "message": "请求超时"}
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return {"code": 0, "message": str(e)}
|
||||||
|
|
||||||
|
def _delete_project_api(self, endpoint: str, data: Any) -> Dict[str, Any]:
|
||||||
|
"""项目接口通用DELETE调用
|
||||||
|
|
||||||
|
参数:
|
||||||
|
endpoint: 接口路径(例如 /api/lims/order/workflows)
|
||||||
|
data: 请求体中的 data 字段内容
|
||||||
|
|
||||||
|
返回:
|
||||||
|
dict: 服务端响应,失败时返回 {code:0,message,...}
|
||||||
|
"""
|
||||||
|
request_data = {
|
||||||
|
"apiKey": API_CONFIG["api_key"],
|
||||||
|
"requestTime": self.hardware_interface.get_current_time_iso8601(),
|
||||||
|
"data": data
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
response = requests.delete(
|
||||||
|
f"{self.hardware_interface.host}{endpoint}",
|
||||||
|
json=request_data,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
result = response.json()
|
||||||
|
return result if isinstance(result, dict) else {"code": 0, "message": "非JSON响应"}
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return {"code": 0, "message": "非JSON响应"}
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
return {"code": 0, "message": "请求超时"}
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return {"code": 0, "message": str(e)}
|
||||||
|
|
||||||
|
def compute_experiment_design(
|
||||||
|
self,
|
||||||
|
ratio: dict,
|
||||||
|
wt_percent: str = "0.25",
|
||||||
|
m_tot: str = "70",
|
||||||
|
titration_percent: str = "0.03",
|
||||||
|
) -> ComputeExperimentDesignReturn:
|
||||||
|
try:
|
||||||
|
if isinstance(ratio, str):
|
||||||
|
try:
|
||||||
|
ratio = json.loads(ratio)
|
||||||
|
except Exception:
|
||||||
|
ratio = {}
|
||||||
|
root = str(Path(__file__).resolve().parents[3])
|
||||||
|
if root not in sys.path:
|
||||||
|
sys.path.append(root)
|
||||||
|
try:
|
||||||
|
mod = importlib.import_module("tem.compute")
|
||||||
|
except Exception as e:
|
||||||
|
raise BioyondException(f"无法导入计算模块: {e}")
|
||||||
|
try:
|
||||||
|
wp = float(wt_percent) if isinstance(wt_percent, str) else wt_percent
|
||||||
|
mt = float(m_tot) if isinstance(m_tot, str) else m_tot
|
||||||
|
tp = float(titration_percent) if isinstance(titration_percent, str) else titration_percent
|
||||||
|
except Exception as e:
|
||||||
|
raise BioyondException(f"参数解析失败: {e}")
|
||||||
|
res = mod.generate_experiment_design(ratio=ratio, wt_percent=wp, m_tot=mt, titration_percent=tp)
|
||||||
|
out = {
|
||||||
|
"solutions": res.get("solutions", []),
|
||||||
|
"titration": res.get("titration", {}),
|
||||||
|
"solvents": res.get("solvents", {}),
|
||||||
|
"feeding_order": res.get("feeding_order", []),
|
||||||
|
"return_info": json.dumps(res, ensure_ascii=False)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
except BioyondException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise BioyondException(str(e))
|
||||||
|
|
||||||
# 90%10%小瓶投料任务创建方法
|
# 90%10%小瓶投料任务创建方法
|
||||||
def create_90_10_vial_feeding_task(self,
|
def create_90_10_vial_feeding_task(self,
|
||||||
order_name: str = None,
|
order_name: str = None,
|
||||||
@@ -270,7 +392,45 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
# 7. 调用create_order方法创建任务
|
# 7. 调用create_order方法创建任务
|
||||||
result = self.hardware_interface.create_order(json_str)
|
result = self.hardware_interface.create_order(json_str)
|
||||||
self.hardware_interface._logger.info(f"创建90%10%小瓶投料任务结果: {result}")
|
self.hardware_interface._logger.info(f"创建90%10%小瓶投料任务结果: {result}")
|
||||||
return json.dumps({"suc": True})
|
|
||||||
|
# 8. 解析结果获取order_id
|
||||||
|
order_id = None
|
||||||
|
if isinstance(result, str):
|
||||||
|
# result 格式: "{'3a1d895c-4d39-d504-1398-18f5a40bac1e': [{'id': '...', ...}]}"
|
||||||
|
# 第一个键就是order_id (UUID)
|
||||||
|
try:
|
||||||
|
# 尝试解析字符串为字典
|
||||||
|
import ast
|
||||||
|
result_dict = ast.literal_eval(result)
|
||||||
|
# 获取第一个键作为order_id
|
||||||
|
if result_dict and isinstance(result_dict, dict):
|
||||||
|
first_key = list(result_dict.keys())[0]
|
||||||
|
order_id = first_key
|
||||||
|
self.hardware_interface._logger.info(f"✓ 成功提取order_id: {order_id}")
|
||||||
|
else:
|
||||||
|
self.hardware_interface._logger.warning(f"result_dict格式异常: {result_dict}")
|
||||||
|
except Exception as e:
|
||||||
|
self.hardware_interface._logger.error(f"✗ 无法从结果中提取order_id: {e}, result类型={type(result)}")
|
||||||
|
elif isinstance(result, dict):
|
||||||
|
# 如果已经是字典
|
||||||
|
if result:
|
||||||
|
first_key = list(result.keys())[0]
|
||||||
|
order_id = first_key
|
||||||
|
self.hardware_interface._logger.info(f"✓ 成功提取order_id(dict): {order_id}")
|
||||||
|
|
||||||
|
if not order_id:
|
||||||
|
self.hardware_interface._logger.warning(
|
||||||
|
f"⚠ 未能提取order_id,result={result[:100] if isinstance(result, str) else result}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 返回成功结果和构建的JSON数据
|
||||||
|
return json.dumps({
|
||||||
|
"suc": True,
|
||||||
|
"order_code": order_code,
|
||||||
|
"order_id": order_id,
|
||||||
|
"result": result,
|
||||||
|
"order_params": order_data
|
||||||
|
})
|
||||||
|
|
||||||
except BioyondException:
|
except BioyondException:
|
||||||
# 重新抛出BioyondException
|
# 重新抛出BioyondException
|
||||||
@@ -398,7 +558,37 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
result = self.hardware_interface.create_order(json_str)
|
result = self.hardware_interface.create_order(json_str)
|
||||||
self.hardware_interface._logger.info(f"创建二胺溶液配置任务结果: {result}")
|
self.hardware_interface._logger.info(f"创建二胺溶液配置任务结果: {result}")
|
||||||
|
|
||||||
return json.dumps({"suc": True})
|
# 8. 解析结果获取order_id
|
||||||
|
order_id = None
|
||||||
|
if isinstance(result, str):
|
||||||
|
try:
|
||||||
|
import ast
|
||||||
|
result_dict = ast.literal_eval(result)
|
||||||
|
if result_dict and isinstance(result_dict, dict):
|
||||||
|
first_key = list(result_dict.keys())[0]
|
||||||
|
order_id = first_key
|
||||||
|
self.hardware_interface._logger.info(f"✓ 成功提取order_id: {order_id}")
|
||||||
|
else:
|
||||||
|
self.hardware_interface._logger.warning(f"result_dict格式异常: {result_dict}")
|
||||||
|
except Exception as e:
|
||||||
|
self.hardware_interface._logger.error(f"✗ 无法从结果中提取order_id: {e}")
|
||||||
|
elif isinstance(result, dict):
|
||||||
|
if result:
|
||||||
|
first_key = list(result.keys())[0]
|
||||||
|
order_id = first_key
|
||||||
|
self.hardware_interface._logger.info(f"✓ 成功提取order_id(dict): {order_id}")
|
||||||
|
|
||||||
|
if not order_id:
|
||||||
|
self.hardware_interface._logger.warning(f"⚠ 未能提取order_id")
|
||||||
|
|
||||||
|
# 返回成功结果和构建的JSON数据
|
||||||
|
return json.dumps({
|
||||||
|
"suc": True,
|
||||||
|
"order_code": order_code,
|
||||||
|
"order_id": order_id,
|
||||||
|
"result": result,
|
||||||
|
"order_params": order_data
|
||||||
|
})
|
||||||
|
|
||||||
except BioyondException:
|
except BioyondException:
|
||||||
# 重新抛出BioyondException
|
# 重新抛出BioyondException
|
||||||
@@ -499,15 +689,24 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
hold_m_name=hold_m_name
|
hold_m_name=hold_m_name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 解析返回结果以获取order_code和order_id
|
||||||
|
result_data = json.loads(result) if isinstance(result, str) else result
|
||||||
|
order_code = result_data.get("order_code")
|
||||||
|
order_id = result_data.get("order_id")
|
||||||
|
order_params = result_data.get("order_params", {})
|
||||||
|
|
||||||
results.append({
|
results.append({
|
||||||
"index": idx + 1,
|
"index": idx + 1,
|
||||||
"name": name,
|
"name": name,
|
||||||
"success": True,
|
"success": True,
|
||||||
"hold_m_name": hold_m_name
|
"order_code": order_code,
|
||||||
|
"order_id": order_id,
|
||||||
|
"hold_m_name": hold_m_name,
|
||||||
|
"order_params": order_params
|
||||||
})
|
})
|
||||||
success_count += 1
|
success_count += 1
|
||||||
self.hardware_interface._logger.info(
|
self.hardware_interface._logger.info(
|
||||||
f"成功创建二胺溶液配置任务: {name}"
|
f"成功创建二胺溶液配置任务: {name}, order_code={order_code}, order_id={order_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
except BioyondException as e:
|
except BioyondException as e:
|
||||||
@@ -533,11 +732,17 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
f"创建第 {idx + 1} 个任务时发生未知错误: {str(e)}"
|
f"创建第 {idx + 1} 个任务时发生未知错误: {str(e)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 提取所有成功任务的order_code和order_id
|
||||||
|
order_codes = [r["order_code"] for r in results if r["success"]]
|
||||||
|
order_ids = [r["order_id"] for r in results if r["success"]]
|
||||||
|
|
||||||
# 返回汇总结果
|
# 返回汇总结果
|
||||||
summary = {
|
summary = {
|
||||||
"total": len(solutions),
|
"total": len(solutions),
|
||||||
"success": success_count,
|
"success": success_count,
|
||||||
"failed": failed_count,
|
"failed": failed_count,
|
||||||
|
"order_codes": order_codes,
|
||||||
|
"order_ids": order_ids,
|
||||||
"details": results
|
"details": results
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,8 +751,13 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
f"成功={success_count}, 失败={failed_count}"
|
f"成功={success_count}, 失败={failed_count}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 返回JSON字符串格式
|
# 构建返回结果
|
||||||
return json.dumps(summary, ensure_ascii=False)
|
summary["return_info"] = {
|
||||||
|
"order_codes": order_codes,
|
||||||
|
"order_ids": order_ids,
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary
|
||||||
|
|
||||||
except BioyondException:
|
except BioyondException:
|
||||||
raise
|
raise
|
||||||
@@ -556,6 +766,40 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
self.hardware_interface._logger.error(error_msg)
|
self.hardware_interface._logger.error(error_msg)
|
||||||
raise BioyondException(error_msg)
|
raise BioyondException(error_msg)
|
||||||
|
|
||||||
|
def brief_step_parameters(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""获取简要步骤参数(站点项目接口)
|
||||||
|
|
||||||
|
参数:
|
||||||
|
data: 查询参数字典
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
dict: 接口返回数据
|
||||||
|
"""
|
||||||
|
return self._post_project_api("/api/lims/order/brief-step-paramerers", data)
|
||||||
|
|
||||||
|
def project_order_report(self, order_id: str) -> Dict[str, Any]:
|
||||||
|
"""查询项目端订单报告(兼容旧路径)
|
||||||
|
|
||||||
|
参数:
|
||||||
|
order_id: 订单ID
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
dict: 报告数据
|
||||||
|
"""
|
||||||
|
return self._post_project_api("/api/lims/order/project-order-report", order_id)
|
||||||
|
|
||||||
|
def workflow_sample_locations(self, workflow_id: str) -> Dict[str, Any]:
|
||||||
|
"""查询工作流样品库位(站点项目接口)
|
||||||
|
|
||||||
|
参数:
|
||||||
|
workflow_id: 工作流ID
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
dict: 位置信息数据
|
||||||
|
"""
|
||||||
|
return self._post_project_api("/api/lims/storage/workflow-sample-locations", workflow_id)
|
||||||
|
|
||||||
|
|
||||||
# 批量创建90%10%小瓶投料任务
|
# 批量创建90%10%小瓶投料任务
|
||||||
def batch_create_90_10_vial_feeding_tasks(self,
|
def batch_create_90_10_vial_feeding_tasks(self,
|
||||||
titration,
|
titration,
|
||||||
@@ -613,22 +857,15 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
if not all([name, main_portion is not None, titration_portion is not None, titration_solvent is not None]):
|
if not all([name, main_portion is not None, titration_portion is not None, titration_solvent is not None]):
|
||||||
raise BioyondException("titration 数据缺少必要参数")
|
raise BioyondException("titration 数据缺少必要参数")
|
||||||
|
|
||||||
# 将main_portion平均分成3份作为90%物料(3个小瓶)
|
|
||||||
portion_90 = main_portion / 3
|
|
||||||
|
|
||||||
# 调用单个任务创建方法
|
# 调用单个任务创建方法
|
||||||
result = self.create_90_10_vial_feeding_task(
|
result = self.create_90_10_vial_feeding_task(
|
||||||
order_name=f"90%10%小瓶投料-{name}",
|
order_name=f"90%10%小瓶投料-{name}",
|
||||||
speed=speed,
|
speed=speed,
|
||||||
temperature=temperature,
|
temperature=temperature,
|
||||||
delay_time=delay_time,
|
delay_time=delay_time,
|
||||||
# 90%物料 - 主称固体平均分成3份
|
# 90%物料 - 主称固体直接使用main_portion
|
||||||
percent_90_1_assign_material_name=name,
|
percent_90_1_assign_material_name=name,
|
||||||
percent_90_1_target_weigh=str(round(portion_90, 6)),
|
percent_90_1_target_weigh=str(round(main_portion, 6)),
|
||||||
percent_90_2_assign_material_name=name,
|
|
||||||
percent_90_2_target_weigh=str(round(portion_90, 6)),
|
|
||||||
percent_90_3_assign_material_name=name,
|
|
||||||
percent_90_3_target_weigh=str(round(portion_90, 6)),
|
|
||||||
# 10%物料 - 滴定固体 + 滴定溶剂(只使用第1个10%小瓶)
|
# 10%物料 - 滴定固体 + 滴定溶剂(只使用第1个10%小瓶)
|
||||||
percent_10_1_assign_material_name=name,
|
percent_10_1_assign_material_name=name,
|
||||||
percent_10_1_target_weigh=str(round(titration_portion, 6)),
|
percent_10_1_target_weigh=str(round(titration_portion, 6)),
|
||||||
@@ -637,29 +874,54 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
hold_m_name=hold_m_name
|
hold_m_name=hold_m_name
|
||||||
)
|
)
|
||||||
|
|
||||||
summary = {
|
# 解析返回结果以获取order_code和order_id
|
||||||
|
result_data = json.loads(result) if isinstance(result, str) else result
|
||||||
|
order_code = result_data.get("order_code")
|
||||||
|
order_id = result_data.get("order_id")
|
||||||
|
order_params = result_data.get("order_params", {})
|
||||||
|
|
||||||
|
# 构建详细信息(保持原有结构)
|
||||||
|
detail = {
|
||||||
|
"index": 1,
|
||||||
|
"name": name,
|
||||||
"success": True,
|
"success": True,
|
||||||
|
"order_code": order_code,
|
||||||
|
"order_id": order_id,
|
||||||
"hold_m_name": hold_m_name,
|
"hold_m_name": hold_m_name,
|
||||||
"material_name": name,
|
|
||||||
"90_vials": {
|
"90_vials": {
|
||||||
"count": 3,
|
"count": 1,
|
||||||
"weight_per_vial": round(portion_90, 6),
|
"weight_per_vial": round(main_portion, 6),
|
||||||
"total_weight": round(main_portion, 6)
|
"total_weight": round(main_portion, 6)
|
||||||
},
|
},
|
||||||
"10_vials": {
|
"10_vials": {
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"solid_weight": round(titration_portion, 6),
|
"solid_weight": round(titration_portion, 6),
|
||||||
"liquid_volume": round(titration_solvent, 6)
|
"liquid_volume": round(titration_solvent, 6)
|
||||||
|
},
|
||||||
|
"order_params": order_params
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 构建批量结果格式(与diamine_solution_tasks保持一致)
|
||||||
|
summary = {
|
||||||
|
"total": 1,
|
||||||
|
"success": 1,
|
||||||
|
"failed": 0,
|
||||||
|
"order_codes": [order_code],
|
||||||
|
"order_ids": [order_id],
|
||||||
|
"details": [detail]
|
||||||
}
|
}
|
||||||
|
|
||||||
self.hardware_interface._logger.info(
|
self.hardware_interface._logger.info(
|
||||||
f"成功创建90%10%小瓶投料任务: {hold_m_name}, "
|
f"成功创建90%10%小瓶投料任务: {name}, order_code={order_code}, order_id={order_id}"
|
||||||
f"90%物料={portion_90:.6f}g×3, 10%物料={titration_portion:.6f}g+{titration_solvent:.6f}mL"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 返回JSON字符串格式
|
# 构建返回结果
|
||||||
return json.dumps(summary, ensure_ascii=False)
|
summary["return_info"] = {
|
||||||
|
"order_codes": [order_code],
|
||||||
|
"order_ids": [order_id],
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary
|
||||||
|
|
||||||
except BioyondException:
|
except BioyondException:
|
||||||
raise
|
raise
|
||||||
@@ -668,6 +930,571 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
self.hardware_interface._logger.error(error_msg)
|
self.hardware_interface._logger.error(error_msg)
|
||||||
raise BioyondException(error_msg)
|
raise BioyondException(error_msg)
|
||||||
|
|
||||||
|
def _extract_actuals_from_report(self, report) -> Dict[str, Any]:
|
||||||
|
data = report.get('data') if isinstance(report, dict) else None
|
||||||
|
actual_target_weigh = None
|
||||||
|
actual_volume = None
|
||||||
|
if data:
|
||||||
|
extra = data.get('extraProperties') or {}
|
||||||
|
if isinstance(extra, dict):
|
||||||
|
for v in extra.values():
|
||||||
|
obj = None
|
||||||
|
try:
|
||||||
|
obj = json.loads(v) if isinstance(v, str) else v
|
||||||
|
except Exception:
|
||||||
|
obj = None
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
tw = obj.get('targetWeigh')
|
||||||
|
vol = obj.get('volume')
|
||||||
|
if tw is not None:
|
||||||
|
try:
|
||||||
|
actual_target_weigh = float(tw)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if vol is not None:
|
||||||
|
try:
|
||||||
|
actual_volume = float(vol)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {
|
||||||
|
'actualTargetWeigh': actual_target_weigh,
|
||||||
|
'actualVolume': actual_volume
|
||||||
|
}
|
||||||
|
|
||||||
|
# 等待多个任务完成并获取实验报告
|
||||||
|
def wait_for_multiple_orders_and_get_reports(self,
|
||||||
|
batch_create_result: str = None,
|
||||||
|
timeout: int = 7200,
|
||||||
|
check_interval: int = 10) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
同时等待多个任务完成并获取实验报告
|
||||||
|
|
||||||
|
参数说明:
|
||||||
|
- batch_create_result: 批量创建任务的返回结果JSON字符串,包含order_codes和order_ids数组
|
||||||
|
- timeout: 超时时间(秒),默认7200秒(2小时)
|
||||||
|
- check_interval: 检查间隔(秒),默认10秒
|
||||||
|
|
||||||
|
返回: 包含所有任务状态和报告的字典
|
||||||
|
{
|
||||||
|
"total": 2,
|
||||||
|
"completed": 2,
|
||||||
|
"timeout": 0,
|
||||||
|
"elapsed_time": 120.5,
|
||||||
|
"reports": [
|
||||||
|
{
|
||||||
|
"order_code": "task_vial_1",
|
||||||
|
"order_id": "uuid1",
|
||||||
|
"status": "completed",
|
||||||
|
"completion_status": 30,
|
||||||
|
"report": {...}
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- BioyondException: 所有任务都超时或发生错误
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 参数类型转换
|
||||||
|
timeout = int(timeout) if timeout else 7200
|
||||||
|
check_interval = int(check_interval) if check_interval else 10
|
||||||
|
|
||||||
|
# 验证batch_create_result参数
|
||||||
|
if not batch_create_result or batch_create_result == "":
|
||||||
|
raise BioyondException("batch_create_result参数为空,请确保从batch_create节点正确连接handle")
|
||||||
|
|
||||||
|
# 解析batch_create_result JSON对象
|
||||||
|
try:
|
||||||
|
# 清理可能存在的截断标记 [...]
|
||||||
|
if isinstance(batch_create_result, str) and '[...]' in batch_create_result:
|
||||||
|
batch_create_result = batch_create_result.replace('[...]', '[]')
|
||||||
|
|
||||||
|
result_obj = json.loads(batch_create_result) if isinstance(batch_create_result, str) else batch_create_result
|
||||||
|
|
||||||
|
# 兼容外层包装格式 {error, suc, return_value}
|
||||||
|
if isinstance(result_obj, dict) and "return_value" in result_obj:
|
||||||
|
inner = result_obj.get("return_value")
|
||||||
|
if isinstance(inner, str):
|
||||||
|
result_obj = json.loads(inner)
|
||||||
|
elif isinstance(inner, dict):
|
||||||
|
result_obj = inner
|
||||||
|
|
||||||
|
# 从summary对象中提取order_codes和order_ids
|
||||||
|
order_codes = result_obj.get("order_codes", [])
|
||||||
|
order_ids = result_obj.get("order_ids", [])
|
||||||
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise BioyondException(f"解析batch_create_result失败: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
raise BioyondException(f"处理batch_create_result时出错: {e}")
|
||||||
|
|
||||||
|
# 验证提取的数据
|
||||||
|
if not order_codes:
|
||||||
|
raise BioyondException("batch_create_result中未找到order_codes字段或为空")
|
||||||
|
if not order_ids:
|
||||||
|
raise BioyondException("batch_create_result中未找到order_ids字段或为空")
|
||||||
|
|
||||||
|
# 确保order_codes和order_ids是列表类型
|
||||||
|
if not isinstance(order_codes, list):
|
||||||
|
order_codes = [order_codes] if order_codes else []
|
||||||
|
if not isinstance(order_ids, list):
|
||||||
|
order_ids = [order_ids] if order_ids else []
|
||||||
|
|
||||||
|
codes_list = order_codes
|
||||||
|
ids_list = order_ids
|
||||||
|
|
||||||
|
if len(codes_list) != len(ids_list):
|
||||||
|
raise BioyondException(
|
||||||
|
f"order_codes数量({len(codes_list)})与order_ids数量({len(ids_list)})不匹配"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not codes_list or not ids_list:
|
||||||
|
raise BioyondException("order_codes和order_ids不能为空")
|
||||||
|
|
||||||
|
# 初始化跟踪变量
|
||||||
|
total = len(codes_list)
|
||||||
|
pending_orders = {code: {"order_id": ids_list[i], "completed": False}
|
||||||
|
for i, code in enumerate(codes_list)}
|
||||||
|
reports = []
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
self.hardware_interface._logger.info(
|
||||||
|
f"开始等待 {total} 个任务完成: {', '.join(codes_list)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 轮询检查任务状态
|
||||||
|
while pending_orders:
|
||||||
|
elapsed_time = time.time() - start_time
|
||||||
|
|
||||||
|
# 检查超时
|
||||||
|
if elapsed_time > timeout:
|
||||||
|
# 收集超时任务
|
||||||
|
timeout_orders = list(pending_orders.keys())
|
||||||
|
self.hardware_interface._logger.error(
|
||||||
|
f"等待任务完成超时,剩余未完成任务: {', '.join(timeout_orders)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 为超时任务添加记录
|
||||||
|
for order_code in timeout_orders:
|
||||||
|
reports.append({
|
||||||
|
"order_code": order_code,
|
||||||
|
"order_id": pending_orders[order_code]["order_id"],
|
||||||
|
"status": "timeout",
|
||||||
|
"completion_status": None,
|
||||||
|
"report": None,
|
||||||
|
"extracted": None,
|
||||||
|
"elapsed_time": elapsed_time
|
||||||
|
})
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
# 检查每个待完成的任务
|
||||||
|
completed_in_this_round = []
|
||||||
|
for order_code in list(pending_orders.keys()):
|
||||||
|
order_id = pending_orders[order_code]["order_id"]
|
||||||
|
|
||||||
|
# 检查任务是否完成
|
||||||
|
if order_code in self.order_completion_status:
|
||||||
|
completion_info = self.order_completion_status[order_code]
|
||||||
|
self.hardware_interface._logger.info(
|
||||||
|
f"检测到任务 {order_code} 已完成,状态: {completion_info.get('status')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 获取实验报告
|
||||||
|
try:
|
||||||
|
report = self.project_order_report(order_id)
|
||||||
|
|
||||||
|
if not report:
|
||||||
|
self.hardware_interface._logger.warning(
|
||||||
|
f"任务 {order_code} 已完成但无法获取报告"
|
||||||
|
)
|
||||||
|
report = {"error": "无法获取报告"}
|
||||||
|
else:
|
||||||
|
self.hardware_interface._logger.info(
|
||||||
|
f"成功获取任务 {order_code} 的实验报告"
|
||||||
|
)
|
||||||
|
|
||||||
|
reports.append({
|
||||||
|
"order_code": order_code,
|
||||||
|
"order_id": order_id,
|
||||||
|
"status": "completed",
|
||||||
|
"completion_status": completion_info.get('status'),
|
||||||
|
"report": report,
|
||||||
|
"extracted": self._extract_actuals_from_report(report),
|
||||||
|
"elapsed_time": elapsed_time
|
||||||
|
})
|
||||||
|
|
||||||
|
# 标记为已完成
|
||||||
|
completed_in_this_round.append(order_code)
|
||||||
|
|
||||||
|
# 清理完成状态记录
|
||||||
|
del self.order_completion_status[order_code]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.hardware_interface._logger.error(
|
||||||
|
f"查询任务 {order_code} 报告失败: {str(e)}"
|
||||||
|
)
|
||||||
|
reports.append({
|
||||||
|
"order_code": order_code,
|
||||||
|
"order_id": order_id,
|
||||||
|
"status": "error",
|
||||||
|
"completion_status": completion_info.get('status'),
|
||||||
|
"report": None,
|
||||||
|
"extracted": None,
|
||||||
|
"error": str(e),
|
||||||
|
"elapsed_time": elapsed_time
|
||||||
|
})
|
||||||
|
completed_in_this_round.append(order_code)
|
||||||
|
|
||||||
|
# 从待完成列表中移除已完成的任务
|
||||||
|
for order_code in completed_in_this_round:
|
||||||
|
del pending_orders[order_code]
|
||||||
|
|
||||||
|
# 如果还有待完成的任务,等待后继续
|
||||||
|
if pending_orders:
|
||||||
|
time.sleep(check_interval)
|
||||||
|
|
||||||
|
# 每分钟记录一次等待状态
|
||||||
|
new_elapsed_time = time.time() - start_time
|
||||||
|
if int(new_elapsed_time) % 60 == 0 and new_elapsed_time > 0:
|
||||||
|
self.hardware_interface._logger.info(
|
||||||
|
f"批量等待任务中... 已完成 {len(reports)}/{total}, "
|
||||||
|
f"待完成: {', '.join(pending_orders.keys())}, "
|
||||||
|
f"已等待 {int(new_elapsed_time/60)} 分钟"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 统计结果
|
||||||
|
completed_count = sum(1 for r in reports if r['status'] == 'completed')
|
||||||
|
timeout_count = sum(1 for r in reports if r['status'] == 'timeout')
|
||||||
|
error_count = sum(1 for r in reports if r['status'] == 'error')
|
||||||
|
|
||||||
|
final_elapsed_time = time.time() - start_time
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
"total": total,
|
||||||
|
"completed": completed_count,
|
||||||
|
"timeout": timeout_count,
|
||||||
|
"error": error_count,
|
||||||
|
"elapsed_time": round(final_elapsed_time, 2),
|
||||||
|
"reports": reports
|
||||||
|
}
|
||||||
|
|
||||||
|
self.hardware_interface._logger.info(
|
||||||
|
f"批量等待任务完成: 总数={total}, 成功={completed_count}, "
|
||||||
|
f"超时={timeout_count}, 错误={error_count}, 耗时={final_elapsed_time:.1f}秒"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 返回字典格式,在顶层包含统计信息
|
||||||
|
return {
|
||||||
|
"return_info": json.dumps(summary, ensure_ascii=False)
|
||||||
|
}
|
||||||
|
|
||||||
|
except BioyondException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"批量等待任务完成时发生未预期的错误: {str(e)}"
|
||||||
|
self.hardware_interface._logger.error(error_msg)
|
||||||
|
raise BioyondException(error_msg)
|
||||||
|
|
||||||
|
def process_order_finish_report(self, report_request, used_materials) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
重写父类方法,处理任务完成报送并记录到 order_completion_status
|
||||||
|
|
||||||
|
Args:
|
||||||
|
report_request: WorkstationReportRequest 对象,包含任务完成信息
|
||||||
|
used_materials: 物料使用记录列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: 处理结果
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 调用父类方法
|
||||||
|
result = super().process_order_finish_report(report_request, used_materials)
|
||||||
|
|
||||||
|
# 记录任务完成状态
|
||||||
|
data = report_request.data
|
||||||
|
order_code = data.get('orderCode')
|
||||||
|
|
||||||
|
if order_code:
|
||||||
|
self.order_completion_status[order_code] = {
|
||||||
|
'status': data.get('status'),
|
||||||
|
'order_name': data.get('orderName'),
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'start_time': data.get('startTime'),
|
||||||
|
'end_time': data.get('endTime')
|
||||||
|
}
|
||||||
|
|
||||||
|
self.hardware_interface._logger.info(
|
||||||
|
f"已记录任务完成状态: {order_code}, status={data.get('status')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.hardware_interface._logger.error(f"处理任务完成报送失败: {e}")
|
||||||
|
return {"processed": False, "error": str(e)}
|
||||||
|
|
||||||
|
def transfer_materials_to_reaction_station(
|
||||||
|
self,
|
||||||
|
target_device_id: str,
|
||||||
|
transfer_groups: list
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
将配液站完成的物料转移到指定反应站的堆栈库位
|
||||||
|
支持多组转移任务,每组包含物料名称、目标堆栈和目标库位
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target_device_id: 目标反应站设备ID(所有转移组使用同一个设备)
|
||||||
|
transfer_groups: 转移任务组列表,每组包含:
|
||||||
|
- materials: 物料名称(字符串,将通过RPC查询)
|
||||||
|
- target_stack: 目标堆栈名称(如"堆栈1左")
|
||||||
|
- target_sites: 目标库位(如"A01")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 转移结果
|
||||||
|
{
|
||||||
|
"success": bool,
|
||||||
|
"total_groups": int,
|
||||||
|
"successful_groups": int,
|
||||||
|
"failed_groups": int,
|
||||||
|
"target_device_id": str,
|
||||||
|
"details": [...]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 验证参数
|
||||||
|
if not target_device_id:
|
||||||
|
raise ValueError("目标设备ID不能为空")
|
||||||
|
|
||||||
|
if not transfer_groups:
|
||||||
|
raise ValueError("转移任务组列表不能为空")
|
||||||
|
|
||||||
|
if not isinstance(transfer_groups, list):
|
||||||
|
raise ValueError("transfer_groups必须是列表类型")
|
||||||
|
|
||||||
|
# 标准化设备ID格式: 确保以 /devices/ 开头
|
||||||
|
if not target_device_id.startswith("/devices/"):
|
||||||
|
if target_device_id.startswith("/"):
|
||||||
|
target_device_id = f"/devices{target_device_id}"
|
||||||
|
else:
|
||||||
|
target_device_id = f"/devices/{target_device_id}"
|
||||||
|
|
||||||
|
self.hardware_interface._logger.info(
|
||||||
|
f"目标设备ID标准化为: {target_device_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.hardware_interface._logger.info(
|
||||||
|
f"开始执行批量物料转移: {len(transfer_groups)}组任务 -> {target_device_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
from .config import WAREHOUSE_MAPPING
|
||||||
|
results = []
|
||||||
|
successful_count = 0
|
||||||
|
failed_count = 0
|
||||||
|
|
||||||
|
for idx, group in enumerate(transfer_groups, 1):
|
||||||
|
try:
|
||||||
|
# 提取参数
|
||||||
|
material_name = group.get("materials", "")
|
||||||
|
target_stack = group.get("target_stack", "")
|
||||||
|
target_sites = group.get("target_sites", "")
|
||||||
|
|
||||||
|
# 验证必填参数
|
||||||
|
if not material_name:
|
||||||
|
raise ValueError(f"第{idx}组: 物料名称不能为空")
|
||||||
|
if not target_stack:
|
||||||
|
raise ValueError(f"第{idx}组: 目标堆栈不能为空")
|
||||||
|
if not target_sites:
|
||||||
|
raise ValueError(f"第{idx}组: 目标库位不能为空")
|
||||||
|
|
||||||
|
self.hardware_interface._logger.info(
|
||||||
|
f"处理第{idx}组转移: {material_name} -> "
|
||||||
|
f"{target_device_id}/{target_stack}/{target_sites}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 通过物料名称从deck获取ResourcePLR对象
|
||||||
|
try:
|
||||||
|
material_resource = self.deck.get_resource(material_name)
|
||||||
|
if not material_resource:
|
||||||
|
raise ValueError(f"在deck中未找到物料: {material_name}")
|
||||||
|
|
||||||
|
self.hardware_interface._logger.info(
|
||||||
|
f"从deck获取到物料 {material_name}: {material_resource}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(
|
||||||
|
f"获取物料 {material_name} 失败: {str(e)},请确认物料已正确加载到deck中"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 验证目标堆栈是否存在
|
||||||
|
if target_stack not in WAREHOUSE_MAPPING:
|
||||||
|
raise ValueError(
|
||||||
|
f"未知的堆栈名称: {target_stack},"
|
||||||
|
f"可选值: {list(WAREHOUSE_MAPPING.keys())}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 验证库位是否有效
|
||||||
|
stack_sites = WAREHOUSE_MAPPING[target_stack].get("site_uuids", {})
|
||||||
|
if target_sites not in stack_sites:
|
||||||
|
raise ValueError(
|
||||||
|
f"库位 {target_sites} 不存在于堆栈 {target_stack} 中,"
|
||||||
|
f"可选库位: {list(stack_sites.keys())}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 获取目标库位的UUID
|
||||||
|
target_site_uuid = stack_sites[target_sites]
|
||||||
|
if not target_site_uuid:
|
||||||
|
raise ValueError(
|
||||||
|
f"库位 {target_sites} 的 UUID 未配置,请在 WAREHOUSE_MAPPING 中完善"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 目标位点(包含UUID)
|
||||||
|
future = ROS2DeviceNode.run_async_func(
|
||||||
|
self._ros_node.get_resource_with_dir,
|
||||||
|
True,
|
||||||
|
**{
|
||||||
|
"resource_id": f"/reaction_station_bioyond/Bioyond_Deck/{target_stack}",
|
||||||
|
"with_children": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# 等待异步完成后再获取结果
|
||||||
|
if not future:
|
||||||
|
raise ValueError(f"获取目标堆栈资源future无效: {target_stack}")
|
||||||
|
while not future.done():
|
||||||
|
time.sleep(0.1)
|
||||||
|
target_site_resource = future.result()
|
||||||
|
|
||||||
|
# 调用父类的 transfer_resource_to_another 方法
|
||||||
|
# 传入ResourcePLR对象和目标位点资源
|
||||||
|
future = self.transfer_resource_to_another(
|
||||||
|
resource=[material_resource],
|
||||||
|
mount_resource=[target_site_resource],
|
||||||
|
sites=[target_sites],
|
||||||
|
mount_device_id=target_device_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# 等待异步任务完成(轮询直到完成,再取结果)
|
||||||
|
if future:
|
||||||
|
try:
|
||||||
|
while not future.done():
|
||||||
|
time.sleep(0.1)
|
||||||
|
future.result()
|
||||||
|
self.hardware_interface._logger.info(
|
||||||
|
f"异步转移任务已完成: {material_name}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(f"转移任务执行失败: {str(e)}")
|
||||||
|
|
||||||
|
self.hardware_interface._logger.info(
|
||||||
|
f"第{idx}组转移成功: {material_name} -> "
|
||||||
|
f"{target_device_id}/{target_stack}/{target_sites}"
|
||||||
|
)
|
||||||
|
|
||||||
|
successful_count += 1
|
||||||
|
results.append({
|
||||||
|
"group_index": idx,
|
||||||
|
"success": True,
|
||||||
|
"material_name": material_name,
|
||||||
|
"target_stack": target_stack,
|
||||||
|
"target_site": target_sites,
|
||||||
|
"message": "转移成功"
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"第{idx}组转移失败: {str(e)}"
|
||||||
|
self.hardware_interface._logger.error(error_msg)
|
||||||
|
failed_count += 1
|
||||||
|
results.append({
|
||||||
|
"group_index": idx,
|
||||||
|
"success": False,
|
||||||
|
"material_name": group.get("materials", ""),
|
||||||
|
"error": str(e)
|
||||||
|
})
|
||||||
|
|
||||||
|
# 返回汇总结果
|
||||||
|
return {
|
||||||
|
"success": failed_count == 0,
|
||||||
|
"total_groups": len(transfer_groups),
|
||||||
|
"successful_groups": successful_count,
|
||||||
|
"failed_groups": failed_count,
|
||||||
|
"target_device_id": target_device_id,
|
||||||
|
"details": results,
|
||||||
|
"message": f"完成 {len(transfer_groups)} 组转移任务到 {target_device_id}: "
|
||||||
|
f"{successful_count} 成功, {failed_count} 失败"
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"批量转移物料失败: {str(e)}"
|
||||||
|
self.hardware_interface._logger.error(error_msg)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"total_groups": len(transfer_groups) if transfer_groups else 0,
|
||||||
|
"successful_groups": 0,
|
||||||
|
"failed_groups": len(transfer_groups) if transfer_groups else 0,
|
||||||
|
"target_device_id": target_device_id if target_device_id else "",
|
||||||
|
"error": error_msg
|
||||||
|
}
|
||||||
|
|
||||||
|
def query_resource_by_name(self, material_name: str):
|
||||||
|
"""
|
||||||
|
通过物料名称查询资源对象(适用于Bioyond系统)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
material_name: 物料名称
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
物料ID或None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Bioyond系统使用material_cache存储物料信息
|
||||||
|
if not hasattr(self.hardware_interface, 'material_cache'):
|
||||||
|
self.hardware_interface._logger.error(
|
||||||
|
"hardware_interface没有material_cache属性"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
material_cache = self.hardware_interface.material_cache
|
||||||
|
|
||||||
|
self.hardware_interface._logger.info(
|
||||||
|
f"查询物料 '{material_name}', 缓存中共有 {len(material_cache)} 个物料"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 调试: 打印前几个物料信息
|
||||||
|
if material_cache:
|
||||||
|
cache_items = list(material_cache.items())[:5]
|
||||||
|
for name, material_id in cache_items:
|
||||||
|
self.hardware_interface._logger.debug(
|
||||||
|
f"缓存物料: name={name}, id={material_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 直接从缓存中查找
|
||||||
|
if material_name in material_cache:
|
||||||
|
material_id = material_cache[material_name]
|
||||||
|
self.hardware_interface._logger.info(
|
||||||
|
f"找到物料: {material_name} -> ID: {material_id}"
|
||||||
|
)
|
||||||
|
return material_id
|
||||||
|
|
||||||
|
self.hardware_interface._logger.warning(
|
||||||
|
f"未找到物料: {material_name} (缓存中无此物料)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 打印所有可用物料名称供参考
|
||||||
|
available_materials = list(material_cache.keys())
|
||||||
|
if available_materials:
|
||||||
|
self.hardware_interface._logger.info(
|
||||||
|
f"可用物料列表(前10个): {available_materials[:10]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.hardware_interface._logger.error(
|
||||||
|
f"查询物料失败 {material_name}: {str(e)}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
bioyond = BioyondDispensingStation(config={
|
bioyond = BioyondDispensingStation(config={
|
||||||
@@ -1089,4 +1916,3 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# id = "3a1bce3c-4f31-c8f3-5525-f3b273bc34dc"
|
# id = "3a1bce3c-4f31-c8f3-5525-f3b273bc34dc"
|
||||||
# bioyond.sample_waste_removal(id)
|
# bioyond.sample_waste_removal(id)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import json
|
import json
|
||||||
|
import time
|
||||||
import requests
|
import requests
|
||||||
from typing import List, Dict, Any
|
from typing import List, Dict, Any
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation
|
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation
|
||||||
|
from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import MachineState
|
||||||
|
from unilabos.ros.msgs.message_converter import convert_to_ros_msg, Float64, String
|
||||||
from unilabos.devices.workstation.bioyond_studio.config import (
|
from unilabos.devices.workstation.bioyond_studio.config import (
|
||||||
WORKFLOW_STEP_IDS,
|
WORKFLOW_STEP_IDS,
|
||||||
WORKFLOW_TO_SECTION_MAP,
|
WORKFLOW_TO_SECTION_MAP,
|
||||||
@@ -10,6 +15,37 @@ from unilabos.devices.workstation.bioyond_studio.config import (
|
|||||||
from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG
|
from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG
|
||||||
|
|
||||||
|
|
||||||
|
class BioyondReactor:
|
||||||
|
def __init__(self, config: dict = None, deck=None, protocol_type=None, **kwargs):
|
||||||
|
self.in_temperature = 0.0
|
||||||
|
self.out_temperature = 0.0
|
||||||
|
self.pt100_temperature = 0.0
|
||||||
|
self.sensor_average_temperature = 0.0
|
||||||
|
self.target_temperature = 0.0
|
||||||
|
self.setting_temperature = 0.0
|
||||||
|
self.viscosity = 0.0
|
||||||
|
self.average_viscosity = 0.0
|
||||||
|
self.speed = 0.0
|
||||||
|
self.force = 0.0
|
||||||
|
|
||||||
|
def update_metrics(self, payload: Dict[str, Any]):
|
||||||
|
def _f(v):
|
||||||
|
try:
|
||||||
|
return float(v)
|
||||||
|
except Exception:
|
||||||
|
return 0.0
|
||||||
|
self.target_temperature = _f(payload.get("targetTemperature"))
|
||||||
|
self.setting_temperature = _f(payload.get("settingTemperature"))
|
||||||
|
self.in_temperature = _f(payload.get("inTemperature"))
|
||||||
|
self.out_temperature = _f(payload.get("outTemperature"))
|
||||||
|
self.pt100_temperature = _f(payload.get("pt100Temperature"))
|
||||||
|
self.sensor_average_temperature = _f(payload.get("sensorAverageTemperature"))
|
||||||
|
self.speed = _f(payload.get("speed"))
|
||||||
|
self.force = _f(payload.get("force"))
|
||||||
|
self.viscosity = _f(payload.get("viscosity"))
|
||||||
|
self.average_viscosity = _f(payload.get("averageViscosity"))
|
||||||
|
|
||||||
|
|
||||||
class BioyondReactionStation(BioyondWorkstation):
|
class BioyondReactionStation(BioyondWorkstation):
|
||||||
"""Bioyond反应站类
|
"""Bioyond反应站类
|
||||||
|
|
||||||
@@ -37,6 +73,19 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
print(f"BioyondReactionStation初始化完成 - workflow_mappings: {self.workflow_mappings}")
|
print(f"BioyondReactionStation初始化完成 - workflow_mappings: {self.workflow_mappings}")
|
||||||
print(f"workflow_mappings长度: {len(self.workflow_mappings)}")
|
print(f"workflow_mappings长度: {len(self.workflow_mappings)}")
|
||||||
|
|
||||||
|
self.in_temperature = 0.0
|
||||||
|
self.out_temperature = 0.0
|
||||||
|
self.pt100_temperature = 0.0
|
||||||
|
self.sensor_average_temperature = 0.0
|
||||||
|
self.target_temperature = 0.0
|
||||||
|
self.setting_temperature = 0.0
|
||||||
|
self.viscosity = 0.0
|
||||||
|
self.average_viscosity = 0.0
|
||||||
|
self.speed = 0.0
|
||||||
|
self.force = 0.0
|
||||||
|
|
||||||
|
self._frame_to_reactor_id = {1: "reactor_1", 2: "reactor_2", 3: "reactor_3", 4: "reactor_4", 5: "reactor_5"}
|
||||||
|
|
||||||
# ==================== 工作流方法 ====================
|
# ==================== 工作流方法 ====================
|
||||||
|
|
||||||
def reactor_taken_out(self):
|
def reactor_taken_out(self):
|
||||||
@@ -232,7 +281,7 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
temperature: 温度设定(°C)
|
temperature: 温度设定(°C)
|
||||||
"""
|
"""
|
||||||
# 处理 volume 参数:优先使用直接传入的 volume,否则从 solvents 中提取
|
# 处理 volume 参数:优先使用直接传入的 volume,否则从 solvents 中提取
|
||||||
if volume is None and solvents is not None:
|
if not volume and solvents is not None:
|
||||||
# 参数类型转换:如果是字符串则解析为字典
|
# 参数类型转换:如果是字符串则解析为字典
|
||||||
if isinstance(solvents, str):
|
if isinstance(solvents, str):
|
||||||
try:
|
try:
|
||||||
@@ -291,22 +340,39 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
|
|
||||||
def liquid_feeding_titration(
|
def liquid_feeding_titration(
|
||||||
self,
|
self,
|
||||||
volume_formula: str,
|
|
||||||
assign_material_name: str,
|
assign_material_name: str,
|
||||||
titration_type: str = "1",
|
volume_formula: str = None,
|
||||||
|
x_value: str = None,
|
||||||
|
feeding_order_data: str = None,
|
||||||
|
extracted_actuals: str = None,
|
||||||
|
titration_type: str = "2",
|
||||||
time: str = "90",
|
time: str = "90",
|
||||||
torque_variation: int = 2,
|
torque_variation: int = 2,
|
||||||
temperature: float = 25.00
|
temperature: float = 25.00
|
||||||
):
|
):
|
||||||
"""液体进料(滴定)
|
"""液体进料(滴定)
|
||||||
|
|
||||||
|
支持两种模式:
|
||||||
|
1. 直接提供 volume_formula (传统方式)
|
||||||
|
2. 自动计算公式: 提供 x_value, feeding_order_data, extracted_actuals (新方式)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
volume_formula: 分液公式(μL)
|
|
||||||
assign_material_name: 物料名称
|
assign_material_name: 物料名称
|
||||||
titration_type: 是否滴定(1=否, 2=是)
|
volume_formula: 分液公式(μL),如果提供则直接使用,否则自动计算
|
||||||
|
x_value: 手工输入的x值,格式如 "1-2-3"
|
||||||
|
feeding_order_data: feeding_order JSON字符串或对象,用于获取m二酐值
|
||||||
|
extracted_actuals: 从报告提取的实际加料量JSON字符串,包含actualTargetWeigh和actualVolume
|
||||||
|
titration_type: 是否滴定(1=否, 2=是),默认2
|
||||||
time: 观察时间(分钟)
|
time: 观察时间(分钟)
|
||||||
torque_variation: 是否观察(int类型, 1=否, 2=是)
|
torque_variation: 是否观察(int类型, 1=否, 2=是)
|
||||||
temperature: 温度(°C)
|
temperature: 温度(°C)
|
||||||
|
|
||||||
|
自动公式模板: 1000*(m二酐-x)*V二酐滴定/m二酐滴定
|
||||||
|
其中:
|
||||||
|
- m二酐滴定 = actualTargetWeigh (从extracted_actuals获取)
|
||||||
|
- V二酐滴定 = actualVolume (从extracted_actuals获取)
|
||||||
|
- x = x_value (手工输入)
|
||||||
|
- m二酐 = feeding_order中type为"main_anhydride"的amount值
|
||||||
"""
|
"""
|
||||||
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding(titration)"}')
|
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding(titration)"}')
|
||||||
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
||||||
@@ -316,6 +382,84 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
if isinstance(temperature, str):
|
if isinstance(temperature, str):
|
||||||
temperature = float(temperature)
|
temperature = float(temperature)
|
||||||
|
|
||||||
|
# 如果没有直接提供volume_formula,则自动计算
|
||||||
|
if not volume_formula and x_value and feeding_order_data and extracted_actuals:
|
||||||
|
# 1. 解析 feeding_order_data 获取 m二酐
|
||||||
|
if isinstance(feeding_order_data, str):
|
||||||
|
try:
|
||||||
|
feeding_order_data = json.loads(feeding_order_data)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise ValueError(f"feeding_order_data JSON解析失败: {str(e)}")
|
||||||
|
|
||||||
|
# 支持两种格式:
|
||||||
|
# 格式1: 直接是数组 [{...}, {...}]
|
||||||
|
# 格式2: 对象包裹 {"feeding_order": [{...}, {...}]}
|
||||||
|
if isinstance(feeding_order_data, list):
|
||||||
|
feeding_order_list = feeding_order_data
|
||||||
|
elif isinstance(feeding_order_data, dict):
|
||||||
|
feeding_order_list = feeding_order_data.get("feeding_order", [])
|
||||||
|
else:
|
||||||
|
raise ValueError("feeding_order_data 必须是数组或包含feeding_order的字典")
|
||||||
|
|
||||||
|
# 从feeding_order中找到main_anhydride的amount
|
||||||
|
m_anhydride = None
|
||||||
|
for item in feeding_order_list:
|
||||||
|
if item.get("type") == "main_anhydride":
|
||||||
|
m_anhydride = item.get("amount")
|
||||||
|
break
|
||||||
|
|
||||||
|
if m_anhydride is None:
|
||||||
|
raise ValueError("在feeding_order中未找到type为'main_anhydride'的条目")
|
||||||
|
|
||||||
|
# 2. 解析 extracted_actuals 获取 actualTargetWeigh 和 actualVolume
|
||||||
|
if isinstance(extracted_actuals, str):
|
||||||
|
try:
|
||||||
|
extracted_actuals_obj = json.loads(extracted_actuals)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise ValueError(f"extracted_actuals JSON解析失败: {str(e)}")
|
||||||
|
else:
|
||||||
|
extracted_actuals_obj = extracted_actuals
|
||||||
|
|
||||||
|
# 获取actuals数组
|
||||||
|
actuals_list = extracted_actuals_obj.get("actuals", [])
|
||||||
|
if not actuals_list:
|
||||||
|
# actuals为空,无法自动生成公式,回退到手动模式
|
||||||
|
print(f"警告: extracted_actuals中actuals数组为空,无法自动生成公式,请手动提供volume_formula")
|
||||||
|
volume_formula = None # 清空,触发后续的错误检查
|
||||||
|
else:
|
||||||
|
# 根据assign_material_name匹配对应的actual数据
|
||||||
|
# 假设order_code中包含物料名称
|
||||||
|
matched_actual = None
|
||||||
|
for actual in actuals_list:
|
||||||
|
order_code = actual.get("order_code", "")
|
||||||
|
# 简单匹配:如果order_code包含物料名称
|
||||||
|
if assign_material_name in order_code:
|
||||||
|
matched_actual = actual
|
||||||
|
break
|
||||||
|
|
||||||
|
# 如果没有匹配到,使用第一个
|
||||||
|
if not matched_actual and actuals_list:
|
||||||
|
matched_actual = actuals_list[0]
|
||||||
|
|
||||||
|
if not matched_actual:
|
||||||
|
raise ValueError("无法从extracted_actuals中获取实际加料量数据")
|
||||||
|
|
||||||
|
m_anhydride_titration = matched_actual.get("actualTargetWeigh") # m二酐滴定
|
||||||
|
v_anhydride_titration = matched_actual.get("actualVolume") # V二酐滴定
|
||||||
|
|
||||||
|
if m_anhydride_titration is None or v_anhydride_titration is None:
|
||||||
|
raise ValueError(f"实际加料量数据不完整: actualTargetWeigh={m_anhydride_titration}, actualVolume={v_anhydride_titration}")
|
||||||
|
|
||||||
|
# 3. 构建公式: 1000*(m二酐-x)*V二酐滴定/m二酐滴定
|
||||||
|
# x_value 格式如 "{{1-2-3}}",保留完整格式(包括花括号)直接替换到公式中
|
||||||
|
volume_formula = f"1000*({m_anhydride}-{x_value})*{v_anhydride_titration}/{m_anhydride_titration}"
|
||||||
|
|
||||||
|
print(f"自动生成滴定公式: {volume_formula}")
|
||||||
|
print(f" m二酐={m_anhydride}, x={x_value}, V二酐滴定={v_anhydride_titration}, m二酐滴定={m_anhydride_titration}")
|
||||||
|
|
||||||
|
elif not volume_formula:
|
||||||
|
raise ValueError("必须提供 volume_formula 或 (x_value + feeding_order_data + extracted_actuals)")
|
||||||
|
|
||||||
liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["liquid"]
|
liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["liquid"]
|
||||||
observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["observe"]
|
observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["observe"]
|
||||||
|
|
||||||
@@ -343,9 +487,288 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
print(f"当前队列长度: {len(self.pending_task_params)}")
|
print(f"当前队列长度: {len(self.pending_task_params)}")
|
||||||
return json.dumps({"suc": True})
|
return json.dumps({"suc": True})
|
||||||
|
|
||||||
|
def _extract_actuals_from_report(self, report) -> Dict[str, Any]:
|
||||||
|
data = report.get('data') if isinstance(report, dict) else None
|
||||||
|
actual_target_weigh = None
|
||||||
|
actual_volume = None
|
||||||
|
if data:
|
||||||
|
extra = data.get('extraProperties') or {}
|
||||||
|
if isinstance(extra, dict):
|
||||||
|
for v in extra.values():
|
||||||
|
obj = None
|
||||||
|
try:
|
||||||
|
obj = json.loads(v) if isinstance(v, str) else v
|
||||||
|
except Exception:
|
||||||
|
obj = None
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
tw = obj.get('targetWeigh')
|
||||||
|
vol = obj.get('volume')
|
||||||
|
if tw is not None:
|
||||||
|
try:
|
||||||
|
actual_target_weigh = float(tw)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if vol is not None:
|
||||||
|
try:
|
||||||
|
actual_volume = float(vol)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {
|
||||||
|
'actualTargetWeigh': actual_target_weigh,
|
||||||
|
'actualVolume': actual_volume
|
||||||
|
}
|
||||||
|
|
||||||
|
def extract_actuals_from_batch_reports(self, batch_reports_result: str) -> dict:
|
||||||
|
print(f"[DEBUG] extract_actuals 收到原始数据: {batch_reports_result[:500]}...") # 打印前500字符
|
||||||
|
try:
|
||||||
|
obj = json.loads(batch_reports_result) if isinstance(batch_reports_result, str) else batch_reports_result
|
||||||
|
if isinstance(obj, dict) and "return_info" in obj:
|
||||||
|
inner = obj["return_info"]
|
||||||
|
obj = json.loads(inner) if isinstance(inner, str) else inner
|
||||||
|
reports = obj.get("reports", []) if isinstance(obj, dict) else []
|
||||||
|
print(f"[DEBUG] 解析后的 reports 数组长度: {len(reports)}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DEBUG] 解析异常: {e}")
|
||||||
|
reports = []
|
||||||
|
|
||||||
|
actuals = []
|
||||||
|
for i, r in enumerate(reports):
|
||||||
|
print(f"[DEBUG] 处理 report[{i}]: order_code={r.get('order_code')}, has_extracted={r.get('extracted') is not None}, has_report={r.get('report') is not None}")
|
||||||
|
order_code = r.get("order_code")
|
||||||
|
order_id = r.get("order_id")
|
||||||
|
ex = r.get("extracted")
|
||||||
|
if isinstance(ex, dict) and (ex.get("actualTargetWeigh") is not None or ex.get("actualVolume") is not None):
|
||||||
|
print(f"[DEBUG] 从 extracted 字段提取: actualTargetWeigh={ex.get('actualTargetWeigh')}, actualVolume={ex.get('actualVolume')}")
|
||||||
|
actuals.append({
|
||||||
|
"order_code": order_code,
|
||||||
|
"order_id": order_id,
|
||||||
|
"actualTargetWeigh": ex.get("actualTargetWeigh"),
|
||||||
|
"actualVolume": ex.get("actualVolume")
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
report = r.get("report")
|
||||||
|
vals = self._extract_actuals_from_report(report) if report else {"actualTargetWeigh": None, "actualVolume": None}
|
||||||
|
print(f"[DEBUG] 从 report 字段提取: {vals}")
|
||||||
|
actuals.append({
|
||||||
|
"order_code": order_code,
|
||||||
|
"order_id": order_id,
|
||||||
|
**vals
|
||||||
|
})
|
||||||
|
|
||||||
|
print(f"[DEBUG] 最终提取的 actuals 数组长度: {len(actuals)}")
|
||||||
|
result = {
|
||||||
|
"return_info": json.dumps({"actuals": actuals}, ensure_ascii=False)
|
||||||
|
}
|
||||||
|
print(f"[DEBUG] 返回结果: {result}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
def process_temperature_cutoff_report(self, report_request) -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
data = report_request.data
|
||||||
|
def _f(v):
|
||||||
|
try:
|
||||||
|
return float(v)
|
||||||
|
except Exception:
|
||||||
|
return 0.0
|
||||||
|
self.target_temperature = _f(data.get("targetTemperature"))
|
||||||
|
self.setting_temperature = _f(data.get("settingTemperature"))
|
||||||
|
self.in_temperature = _f(data.get("inTemperature"))
|
||||||
|
self.out_temperature = _f(data.get("outTemperature"))
|
||||||
|
self.pt100_temperature = _f(data.get("pt100Temperature"))
|
||||||
|
self.sensor_average_temperature = _f(data.get("sensorAverageTemperature"))
|
||||||
|
self.speed = _f(data.get("speed"))
|
||||||
|
self.force = _f(data.get("force"))
|
||||||
|
self.viscosity = _f(data.get("viscosity"))
|
||||||
|
self.average_viscosity = _f(data.get("averageViscosity"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
if hasattr(self, "_ros_node") and self._ros_node is not None:
|
||||||
|
props = [
|
||||||
|
"in_temperature","out_temperature","pt100_temperature","sensor_average_temperature",
|
||||||
|
"target_temperature","setting_temperature","viscosity","average_viscosity",
|
||||||
|
"speed","force"
|
||||||
|
]
|
||||||
|
for name in props:
|
||||||
|
pub = self._ros_node._property_publishers.get(name)
|
||||||
|
if pub:
|
||||||
|
pub.publish_property()
|
||||||
|
frame = data.get("frameCode")
|
||||||
|
reactor_id = None
|
||||||
|
try:
|
||||||
|
reactor_id = self._frame_to_reactor_id.get(int(frame))
|
||||||
|
except Exception:
|
||||||
|
reactor_id = None
|
||||||
|
if reactor_id and hasattr(self._ros_node, "sub_devices"):
|
||||||
|
child = self._ros_node.sub_devices.get(reactor_id)
|
||||||
|
if child and hasattr(child, "driver_instance"):
|
||||||
|
child.driver_instance.update_metrics(data)
|
||||||
|
pubs = getattr(child.ros_node_instance, "_property_publishers", {})
|
||||||
|
for name in props:
|
||||||
|
p = pubs.get(name)
|
||||||
|
if p:
|
||||||
|
p.publish_property()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
event = {
|
||||||
|
"frameCode": data.get("frameCode"),
|
||||||
|
"generateTime": data.get("generateTime"),
|
||||||
|
"targetTemperature": data.get("targetTemperature"),
|
||||||
|
"settingTemperature": data.get("settingTemperature"),
|
||||||
|
"inTemperature": data.get("inTemperature"),
|
||||||
|
"outTemperature": data.get("outTemperature"),
|
||||||
|
"pt100Temperature": data.get("pt100Temperature"),
|
||||||
|
"sensorAverageTemperature": data.get("sensorAverageTemperature"),
|
||||||
|
"speed": data.get("speed"),
|
||||||
|
"force": data.get("force"),
|
||||||
|
"viscosity": data.get("viscosity"),
|
||||||
|
"averageViscosity": data.get("averageViscosity"),
|
||||||
|
"request_time": report_request.request_time,
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
"reactor_id": self._frame_to_reactor_id.get(int(data.get("frameCode", 0))) if str(data.get("frameCode", "")).isdigit() else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
base_dir = Path(__file__).resolve().parents[3] / "unilabos_data"
|
||||||
|
base_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
out_file = base_dir / "temperature_cutoff_events.json"
|
||||||
|
try:
|
||||||
|
existing = json.loads(out_file.read_text(encoding="utf-8")) if out_file.exists() else []
|
||||||
|
if not isinstance(existing, list):
|
||||||
|
existing = []
|
||||||
|
except Exception:
|
||||||
|
existing = []
|
||||||
|
existing.append(event)
|
||||||
|
out_file.write_text(json.dumps(existing, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
|
||||||
|
if hasattr(self, "_ros_node") and self._ros_node is not None:
|
||||||
|
ns = self._ros_node.namespace
|
||||||
|
topics = {
|
||||||
|
"targetTemperature": f"{ns}/metrics/temperature_cutoff/target_temperature",
|
||||||
|
"settingTemperature": f"{ns}/metrics/temperature_cutoff/setting_temperature",
|
||||||
|
"inTemperature": f"{ns}/metrics/temperature_cutoff/in_temperature",
|
||||||
|
"outTemperature": f"{ns}/metrics/temperature_cutoff/out_temperature",
|
||||||
|
"pt100Temperature": f"{ns}/metrics/temperature_cutoff/pt100_temperature",
|
||||||
|
"sensorAverageTemperature": f"{ns}/metrics/temperature_cutoff/sensor_average_temperature",
|
||||||
|
"speed": f"{ns}/metrics/temperature_cutoff/speed",
|
||||||
|
"force": f"{ns}/metrics/temperature_cutoff/force",
|
||||||
|
"viscosity": f"{ns}/metrics/temperature_cutoff/viscosity",
|
||||||
|
"averageViscosity": f"{ns}/metrics/temperature_cutoff/average_viscosity",
|
||||||
|
}
|
||||||
|
for k, t in topics.items():
|
||||||
|
v = data.get(k)
|
||||||
|
if v is not None:
|
||||||
|
pub = self._ros_node.create_publisher(Float64, t, 10)
|
||||||
|
pub.publish(convert_to_ros_msg(Float64, float(v)))
|
||||||
|
|
||||||
|
evt_pub = self._ros_node.create_publisher(String, f"{ns}/events/temperature_cutoff", 10)
|
||||||
|
evt_pub.publish(convert_to_ros_msg(String, json.dumps(event, ensure_ascii=False)))
|
||||||
|
|
||||||
|
return {"processed": True, "frame": data.get("frameCode")}
|
||||||
|
except Exception as e:
|
||||||
|
return {"processed": False, "error": str(e)}
|
||||||
|
|
||||||
|
def wait_for_multiple_orders_and_get_reports(self, batch_create_result: str = None, timeout: int = 7200, check_interval: int = 10) -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
timeout = int(timeout) if timeout else 7200
|
||||||
|
check_interval = int(check_interval) if check_interval else 10
|
||||||
|
if not batch_create_result or batch_create_result == "":
|
||||||
|
raise ValueError("batch_create_result为空")
|
||||||
|
try:
|
||||||
|
if isinstance(batch_create_result, str) and '[...]' in batch_create_result:
|
||||||
|
batch_create_result = batch_create_result.replace('[...]', '[]')
|
||||||
|
result_obj = json.loads(batch_create_result) if isinstance(batch_create_result, str) else batch_create_result
|
||||||
|
if isinstance(result_obj, dict) and "return_value" in result_obj:
|
||||||
|
inner = result_obj.get("return_value")
|
||||||
|
if isinstance(inner, str):
|
||||||
|
result_obj = json.loads(inner)
|
||||||
|
elif isinstance(inner, dict):
|
||||||
|
result_obj = inner
|
||||||
|
order_codes = result_obj.get("order_codes", [])
|
||||||
|
order_ids = result_obj.get("order_ids", [])
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(f"解析batch_create_result失败: {e}")
|
||||||
|
if not order_codes or not order_ids:
|
||||||
|
raise ValueError("缺少order_codes或order_ids")
|
||||||
|
if not isinstance(order_codes, list):
|
||||||
|
order_codes = [order_codes]
|
||||||
|
if not isinstance(order_ids, list):
|
||||||
|
order_ids = [order_ids]
|
||||||
|
if len(order_codes) != len(order_ids):
|
||||||
|
raise ValueError("order_codes与order_ids数量不匹配")
|
||||||
|
total = len(order_codes)
|
||||||
|
pending = {c: {"order_id": order_ids[i], "completed": False} for i, c in enumerate(order_codes)}
|
||||||
|
reports = []
|
||||||
|
start_time = time.time()
|
||||||
|
while pending:
|
||||||
|
elapsed_time = time.time() - start_time
|
||||||
|
if elapsed_time > timeout:
|
||||||
|
for oc in list(pending.keys()):
|
||||||
|
reports.append({
|
||||||
|
"order_code": oc,
|
||||||
|
"order_id": pending[oc]["order_id"],
|
||||||
|
"status": "timeout",
|
||||||
|
"completion_status": None,
|
||||||
|
"report": None,
|
||||||
|
"extracted": None,
|
||||||
|
"elapsed_time": elapsed_time
|
||||||
|
})
|
||||||
|
break
|
||||||
|
completed_round = []
|
||||||
|
for oc in list(pending.keys()):
|
||||||
|
oid = pending[oc]["order_id"]
|
||||||
|
if oc in self.order_completion_status:
|
||||||
|
info = self.order_completion_status[oc]
|
||||||
|
try:
|
||||||
|
rep = self.hardware_interface.order_report(oid)
|
||||||
|
if not rep:
|
||||||
|
rep = {"error": "无法获取报告"}
|
||||||
|
reports.append({
|
||||||
|
"order_code": oc,
|
||||||
|
"order_id": oid,
|
||||||
|
"status": "completed",
|
||||||
|
"completion_status": info.get('status'),
|
||||||
|
"report": rep,
|
||||||
|
"extracted": self._extract_actuals_from_report(rep),
|
||||||
|
"elapsed_time": elapsed_time
|
||||||
|
})
|
||||||
|
completed_round.append(oc)
|
||||||
|
del self.order_completion_status[oc]
|
||||||
|
except Exception as e:
|
||||||
|
reports.append({
|
||||||
|
"order_code": oc,
|
||||||
|
"order_id": oid,
|
||||||
|
"status": "error",
|
||||||
|
"completion_status": info.get('status') if 'info' in locals() else None,
|
||||||
|
"report": None,
|
||||||
|
"extracted": None,
|
||||||
|
"error": str(e),
|
||||||
|
"elapsed_time": elapsed_time
|
||||||
|
})
|
||||||
|
completed_round.append(oc)
|
||||||
|
for oc in completed_round:
|
||||||
|
del pending[oc]
|
||||||
|
if pending:
|
||||||
|
time.sleep(check_interval)
|
||||||
|
completed_count = sum(1 for r in reports if r['status'] == 'completed')
|
||||||
|
timeout_count = sum(1 for r in reports if r['status'] == 'timeout')
|
||||||
|
error_count = sum(1 for r in reports if r['status'] == 'error')
|
||||||
|
final_elapsed_time = time.time() - start_time
|
||||||
|
summary = {
|
||||||
|
"total": total,
|
||||||
|
"completed": completed_count,
|
||||||
|
"timeout": timeout_count,
|
||||||
|
"error": error_count,
|
||||||
|
"elapsed_time": round(final_elapsed_time, 2),
|
||||||
|
"reports": reports
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"return_info": json.dumps(summary, ensure_ascii=False)
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise
|
||||||
|
|
||||||
def liquid_feeding_beaker(
|
def liquid_feeding_beaker(
|
||||||
self,
|
self,
|
||||||
volume: str = "35000",
|
volume: str = "350",
|
||||||
assign_material_name: str = "BAPP",
|
assign_material_name: str = "BAPP",
|
||||||
time: str = "0",
|
time: str = "0",
|
||||||
torque_variation: int = 1,
|
torque_variation: int = 1,
|
||||||
@@ -355,7 +778,7 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
"""液体进料烧杯
|
"""液体进料烧杯
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
volume: 分液量(μL)
|
volume: 分液质量(g)
|
||||||
assign_material_name: 物料名称(试剂瓶位)
|
assign_material_name: 物料名称(试剂瓶位)
|
||||||
time: 观察时间(分钟)
|
time: 观察时间(分钟)
|
||||||
torque_variation: 是否观察(int类型, 1=否, 2=是)
|
torque_variation: 是否观察(int类型, 1=否, 2=是)
|
||||||
@@ -489,6 +912,106 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
"""
|
"""
|
||||||
return self.hardware_interface.create_order(json_str)
|
return self.hardware_interface.create_order(json_str)
|
||||||
|
|
||||||
|
def hard_delete_merged_workflows(self, workflow_ids: List[str]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
调用新接口:硬删除合并后的工作流
|
||||||
|
|
||||||
|
Args:
|
||||||
|
workflow_ids: 要删除的工作流ID数组
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
删除结果
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not isinstance(workflow_ids, list):
|
||||||
|
raise ValueError("workflow_ids必须是字符串数组")
|
||||||
|
return self._delete_project_api("/api/lims/order/workflows", workflow_ids)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 硬删除异常: {str(e)}")
|
||||||
|
return {"code": 0, "message": str(e), "timestamp": int(time.time())}
|
||||||
|
|
||||||
|
# ==================== 项目接口通用方法 ====================
|
||||||
|
|
||||||
|
def _post_project_api(self, endpoint: str, data: Any) -> Dict[str, Any]:
|
||||||
|
"""项目接口通用POST调用
|
||||||
|
|
||||||
|
参数:
|
||||||
|
endpoint: 接口路径(例如 /api/lims/order/skip-titration-steps)
|
||||||
|
data: 请求体中的 data 字段内容
|
||||||
|
|
||||||
|
返回:
|
||||||
|
dict: 服务端响应,失败时返回 {code:0,message,...}
|
||||||
|
"""
|
||||||
|
request_data = {
|
||||||
|
"apiKey": API_CONFIG["api_key"],
|
||||||
|
"requestTime": self.hardware_interface.get_current_time_iso8601(),
|
||||||
|
"data": data
|
||||||
|
}
|
||||||
|
print(f"\n📤 项目POST请求: {self.hardware_interface.host}{endpoint}")
|
||||||
|
print(json.dumps(request_data, indent=4, ensure_ascii=False))
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
f"{self.hardware_interface.host}{endpoint}",
|
||||||
|
json=request_data,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
result = response.json()
|
||||||
|
if result.get("code") == 1:
|
||||||
|
print("✅ 请求成功")
|
||||||
|
else:
|
||||||
|
print(f"❌ 请求失败: {result.get('message','未知错误')}")
|
||||||
|
return result
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print("❌ 非JSON响应")
|
||||||
|
return {"code": 0, "message": "非JSON响应", "timestamp": int(time.time())}
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
print("❌ 请求超时")
|
||||||
|
return {"code": 0, "message": "请求超时", "timestamp": int(time.time())}
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"❌ 网络异常: {str(e)}")
|
||||||
|
return {"code": 0, "message": str(e), "timestamp": int(time.time())}
|
||||||
|
|
||||||
|
def _delete_project_api(self, endpoint: str, data: Any) -> Dict[str, Any]:
|
||||||
|
"""项目接口通用DELETE调用
|
||||||
|
|
||||||
|
参数:
|
||||||
|
endpoint: 接口路径(例如 /api/lims/order/workflows)
|
||||||
|
data: 请求体中的 data 字段内容
|
||||||
|
|
||||||
|
返回:
|
||||||
|
dict: 服务端响应,失败时返回 {code:0,message,...}
|
||||||
|
"""
|
||||||
|
request_data = {
|
||||||
|
"apiKey": API_CONFIG["api_key"],
|
||||||
|
"requestTime": self.hardware_interface.get_current_time_iso8601(),
|
||||||
|
"data": data
|
||||||
|
}
|
||||||
|
print(f"\n📤 项目DELETE请求: {self.hardware_interface.host}{endpoint}")
|
||||||
|
print(json.dumps(request_data, indent=4, ensure_ascii=False))
|
||||||
|
try:
|
||||||
|
response = requests.delete(
|
||||||
|
f"{self.hardware_interface.host}{endpoint}",
|
||||||
|
json=request_data,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
result = response.json()
|
||||||
|
if result.get("code") == 1:
|
||||||
|
print("✅ 请求成功")
|
||||||
|
else:
|
||||||
|
print(f"❌ 请求失败: {result.get('message','未知错误')}")
|
||||||
|
return result
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print("❌ 非JSON响应")
|
||||||
|
return {"code": 0, "message": "非JSON响应", "timestamp": int(time.time())}
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
print("❌ 请求超时")
|
||||||
|
return {"code": 0, "message": "请求超时", "timestamp": int(time.time())}
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"❌ 网络异常: {str(e)}")
|
||||||
|
return {"code": 0, "message": str(e), "timestamp": int(time.time())}
|
||||||
|
|
||||||
# ==================== 工作流执行核心方法 ====================
|
# ==================== 工作流执行核心方法 ====================
|
||||||
|
|
||||||
def process_web_workflows(self, web_workflow_json: str) -> List[Dict[str, str]]:
|
def process_web_workflows(self, web_workflow_json: str) -> List[Dict[str, str]]:
|
||||||
@@ -519,69 +1042,6 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
print(f"错误:处理工作流失败: {e}")
|
print(f"错误:处理工作流失败: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def process_and_execute_workflow(self, workflow_name: str, task_name: str) -> dict:
|
|
||||||
"""
|
|
||||||
一站式处理工作流程:解析网页工作流列表,合并工作流(带参数),然后发布任务
|
|
||||||
|
|
||||||
Args:
|
|
||||||
workflow_name: 合并后的工作流名称
|
|
||||||
task_name: 任务名称
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
任务创建结果
|
|
||||||
"""
|
|
||||||
web_workflow_list = self.get_workflow_sequence()
|
|
||||||
print(f"\n{'='*60}")
|
|
||||||
print(f"📋 处理网页工作流列表: {web_workflow_list}")
|
|
||||||
print(f"{'='*60}")
|
|
||||||
|
|
||||||
web_workflow_json = json.dumps({"web_workflow_list": web_workflow_list})
|
|
||||||
workflows_result = self.process_web_workflows(web_workflow_json)
|
|
||||||
|
|
||||||
if not workflows_result:
|
|
||||||
return self._create_error_result("处理网页工作流列表失败", "process_web_workflows")
|
|
||||||
|
|
||||||
print(f"workflows_result 类型: {type(workflows_result)}")
|
|
||||||
print(f"workflows_result 内容: {workflows_result}")
|
|
||||||
|
|
||||||
workflows_with_params = self._build_workflows_with_parameters(workflows_result)
|
|
||||||
|
|
||||||
merge_data = {
|
|
||||||
"name": workflow_name,
|
|
||||||
"workflows": workflows_with_params
|
|
||||||
}
|
|
||||||
|
|
||||||
# print(f"\n🔄 合并工作流(带参数),名称: {workflow_name}")
|
|
||||||
merged_workflow = self.merge_workflow_with_parameters(json.dumps(merge_data))
|
|
||||||
|
|
||||||
if not merged_workflow:
|
|
||||||
return self._create_error_result("合并工作流失败", "merge_workflow_with_parameters")
|
|
||||||
|
|
||||||
workflow_id = merged_workflow.get("subWorkflows", [{}])[0].get("id", "")
|
|
||||||
# print(f"\n📤 使用工作流创建任务: {workflow_name} (ID: {workflow_id})")
|
|
||||||
|
|
||||||
order_params = [{
|
|
||||||
"orderCode": f"task_{self.hardware_interface.get_current_time_iso8601()}",
|
|
||||||
"orderName": task_name,
|
|
||||||
"workFlowId": workflow_id,
|
|
||||||
"borderNumber": 1,
|
|
||||||
"paramValues": {}
|
|
||||||
}]
|
|
||||||
|
|
||||||
result = self.create_order(json.dumps(order_params))
|
|
||||||
|
|
||||||
if not result:
|
|
||||||
return self._create_error_result("创建任务失败", "create_order")
|
|
||||||
|
|
||||||
# 清空工作流序列和参数,防止下次执行时累积重复
|
|
||||||
self.pending_task_params = []
|
|
||||||
self.clear_workflows() # 清空工作流序列,避免重复累积
|
|
||||||
|
|
||||||
# print(f"\n✅ 任务创建成功: {result}")
|
|
||||||
# print(f"\n✅ 任务创建成功")
|
|
||||||
print(f"{'='*60}\n")
|
|
||||||
return json.dumps({"success": True, "result": result})
|
|
||||||
|
|
||||||
def _build_workflows_with_parameters(self, workflows_result: list) -> list:
|
def _build_workflows_with_parameters(self, workflows_result: list) -> list:
|
||||||
"""
|
"""
|
||||||
构建带参数的工作流列表
|
构建带参数的工作流列表
|
||||||
@@ -781,3 +1241,90 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
print(f" ❌ 工作流ID验证失败: {e}")
|
print(f" ❌ 工作流ID验证失败: {e}")
|
||||||
print(f" 💡 将重新合并工作流")
|
print(f" 💡 将重新合并工作流")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def process_and_execute_workflow(self, workflow_name: str, task_name: str) -> dict:
|
||||||
|
"""
|
||||||
|
一站式处理工作流程:解析网页工作流列表,合并工作流(带参数),然后发布任务
|
||||||
|
|
||||||
|
Args:
|
||||||
|
workflow_name: 合并后的工作流名称
|
||||||
|
task_name: 任务名称
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
任务创建结果
|
||||||
|
"""
|
||||||
|
web_workflow_list = self.get_workflow_sequence()
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"📋 处理网页工作流列表: {web_workflow_list}")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
|
||||||
|
web_workflow_json = json.dumps({"web_workflow_list": web_workflow_list})
|
||||||
|
workflows_result = self.process_web_workflows(web_workflow_json)
|
||||||
|
|
||||||
|
if not workflows_result:
|
||||||
|
return self._create_error_result("处理网页工作流列表失败", "process_web_workflows")
|
||||||
|
|
||||||
|
print(f"workflows_result 类型: {type(workflows_result)}")
|
||||||
|
print(f"workflows_result 内容: {workflows_result}")
|
||||||
|
|
||||||
|
workflows_with_params = self._build_workflows_with_parameters(workflows_result)
|
||||||
|
|
||||||
|
merge_data = {
|
||||||
|
"name": workflow_name,
|
||||||
|
"workflows": workflows_with_params
|
||||||
|
}
|
||||||
|
|
||||||
|
# print(f"\n🔄 合并工作流(带参数),名称: {workflow_name}")
|
||||||
|
merged_workflow = self.merge_workflow_with_parameters(json.dumps(merge_data))
|
||||||
|
|
||||||
|
if not merged_workflow:
|
||||||
|
return self._create_error_result("合并工作流失败", "merge_workflow_with_parameters")
|
||||||
|
|
||||||
|
workflow_id = merged_workflow.get("subWorkflows", [{}])[0].get("id", "")
|
||||||
|
# print(f"\n📤 使用工作流创建任务: {workflow_name} (ID: {workflow_id})")
|
||||||
|
|
||||||
|
order_params = [{
|
||||||
|
"orderCode": f"task_{self.hardware_interface.get_current_time_iso8601()}",
|
||||||
|
"orderName": task_name,
|
||||||
|
"workFlowId": workflow_id,
|
||||||
|
"borderNumber": 1,
|
||||||
|
"paramValues": {}
|
||||||
|
}]
|
||||||
|
|
||||||
|
result = self.create_order(json.dumps(order_params))
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
return self._create_error_result("创建任务失败", "create_order")
|
||||||
|
|
||||||
|
# 清空工作流序列和参数,防止下次执行时累积重复
|
||||||
|
self.pending_task_params = []
|
||||||
|
self.clear_workflows() # 清空工作流序列,避免重复累积
|
||||||
|
|
||||||
|
# print(f"\n✅ 任务创建成功: {result}")
|
||||||
|
# print(f"\n✅ 任务创建成功")
|
||||||
|
print(f"{'='*60}\n")
|
||||||
|
|
||||||
|
# 返回结果,包含合并后的工作流数据和订单参数
|
||||||
|
return json.dumps({
|
||||||
|
"success": True,
|
||||||
|
"result": result,
|
||||||
|
"merged_workflow": merged_workflow,
|
||||||
|
"order_params": order_params
|
||||||
|
})
|
||||||
|
|
||||||
|
# ==================== 反应器操作接口 ====================
|
||||||
|
|
||||||
|
def skip_titration_steps(self, preintake_id: str) -> Dict[str, Any]:
|
||||||
|
"""跳过当前正在进行的滴定步骤
|
||||||
|
|
||||||
|
Args:
|
||||||
|
preintake_id: 通量ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: 服务器响应,包含状态码、消息和时间戳
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self._post_project_api("/api/lims/order/skip-titration-steps", preintake_id)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 跳过滴定异常: {str(e)}")
|
||||||
|
return {"code": 0, "message": str(e), "timestamp": int(time.time())}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,639 +0,0 @@
|
|||||||
"""
|
|
||||||
纽扣电池组装工作站物料类定义
|
|
||||||
Button Battery Assembly Station Resource Classes
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from collections import OrderedDict
|
|
||||||
from typing import Any, Dict, List, Optional, TypedDict, Union, cast
|
|
||||||
|
|
||||||
from pylabrobot.resources.coordinate import Coordinate
|
|
||||||
from pylabrobot.resources.container import Container
|
|
||||||
from pylabrobot.resources.deck import Deck
|
|
||||||
from pylabrobot.resources.itemized_resource import ItemizedResource
|
|
||||||
from pylabrobot.resources.resource import Resource
|
|
||||||
from pylabrobot.resources.resource_stack import ResourceStack
|
|
||||||
from pylabrobot.resources.tip_rack import TipRack, TipSpot
|
|
||||||
from pylabrobot.resources.trash import Trash
|
|
||||||
from pylabrobot.resources.utils import create_ordered_items_2d
|
|
||||||
|
|
||||||
from unilabos.resources.battery.magazine import MagazineHolder_4_Cathode, MagazineHolder_6_Cathode, MagazineHolder_6_Anode, MagazineHolder_6_Battery
|
|
||||||
from unilabos.resources.battery.bottle_carriers import YIHUA_Electrolyte_12VialCarrier
|
|
||||||
from unilabos.resources.battery.electrode_sheet import ElectrodeSheet
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: 这个应该只能放一个极片
|
|
||||||
class MaterialHoleState(TypedDict):
|
|
||||||
diameter: int
|
|
||||||
depth: int
|
|
||||||
max_sheets: int
|
|
||||||
info: Optional[str] # 附加信息
|
|
||||||
|
|
||||||
class MaterialHole(Resource):
|
|
||||||
"""料板洞位类"""
|
|
||||||
children: List[ElectrodeSheet] = []
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
size_x: float,
|
|
||||||
size_y: float,
|
|
||||||
size_z: float,
|
|
||||||
category: str = "material_hole",
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
super().__init__(
|
|
||||||
name=name,
|
|
||||||
size_x=size_x,
|
|
||||||
size_y=size_y,
|
|
||||||
size_z=size_z,
|
|
||||||
category=category,
|
|
||||||
)
|
|
||||||
self._unilabos_state: MaterialHoleState = MaterialHoleState(
|
|
||||||
diameter=20,
|
|
||||||
depth=10,
|
|
||||||
max_sheets=1,
|
|
||||||
info=None
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_all_sheet_info(self):
|
|
||||||
info_list = []
|
|
||||||
for sheet in self.children:
|
|
||||||
info_list.append(sheet._unilabos_state["info"])
|
|
||||||
return info_list
|
|
||||||
|
|
||||||
#这个函数函数好像没用,一般不会集中赋值质量
|
|
||||||
def set_all_sheet_mass(self):
|
|
||||||
for sheet in self.children:
|
|
||||||
sheet._unilabos_state["mass"] = 0.5 # 示例:设置质量为0.5g
|
|
||||||
|
|
||||||
def load_state(self, state: Dict[str, Any]) -> None:
|
|
||||||
"""格式不变"""
|
|
||||||
super().load_state(state)
|
|
||||||
self._unilabos_state = state
|
|
||||||
|
|
||||||
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
|
||||||
"""格式不变"""
|
|
||||||
data = super().serialize_state()
|
|
||||||
data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等)
|
|
||||||
return data
|
|
||||||
#移动极片前先取出对象
|
|
||||||
def get_sheet_with_name(self, name: str) -> Optional[ElectrodeSheet]:
|
|
||||||
for sheet in self.children:
|
|
||||||
if sheet.name == name:
|
|
||||||
return sheet
|
|
||||||
return None
|
|
||||||
|
|
||||||
def has_electrode_sheet(self) -> bool:
|
|
||||||
"""检查洞位是否有极片"""
|
|
||||||
return len(self.children) > 0
|
|
||||||
|
|
||||||
def assign_child_resource(
|
|
||||||
self,
|
|
||||||
resource: ElectrodeSheet,
|
|
||||||
location: Optional[Coordinate],
|
|
||||||
reassign: bool = True,
|
|
||||||
):
|
|
||||||
"""放置极片"""
|
|
||||||
# TODO: 这里要改,diameter找不到,加入._unilabos_state后应该没问题
|
|
||||||
#if resource._unilabos_state["diameter"] > self._unilabos_state["diameter"]:
|
|
||||||
# raise ValueError(f"极片直径 {resource._unilabos_state['diameter']} 超过洞位直径 {self._unilabos_state['diameter']}")
|
|
||||||
#if len(self.children) >= self._unilabos_state["max_sheets"]:
|
|
||||||
# raise ValueError(f"洞位已满,无法放置更多极片")
|
|
||||||
super().assign_child_resource(resource, location, reassign)
|
|
||||||
|
|
||||||
# 根据children的编号取物料对象。
|
|
||||||
def get_electrode_sheet_info(self, index: int) -> ElectrodeSheet:
|
|
||||||
return self.children[index]
|
|
||||||
|
|
||||||
|
|
||||||
class MaterialPlateState(TypedDict):
|
|
||||||
hole_spacing_x: float
|
|
||||||
hole_spacing_y: float
|
|
||||||
hole_diameter: float
|
|
||||||
info: Optional[str] # 附加信息
|
|
||||||
|
|
||||||
class MaterialPlate(ItemizedResource[MaterialHole]):
|
|
||||||
"""料板类 - 4x4个洞位,每个洞位放1个极片"""
|
|
||||||
|
|
||||||
children: List[MaterialHole]
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
size_x: float,
|
|
||||||
size_y: float,
|
|
||||||
size_z: float,
|
|
||||||
ordered_items: Optional[Dict[str, MaterialHole]] = None,
|
|
||||||
ordering: Optional[OrderedDict[str, str]] = None,
|
|
||||||
category: str = "material_plate",
|
|
||||||
model: Optional[str] = None,
|
|
||||||
fill: bool = False
|
|
||||||
):
|
|
||||||
"""初始化料板
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: 料板名称
|
|
||||||
size_x: 长度 (mm)
|
|
||||||
size_y: 宽度 (mm)
|
|
||||||
size_z: 高度 (mm)
|
|
||||||
hole_diameter: 洞直径 (mm)
|
|
||||||
hole_depth: 洞深度 (mm)
|
|
||||||
hole_spacing_x: X方向洞位间距 (mm)
|
|
||||||
hole_spacing_y: Y方向洞位间距 (mm)
|
|
||||||
number: 编号
|
|
||||||
category: 类别
|
|
||||||
model: 型号
|
|
||||||
"""
|
|
||||||
self._unilabos_state: MaterialPlateState = MaterialPlateState(
|
|
||||||
hole_spacing_x=24.0,
|
|
||||||
hole_spacing_y=24.0,
|
|
||||||
hole_diameter=20.0,
|
|
||||||
info="",
|
|
||||||
)
|
|
||||||
# 创建4x4的洞位
|
|
||||||
# TODO: 这里要改,对应不同形状
|
|
||||||
holes = create_ordered_items_2d(
|
|
||||||
klass=MaterialHole,
|
|
||||||
num_items_x=4,
|
|
||||||
num_items_y=4,
|
|
||||||
dx=(size_x - 4 * self._unilabos_state["hole_spacing_x"]) / 2, # 居中
|
|
||||||
dy=(size_y - 4 * self._unilabos_state["hole_spacing_y"]) / 2, # 居中
|
|
||||||
dz=size_z,
|
|
||||||
item_dx=self._unilabos_state["hole_spacing_x"],
|
|
||||||
item_dy=self._unilabos_state["hole_spacing_y"],
|
|
||||||
size_x = 16,
|
|
||||||
size_y = 16,
|
|
||||||
size_z = 16,
|
|
||||||
)
|
|
||||||
if fill:
|
|
||||||
super().__init__(
|
|
||||||
name=name,
|
|
||||||
size_x=size_x,
|
|
||||||
size_y=size_y,
|
|
||||||
size_z=size_z,
|
|
||||||
ordered_items=holes,
|
|
||||||
category=category,
|
|
||||||
model=model,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
super().__init__(
|
|
||||||
name=name,
|
|
||||||
size_x=size_x,
|
|
||||||
size_y=size_y,
|
|
||||||
size_z=size_z,
|
|
||||||
ordered_items=ordered_items,
|
|
||||||
ordering=ordering,
|
|
||||||
category=category,
|
|
||||||
model=model,
|
|
||||||
)
|
|
||||||
|
|
||||||
def update_locations(self):
|
|
||||||
# TODO:调多次相加
|
|
||||||
holes = create_ordered_items_2d(
|
|
||||||
klass=MaterialHole,
|
|
||||||
num_items_x=4,
|
|
||||||
num_items_y=4,
|
|
||||||
dx=(self._size_x - 3 * self._unilabos_state["hole_spacing_x"]) / 2, # 居中
|
|
||||||
dy=(self._size_y - 3 * self._unilabos_state["hole_spacing_y"]) / 2, # 居中
|
|
||||||
dz=self._size_z,
|
|
||||||
item_dx=self._unilabos_state["hole_spacing_x"],
|
|
||||||
item_dy=self._unilabos_state["hole_spacing_y"],
|
|
||||||
size_x = 1,
|
|
||||||
size_y = 1,
|
|
||||||
size_z = 1,
|
|
||||||
)
|
|
||||||
for item, original_item in zip(holes.items(), self.children):
|
|
||||||
original_item.location = item[1].location
|
|
||||||
|
|
||||||
|
|
||||||
class PlateSlot(ResourceStack):
|
|
||||||
"""板槽位类 - 1个槽上能堆放8个板,移板只能操作最上方的板"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
size_x: float,
|
|
||||||
size_y: float,
|
|
||||||
size_z: float,
|
|
||||||
max_plates: int = 8,
|
|
||||||
category: str = "plate_slot",
|
|
||||||
model: Optional[str] = None
|
|
||||||
):
|
|
||||||
"""初始化板槽位
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: 槽位名称
|
|
||||||
max_plates: 最大板数量
|
|
||||||
category: 类别
|
|
||||||
"""
|
|
||||||
super().__init__(
|
|
||||||
name=name,
|
|
||||||
direction="z", # Z方向堆叠
|
|
||||||
resources=[],
|
|
||||||
)
|
|
||||||
self.max_plates = max_plates
|
|
||||||
self.category = category
|
|
||||||
|
|
||||||
def can_add_plate(self) -> bool:
|
|
||||||
"""检查是否可以添加板"""
|
|
||||||
return len(self.children) < self.max_plates
|
|
||||||
|
|
||||||
def add_plate(self, plate: MaterialPlate) -> None:
|
|
||||||
"""添加料板"""
|
|
||||||
if not self.can_add_plate():
|
|
||||||
raise ValueError(f"槽位 {self.name} 已满,无法添加更多板")
|
|
||||||
self.assign_child_resource(plate)
|
|
||||||
|
|
||||||
def get_top_plate(self) -> MaterialPlate:
|
|
||||||
"""获取最上方的板"""
|
|
||||||
if len(self.children) == 0:
|
|
||||||
raise ValueError(f"槽位 {self.name} 为空")
|
|
||||||
return cast(MaterialPlate, self.get_top_item())
|
|
||||||
|
|
||||||
def take_top_plate(self) -> MaterialPlate:
|
|
||||||
"""取出最上方的板"""
|
|
||||||
top_plate = self.get_top_plate()
|
|
||||||
self.unassign_child_resource(top_plate)
|
|
||||||
return top_plate
|
|
||||||
|
|
||||||
def can_access_for_picking(self) -> bool:
|
|
||||||
"""检查是否可以进行取料操作(只有最上方的板能进行取料操作)"""
|
|
||||||
return len(self.children) > 0
|
|
||||||
|
|
||||||
def serialize(self) -> dict:
|
|
||||||
return {
|
|
||||||
**super().serialize(),
|
|
||||||
"max_plates": self.max_plates,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#是一种类型注解,不用self
|
|
||||||
class BatteryState(TypedDict):
|
|
||||||
"""电池状态字典"""
|
|
||||||
diameter: float
|
|
||||||
height: float
|
|
||||||
assembly_pressure: float
|
|
||||||
electrolyte_volume: float
|
|
||||||
electrolyte_name: str
|
|
||||||
|
|
||||||
class Battery(Resource):
|
|
||||||
"""电池类 - 可容纳极片"""
|
|
||||||
children: List[ElectrodeSheet] = []
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
size_x=1,
|
|
||||||
size_y=1,
|
|
||||||
size_z=1,
|
|
||||||
category: str = "battery",
|
|
||||||
):
|
|
||||||
"""初始化电池
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: 电池名称
|
|
||||||
diameter: 直径 (mm)
|
|
||||||
height: 高度 (mm)
|
|
||||||
max_volume: 最大容量 (μL)
|
|
||||||
barcode: 二维码编号
|
|
||||||
category: 类别
|
|
||||||
model: 型号
|
|
||||||
"""
|
|
||||||
super().__init__(
|
|
||||||
name=name,
|
|
||||||
size_x=1,
|
|
||||||
size_y=1,
|
|
||||||
size_z=1,
|
|
||||||
category=category,
|
|
||||||
)
|
|
||||||
self._unilabos_state: BatteryState = BatteryState(
|
|
||||||
diameter = 1.0,
|
|
||||||
height = 1.0,
|
|
||||||
assembly_pressure = 1.0,
|
|
||||||
electrolyte_volume = 1.0,
|
|
||||||
electrolyte_name = "DP001"
|
|
||||||
)
|
|
||||||
|
|
||||||
def add_electrolyte_with_bottle(self, bottle: Bottle) -> bool:
|
|
||||||
to_add_name = bottle._unilabos_state["electrolyte_name"]
|
|
||||||
if bottle.aspirate_electrolyte(10):
|
|
||||||
if self.add_electrolyte(to_add_name, 10):
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
bottle._unilabos_state["electrolyte_volume"] += 10
|
|
||||||
|
|
||||||
def set_electrolyte(self, name: str, volume: float) -> None:
|
|
||||||
"""设置电解液信息"""
|
|
||||||
self._unilabos_state["electrolyte_name"] = name
|
|
||||||
self._unilabos_state["electrolyte_volume"] = volume
|
|
||||||
#这个应该没用,不会有加了后再加的事情
|
|
||||||
def add_electrolyte(self, name: str, volume: float) -> bool:
|
|
||||||
"""添加电解液信息"""
|
|
||||||
if name != self._unilabos_state["electrolyte_name"]:
|
|
||||||
return False
|
|
||||||
self._unilabos_state["electrolyte_volume"] += volume
|
|
||||||
|
|
||||||
def load_state(self, state: Dict[str, Any]) -> None:
|
|
||||||
"""格式不变"""
|
|
||||||
super().load_state(state)
|
|
||||||
self._unilabos_state = state
|
|
||||||
|
|
||||||
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
|
||||||
"""格式不变"""
|
|
||||||
data = super().serialize_state()
|
|
||||||
data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等)
|
|
||||||
return data
|
|
||||||
|
|
||||||
# 电解液作为属性放进去
|
|
||||||
|
|
||||||
class BatteryPressSlotState(TypedDict):
|
|
||||||
"""电池状态字典"""
|
|
||||||
diameter: float =20.0
|
|
||||||
depth: float = 4.0
|
|
||||||
|
|
||||||
class BatteryPressSlot(Resource):
|
|
||||||
"""电池压制槽类 - 设备,可容纳一个电池"""
|
|
||||||
children: List[Battery] = []
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
name: str = "BatteryPressSlot",
|
|
||||||
category: str = "battery_press_slot",
|
|
||||||
):
|
|
||||||
"""初始化电池压制槽
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: 压制槽名称
|
|
||||||
diameter: 直径 (mm)
|
|
||||||
depth: 深度 (mm)
|
|
||||||
category: 类别
|
|
||||||
model: 型号
|
|
||||||
"""
|
|
||||||
super().__init__(
|
|
||||||
name=name,
|
|
||||||
size_x=10,
|
|
||||||
size_y=12,
|
|
||||||
size_z=13,
|
|
||||||
category=category,
|
|
||||||
)
|
|
||||||
self._unilabos_state: BatteryPressSlotState = BatteryPressSlotState()
|
|
||||||
|
|
||||||
def has_battery(self) -> bool:
|
|
||||||
"""检查是否有电池"""
|
|
||||||
return len(self.children) > 0
|
|
||||||
|
|
||||||
def load_state(self, state: Dict[str, Any]) -> None:
|
|
||||||
"""格式不变"""
|
|
||||||
super().load_state(state)
|
|
||||||
self._unilabos_state = state
|
|
||||||
|
|
||||||
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
|
||||||
"""格式不变"""
|
|
||||||
data = super().serialize_state()
|
|
||||||
data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def assign_child_resource(
|
|
||||||
self,
|
|
||||||
resource: Battery,
|
|
||||||
location: Optional[Coordinate],
|
|
||||||
reassign: bool = True,
|
|
||||||
):
|
|
||||||
"""放置极片"""
|
|
||||||
# TODO: 让高京看下槽位只有一个电池时是否这么写。
|
|
||||||
if self.has_battery():
|
|
||||||
raise ValueError(f"槽位已含有一个电池,无法再放置其他电池")
|
|
||||||
super().assign_child_resource(resource, location, reassign)
|
|
||||||
|
|
||||||
# 根据children的编号取物料对象。
|
|
||||||
def get_battery_info(self, index: int) -> Battery:
|
|
||||||
return self.children[0]
|
|
||||||
|
|
||||||
|
|
||||||
def TipBox64(
|
|
||||||
name: str,
|
|
||||||
size_x: float = 127.8,
|
|
||||||
size_y: float = 85.5,
|
|
||||||
size_z: float = 60.0,
|
|
||||||
category: str = "tip_rack",
|
|
||||||
model: Optional[str] = None,
|
|
||||||
):
|
|
||||||
"""64孔枪头盒类"""
|
|
||||||
from pylabrobot.resources.tip import Tip
|
|
||||||
|
|
||||||
# 创建12x8=96个枪头位
|
|
||||||
def make_tip():
|
|
||||||
return Tip(
|
|
||||||
has_filter=False,
|
|
||||||
total_tip_length=20.0,
|
|
||||||
maximal_volume=1000, # 1mL
|
|
||||||
fitting_depth=8.0,
|
|
||||||
)
|
|
||||||
|
|
||||||
tip_spots = create_ordered_items_2d(
|
|
||||||
klass=TipSpot,
|
|
||||||
num_items_x=12,
|
|
||||||
num_items_y=8,
|
|
||||||
dx=8.0,
|
|
||||||
dy=8.0,
|
|
||||||
dz=0.0,
|
|
||||||
item_dx=9.0,
|
|
||||||
item_dy=9.0,
|
|
||||||
size_x=10,
|
|
||||||
size_y=10,
|
|
||||||
size_z=0.0,
|
|
||||||
make_tip=make_tip,
|
|
||||||
)
|
|
||||||
idx_available = list(range(0, 32)) + list(range(64, 96))
|
|
||||||
tip_spots_available = {k: v for i, (k, v) in enumerate(tip_spots.items()) if i in idx_available}
|
|
||||||
tip_rack = TipRack(
|
|
||||||
name=name,
|
|
||||||
size_x=size_x,
|
|
||||||
size_y=size_y,
|
|
||||||
size_z=size_z,
|
|
||||||
# ordered_items=tip_spots_available,
|
|
||||||
ordered_items=tip_spots,
|
|
||||||
category=category,
|
|
||||||
model=model,
|
|
||||||
with_tips=False,
|
|
||||||
)
|
|
||||||
tip_rack.set_tip_state([True]*32 + [False]*32 + [True]*32) # 前32和后32个有枪头,中间32个无枪头
|
|
||||||
return tip_rack
|
|
||||||
|
|
||||||
|
|
||||||
class WasteTipBoxstate(TypedDict):
|
|
||||||
""""废枪头盒状态字典"""
|
|
||||||
max_tips: int = 100
|
|
||||||
tip_count: int = 0
|
|
||||||
|
|
||||||
#枪头不是一次性的(同一溶液则反复使用),根据寄存器判断
|
|
||||||
class WasteTipBox(Trash):
|
|
||||||
"""废枪头盒类 - 100个枪头容量"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
size_x: float = 127.8,
|
|
||||||
size_y: float = 85.5,
|
|
||||||
size_z: float = 60.0,
|
|
||||||
material_z_thickness=0,
|
|
||||||
max_volume=float("inf"),
|
|
||||||
category="trash",
|
|
||||||
model=None,
|
|
||||||
compute_volume_from_height=None,
|
|
||||||
compute_height_from_volume=None,
|
|
||||||
):
|
|
||||||
"""初始化废枪头盒
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: 废枪头盒名称
|
|
||||||
size_x: 长度 (mm)
|
|
||||||
size_y: 宽度 (mm)
|
|
||||||
size_z: 高度 (mm)
|
|
||||||
max_tips: 最大枪头容量
|
|
||||||
category: 类别
|
|
||||||
model: 型号
|
|
||||||
"""
|
|
||||||
super().__init__(
|
|
||||||
name=name,
|
|
||||||
size_x=size_x,
|
|
||||||
size_y=size_y,
|
|
||||||
size_z=size_z,
|
|
||||||
category=category,
|
|
||||||
model=model,
|
|
||||||
)
|
|
||||||
self._unilabos_state: WasteTipBoxstate = WasteTipBoxstate()
|
|
||||||
|
|
||||||
def add_tip(self) -> None:
|
|
||||||
"""添加废枪头"""
|
|
||||||
if self._unilabos_state["tip_count"] >= self._unilabos_state["max_tips"]:
|
|
||||||
raise ValueError(f"废枪头盒 {self.name} 已满")
|
|
||||||
self._unilabos_state["tip_count"] += 1
|
|
||||||
|
|
||||||
def get_tip_count(self) -> int:
|
|
||||||
"""获取枪头数量"""
|
|
||||||
return self._unilabos_state["tip_count"]
|
|
||||||
|
|
||||||
def empty(self) -> None:
|
|
||||||
"""清空废枪头盒"""
|
|
||||||
self._unilabos_state["tip_count"] = 0
|
|
||||||
|
|
||||||
|
|
||||||
def load_state(self, state: Dict[str, Any]) -> None:
|
|
||||||
"""格式不变"""
|
|
||||||
super().load_state(state)
|
|
||||||
self._unilabos_state = state
|
|
||||||
|
|
||||||
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
|
||||||
"""格式不变"""
|
|
||||||
data = super().serialize_state()
|
|
||||||
data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class CoincellDeck(Deck):
|
|
||||||
"""纽扣电池组装工作站台面类"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
name: str = "coin_cell_deck",
|
|
||||||
size_x: float = 1450.0, # 1m
|
|
||||||
size_y: float = 1450.0, # 1m
|
|
||||||
size_z: float = 100.0, # 0.9m
|
|
||||||
origin: Coordinate = Coordinate(-2200, 0, 0),
|
|
||||||
category: str = "coin_cell_deck",
|
|
||||||
setup: bool = False, # 是否自动执行 setup
|
|
||||||
):
|
|
||||||
"""初始化纽扣电池组装工作站台面
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: 台面名称
|
|
||||||
size_x: 长度 (mm) - 1m
|
|
||||||
size_y: 宽度 (mm) - 1m
|
|
||||||
size_z: 高度 (mm) - 0.9m
|
|
||||||
origin: 原点坐标
|
|
||||||
category: 类别
|
|
||||||
setup: 是否自动执行 setup 配置标准布局
|
|
||||||
"""
|
|
||||||
super().__init__(
|
|
||||||
name=name,
|
|
||||||
size_x=1450.0,
|
|
||||||
size_y=1450.0,
|
|
||||||
size_z=100.0,
|
|
||||||
origin=origin,
|
|
||||||
)
|
|
||||||
if setup:
|
|
||||||
self.setup()
|
|
||||||
|
|
||||||
def setup(self) -> None:
|
|
||||||
"""设置工作站的标准布局 - 包含子弹夹、料盘、瓶架等完整配置"""
|
|
||||||
# ====================================== 子弹夹 ============================================
|
|
||||||
|
|
||||||
# 正极片(4个洞位,2x2布局)
|
|
||||||
zhengji_zip = MagazineHolder_4_Cathode("正极&铝箔弹夹")
|
|
||||||
self.assign_child_resource(zhengji_zip, Coordinate(x=402.0, y=830.0, z=0))
|
|
||||||
|
|
||||||
# 正极壳、平垫片(6个洞位,2x2+2布局)
|
|
||||||
zhengjike_zip = MagazineHolder_6_Cathode("正极壳&平垫片弹夹")
|
|
||||||
self.assign_child_resource(zhengjike_zip, Coordinate(x=566.0, y=272.0, z=0))
|
|
||||||
|
|
||||||
# 负极壳、弹垫片(6个洞位,2x2+2布局)
|
|
||||||
fujike_zip = MagazineHolder_6_Anode("负极壳&弹垫片弹夹")
|
|
||||||
self.assign_child_resource(fujike_zip, Coordinate(x=474.0, y=276.0, z=0))
|
|
||||||
|
|
||||||
# 成品弹夹(6个洞位,3x2布局)
|
|
||||||
chengpindanjia_zip = MagazineHolder_6_Battery("成品弹夹")
|
|
||||||
self.assign_child_resource(chengpindanjia_zip, Coordinate(x=260.0, y=156.0, z=0))
|
|
||||||
|
|
||||||
# ====================================== 物料板 ============================================
|
|
||||||
# 创建物料板(料盘carrier)- 4x4布局
|
|
||||||
# 负极料盘
|
|
||||||
fujiliaopan = MaterialPlate(name="负极料盘", size_x=120, size_y=100, size_z=10.0, fill=True)
|
|
||||||
self.assign_child_resource(fujiliaopan, Coordinate(x=708.0, y=794.0, z=0))
|
|
||||||
# for i in range(16):
|
|
||||||
# fujipian = ElectrodeSheet(name=f"{fujiliaopan.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
|
|
||||||
# fujiliaopan.children[i].assign_child_resource(fujipian, location=None)
|
|
||||||
|
|
||||||
# 隔膜料盘
|
|
||||||
gemoliaopan = MaterialPlate(name="隔膜料盘", size_x=120, size_y=100, size_z=10.0, fill=True)
|
|
||||||
self.assign_child_resource(gemoliaopan, Coordinate(x=718.0, y=918.0, z=0))
|
|
||||||
# for i in range(16):
|
|
||||||
# gemopian = ElectrodeSheet(name=f"{gemoliaopan.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
|
|
||||||
# gemoliaopan.children[i].assign_child_resource(gemopian, location=None)
|
|
||||||
|
|
||||||
# ====================================== 瓶架、移液枪 ============================================
|
|
||||||
# 在台面上放置 3x4 瓶架、6x2 瓶架 与 64孔移液枪头盒
|
|
||||||
# 奔耀上料5ml分液瓶小板 - 由奔曜跨站转运而来,不单独写,但是这里应该有一个堆栈用于摆放分液瓶小板
|
|
||||||
|
|
||||||
# bottle_rack_3x4 = BottleRack(
|
|
||||||
# name="bottle_rack_3x4",
|
|
||||||
# size_x=210.0,
|
|
||||||
# size_y=140.0,
|
|
||||||
# size_z=100.0,
|
|
||||||
# num_items_x=2,
|
|
||||||
# num_items_y=4,
|
|
||||||
# position_spacing=35.0,
|
|
||||||
# orientation="vertical",
|
|
||||||
# )
|
|
||||||
# self.assign_child_resource(bottle_rack_3x4, Coordinate(x=1542.0, y=717.0, z=0))
|
|
||||||
|
|
||||||
# 电解液缓存位 - 6x2布局
|
|
||||||
bottle_rack_6x2 = YIHUA_Electrolyte_12VialCarrier(name="bottle_rack_6x2")
|
|
||||||
self.assign_child_resource(bottle_rack_6x2, Coordinate(x=1050.0, y=358.0, z=0))
|
|
||||||
# 电解液回收位6x2
|
|
||||||
bottle_rack_6x2_2 = YIHUA_Electrolyte_12VialCarrier(name="bottle_rack_6x2_2")
|
|
||||||
self.assign_child_resource(bottle_rack_6x2_2, Coordinate(x=914.0, y=358.0, z=0))
|
|
||||||
|
|
||||||
tip_box = TipBox64(name="tip_box_64")
|
|
||||||
self.assign_child_resource(tip_box, Coordinate(x=782.0, y=514.0, z=0))
|
|
||||||
|
|
||||||
waste_tip_box = WasteTipBox(name="waste_tip_box")
|
|
||||||
self.assign_child_resource(waste_tip_box, Coordinate(x=778.0, y=622.0, z=0))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
deck = create_coin_cell_deck()
|
|
||||||
print(deck)
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,133 +1,44 @@
|
|||||||
import csv
|
import csv
|
||||||
import inspect
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import types
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
from functools import wraps
|
from pylabrobot.resources import Resource as PLRResource
|
||||||
from pylabrobot.resources import Deck, Resource as PLRResource
|
|
||||||
from unilabos_msgs.msg import Resource
|
from unilabos_msgs.msg import Resource
|
||||||
from unilabos.device_comms.modbus_plc.client import ModbusTcpClient
|
from unilabos.device_comms.modbus_plc.client import ModbusTcpClient
|
||||||
|
from unilabos.devices.workstation.coin_cell_assembly.button_battery_station import MaterialHole, MaterialPlate
|
||||||
from unilabos.devices.workstation.workstation_base import WorkstationBase
|
from unilabos.devices.workstation.workstation_base import WorkstationBase
|
||||||
from unilabos.device_comms.modbus_plc.client import TCPClient, ModbusNode, PLCWorkflow, ModbusWorkflow, WorkflowAction, BaseClient
|
from unilabos.device_comms.modbus_plc.client import TCPClient, ModbusNode, PLCWorkflow, ModbusWorkflow, WorkflowAction, BaseClient
|
||||||
from unilabos.device_comms.modbus_plc.modbus import DeviceType, Base as ModbusNodeBase, DataType, WorderOrder
|
from unilabos.device_comms.modbus_plc.modbus import DeviceType, Base as ModbusNodeBase, DataType, WorderOrder
|
||||||
from unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials import *
|
from unilabos.devices.workstation.coin_cell_assembly.button_battery_station import *
|
||||||
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode
|
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode
|
||||||
from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
|
from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
|
||||||
from unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials import CoincellDeck
|
|
||||||
from unilabos.resources.graphio import convert_resources_to_type
|
|
||||||
from unilabos.utils.log import logger
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_modbus_slave_kw_alias(modbus_client):
|
|
||||||
if modbus_client is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
method_names = [
|
|
||||||
"read_coils",
|
|
||||||
"write_coils",
|
|
||||||
"write_coil",
|
|
||||||
"read_discrete_inputs",
|
|
||||||
"read_holding_registers",
|
|
||||||
"write_register",
|
|
||||||
"write_registers",
|
|
||||||
]
|
|
||||||
|
|
||||||
def _wrap(func):
|
|
||||||
signature = inspect.signature(func)
|
|
||||||
has_var_kwargs = any(param.kind == param.VAR_KEYWORD for param in signature.parameters.values())
|
|
||||||
accepts_unit = has_var_kwargs or "unit" in signature.parameters
|
|
||||||
accepts_slave = has_var_kwargs or "slave" in signature.parameters
|
|
||||||
|
|
||||||
@wraps(func)
|
|
||||||
def _wrapped(self, *args, **kwargs):
|
|
||||||
if "slave" in kwargs and not accepts_slave:
|
|
||||||
slave_value = kwargs.pop("slave")
|
|
||||||
if accepts_unit and "unit" not in kwargs:
|
|
||||||
kwargs["unit"] = slave_value
|
|
||||||
if "unit" in kwargs and not accepts_unit:
|
|
||||||
unit_value = kwargs.pop("unit")
|
|
||||||
if accepts_slave and "slave" not in kwargs:
|
|
||||||
kwargs["slave"] = unit_value
|
|
||||||
return func(self, *args, **kwargs)
|
|
||||||
|
|
||||||
_wrapped._has_slave_alias = True
|
|
||||||
return _wrapped
|
|
||||||
|
|
||||||
for name in method_names:
|
|
||||||
if not hasattr(modbus_client, name):
|
|
||||||
continue
|
|
||||||
bound_method = getattr(modbus_client, name)
|
|
||||||
func = getattr(bound_method, "__func__", None)
|
|
||||||
if func is None:
|
|
||||||
continue
|
|
||||||
if getattr(func, "_has_slave_alias", False):
|
|
||||||
continue
|
|
||||||
wrapped = _wrap(func)
|
|
||||||
setattr(modbus_client, name, types.MethodType(wrapped, modbus_client))
|
|
||||||
|
|
||||||
|
|
||||||
def _coerce_deck_input(deck: Any) -> Optional[Deck]:
|
|
||||||
if deck is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if isinstance(deck, Deck):
|
|
||||||
return deck
|
|
||||||
|
|
||||||
if isinstance(deck, PLRResource):
|
|
||||||
return deck if isinstance(deck, Deck) else None
|
|
||||||
|
|
||||||
candidates = None
|
|
||||||
if isinstance(deck, dict):
|
|
||||||
if "nodes" in deck and isinstance(deck["nodes"], list):
|
|
||||||
candidates = deck["nodes"]
|
|
||||||
else:
|
|
||||||
candidates = [deck]
|
|
||||||
elif isinstance(deck, list):
|
|
||||||
candidates = deck
|
|
||||||
|
|
||||||
if candidates is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
converted = convert_resources_to_type(resources_list=candidates, resource_type=Deck)
|
|
||||||
if isinstance(converted, Deck):
|
|
||||||
return converted
|
|
||||||
if isinstance(converted, list):
|
|
||||||
for item in converted:
|
|
||||||
if isinstance(item, Deck):
|
|
||||||
return item
|
|
||||||
except Exception as exc:
|
|
||||||
logger.warning(f"deck 转换 Deck 失败: {exc}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
#构建物料系统
|
#构建物料系统
|
||||||
|
|
||||||
class CoinCellAssemblyWorkstation(WorkstationBase):
|
class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||||
def __init__(self,
|
def __init__(
|
||||||
config: dict = None,
|
self,
|
||||||
deck=None,
|
deck: CoincellDeck,
|
||||||
address: str = "172.16.28.102",
|
address: str = "192.168.1.20",
|
||||||
port: str = "502",
|
port: str = "502",
|
||||||
debug_mode: bool = False,
|
debug_mode: bool = True,
|
||||||
*args,
|
*args,
|
||||||
**kwargs):
|
**kwargs,
|
||||||
|
):
|
||||||
if deck is None and config:
|
super().__init__(
|
||||||
deck = config.get('deck')
|
#桌子
|
||||||
if deck is None:
|
deck=deck,
|
||||||
logger.info("没有传入依华deck,检查启动json文件")
|
*args,
|
||||||
super().__init__(deck=deck, *args, **kwargs,)
|
**kwargs,
|
||||||
|
)
|
||||||
self.debug_mode = debug_mode
|
self.debug_mode = debug_mode
|
||||||
|
self.deck = deck
|
||||||
""" 连接初始化 """
|
""" 连接初始化 """
|
||||||
modbus_client = TCPClient(addr=address, port=port)
|
modbus_client = TCPClient(addr=address, port=port)
|
||||||
logger.debug(f"创建 Modbus 客户端: {modbus_client}")
|
print("modbus_client", modbus_client)
|
||||||
_ensure_modbus_slave_kw_alias(modbus_client.client)
|
|
||||||
if not debug_mode:
|
if not debug_mode:
|
||||||
modbus_client.client.connect()
|
modbus_client.client.connect()
|
||||||
count = 100
|
count = 100
|
||||||
@@ -138,19 +49,26 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
if not modbus_client.client.is_socket_open():
|
if not modbus_client.client.is_socket_open():
|
||||||
raise ValueError('modbus tcp connection failed')
|
raise ValueError('modbus tcp connection failed')
|
||||||
self.nodes = BaseClient.load_csv(os.path.join(os.path.dirname(__file__), 'coin_cell_assembly_1105.csv'))
|
|
||||||
self.client = modbus_client.register_node_list(self.nodes)
|
|
||||||
else:
|
else:
|
||||||
print("测试模式,跳过连接")
|
print("测试模式,跳过连接")
|
||||||
self.nodes, self.client = None, None
|
|
||||||
""" 工站的配置 """
|
|
||||||
|
|
||||||
|
""" 工站的配置 """
|
||||||
|
self.nodes = BaseClient.load_csv(os.path.join(os.path.dirname(__file__), 'coin_cell_assembly_a.csv'))
|
||||||
|
self.client = modbus_client.register_node_list(self.nodes)
|
||||||
self.success = False
|
self.success = False
|
||||||
self.allow_data_read = False #允许读取函数运行标志位
|
self.allow_data_read = False #允许读取函数运行标志位
|
||||||
self.csv_export_thread = None
|
self.csv_export_thread = None
|
||||||
self.csv_export_running = False
|
self.csv_export_running = False
|
||||||
self.csv_export_file = None
|
self.csv_export_file = None
|
||||||
self.coin_num_N = 0 #已组装电池数量
|
#创建一个物料台面,包含两个极片板
|
||||||
|
#self.deck = create_a_coin_cell_deck()
|
||||||
|
|
||||||
|
#self._ros_node.update_resource(self.deck)
|
||||||
|
|
||||||
|
#ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
|
||||||
|
# "resources": [self.deck]
|
||||||
|
#})
|
||||||
|
|
||||||
|
|
||||||
def post_init(self, ros_node: ROS2WorkstationNode):
|
def post_init(self, ros_node: ROS2WorkstationNode):
|
||||||
self._ros_node = ros_node
|
self._ros_node = ros_node
|
||||||
@@ -159,27 +77,6 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
"resources": [self.deck]
|
"resources": [self.deck]
|
||||||
})
|
})
|
||||||
|
|
||||||
def sync_transfer_resources(self) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
供跨工站转运完成后调用,强制将当前台面资源同步到云端/前端。
|
|
||||||
"""
|
|
||||||
if not hasattr(self, "_ros_node") or self._ros_node is None:
|
|
||||||
return {"status": "failed", "error": "ros_node_not_ready"}
|
|
||||||
if self.deck is None:
|
|
||||||
return {"status": "failed", "error": "deck_not_initialized"}
|
|
||||||
try:
|
|
||||||
future = ROS2DeviceNode.run_async_func(
|
|
||||||
self._ros_node.update_resource,
|
|
||||||
True,
|
|
||||||
resources=[self.deck],
|
|
||||||
)
|
|
||||||
if future:
|
|
||||||
future.result()
|
|
||||||
return {"status": "success"}
|
|
||||||
except Exception as exc:
|
|
||||||
logger.error(f"同步转运资源失败: {exc}", exc_info=True)
|
|
||||||
return {"status": "failed", "error": str(exc)}
|
|
||||||
|
|
||||||
# 批量操作在这里写
|
# 批量操作在这里写
|
||||||
async def change_hole_sheet_to_2(self, hole: MaterialHole):
|
async def change_hole_sheet_to_2(self, hole: MaterialHole):
|
||||||
hole._unilabos_state["max_sheets"] = 2
|
hole._unilabos_state["max_sheets"] = 2
|
||||||
@@ -594,11 +491,11 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
try:
|
try:
|
||||||
# 尝试不同的字节序读取
|
# 尝试不同的字节序读取
|
||||||
code_little, read_err = self.client.use_node('REG_DATA_COIN_CELL_CODE').read(10, word_order=WorderOrder.LITTLE)
|
code_little, read_err = self.client.use_node('REG_DATA_COIN_CELL_CODE').read(10, word_order=WorderOrder.LITTLE)
|
||||||
# logger.debug(f"读取电池二维码原始数据: {code_little}")
|
print(code_little)
|
||||||
clean_code = code_little[-8:][::-1]
|
clean_code = code_little[-8:][::-1]
|
||||||
return clean_code
|
return clean_code
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"读取电池二维码失败: {e}")
|
print(f"读取电池二维码失败: {e}")
|
||||||
return "N/A"
|
return "N/A"
|
||||||
|
|
||||||
|
|
||||||
@@ -607,11 +504,11 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
try:
|
try:
|
||||||
# 尝试不同的字节序读取
|
# 尝试不同的字节序读取
|
||||||
code_little, read_err = self.client.use_node('REG_DATA_ELECTROLYTE_CODE').read(10, word_order=WorderOrder.LITTLE)
|
code_little, read_err = self.client.use_node('REG_DATA_ELECTROLYTE_CODE').read(10, word_order=WorderOrder.LITTLE)
|
||||||
# logger.debug(f"读取电解液二维码原始数据: {code_little}")
|
print(code_little)
|
||||||
clean_code = code_little[-8:][::-1]
|
clean_code = code_little[-8:][::-1]
|
||||||
return clean_code
|
return clean_code
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"读取电解液二维码失败: {e}")
|
print(f"读取电解液二维码失败: {e}")
|
||||||
return "N/A"
|
return "N/A"
|
||||||
|
|
||||||
# ===================== 环境监控区 ======================
|
# ===================== 环境监控区 ======================
|
||||||
@@ -709,8 +606,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
print("waiting for start_cmd")
|
print("waiting for start_cmd")
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
def func_pack_send_bottle_num(self, bottle_num):
|
def func_pack_send_bottle_num(self, bottle_num: int):
|
||||||
bottle_num = int(bottle_num)
|
|
||||||
#发送电解液平台数
|
#发送电解液平台数
|
||||||
print("启动")
|
print("启动")
|
||||||
while (self._unilab_rece_electrolyte_bottle_num()) == False:
|
while (self._unilab_rece_electrolyte_bottle_num()) == False:
|
||||||
@@ -758,25 +654,16 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
# self.success = True
|
# self.success = True
|
||||||
# return self.success
|
# return self.success
|
||||||
|
|
||||||
def func_pack_send_msg_cmd(self, elec_use_num, elec_vol, assembly_type, assembly_pressure) -> bool:
|
def func_pack_send_msg_cmd(self, elec_use_num) -> bool:
|
||||||
"""UNILAB写参数"""
|
"""UNILAB写参数"""
|
||||||
while (self.request_rec_msg_status) == False:
|
while (self.request_rec_msg_status) == False:
|
||||||
print("wait for request_rec_msg_status to True")
|
print("wait for request_rec_msg_status to True")
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
self.success = False
|
self.success = False
|
||||||
#self._unilab_send_msg_electrolyte_num(elec_num)
|
#self._unilab_send_msg_electrolyte_num(elec_num)
|
||||||
#设置平行样数目
|
time.sleep(1)
|
||||||
self._unilab_send_msg_electrolyte_use_num(elec_use_num)
|
self._unilab_send_msg_electrolyte_use_num(elec_use_num)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
#发送电解液加注量
|
|
||||||
self._unilab_send_msg_electrolyte_vol(elec_vol)
|
|
||||||
time.sleep(1)
|
|
||||||
#发送电解液组装类型
|
|
||||||
self._unilab_send_msg_assembly_type(assembly_type)
|
|
||||||
time.sleep(1)
|
|
||||||
#发送电池压制力
|
|
||||||
self._unilab_send_msg_assembly_pressure(assembly_pressure)
|
|
||||||
time.sleep(1)
|
|
||||||
self._unilab_send_msg_succ_cmd(True)
|
self._unilab_send_msg_succ_cmd(True)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
while (self.request_rec_msg_status) == True:
|
while (self.request_rec_msg_status) == True:
|
||||||
@@ -801,32 +688,15 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
data_coin_num = self.data_coin_num
|
data_coin_num = self.data_coin_num
|
||||||
data_electrolyte_code = self.data_electrolyte_code
|
data_electrolyte_code = self.data_electrolyte_code
|
||||||
data_coin_cell_code = self.data_coin_cell_code
|
data_coin_cell_code = self.data_coin_cell_code
|
||||||
logger.debug(f"data_open_circuit_voltage: {data_open_circuit_voltage}")
|
print("data_open_circuit_voltage", data_open_circuit_voltage)
|
||||||
logger.debug(f"data_pole_weight: {data_pole_weight}")
|
print("data_pole_weight", data_pole_weight)
|
||||||
logger.debug(f"data_assembly_time: {data_assembly_time}")
|
print("data_assembly_time", data_assembly_time)
|
||||||
logger.debug(f"data_assembly_pressure: {data_assembly_pressure}")
|
print("data_assembly_pressure", data_assembly_pressure)
|
||||||
logger.debug(f"data_electrolyte_volume: {data_electrolyte_volume}")
|
print("data_electrolyte_volume", data_electrolyte_volume)
|
||||||
logger.debug(f"data_coin_num: {data_coin_num}")
|
print("data_coin_num", data_coin_num)
|
||||||
logger.debug(f"data_electrolyte_code: {data_electrolyte_code}")
|
print("data_electrolyte_code", data_electrolyte_code)
|
||||||
logger.debug(f"data_coin_cell_code: {data_coin_cell_code}")
|
print("data_coin_cell_code", data_coin_cell_code)
|
||||||
#接收完信息后,读取完毕标志位置True
|
#接收完信息后,读取完毕标志位置True
|
||||||
liaopan3 = self.deck.get_resource("成品弹夹")
|
|
||||||
#把物料解绑后放到另一盘上
|
|
||||||
battery = ElectrodeSheet(name=f"battery_{self.coin_num_N}", size_x=14, size_y=14, size_z=2)
|
|
||||||
battery._unilabos_state = {
|
|
||||||
"electrolyte_name": data_coin_cell_code,
|
|
||||||
"data_electrolyte_code": data_electrolyte_code,
|
|
||||||
"open_circuit_voltage": data_open_circuit_voltage,
|
|
||||||
"assembly_pressure": data_assembly_pressure,
|
|
||||||
"electrolyte_volume": data_electrolyte_volume
|
|
||||||
}
|
|
||||||
liaopan3.children[self.coin_num_N].assign_child_resource(battery, location=None)
|
|
||||||
#print(jipian2.parent)
|
|
||||||
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
|
|
||||||
"resources": [self.deck]
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
self._unilab_rec_msg_succ_cmd(True)
|
self._unilab_rec_msg_succ_cmd(True)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
#等待允许读取标志位置False
|
#等待允许读取标志位置False
|
||||||
@@ -884,25 +754,11 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
self.success = True
|
self.success = True
|
||||||
return self.success
|
return self.success
|
||||||
|
|
||||||
def qiming_coin_cell_code(self, fujipian_panshu:int, fujipian_juzhendianwei:int=0, gemopanshu:int=0, gemo_juzhendianwei:int=0, lvbodian:bool=True, battery_pressure_mode:bool=True, battery_pressure:int=4000, battery_clean_ignore:bool=False) -> bool:
|
|
||||||
self.success = False
|
|
||||||
self.client.use_node('REG_MSG_NE_PLATE_NUM').write(fujipian_panshu)
|
|
||||||
self.client.use_node('REG_MSG_NE_PLATE_MATRIX').write(fujipian_juzhendianwei)
|
|
||||||
self.client.use_node('REG_MSG_SEPARATOR_PLATE_NUM').write(gemopanshu)
|
|
||||||
self.client.use_node('REG_MSG_SEPARATOR_PLATE_MATRIX').write(gemo_juzhendianwei)
|
|
||||||
self.client.use_node('COIL_ALUMINUM_FOIL').write(not lvbodian)
|
|
||||||
self.client.use_node('REG_MSG_PRESS_MODE').write(not battery_pressure_mode)
|
|
||||||
# self.client.use_node('REG_MSG_ASSEMBLY_PRESSURE').write(battery_pressure)
|
|
||||||
self.client.use_node('REG_MSG_BATTERY_CLEAN_IGNORE').write(battery_clean_ignore)
|
|
||||||
self.success = True
|
|
||||||
|
|
||||||
return self.success
|
|
||||||
|
|
||||||
def func_allpack_cmd(self, elec_num, elec_use_num, elec_vol:int=50, assembly_type:int=7, assembly_pressure:int=4200, file_path: str="/Users/sml/work") -> bool:
|
def func_allpack_cmd(self, elec_num, elec_use_num, file_path: str="D:\\coin_cell_data") -> bool:
|
||||||
elec_num, elec_use_num, elec_vol, assembly_type, assembly_pressure = int(elec_num), int(elec_use_num), int(elec_vol), int(assembly_type), int(assembly_pressure)
|
|
||||||
summary_csv_file = os.path.join(file_path, "duandian.csv")
|
summary_csv_file = os.path.join(file_path, "duandian.csv")
|
||||||
# 如果断点文件存在,先读取之前的进度
|
# 如果断点文件存在,先读取之前的进度
|
||||||
|
|
||||||
if os.path.exists(summary_csv_file):
|
if os.path.exists(summary_csv_file):
|
||||||
read_status_flag = True
|
read_status_flag = True
|
||||||
with open(summary_csv_file, 'r', newline='', encoding='utf-8') as csvfile:
|
with open(summary_csv_file, 'r', newline='', encoding='utf-8') as csvfile:
|
||||||
@@ -928,37 +784,53 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
elec_num_N = 0
|
elec_num_N = 0
|
||||||
elec_use_num_N = 0
|
elec_use_num_N = 0
|
||||||
coin_num_N = 0
|
coin_num_N = 0
|
||||||
for i in range(20):
|
|
||||||
print(f"剩余电解液瓶数: {elec_num}, 已组装电池数: {elec_use_num}")
|
print(f"剩余电解液瓶数: {elec_num}, 已组装电池数: {elec_use_num}")
|
||||||
print(f"剩余电解液瓶数: {type(elec_num)}, 已组装电池数: {type(elec_use_num)}")
|
|
||||||
print(f"剩余电解液瓶数: {type(int(elec_num))}, 已组装电池数: {type(int(elec_use_num))}")
|
|
||||||
|
|
||||||
#如果是第一次运行,则进行初始化、切换自动、启动, 如果是断点重启则跳过。
|
#如果是第一次运行,则进行初始化、切换自动、启动, 如果是断点重启则跳过。
|
||||||
if read_status_flag == False:
|
if read_status_flag == False:
|
||||||
pass
|
|
||||||
#初始化
|
#初始化
|
||||||
#self.func_pack_device_init()
|
self.func_pack_device_init()
|
||||||
#切换自动
|
#切换自动
|
||||||
#self.func_pack_device_auto()
|
self.func_pack_device_auto()
|
||||||
#启动,小车收回
|
#启动,小车收回
|
||||||
#self.func_pack_device_start()
|
self.func_pack_device_start()
|
||||||
#发送电解液瓶数量,启动搬运,多搬运没事
|
#发送电解液瓶数量,启动搬运,多搬运没事
|
||||||
#self.func_pack_send_bottle_num(elec_num)
|
self.func_pack_send_bottle_num(elec_num)
|
||||||
last_i = elec_num_N
|
last_i = elec_num_N
|
||||||
last_j = elec_use_num_N
|
last_j = elec_use_num_N
|
||||||
for i in range(last_i, elec_num):
|
for i in range(last_i, elec_num):
|
||||||
print(f"开始第{last_i+i+1}瓶电解液的组装")
|
print(f"开始第{last_i+i+1}瓶电解液的组装")
|
||||||
#第一个循环从上次断点继续,后续循环从0开始
|
#第一个循环从上次断点继续,后续循环从0开始
|
||||||
j_start = last_j if i == last_i else 0
|
j_start = last_j if i == last_i else 0
|
||||||
self.func_pack_send_msg_cmd(elec_use_num-j_start, elec_vol, assembly_type, assembly_pressure)
|
self.func_pack_send_msg_cmd(elec_use_num-j_start)
|
||||||
|
|
||||||
for j in range(j_start, elec_use_num):
|
for j in range(j_start, elec_use_num):
|
||||||
print(f"开始第{last_i+i+1}瓶电解液的第{j+j_start+1}个电池组装")
|
print(f"开始第{last_i+i+1}瓶电解液的第{j+j_start+1}个电池组装")
|
||||||
#读取电池组装数据并存入csv
|
#读取电池组装数据并存入csv
|
||||||
self.func_pack_get_msg_cmd(file_path)
|
self.func_pack_get_msg_cmd(file_path)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
# TODO:读完再将电池数加一还是进入循环就将电池数加一需要考虑
|
|
||||||
|
|
||||||
|
#这里定义物料系统
|
||||||
|
# TODO:读完再将电池数加一还是进入循环就将电池数加一需要考虑
|
||||||
|
liaopan1 = self.deck.get_resource("liaopan1")
|
||||||
|
liaopan4 = self.deck.get_resource("liaopan4")
|
||||||
|
jipian1 = liaopan1.children[coin_num_N].children[0]
|
||||||
|
jipian4 = liaopan4.children[coin_num_N].children[0]
|
||||||
|
#print(jipian1)
|
||||||
|
#从料盘上去物料解绑后放到另一盘上
|
||||||
|
jipian1.parent.unassign_child_resource(jipian1)
|
||||||
|
jipian4.parent.unassign_child_resource(jipian4)
|
||||||
|
|
||||||
|
#print(jipian2.parent)
|
||||||
|
battery = Battery(name = f"battery_{coin_num_N}")
|
||||||
|
battery.assign_child_resource(jipian1, location=None)
|
||||||
|
battery.assign_child_resource(jipian4, location=None)
|
||||||
|
|
||||||
|
zidanjia6 = self.deck.get_resource("zi_dan_jia6")
|
||||||
|
|
||||||
|
zidanjia6.children[0].assign_child_resource(battery, location=None)
|
||||||
|
|
||||||
|
|
||||||
# 生成断点文件
|
# 生成断点文件
|
||||||
@@ -970,7 +842,6 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
writer.writerow([elec_num, elec_use_num, elec_num_N, elec_use_num_N, coin_num_N, timestamp])
|
writer.writerow([elec_num, elec_use_num, elec_num_N, elec_use_num_N, coin_num_N, timestamp])
|
||||||
csvfile.flush()
|
csvfile.flush()
|
||||||
coin_num_N += 1
|
coin_num_N += 1
|
||||||
self.coin_num_N = coin_num_N
|
|
||||||
elec_use_num_N += 1
|
elec_use_num_N += 1
|
||||||
elec_num_N += 1
|
elec_num_N += 1
|
||||||
elec_use_num_N = 0
|
elec_use_num_N = 0
|
||||||
@@ -1005,54 +876,38 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
#self.success = True
|
#self.success = True
|
||||||
#return self.success
|
#return self.success
|
||||||
|
|
||||||
def run_packaging_workflow(self, workflow_config: Dict[str, Any]) -> "CoinCellAssemblyWorkstation":
|
|
||||||
config = workflow_config or {}
|
|
||||||
|
|
||||||
qiming_params = config.get("qiming") or {}
|
|
||||||
if qiming_params:
|
|
||||||
self.qiming_coin_cell_code(**qiming_params)
|
|
||||||
|
|
||||||
if config.get("init", True):
|
|
||||||
self.func_pack_device_init()
|
|
||||||
if config.get("auto", True):
|
|
||||||
self.func_pack_device_auto()
|
|
||||||
if config.get("start", True):
|
|
||||||
self.func_pack_device_start()
|
|
||||||
|
|
||||||
packaging_config = config.get("packaging") or {}
|
|
||||||
bottle_num = packaging_config.get("bottle_num")
|
|
||||||
if bottle_num is not None:
|
|
||||||
self.func_pack_send_bottle_num(bottle_num)
|
|
||||||
|
|
||||||
allpack_params = packaging_config.get("command") or {}
|
|
||||||
if allpack_params:
|
|
||||||
self.func_allpack_cmd(**allpack_params)
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def fun_wuliao_test(self) -> bool:
|
def fun_wuliao_test(self) -> bool:
|
||||||
#找到data_init中构建的2个物料盘
|
#找到data_init中构建的2个物料盘
|
||||||
liaopan3 = self.deck.get_resource("\u7535\u6c60\u6599\u76d8")
|
#liaopan1 = self.deck.get_resource("liaopan1")
|
||||||
for i in range(16):
|
#liaopan4 = self.deck.get_resource("liaopan4")
|
||||||
battery = ElectrodeSheet(name=f"battery_{i}", size_x=16, size_y=16, size_z=2)
|
#for coin_num_N in range(16):
|
||||||
battery._unilabos_state = {
|
# liaopan1 = self.deck.get_resource("liaopan1")
|
||||||
"diameter": 20.0,
|
# liaopan4 = self.deck.get_resource("liaopan4")
|
||||||
"height": 20.0,
|
# jipian1 = liaopan1.children[coin_num_N].children[0]
|
||||||
"assembly_pressure": i,
|
# jipian4 = liaopan4.children[coin_num_N].children[0]
|
||||||
"electrolyte_volume": 20.0,
|
# #print(jipian1)
|
||||||
"electrolyte_name": f"DP{i}"
|
# #从料盘上去物料解绑后放到另一盘上
|
||||||
}
|
# jipian1.parent.unassign_child_resource(jipian1)
|
||||||
liaopan3.children[i].assign_child_resource(battery, location=None)
|
# jipian4.parent.unassign_child_resource(jipian4)
|
||||||
|
#
|
||||||
|
# #print(jipian2.parent)
|
||||||
|
# battery = Battery(name = f"battery_{coin_num_N}")
|
||||||
|
# battery.assign_child_resource(jipian1, location=None)
|
||||||
|
# battery.assign_child_resource(jipian4, location=None)
|
||||||
|
#
|
||||||
|
# zidanjia6 = self.deck.get_resource("zi_dan_jia6")
|
||||||
|
# zidanjia6.children[0].assign_child_resource(battery, location=None)
|
||||||
|
# ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
|
||||||
|
# "resources": [self.deck]
|
||||||
|
# })
|
||||||
|
# time.sleep(2)
|
||||||
|
for i in range(20):
|
||||||
|
print(f"输出{i}")
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
|
||||||
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
|
|
||||||
"resources": [self.deck]
|
|
||||||
})
|
|
||||||
# for i in range(40):
|
|
||||||
# print(f"fun_wuliao_test 运行结束{i}")
|
|
||||||
# time.sleep(1)
|
|
||||||
# time.sleep(40)
|
|
||||||
# 数据读取与输出
|
# 数据读取与输出
|
||||||
def func_read_data_and_output(self, file_path: str="/Users/sml/work"):
|
def func_read_data_and_output(self, file_path: str="D:\\coin_cell_data"):
|
||||||
# 检查CSV导出是否正在运行,已运行则跳出,防止同时启动两个while循环
|
# 检查CSV导出是否正在运行,已运行则跳出,防止同时启动两个while循环
|
||||||
if self.csv_export_running:
|
if self.csv_export_running:
|
||||||
return False, "读取已在运行中"
|
return False, "读取已在运行中"
|
||||||
@@ -1157,7 +1012,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
# else:
|
# else:
|
||||||
# print("子弹夹洞位0没有极片")
|
# print("子弹夹洞位0没有极片")
|
||||||
#
|
#
|
||||||
# # TODO:#把电解液从瓶中取到电池夹子中
|
# #把电解液从瓶中取到电池夹子中
|
||||||
# battery_site = deck.get_resource("battery_press_1")
|
# battery_site = deck.get_resource("battery_press_1")
|
||||||
# clip_magazine_battery = deck.get_resource("clip_magazine_battery")
|
# clip_magazine_battery = deck.get_resource("clip_magazine_battery")
|
||||||
# if battery_site.has_battery():
|
# if battery_site.has_battery():
|
||||||
@@ -1246,92 +1101,42 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def run_coin_cell_assembly_workflow(
|
|
||||||
self,
|
|
||||||
workflow_config: Optional[Dict[str, Any]] = None,
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
config: Dict[str, Any]
|
|
||||||
if workflow_config is None:
|
|
||||||
config = {}
|
|
||||||
elif isinstance(workflow_config, list):
|
|
||||||
config = {"materials": workflow_config}
|
|
||||||
else:
|
|
||||||
config = workflow_config
|
|
||||||
qiming_defaults = {
|
|
||||||
"fujipian_panshu": 1,
|
|
||||||
"fujipian_juzhendianwei": 0,
|
|
||||||
"gemopanshu": 1,
|
|
||||||
"gemo_juzhendianwei": 0,
|
|
||||||
"lvbodian": True,
|
|
||||||
"battery_pressure_mode": True,
|
|
||||||
"battery_pressure": 4200,
|
|
||||||
"battery_clean_ignore": False,
|
|
||||||
}
|
|
||||||
qiming_params = {**qiming_defaults, **(config.get("qiming") or {})}
|
|
||||||
qiming_success = self.qiming_coin_cell_code(**qiming_params)
|
|
||||||
|
|
||||||
step_results: Dict[str, Any] = {}
|
|
||||||
try:
|
|
||||||
self.func_pack_device_init()
|
|
||||||
step_results["init"] = True
|
|
||||||
except Exception as exc:
|
|
||||||
step_results["init"] = f"error: {exc}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.func_pack_device_auto()
|
|
||||||
step_results["auto"] = True
|
|
||||||
except Exception as exc:
|
|
||||||
step_results["auto"] = f"error: {exc}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.func_pack_device_start()
|
|
||||||
step_results["start"] = True
|
|
||||||
except Exception as exc:
|
|
||||||
step_results["start"] = f"error: {exc}"
|
|
||||||
|
|
||||||
packaging_cfg = config.get("packaging") or {}
|
|
||||||
bottle_num = packaging_cfg.get("bottle_num", 1)
|
|
||||||
try:
|
|
||||||
self.func_pack_send_bottle_num(bottle_num)
|
|
||||||
step_results["send_bottle_num"] = True
|
|
||||||
except Exception as exc:
|
|
||||||
step_results["send_bottle_num"] = f"error: {exc}"
|
|
||||||
|
|
||||||
command_defaults = {
|
|
||||||
"elec_num": 1,
|
|
||||||
"elec_use_num": 1,
|
|
||||||
"elec_vol": 50,
|
|
||||||
"assembly_type": 7,
|
|
||||||
"assembly_pressure": 4200,
|
|
||||||
"file_path": "/Users/sml/work",
|
|
||||||
}
|
|
||||||
command_params = {**command_defaults, **(packaging_cfg.get("command") or {})}
|
|
||||||
packaging_result = self.func_allpack_cmd(**command_params)
|
|
||||||
|
|
||||||
finished_result = self.func_pack_send_finished_cmd()
|
|
||||||
stop_result = self.func_pack_device_stop()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"qiming": {
|
|
||||||
"params": qiming_params,
|
|
||||||
"success": qiming_success,
|
|
||||||
},
|
|
||||||
"workflow_steps": step_results,
|
|
||||||
"packaging": {
|
|
||||||
"bottle_num": bottle_num,
|
|
||||||
"command": command_params,
|
|
||||||
"result": packaging_result,
|
|
||||||
},
|
|
||||||
"finish": {
|
|
||||||
"send_finished": finished_result,
|
|
||||||
"stop": stop_result,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
deck = CoincellDeck(setup=True, name="coin_cell_deck")
|
from pylabrobot.resources import Resource
|
||||||
w = CoinCellAssemblyWorkstation(deck=deck, address="172.16.28.102", port="502", debug_mode=False)
|
Coin_Cell = CoinCellAssemblyWorkstation(Resource("1", 1, 1, 1), debug_mode=True)
|
||||||
w.run_coin_cell_assembly_workflow()
|
#Coin_Cell.func_pack_device_init()
|
||||||
|
#Coin_Cell.func_pack_device_auto()
|
||||||
|
#Coin_Cell.func_pack_device_start()
|
||||||
|
#Coin_Cell.func_pack_send_bottle_num(2)
|
||||||
|
#Coin_Cell.func_pack_send_msg_cmd(2)
|
||||||
|
#Coin_Cell.func_pack_get_msg_cmd()
|
||||||
|
#Coin_Cell.func_pack_get_msg_cmd()
|
||||||
|
#Coin_Cell.func_pack_send_finished_cmd()
|
||||||
|
#
|
||||||
|
#Coin_Cell.func_allpack_cmd(3, 2)
|
||||||
|
#print(Coin_Cell.data_stack_vision_code)
|
||||||
|
#print("success")
|
||||||
|
#创建一个物料台面
|
||||||
|
|
||||||
|
#deck = create_a_coin_cell_deck()
|
||||||
|
|
||||||
|
##在台面上找到料盘和极片
|
||||||
|
#liaopan1 = deck.get_resource("liaopan1")
|
||||||
|
#liaopan2 = deck.get_resource("liaopan2")
|
||||||
|
#jipian1 = liaopan1.children[1].children[0]
|
||||||
|
#
|
||||||
|
##print(jipian1)
|
||||||
|
##把物料解绑后放到另一盘上
|
||||||
|
#jipian1.parent.unassign_child_resource(jipian1)
|
||||||
|
#liaopan2.children[1].assign_child_resource(jipian1, location=None)
|
||||||
|
##print(jipian2.parent)
|
||||||
|
from unilabos.resources.graphio import resource_ulab_to_plr, convert_resources_to_type
|
||||||
|
|
||||||
|
with open("./button_battery_decks_unilab.json", "r", encoding="utf-8") as f:
|
||||||
|
bioyond_resources_unilab = json.load(f)
|
||||||
|
print(f"成功读取 JSON 文件,包含 {len(bioyond_resources_unilab)} 个资源")
|
||||||
|
ulab_resources = convert_resources_to_type(bioyond_resources_unilab, List[PLRResource])
|
||||||
|
print(f"转换结果类型: {type(ulab_resources)}")
|
||||||
|
print(ulab_resources)
|
||||||
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
Name,DataType,InitValue,Comment,Attribute,DeviceType,Address,
|
|
||||||
COIL_SYS_START_CMD,BOOL,,,,coil,9010,
|
|
||||||
COIL_SYS_STOP_CMD,BOOL,,,,coil,9020,
|
|
||||||
COIL_SYS_RESET_CMD,BOOL,,,,coil,9030,
|
|
||||||
COIL_SYS_HAND_CMD,BOOL,,,,coil,9040,
|
|
||||||
COIL_SYS_AUTO_CMD,BOOL,,,,coil,9050,
|
|
||||||
COIL_SYS_INIT_CMD,BOOL,,,,coil,9060,
|
|
||||||
COIL_UNILAB_SEND_MSG_SUCC_CMD,BOOL,,,,coil,9700,
|
|
||||||
COIL_UNILAB_REC_MSG_SUCC_CMD,BOOL,,,,coil,9710,unilab_rec_msg_succ_cmd
|
|
||||||
COIL_SYS_START_STATUS,BOOL,,,,coil,9210,
|
|
||||||
COIL_SYS_STOP_STATUS,BOOL,,,,coil,9220,
|
|
||||||
COIL_SYS_RESET_STATUS,BOOL,,,,coil,9230,
|
|
||||||
COIL_SYS_HAND_STATUS,BOOL,,,,coil,9240,
|
|
||||||
COIL_SYS_AUTO_STATUS,BOOL,,,,coil,9250,
|
|
||||||
COIL_SYS_INIT_STATUS,BOOL,,,,coil,9260,
|
|
||||||
COIL_REQUEST_REC_MSG_STATUS,BOOL,,,,coil,9500,
|
|
||||||
COIL_REQUEST_SEND_MSG_STATUS,BOOL,,,,coil,9510,request_send_msg_status
|
|
||||||
REG_MSG_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,17000,
|
|
||||||
REG_MSG_ELECTROLYTE_NUM,INT16,,,,hold_register,17002,unilab_send_msg_electrolyte_num
|
|
||||||
REG_MSG_ELECTROLYTE_VOLUME,INT16,,,,hold_register,17004,unilab_send_msg_electrolyte_vol
|
|
||||||
REG_MSG_ASSEMBLY_TYPE,INT16,,,,hold_register,17006,unilab_send_msg_assembly_type
|
|
||||||
REG_MSG_ASSEMBLY_PRESSURE,INT16,,,,hold_register,17008,unilab_send_msg_assembly_pressure
|
|
||||||
REG_DATA_ASSEMBLY_COIN_CELL_NUM,INT16,,,,hold_register,16000,data_assembly_coin_cell_num
|
|
||||||
REG_DATA_OPEN_CIRCUIT_VOLTAGE,FLOAT32,,,,hold_register,16002,data_open_circuit_voltage
|
|
||||||
REG_DATA_AXIS_X_POS,FLOAT32,,,,hold_register,16004,
|
|
||||||
REG_DATA_AXIS_Y_POS,FLOAT32,,,,hold_register,16006,
|
|
||||||
REG_DATA_AXIS_Z_POS,FLOAT32,,,,hold_register,16008,
|
|
||||||
REG_DATA_POLE_WEIGHT,FLOAT32,,,,hold_register,16010,data_pole_weight
|
|
||||||
REG_DATA_ASSEMBLY_PER_TIME,FLOAT32,,,,hold_register,16012,data_assembly_time
|
|
||||||
REG_DATA_ASSEMBLY_PRESSURE,INT16,,,,hold_register,16014,data_assembly_pressure
|
|
||||||
REG_DATA_ELECTROLYTE_VOLUME,INT16,,,,hold_register,16016,data_electrolyte_volume
|
|
||||||
REG_DATA_COIN_NUM,INT16,,,,hold_register,16018,data_coin_num
|
|
||||||
REG_DATA_ELECTROLYTE_CODE,STRING,,,,hold_register,16020,data_electrolyte_code()
|
|
||||||
REG_DATA_COIN_CELL_CODE,STRING,,,,hold_register,16030,data_coin_cell_code()
|
|
||||||
REG_DATA_STACK_VISON_CODE,STRING,,,,hold_register,18004,data_stack_vision_code()
|
|
||||||
REG_DATA_GLOVE_BOX_PRESSURE,FLOAT32,,,,hold_register,16050,data_glove_box_pressure
|
|
||||||
REG_DATA_GLOVE_BOX_WATER_CONTENT,FLOAT32,,,,hold_register,16052,data_glove_box_water_content
|
|
||||||
REG_DATA_GLOVE_BOX_O2_CONTENT,FLOAT32,,,,hold_register,16054,data_glove_box_o2_content
|
|
||||||
UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,9720,
|
|
||||||
UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,9520,
|
|
||||||
REG_MSG_ELECTROLYTE_NUM_USED,INT16,,,,hold_register,17496,
|
|
||||||
REG_DATA_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,16000,
|
|
||||||
UNILAB_SEND_FINISHED_CMD,BOOL,,,,coil,9730,
|
|
||||||
UNILAB_RECE_FINISHED_CMD,BOOL,,,,coil,9530,
|
|
||||||
REG_DATA_ASSEMBLY_TYPE,INT16,,,,hold_register,16018,ASSEMBLY_TYPE7or8
|
|
||||||
COIL_ALUMINUM_FOIL,BOOL,,使用铝箔垫,,coil,9340,
|
|
||||||
REG_MSG_NE_PLATE_MATRIX,INT16,,负极片矩阵点位,,hold_register,17440,
|
|
||||||
REG_MSG_SEPARATOR_PLATE_MATRIX,INT16,,隔膜矩阵点位,,hold_register,17450,
|
|
||||||
REG_MSG_TIP_BOX_MATRIX,INT16,,移液枪头矩阵点位,,hold_register,17480,
|
|
||||||
REG_MSG_NE_PLATE_NUM,INT16,,负极片盘数,,hold_register,17443,
|
|
||||||
REG_MSG_SEPARATOR_PLATE_NUM,INT16,,隔膜盘数,,hold_register,17453,
|
|
||||||
REG_MSG_PRESS_MODE,BOOL,,压制模式(false:压力检测模式,True:距离模式),,coil,9360,电池压制模式
|
|
||||||
,,,,,,,
|
|
||||||
,BOOL,,视觉对位(false:使用,true:忽略),,coil,9300,视觉对位
|
|
||||||
,BOOL,,复检(false:使用,true:忽略),,coil,9310,视觉复检
|
|
||||||
,BOOL,,手套箱_左仓(false:使用,true:忽略),,coil,9320,手套箱左仓
|
|
||||||
,BOOL,,手套箱_右仓(false:使用,true:忽略),,coil,9420,手套箱右仓
|
|
||||||
,BOOL,,真空检知(false:使用,true:忽略),,coil,9350,真空检知
|
|
||||||
,BOOL,,电解液添加模式(false:单次滴液,true:二次滴液),,coil,9370,滴液模式
|
|
||||||
,BOOL,,正极片称重(false:使用,true:忽略),,coil,9380,正极片称重
|
|
||||||
,BOOL,,正负极片组装方式(false:正装,true:倒装),,coil,9390,正负极反装
|
|
||||||
,BOOL,,压制清洁(false:使用,true:忽略),,coil,9400,压制清洁
|
|
||||||
,BOOL,,物料盘摆盘方式(false:水平摆盘,true:堆叠摆盘),,coil,9410,负极片摆盘方式
|
|
||||||
REG_MSG_BATTERY_CLEAN_IGNORE,BOOL,,忽略电池清洁(false:使用,true:忽略),,coil,9460,
|
|
||||||
|
@@ -1,64 +0,0 @@
|
|||||||
Name,DataType,InitValue,Comment,Attribute,DeviceType,Address,
|
|
||||||
COIL_SYS_START_CMD,BOOL,,,,coil,8010,
|
|
||||||
COIL_SYS_STOP_CMD,BOOL,,,,coil,8020,
|
|
||||||
COIL_SYS_RESET_CMD,BOOL,,,,coil,8030,
|
|
||||||
COIL_SYS_HAND_CMD,BOOL,,,,coil,8040,
|
|
||||||
COIL_SYS_AUTO_CMD,BOOL,,,,coil,8050,
|
|
||||||
COIL_SYS_INIT_CMD,BOOL,,,,coil,8060,
|
|
||||||
COIL_UNILAB_SEND_MSG_SUCC_CMD,BOOL,,,,coil,8700,
|
|
||||||
COIL_UNILAB_REC_MSG_SUCC_CMD,BOOL,,,,coil,8710,unilab_rec_msg_succ_cmd
|
|
||||||
COIL_SYS_START_STATUS,BOOL,,,,coil,8210,
|
|
||||||
COIL_SYS_STOP_STATUS,BOOL,,,,coil,8220,
|
|
||||||
COIL_SYS_RESET_STATUS,BOOL,,,,coil,8230,
|
|
||||||
COIL_SYS_HAND_STATUS,BOOL,,,,coil,8240,
|
|
||||||
COIL_SYS_AUTO_STATUS,BOOL,,,,coil,8250,
|
|
||||||
COIL_SYS_INIT_STATUS,BOOL,,,,coil,8260,
|
|
||||||
COIL_REQUEST_REC_MSG_STATUS,BOOL,,,,coil,8500,
|
|
||||||
COIL_REQUEST_SEND_MSG_STATUS,BOOL,,,,coil,8510,request_send_msg_status
|
|
||||||
REG_MSG_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,11000,
|
|
||||||
REG_MSG_ELECTROLYTE_NUM,INT16,,,,hold_register,11002,unilab_send_msg_electrolyte_num
|
|
||||||
REG_MSG_ELECTROLYTE_VOLUME,INT16,,,,hold_register,11004,unilab_send_msg_electrolyte_vol
|
|
||||||
REG_MSG_ASSEMBLY_TYPE,INT16,,,,hold_register,11006,unilab_send_msg_assembly_type
|
|
||||||
REG_MSG_ASSEMBLY_PRESSURE,INT16,,,,hold_register,11008,unilab_send_msg_assembly_pressure
|
|
||||||
REG_DATA_ASSEMBLY_COIN_CELL_NUM,INT16,,,,hold_register,10000,data_assembly_coin_cell_num
|
|
||||||
REG_DATA_OPEN_CIRCUIT_VOLTAGE,FLOAT32,,,,hold_register,10002,data_open_circuit_voltage
|
|
||||||
REG_DATA_AXIS_X_POS,FLOAT32,,,,hold_register,10004,
|
|
||||||
REG_DATA_AXIS_Y_POS,FLOAT32,,,,hold_register,10006,
|
|
||||||
REG_DATA_AXIS_Z_POS,FLOAT32,,,,hold_register,10008,
|
|
||||||
REG_DATA_POLE_WEIGHT,FLOAT32,,,,hold_register,10010,data_pole_weight
|
|
||||||
REG_DATA_ASSEMBLY_PER_TIME,FLOAT32,,,,hold_register,10012,data_assembly_time
|
|
||||||
REG_DATA_ASSEMBLY_PRESSURE,INT16,,,,hold_register,10014,data_assembly_pressure
|
|
||||||
REG_DATA_ELECTROLYTE_VOLUME,INT16,,,,hold_register,10016,data_electrolyte_volume
|
|
||||||
REG_DATA_COIN_NUM,INT16,,,,hold_register,10018,data_coin_num
|
|
||||||
REG_DATA_ELECTROLYTE_CODE,STRING,,,,hold_register,10020,data_electrolyte_code()
|
|
||||||
REG_DATA_COIN_CELL_CODE,STRING,,,,hold_register,10030,data_coin_cell_code()
|
|
||||||
REG_DATA_STACK_VISON_CODE,STRING,,,,hold_register,12004,data_stack_vision_code()
|
|
||||||
REG_DATA_GLOVE_BOX_PRESSURE,FLOAT32,,,,hold_register,10050,data_glove_box_pressure
|
|
||||||
REG_DATA_GLOVE_BOX_WATER_CONTENT,FLOAT32,,,,hold_register,10052,data_glove_box_water_content
|
|
||||||
REG_DATA_GLOVE_BOX_O2_CONTENT,FLOAT32,,,,hold_register,10054,data_glove_box_o2_content
|
|
||||||
UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,8720,
|
|
||||||
UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,8520,
|
|
||||||
REG_MSG_ELECTROLYTE_NUM_USED,INT16,,,,hold_register,496,
|
|
||||||
REG_DATA_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,10000,
|
|
||||||
UNILAB_SEND_FINISHED_CMD,BOOL,,,,coil,8730,
|
|
||||||
UNILAB_RECE_FINISHED_CMD,BOOL,,,,coil,8530,
|
|
||||||
REG_DATA_ASSEMBLY_TYPE,INT16,,,,hold_register,10018,ASSEMBLY_TYPE7or8
|
|
||||||
COIL_ALUMINUM_FOIL,BOOL,,使用铝箔垫,,coil,8340,
|
|
||||||
REG_MSG_NE_PLATE_MATRIX,INT16,,负极片矩阵点位,,hold_register,440,
|
|
||||||
REG_MSG_SEPARATOR_PLATE_MATRIX,INT16,,隔膜矩阵点位,,hold_register,450,
|
|
||||||
REG_MSG_TIP_BOX_MATRIX,INT16,,移液枪头矩阵点位,,hold_register,480,
|
|
||||||
REG_MSG_NE_PLATE_NUM,INT16,,负极片盘数,,hold_register,443,
|
|
||||||
REG_MSG_SEPARATOR_PLATE_NUM,INT16,,隔膜盘数,,hold_register,453,
|
|
||||||
REG_MSG_PRESS_MODE,BOOL,,压制模式(false:压力检测模式,True:距离模式),,coil,8360,电池压制模式
|
|
||||||
,,,,,,,
|
|
||||||
,BOOL,,视觉对位(false:使用,true:忽略),,coil,8300,视觉对位
|
|
||||||
,BOOL,,复检(false:使用,true:忽略),,coil,8310,视觉复检
|
|
||||||
,BOOL,,手套箱_左仓(false:使用,true:忽略),,coil,8320,手套箱左仓
|
|
||||||
,BOOL,,手套箱_右仓(false:使用,true:忽略),,coil,8420,手套箱右仓
|
|
||||||
,BOOL,,真空检知(false:使用,true:忽略),,coil,8350,真空检知
|
|
||||||
,BOOL,,电解液添加模式(false:单次滴液,true:二次滴液),,coil,8370,滴液模式
|
|
||||||
,BOOL,,正极片称重(false:使用,true:忽略),,coil,8380,正极片称重
|
|
||||||
,BOOL,,正负极片组装方式(false:正装,true:倒装),,coil,8390,正负极反装
|
|
||||||
,BOOL,,压制清洁(false:使用,true:忽略),,coil,8400,压制清洁
|
|
||||||
,BOOL,,物料盘摆盘方式(false:水平摆盘,true:堆叠摆盘),,coil,8410,负极片摆盘方式
|
|
||||||
REG_MSG_BATTERY_CLEAN_IGNORE,BOOL,,忽略电池清洁(false:使用,true:忽略),,coil,8460,
|
|
||||||
|
@@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"id": "bioyond_cell_workstation",
|
|
||||||
"name": "配液分液工站",
|
|
||||||
"children": [
|
|
||||||
],
|
|
||||||
"parent": null,
|
|
||||||
"type": "device",
|
|
||||||
"class": "bioyond_cell",
|
|
||||||
"config": {
|
|
||||||
"protocol_type": [],
|
|
||||||
"station_resource": {}
|
|
||||||
},
|
|
||||||
"data": {}
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"id": "BatteryStation",
|
|
||||||
"name": "扣电工作站",
|
|
||||||
"children": [
|
|
||||||
"coin_cell_deck"
|
|
||||||
],
|
|
||||||
"parent": null,
|
|
||||||
"type": "device",
|
|
||||||
"class": "coincellassemblyworkstation_device",
|
|
||||||
"position": {
|
|
||||||
"x": -600,
|
|
||||||
"y": -400,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"debug_mode": false,
|
|
||||||
"protocol_type": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"links": []
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
|
||||||
"id": "bioyond_cell_workstation",
|
|
||||||
"name": "配液分液工站",
|
|
||||||
"children": [
|
|
||||||
],
|
|
||||||
"parent": null,
|
|
||||||
"type": "device",
|
|
||||||
"class": "bioyond_cell",
|
|
||||||
"config": {
|
|
||||||
"protocol_type": [],
|
|
||||||
"station_resource": {}
|
|
||||||
|
|
||||||
},
|
|
||||||
"data": {}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "BatteryStation",
|
"id": "BatteryStation",
|
||||||
"name": "扣电组装工作站",
|
"name": "扣电工作站",
|
||||||
"children": [
|
"children": [
|
||||||
"coin_cell_deck"
|
"coin_cell_deck"
|
||||||
],
|
],
|
||||||
@@ -113,7 +98,7 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "MagazineHolder_4",
|
"type": "ClipMagazine_four",
|
||||||
"size_x": 80,
|
"size_x": 80,
|
||||||
"size_y": 80,
|
"size_y": 80,
|
||||||
"size_z": 10,
|
"size_z": 10,
|
||||||
@@ -154,7 +139,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -249,7 +234,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -344,7 +329,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -439,7 +424,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -537,7 +522,7 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "MagazineHolder_4",
|
"type": "ClipMagazine_four",
|
||||||
"size_x": 80,
|
"size_x": 80,
|
||||||
"size_y": 80,
|
"size_y": 80,
|
||||||
"size_z": 10,
|
"size_z": 10,
|
||||||
@@ -578,7 +563,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -673,7 +658,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -768,7 +753,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -863,7 +848,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -963,7 +948,7 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "MagazineHolder_6",
|
"type": "ClipMagazine",
|
||||||
"size_x": 80,
|
"size_x": 80,
|
||||||
"size_y": 80,
|
"size_y": 80,
|
||||||
"size_z": 10,
|
"size_z": 10,
|
||||||
@@ -1006,7 +991,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -1101,7 +1086,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -1196,7 +1181,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -1291,7 +1276,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -1386,7 +1371,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -1481,7 +1466,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -1581,7 +1566,7 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "MagazineHolder_6",
|
"type": "ClipMagazine",
|
||||||
"size_x": 80,
|
"size_x": 80,
|
||||||
"size_y": 80,
|
"size_y": 80,
|
||||||
"size_z": 10,
|
"size_z": 10,
|
||||||
@@ -1624,7 +1609,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -1719,7 +1704,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -1814,7 +1799,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -1909,7 +1894,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -2004,7 +1989,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -2099,7 +2084,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -2199,7 +2184,7 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "MagazineHolder_6",
|
"type": "ClipMagazine",
|
||||||
"size_x": 80,
|
"size_x": 80,
|
||||||
"size_y": 80,
|
"size_y": 80,
|
||||||
"size_z": 10,
|
"size_z": 10,
|
||||||
@@ -2242,7 +2227,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -2337,7 +2322,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -2432,7 +2417,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -2527,7 +2512,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -2622,7 +2607,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -2717,7 +2702,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -2817,7 +2802,7 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "MagazineHolder_6",
|
"type": "ClipMagazine",
|
||||||
"size_x": 80,
|
"size_x": 80,
|
||||||
"size_y": 80,
|
"size_y": 80,
|
||||||
"size_z": 10,
|
"size_z": 10,
|
||||||
@@ -2860,7 +2845,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -2955,7 +2940,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -3050,7 +3035,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -3145,7 +3130,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -3240,7 +3225,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -3335,7 +3320,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -3435,7 +3420,7 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "MagazineHolder_6",
|
"type": "ClipMagazine",
|
||||||
"size_x": 80,
|
"size_x": 80,
|
||||||
"size_y": 80,
|
"size_y": 80,
|
||||||
"size_z": 10,
|
"size_z": 10,
|
||||||
@@ -3478,7 +3463,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -3573,7 +3558,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -3668,7 +3653,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -3763,7 +3748,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -3858,7 +3843,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -3953,7 +3938,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -4053,7 +4038,7 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "MagazineHolder_6",
|
"type": "ClipMagazine",
|
||||||
"size_x": 80,
|
"size_x": 80,
|
||||||
"size_y": 80,
|
"size_y": 80,
|
||||||
"size_z": 10,
|
"size_z": 10,
|
||||||
@@ -4096,7 +4081,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -4191,7 +4176,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -4286,7 +4271,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -4381,7 +4366,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -4476,7 +4461,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -4571,7 +4556,7 @@
|
|||||||
"z": 10
|
"z": 10
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "Magazine",
|
"type": "ClipMagazineHole",
|
||||||
"size_x": 14.0,
|
"size_x": 14.0,
|
||||||
"size_y": 14.0,
|
"size_y": 14.0,
|
||||||
"size_z": 10.0,
|
"size_z": 10.0,
|
||||||
@@ -147,7 +147,7 @@ class WorkstationBase(ABC):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
deck: Deck,
|
deck: Optional[Deck],
|
||||||
*args,
|
*args,
|
||||||
**kwargs, # 必须有kwargs
|
**kwargs, # 必须有kwargs
|
||||||
):
|
):
|
||||||
@@ -349,5 +349,5 @@ class WorkstationBase(ABC):
|
|||||||
|
|
||||||
|
|
||||||
class ProtocolNode(WorkstationBase):
|
class ProtocolNode(WorkstationBase):
|
||||||
def __init__(self, deck: Optional[PLRResource], *args, **kwargs):
|
def __init__(self, protocol_type: List[str], deck: Optional[PLRResource], *args, **kwargs):
|
||||||
super().__init__(deck, *args, **kwargs)
|
super().__init__(deck, *args, **kwargs)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from dataclasses import dataclass, asdict
|
from dataclasses import dataclass, asdict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from unilabos.utils.log import logger
|
from unilabos.utils.log import logger
|
||||||
|
|
||||||
@@ -76,6 +77,14 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
logger.info(f"收到工作站报送: {endpoint} - {request_data.get('token', 'unknown')}")
|
logger.info(f"收到工作站报送: {endpoint} - {request_data.get('token', 'unknown')}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload_for_log = {"method": "POST", **request_data}
|
||||||
|
self._save_raw_request(endpoint, payload_for_log)
|
||||||
|
if hasattr(self.workstation, '_reports_received_count'):
|
||||||
|
self.workstation._reports_received_count += 1
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# 统一的报送端点路由(基于LIMS协议规范)
|
# 统一的报送端点路由(基于LIMS协议规范)
|
||||||
if endpoint == '/report/step_finish':
|
if endpoint == '/report/step_finish':
|
||||||
response = self._handle_step_finish_report(request_data)
|
response = self._handle_step_finish_report(request_data)
|
||||||
@@ -90,6 +99,8 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
response = self._handle_material_change_report(request_data)
|
response = self._handle_material_change_report(request_data)
|
||||||
elif endpoint == '/report/error_handling':
|
elif endpoint == '/report/error_handling':
|
||||||
response = self._handle_error_handling_report(request_data)
|
response = self._handle_error_handling_report(request_data)
|
||||||
|
elif endpoint == '/report/temperature-cutoff':
|
||||||
|
response = self._handle_temperature_cutoff_report(request_data)
|
||||||
# 保留LIMS协议端点以兼容现有系统
|
# 保留LIMS协议端点以兼容现有系统
|
||||||
elif endpoint == '/LIMS/step_finish':
|
elif endpoint == '/LIMS/step_finish':
|
||||||
response = self._handle_step_finish_report(request_data)
|
response = self._handle_step_finish_report(request_data)
|
||||||
@@ -107,7 +118,8 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
"/report/order_finish",
|
"/report/order_finish",
|
||||||
"/report/batch_update",
|
"/report/batch_update",
|
||||||
"/report/material_change",
|
"/report/material_change",
|
||||||
"/report/error_handling"
|
"/report/error_handling",
|
||||||
|
"/report/temperature-cutoff"
|
||||||
]}
|
]}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -128,6 +140,11 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
parsed_path = urlparse(self.path)
|
parsed_path = urlparse(self.path)
|
||||||
endpoint = parsed_path.path
|
endpoint = parsed_path.path
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._save_raw_request(endpoint, {"method": "GET"})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
if endpoint == '/status':
|
if endpoint == '/status':
|
||||||
response = self._handle_status_check()
|
response = self._handle_status_check()
|
||||||
elif endpoint == '/health':
|
elif endpoint == '/health':
|
||||||
@@ -318,6 +335,50 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
message=f"任务完成报送处理失败: {str(e)}"
|
message=f"任务完成报送处理失败: {str(e)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _handle_temperature_cutoff_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||||||
|
try:
|
||||||
|
required_fields = ['token', 'request_time', 'data']
|
||||||
|
if missing := [f for f in required_fields if f not in request_data]:
|
||||||
|
return HttpResponse(success=False, message=f"缺少必要字段: {', '.join(missing)}")
|
||||||
|
|
||||||
|
data = request_data['data']
|
||||||
|
metrics = [
|
||||||
|
'frameCode',
|
||||||
|
'generateTime',
|
||||||
|
'targetTemperature',
|
||||||
|
'settingTemperature',
|
||||||
|
'inTemperature',
|
||||||
|
'outTemperature',
|
||||||
|
'pt100Temperature',
|
||||||
|
'sensorAverageTemperature',
|
||||||
|
'speed',
|
||||||
|
'force',
|
||||||
|
'viscosity',
|
||||||
|
'averageViscosity'
|
||||||
|
]
|
||||||
|
if miss := [f for f in metrics if f not in data]:
|
||||||
|
return HttpResponse(success=False, message=f"data字段缺少必要内容: {', '.join(miss)}")
|
||||||
|
|
||||||
|
report_request = WorkstationReportRequest(
|
||||||
|
token=request_data['token'],
|
||||||
|
request_time=request_data['request_time'],
|
||||||
|
data=data
|
||||||
|
)
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
if hasattr(self.workstation, 'process_temperature_cutoff_report'):
|
||||||
|
result = self.workstation.process_temperature_cutoff_report(report_request)
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
success=True,
|
||||||
|
message=f"温度/粘度报送已处理: 帧{data['frameCode']}",
|
||||||
|
acknowledgment_id=f"TEMP_CUTOFF_{int(time.time()*1000)}_{data['frameCode']}",
|
||||||
|
data=result
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理温度/粘度报送失败: {e}\n{traceback.format_exc()}")
|
||||||
|
return HttpResponse(success=False, message=f"温度/粘度报送处理失败: {str(e)}")
|
||||||
|
|
||||||
def _handle_batch_update_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
def _handle_batch_update_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||||||
"""处理批量报送"""
|
"""处理批量报送"""
|
||||||
try:
|
try:
|
||||||
@@ -488,11 +549,18 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
def _handle_status_check(self) -> HttpResponse:
|
def _handle_status_check(self) -> HttpResponse:
|
||||||
"""处理状态查询"""
|
"""处理状态查询"""
|
||||||
try:
|
try:
|
||||||
|
# 安全地获取 device_id
|
||||||
|
device_id = "unknown"
|
||||||
|
if hasattr(self.workstation, 'device_id'):
|
||||||
|
device_id = self.workstation.device_id
|
||||||
|
elif hasattr(self.workstation, '_ros_node') and hasattr(self.workstation._ros_node, 'device_id'):
|
||||||
|
device_id = self.workstation._ros_node.device_id
|
||||||
|
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
success=True,
|
success=True,
|
||||||
message="工作站报送服务正常运行",
|
message="工作站报送服务正常运行",
|
||||||
data={
|
data={
|
||||||
"workstation_id": self.workstation.device_id,
|
"workstation_id": device_id,
|
||||||
"service_type": "unified_reporting_service",
|
"service_type": "unified_reporting_service",
|
||||||
"uptime": time.time() - getattr(self.workstation, '_start_time', time.time()),
|
"uptime": time.time() - getattr(self.workstation, '_start_time', time.time()),
|
||||||
"reports_received": getattr(self.workstation, '_reports_received_count', 0),
|
"reports_received": getattr(self.workstation, '_reports_received_count', 0),
|
||||||
@@ -503,6 +571,7 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
"POST /report/batch_update",
|
"POST /report/batch_update",
|
||||||
"POST /report/material_change",
|
"POST /report/material_change",
|
||||||
"POST /report/error_handling",
|
"POST /report/error_handling",
|
||||||
|
"POST /report/temperature-cutoff",
|
||||||
"GET /status",
|
"GET /status",
|
||||||
"GET /health"
|
"GET /health"
|
||||||
]
|
]
|
||||||
@@ -540,6 +609,22 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
"""重写日志方法"""
|
"""重写日志方法"""
|
||||||
logger.debug(f"HTTP请求: {format % args}")
|
logger.debug(f"HTTP请求: {format % args}")
|
||||||
|
|
||||||
|
def _save_raw_request(self, endpoint: str, request_data: Dict[str, Any]) -> None:
|
||||||
|
try:
|
||||||
|
base_dir = Path(__file__).resolve().parents[3] / "unilabos_data" / "http_reports"
|
||||||
|
base_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
log_path = getattr(self.workstation, "_http_log_path", None)
|
||||||
|
log_file = Path(log_path) if log_path else (base_dir / f"http_{int(time.time()*1000)}.log")
|
||||||
|
payload = {
|
||||||
|
"endpoint": endpoint,
|
||||||
|
"received_at": datetime.now().isoformat(),
|
||||||
|
"body": request_data
|
||||||
|
}
|
||||||
|
with open(log_file, "a", encoding="utf-8") as f:
|
||||||
|
f.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class WorkstationHTTPService:
|
class WorkstationHTTPService:
|
||||||
"""工作站HTTP服务"""
|
"""工作站HTTP服务"""
|
||||||
@@ -565,12 +650,23 @@ class WorkstationHTTPService:
|
|||||||
|
|
||||||
# 创建HTTP服务器
|
# 创建HTTP服务器
|
||||||
self.server = HTTPServer((self.host, self.port), handler_factory)
|
self.server = HTTPServer((self.host, self.port), handler_factory)
|
||||||
|
base_dir = Path(__file__).resolve().parents[3] / "unilabos_data" / "http_reports"
|
||||||
|
base_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
session_log = base_dir / f"http_{int(time.time()*1000)}.log"
|
||||||
|
setattr(self.workstation, "_http_log_path", str(session_log))
|
||||||
|
|
||||||
|
# 安全地获取 device_id 用于线程命名
|
||||||
|
device_id = "unknown"
|
||||||
|
if hasattr(self.workstation, 'device_id'):
|
||||||
|
device_id = self.workstation.device_id
|
||||||
|
elif hasattr(self.workstation, '_ros_node') and hasattr(self.workstation._ros_node, 'device_id'):
|
||||||
|
device_id = self.workstation._ros_node.device_id
|
||||||
|
|
||||||
# 在单独线程中运行服务器
|
# 在单独线程中运行服务器
|
||||||
self.server_thread = threading.Thread(
|
self.server_thread = threading.Thread(
|
||||||
target=self._run_server,
|
target=self._run_server,
|
||||||
daemon=True,
|
daemon=True,
|
||||||
name=f"WorkstationHTTP-{self.workstation.device_id}"
|
name=f"WorkstationHTTP-{device_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.running = True
|
self.running = True
|
||||||
@@ -585,6 +681,7 @@ class WorkstationHTTPService:
|
|||||||
logger.info("扩展报送端点:")
|
logger.info("扩展报送端点:")
|
||||||
logger.info(" - POST /report/material_change # 物料变更报送")
|
logger.info(" - POST /report/material_change # 物料变更报送")
|
||||||
logger.info(" - POST /report/error_handling # 错误处理报送")
|
logger.info(" - POST /report/error_handling # 错误处理报送")
|
||||||
|
logger.info(" - POST /report/temperature-cutoff # 温度/粘度报送")
|
||||||
logger.info("兼容端点:")
|
logger.info("兼容端点:")
|
||||||
logger.info(" - POST /LIMS/step_finish # 兼容LIMS步骤完成")
|
logger.info(" - POST /LIMS/step_finish # 兼容LIMS步骤完成")
|
||||||
logger.info(" - POST /LIMS/preintake_finish # 兼容LIMS通量完成")
|
logger.info(" - POST /LIMS/preintake_finish # 兼容LIMS通量完成")
|
||||||
@@ -668,7 +765,7 @@ __all__ = [
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# 简单测试HTTP服务
|
# 简单测试HTTP服务
|
||||||
class DummyWorkstation:
|
class BioyondWorkstation:
|
||||||
device_id = "WS-001"
|
device_id = "WS-001"
|
||||||
|
|
||||||
def process_step_finish_report(self, report_request):
|
def process_step_finish_report(self, report_request):
|
||||||
@@ -686,7 +783,10 @@ if __name__ == "__main__":
|
|||||||
def handle_external_error(self, error_data):
|
def handle_external_error(self, error_data):
|
||||||
return {"handled": True}
|
return {"handled": True}
|
||||||
|
|
||||||
workstation = DummyWorkstation()
|
def process_temperature_cutoff_report(self, report_request):
|
||||||
|
return {"processed": True, "metrics": report_request.data}
|
||||||
|
|
||||||
|
workstation = BioyondWorkstation()
|
||||||
http_service = WorkstationHTTPService(workstation)
|
http_service = WorkstationHTTPService(workstation)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -709,4 +809,3 @@ if __name__ == "__main__":
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"服务器运行错误: {e}")
|
print(f"服务器运行错误: {e}")
|
||||||
http_service.stop()
|
http_service.stop()
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,231 @@
|
|||||||
|
hplc.agilent:
|
||||||
|
category:
|
||||||
|
- characterization_chromatic
|
||||||
|
class:
|
||||||
|
action_value_mappings:
|
||||||
|
auto-check_status:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 检查安捷伦HPLC设备状态的函数。用于监控设备的运行状态、连接状态、错误信息等关键指标。该函数定期查询设备状态,确保系统稳定运行,及时发现和报告设备异常。适用于自动化流程中的设备监控、故障诊断、系统维护等场景。
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: check_status参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-extract_data_from_txt:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
file_path: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 从文本文件中提取分析数据的函数。用于解析安捷伦HPLC生成的结果文件,提取峰面积、保留时间、浓度等关键分析数据。支持多种文件格式的自动识别和数据结构化处理,为后续数据分析和报告生成提供标准化的数据格式。适用于批量数据处理、结果验证、质量控制等分析工作流程。
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
file_path:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- file_path
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: extract_data_from_txt参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-start_sequence:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
params: null
|
||||||
|
resource: null
|
||||||
|
wf_name: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 启动安捷伦HPLC分析序列的函数。用于执行预定义的分析方法序列,包括样品进样、色谱分离、检测等完整的分析流程。支持参数配置、资源分配、工作流程管理等功能,实现全自动的样品分析。适用于批量样品处理、标准化分析、质量检测等需要连续自动分析的应用场景。
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
params:
|
||||||
|
type: string
|
||||||
|
resource:
|
||||||
|
type: object
|
||||||
|
wf_name:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- wf_name
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: start_sequence参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-try_close_sub_device:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
device_name: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 尝试关闭HPLC子设备的函数。用于安全地关闭泵、检测器、进样器等各个子模块,确保设备正常断开连接并保护硬件安全。该函数提供错误处理和状态确认机制,避免强制关闭可能造成的设备损坏。适用于设备维护、系统重启、紧急停机等需要安全关闭设备的场景。
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
device_name:
|
||||||
|
type: string
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: try_close_sub_device参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-try_open_sub_device:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
device_name: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 尝试打开HPLC子设备的函数。用于初始化和连接泵、检测器、进样器等各个子模块,建立设备通信并进行自检。该函数提供连接验证和错误恢复机制,确保子设备正常启动并准备就绪。适用于设备初始化、系统启动、设备重连等需要建立设备连接的场景。
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
device_name:
|
||||||
|
type: string
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: try_open_sub_device参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
execute_command_from_outer:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
command: command
|
||||||
|
goal_default:
|
||||||
|
command: ''
|
||||||
|
handles: {}
|
||||||
|
result:
|
||||||
|
success: success
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback:
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
|
title: SendCmd_Feedback
|
||||||
|
type: object
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
command:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- command
|
||||||
|
title: SendCmd_Goal
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
type: string
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
|
title: SendCmd_Result
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: SendCmd
|
||||||
|
type: object
|
||||||
|
type: SendCmd
|
||||||
|
module: unilabos.devices.hplc.AgilentHPLC:HPLCDriver
|
||||||
|
status_types:
|
||||||
|
could_run: bool
|
||||||
|
data_file: String
|
||||||
|
device_status: str
|
||||||
|
driver_init_ok: bool
|
||||||
|
finish_status: str
|
||||||
|
is_running: bool
|
||||||
|
status_text: str
|
||||||
|
success: bool
|
||||||
|
type: python
|
||||||
|
config_info: []
|
||||||
|
description: 安捷伦高效液相色谱(HPLC)分析设备,用于复杂化合物的分离、检测和定量分析。该设备通过UI自动化技术控制安捷伦ChemStation软件,实现全自动的样品分析流程。具备序列启动、设备状态监控、数据文件提取、结果处理等功能。支持多样品批量处理和实时状态反馈,适用于药物分析、环境检测、食品安全、化学研究等需要高精度色谱分析的实验室应用。
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema:
|
||||||
|
config:
|
||||||
|
properties:
|
||||||
|
driver_debug:
|
||||||
|
default: false
|
||||||
|
type: string
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
data:
|
||||||
|
properties:
|
||||||
|
could_run:
|
||||||
|
type: boolean
|
||||||
|
data_file:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
device_status:
|
||||||
|
type: string
|
||||||
|
driver_init_ok:
|
||||||
|
type: boolean
|
||||||
|
finish_status:
|
||||||
|
type: string
|
||||||
|
is_running:
|
||||||
|
type: boolean
|
||||||
|
status_text:
|
||||||
|
type: string
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- status_text
|
||||||
|
- device_status
|
||||||
|
- could_run
|
||||||
|
- driver_init_ok
|
||||||
|
- is_running
|
||||||
|
- success
|
||||||
|
- finish_status
|
||||||
|
- data_file
|
||||||
|
type: object
|
||||||
|
version: 1.0.0
|
||||||
hplc.agilent-zhida:
|
hplc.agilent-zhida:
|
||||||
category:
|
category:
|
||||||
- characterization_chromatic
|
- characterization_chromatic
|
||||||
|
|||||||
@@ -1 +1,194 @@
|
|||||||
{}
|
raman.home_made:
|
||||||
|
category:
|
||||||
|
- characterization_optic
|
||||||
|
class:
|
||||||
|
action_value_mappings:
|
||||||
|
auto-ccd_time:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
int_time: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 设置CCD检测器积分时间的函数。用于配置拉曼光谱仪的信号采集时间,控制光谱数据的质量和信噪比。较长的积分时间可获得更高的信号强度和更好的光谱质量,但会增加测量时间。该函数允许根据样品特性和测量要求动态调整检测参数,优化测量效果。
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
int_time:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- int_time
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: ccd_time参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-laser_on_power:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
output_voltage_laser: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 设置激光器输出功率的函数。用于控制拉曼光谱仪激光器的功率输出,调节激光强度以适应不同样品的测量需求。适当的激光功率能够获得良好的拉曼信号同时避免样品损伤。该函数支持精确的功率控制,确保测量结果的稳定性和重现性。
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
output_voltage_laser:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- output_voltage_laser
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: laser_on_power参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-raman_without_background:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
int_time: null
|
||||||
|
laser_power: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 执行无背景扣除的拉曼光谱测量函数。用于直接采集样品的拉曼光谱信号,不进行背景校正处理。该函数配置积分时间和激光功率参数,获取原始光谱数据用于后续的数据处理分析。适用于对光谱数据质量要求较高或需要自定义背景处理流程的测量场景。
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
int_time:
|
||||||
|
type: string
|
||||||
|
laser_power:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- int_time
|
||||||
|
- laser_power
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: raman_without_background参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-raman_without_background_average:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
average: null
|
||||||
|
int_time: null
|
||||||
|
laser_power: null
|
||||||
|
sample_name: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 执行多次平均的无背景拉曼光谱测量函数。通过多次测量取平均值来提高光谱数据的信噪比和测量精度,减少随机噪声影响。该函数支持自定义平均次数、积分时间、激光功率等参数,并可为样品指定名称便于数据管理。适用于对测量精度要求较高的定量分析和研究应用。
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
average:
|
||||||
|
type: string
|
||||||
|
int_time:
|
||||||
|
type: string
|
||||||
|
laser_power:
|
||||||
|
type: string
|
||||||
|
sample_name:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- sample_name
|
||||||
|
- int_time
|
||||||
|
- laser_power
|
||||||
|
- average
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: raman_without_background_average参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
raman_cmd:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
command: command
|
||||||
|
goal_default:
|
||||||
|
command: ''
|
||||||
|
handles: {}
|
||||||
|
result:
|
||||||
|
success: success
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback:
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
|
title: SendCmd_Feedback
|
||||||
|
type: object
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
command:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- command
|
||||||
|
title: SendCmd_Goal
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
type: string
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
|
title: SendCmd_Result
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: SendCmd
|
||||||
|
type: object
|
||||||
|
type: SendCmd
|
||||||
|
module: unilabos.devices.raman_uv.home_made_raman:RamanObj
|
||||||
|
status_types: {}
|
||||||
|
type: python
|
||||||
|
config_info: []
|
||||||
|
description: 拉曼光谱分析设备,用于物质的分子结构和化学成分表征。该设备集成激光器和CCD检测器,通过串口通信控制激光功率和光谱采集。具备背景扣除、多次平均、自动数据处理等功能,支持高精度的拉曼光谱测量。适用于材料表征、化学分析、质量控制、研究开发等需要分子指纹识别和结构分析的实验应用。
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema:
|
||||||
|
config:
|
||||||
|
properties:
|
||||||
|
baudrate_ccd:
|
||||||
|
default: 921600
|
||||||
|
type: string
|
||||||
|
baudrate_laser:
|
||||||
|
default: 9600
|
||||||
|
type: string
|
||||||
|
port_ccd:
|
||||||
|
type: string
|
||||||
|
port_laser:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- port_laser
|
||||||
|
- port_ccd
|
||||||
|
type: object
|
||||||
|
data:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
version: 1.0.0
|
||||||
|
|||||||
323
unilabos/registry/devices/chinwe.yaml
Normal file
323
unilabos/registry/devices/chinwe.yaml
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
separator.chinwe:
|
||||||
|
category:
|
||||||
|
- separator
|
||||||
|
- chinwe
|
||||||
|
class:
|
||||||
|
action_value_mappings:
|
||||||
|
motor_rotate_quarter:
|
||||||
|
goal:
|
||||||
|
direction: 顺时针
|
||||||
|
motor_id: 4
|
||||||
|
speed: 60
|
||||||
|
handles: {}
|
||||||
|
schema:
|
||||||
|
description: 电机旋转 1/4 圈
|
||||||
|
properties:
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
direction:
|
||||||
|
default: 顺时针
|
||||||
|
description: 旋转方向
|
||||||
|
enum:
|
||||||
|
- 顺时针
|
||||||
|
- 逆时针
|
||||||
|
type: string
|
||||||
|
motor_id:
|
||||||
|
default: '4'
|
||||||
|
description: 选择电机 (4:搅拌, 5:旋钮)
|
||||||
|
enum:
|
||||||
|
- '4'
|
||||||
|
- '5'
|
||||||
|
type: string
|
||||||
|
speed:
|
||||||
|
default: 60
|
||||||
|
description: 速度 (RPM)
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- motor_id
|
||||||
|
- speed
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
motor_run_continuous:
|
||||||
|
goal:
|
||||||
|
direction: 顺时针
|
||||||
|
motor_id: 4
|
||||||
|
speed: 60
|
||||||
|
handles: {}
|
||||||
|
schema:
|
||||||
|
description: 电机一直旋转 (速度模式)
|
||||||
|
properties:
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
direction:
|
||||||
|
default: 顺时针
|
||||||
|
description: 旋转方向
|
||||||
|
enum:
|
||||||
|
- 顺时针
|
||||||
|
- 逆时针
|
||||||
|
type: string
|
||||||
|
motor_id:
|
||||||
|
default: '4'
|
||||||
|
description: 选择电机 (4:搅拌, 5:旋钮)
|
||||||
|
enum:
|
||||||
|
- '4'
|
||||||
|
- '5'
|
||||||
|
type: string
|
||||||
|
speed:
|
||||||
|
default: 60
|
||||||
|
description: 速度 (RPM)
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- motor_id
|
||||||
|
- speed
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
motor_stop:
|
||||||
|
goal:
|
||||||
|
motor_id: 4
|
||||||
|
handles: {}
|
||||||
|
schema:
|
||||||
|
description: 停止指定步进电机
|
||||||
|
properties:
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
motor_id:
|
||||||
|
default: '4'
|
||||||
|
description: 选择电机
|
||||||
|
enum:
|
||||||
|
- '4'
|
||||||
|
- '5'
|
||||||
|
title: '注: 4=搅拌, 5=旋钮'
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- motor_id
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
pump_aspirate:
|
||||||
|
goal:
|
||||||
|
pump_id: 1
|
||||||
|
valve_port: 1
|
||||||
|
volume: 1000
|
||||||
|
handles: {}
|
||||||
|
schema:
|
||||||
|
description: 注射泵吸液
|
||||||
|
properties:
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
pump_id:
|
||||||
|
default: '1'
|
||||||
|
description: 选择泵
|
||||||
|
enum:
|
||||||
|
- '1'
|
||||||
|
- '2'
|
||||||
|
- '3'
|
||||||
|
type: string
|
||||||
|
valve_port:
|
||||||
|
default: '1'
|
||||||
|
description: 阀门端口
|
||||||
|
enum:
|
||||||
|
- '1'
|
||||||
|
- '2'
|
||||||
|
- '3'
|
||||||
|
- '4'
|
||||||
|
- '5'
|
||||||
|
- '6'
|
||||||
|
- '7'
|
||||||
|
- '8'
|
||||||
|
type: string
|
||||||
|
volume:
|
||||||
|
default: 1000
|
||||||
|
description: 吸液步数
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- pump_id
|
||||||
|
- volume
|
||||||
|
- valve_port
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
pump_dispense:
|
||||||
|
goal:
|
||||||
|
pump_id: 1
|
||||||
|
valve_port: 1
|
||||||
|
volume: 1000
|
||||||
|
handles: {}
|
||||||
|
schema:
|
||||||
|
description: 注射泵排液
|
||||||
|
properties:
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
pump_id:
|
||||||
|
default: '1'
|
||||||
|
description: 选择泵
|
||||||
|
enum:
|
||||||
|
- '1'
|
||||||
|
- '2'
|
||||||
|
- '3'
|
||||||
|
type: string
|
||||||
|
valve_port:
|
||||||
|
default: '1'
|
||||||
|
description: 阀门端口
|
||||||
|
enum:
|
||||||
|
- '1'
|
||||||
|
- '2'
|
||||||
|
- '3'
|
||||||
|
- '4'
|
||||||
|
- '5'
|
||||||
|
- '6'
|
||||||
|
- '7'
|
||||||
|
- '8'
|
||||||
|
type: string
|
||||||
|
volume:
|
||||||
|
default: 1000
|
||||||
|
description: 排液步数
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- pump_id
|
||||||
|
- volume
|
||||||
|
- valve_port
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
pump_initialize:
|
||||||
|
goal:
|
||||||
|
drain_port: 0
|
||||||
|
output_port: 0
|
||||||
|
pump_id: 1
|
||||||
|
speed: 10
|
||||||
|
handles: {}
|
||||||
|
schema:
|
||||||
|
description: 初始化指定注射泵
|
||||||
|
properties:
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
drain_port:
|
||||||
|
default: 0
|
||||||
|
description: 排液口索引
|
||||||
|
type: integer
|
||||||
|
output_port:
|
||||||
|
default: 0
|
||||||
|
description: 输出口索引
|
||||||
|
type: integer
|
||||||
|
pump_id:
|
||||||
|
default: '1'
|
||||||
|
description: 选择泵
|
||||||
|
enum:
|
||||||
|
- '1'
|
||||||
|
- '2'
|
||||||
|
- '3'
|
||||||
|
title: '注: 1号泵, 2号泵, 3号泵'
|
||||||
|
type: string
|
||||||
|
speed:
|
||||||
|
default: 10
|
||||||
|
description: 运动速度
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- pump_id
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
pump_valve:
|
||||||
|
goal:
|
||||||
|
port: 1
|
||||||
|
pump_id: 1
|
||||||
|
handles: {}
|
||||||
|
schema:
|
||||||
|
description: 切换指定泵的阀门端口
|
||||||
|
properties:
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
port:
|
||||||
|
default: '1'
|
||||||
|
description: 阀门端口号 (1-8)
|
||||||
|
enum:
|
||||||
|
- '1'
|
||||||
|
- '2'
|
||||||
|
- '3'
|
||||||
|
- '4'
|
||||||
|
- '5'
|
||||||
|
- '6'
|
||||||
|
- '7'
|
||||||
|
- '8'
|
||||||
|
type: string
|
||||||
|
pump_id:
|
||||||
|
default: '1'
|
||||||
|
description: 选择泵
|
||||||
|
enum:
|
||||||
|
- '1'
|
||||||
|
- '2'
|
||||||
|
- '3'
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- pump_id
|
||||||
|
- port
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
wait_sensor_level:
|
||||||
|
goal:
|
||||||
|
target_state: 有液
|
||||||
|
timeout: 30
|
||||||
|
handles: {}
|
||||||
|
schema:
|
||||||
|
description: 等待传感器液位条件
|
||||||
|
properties:
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
target_state:
|
||||||
|
default: 有液
|
||||||
|
description: 目标液位状态
|
||||||
|
enum:
|
||||||
|
- 有液
|
||||||
|
- 无液
|
||||||
|
type: string
|
||||||
|
timeout:
|
||||||
|
default: 30
|
||||||
|
description: 超时时间 (秒)
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- target_state
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
module: unilabos.devices.separator.chinwe:ChinweDevice
|
||||||
|
status_types:
|
||||||
|
is_connected: bool
|
||||||
|
sensor_level: bool
|
||||||
|
sensor_rssi: int
|
||||||
|
type: python
|
||||||
|
config_info: []
|
||||||
|
description: ChinWe 简易工作站控制器 (3泵, 2电机, 1传感器)
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema:
|
||||||
|
goal:
|
||||||
|
baudrate:
|
||||||
|
default: 9600
|
||||||
|
description: 串口波特率
|
||||||
|
type: integer
|
||||||
|
motor_ids:
|
||||||
|
default:
|
||||||
|
- 4
|
||||||
|
- 5
|
||||||
|
description: 步进电机ID列表
|
||||||
|
items:
|
||||||
|
type: integer
|
||||||
|
type: array
|
||||||
|
port:
|
||||||
|
default: 192.168.1.200:8899
|
||||||
|
description: 串口号或 IP:Port
|
||||||
|
type: string
|
||||||
|
pump_ids:
|
||||||
|
default:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
- 3
|
||||||
|
description: 注射泵ID列表
|
||||||
|
items:
|
||||||
|
type: integer
|
||||||
|
type: array
|
||||||
|
sensor_id:
|
||||||
|
default: 6
|
||||||
|
description: XKC传感器ID
|
||||||
|
type: integer
|
||||||
|
sensor_threshold:
|
||||||
|
default: 300
|
||||||
|
description: 传感器液位判定阈值
|
||||||
|
type: integer
|
||||||
|
version: 2.1.0
|
||||||
@@ -1,751 +0,0 @@
|
|||||||
coincellassemblyworkstation_device:
|
|
||||||
category:
|
|
||||||
- coin_cell_workstation
|
|
||||||
class:
|
|
||||||
action_value_mappings:
|
|
||||||
auto-change_hole_sheet_to_2:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
hole: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
hole:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- hole
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: change_hole_sheet_to_2参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommandAsync
|
|
||||||
auto-fill_plate:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: fill_plate参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommandAsync
|
|
||||||
auto-fun_wuliao_test:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: fun_wuliao_test参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_allpack_cmd:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
assembly_pressure: 4200
|
|
||||||
assembly_type: 7
|
|
||||||
elec_num: null
|
|
||||||
elec_use_num: null
|
|
||||||
elec_vol: 50
|
|
||||||
file_path: /Users/sml/work
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
assembly_pressure:
|
|
||||||
default: 4200
|
|
||||||
type: integer
|
|
||||||
assembly_type:
|
|
||||||
default: 7
|
|
||||||
type: integer
|
|
||||||
elec_num:
|
|
||||||
type: string
|
|
||||||
elec_use_num:
|
|
||||||
type: string
|
|
||||||
elec_vol:
|
|
||||||
default: 50
|
|
||||||
type: integer
|
|
||||||
file_path:
|
|
||||||
default: /Users/sml/work
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- elec_num
|
|
||||||
- elec_use_num
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_allpack_cmd参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_get_csv_export_status:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_get_csv_export_status参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_pack_device_auto:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_pack_device_auto参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_pack_device_init:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_pack_device_init参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_pack_device_start:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_pack_device_start参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_pack_device_stop:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_pack_device_stop参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_pack_get_msg_cmd:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
file_path: D:\coin_cell_data
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
file_path:
|
|
||||||
default: D:\coin_cell_data
|
|
||||||
type: string
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_pack_get_msg_cmd参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_pack_send_bottle_num:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
bottle_num: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
bottle_num:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- bottle_num
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_pack_send_bottle_num参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_pack_send_finished_cmd:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_pack_send_finished_cmd参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_pack_send_msg_cmd:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
assembly_pressure: null
|
|
||||||
assembly_type: null
|
|
||||||
elec_use_num: null
|
|
||||||
elec_vol: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
assembly_pressure:
|
|
||||||
type: string
|
|
||||||
assembly_type:
|
|
||||||
type: string
|
|
||||||
elec_use_num:
|
|
||||||
type: string
|
|
||||||
elec_vol:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- elec_use_num
|
|
||||||
- elec_vol
|
|
||||||
- assembly_type
|
|
||||||
- assembly_pressure
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_pack_send_msg_cmd参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_read_data_and_output:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
file_path: /Users/sml/work
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
file_path:
|
|
||||||
default: /Users/sml/work
|
|
||||||
type: string
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_read_data_and_output参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_stop_read_data:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_stop_read_data参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-modify_deck_name:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
resource_name: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
resource_name:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- resource_name
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: modify_deck_name参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-post_init:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
ros_node: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
ros_node:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- ros_node
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: post_init参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-qiming_coin_cell_code:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
battery_clean_ignore: false
|
|
||||||
battery_pressure: 4000
|
|
||||||
battery_pressure_mode: true
|
|
||||||
fujipian_juzhendianwei: 0
|
|
||||||
fujipian_panshu: null
|
|
||||||
gemo_juzhendianwei: 0
|
|
||||||
gemopanshu: 0
|
|
||||||
lvbodian: true
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
battery_clean_ignore:
|
|
||||||
default: false
|
|
||||||
type: boolean
|
|
||||||
battery_pressure:
|
|
||||||
default: 4000
|
|
||||||
type: integer
|
|
||||||
battery_pressure_mode:
|
|
||||||
default: true
|
|
||||||
type: boolean
|
|
||||||
fujipian_juzhendianwei:
|
|
||||||
default: 0
|
|
||||||
type: integer
|
|
||||||
fujipian_panshu:
|
|
||||||
type: integer
|
|
||||||
gemo_juzhendianwei:
|
|
||||||
default: 0
|
|
||||||
type: integer
|
|
||||||
gemopanshu:
|
|
||||||
default: 0
|
|
||||||
type: integer
|
|
||||||
lvbodian:
|
|
||||||
default: true
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- fujipian_panshu
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: qiming_coin_cell_code参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-run_coin_cell_assembly_workflow:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
workflow_config:
|
|
||||||
type: object
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
goal_default:
|
|
||||||
workflow_config: {}
|
|
||||||
handles:
|
|
||||||
input:
|
|
||||||
- data_key: workflow_config
|
|
||||||
data_source: handle
|
|
||||||
data_type: resource
|
|
||||||
handler_key: WorkflowConfig
|
|
||||||
label: Workflow Config
|
|
||||||
output:
|
|
||||||
- data_key: qiming
|
|
||||||
data_source: executor
|
|
||||||
data_type: resource
|
|
||||||
handler_key: QimingResult
|
|
||||||
label: Qiming Result
|
|
||||||
- data_key: workflow_steps
|
|
||||||
data_source: executor
|
|
||||||
data_type: resource
|
|
||||||
handler_key: WorkflowSteps
|
|
||||||
label: Workflow Steps
|
|
||||||
- data_key: packaging
|
|
||||||
data_source: executor
|
|
||||||
data_type: resource
|
|
||||||
handler_key: PackagingResult
|
|
||||||
label: Packaging Result
|
|
||||||
- data_key: finish
|
|
||||||
data_source: executor
|
|
||||||
data_type: resource
|
|
||||||
handler_key: FinishResult
|
|
||||||
label: Finish Result
|
|
||||||
placeholder_keys: {}
|
|
||||||
result:
|
|
||||||
properties:
|
|
||||||
finish:
|
|
||||||
properties:
|
|
||||||
send_finished:
|
|
||||||
type: object
|
|
||||||
stop:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- send_finished
|
|
||||||
- stop
|
|
||||||
type: object
|
|
||||||
packaging:
|
|
||||||
properties:
|
|
||||||
bottle_num:
|
|
||||||
type: integer
|
|
||||||
command:
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- bottle_num
|
|
||||||
- command
|
|
||||||
- result
|
|
||||||
type: object
|
|
||||||
qiming:
|
|
||||||
properties:
|
|
||||||
params:
|
|
||||||
type: object
|
|
||||||
success:
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- params
|
|
||||||
- success
|
|
||||||
type: object
|
|
||||||
workflow_steps:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- qiming
|
|
||||||
- workflow_steps
|
|
||||||
- packaging
|
|
||||||
- finish
|
|
||||||
type: object
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
workflow_config:
|
|
||||||
type: object
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
properties:
|
|
||||||
finish:
|
|
||||||
properties:
|
|
||||||
send_finished:
|
|
||||||
type: object
|
|
||||||
stop:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- send_finished
|
|
||||||
- stop
|
|
||||||
type: object
|
|
||||||
packaging:
|
|
||||||
properties:
|
|
||||||
bottle_num:
|
|
||||||
type: integer
|
|
||||||
command:
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- bottle_num
|
|
||||||
- command
|
|
||||||
- result
|
|
||||||
type: object
|
|
||||||
qiming:
|
|
||||||
properties:
|
|
||||||
params:
|
|
||||||
type: object
|
|
||||||
success:
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- params
|
|
||||||
- success
|
|
||||||
type: object
|
|
||||||
workflow_steps:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- qiming
|
|
||||||
- workflow_steps
|
|
||||||
- packaging
|
|
||||||
- finish
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: run_coin_cell_assembly_workflow参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-run_packaging_workflow:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
workflow_config: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
workflow_config:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- workflow_config
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: run_packaging_workflow参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
module: unilabos.devices.workstation.coin_cell_assembly.coin_cell_assembly:CoinCellAssemblyWorkstation
|
|
||||||
status_types:
|
|
||||||
data_assembly_coin_cell_num: int
|
|
||||||
data_assembly_pressure: int
|
|
||||||
data_assembly_time: float
|
|
||||||
data_axis_x_pos: float
|
|
||||||
data_axis_y_pos: float
|
|
||||||
data_axis_z_pos: float
|
|
||||||
data_coin_cell_code: str
|
|
||||||
data_coin_num: int
|
|
||||||
data_electrolyte_code: str
|
|
||||||
data_electrolyte_volume: int
|
|
||||||
data_glove_box_o2_content: float
|
|
||||||
data_glove_box_pressure: float
|
|
||||||
data_glove_box_water_content: float
|
|
||||||
data_open_circuit_voltage: float
|
|
||||||
data_pole_weight: float
|
|
||||||
request_rec_msg_status: bool
|
|
||||||
request_send_msg_status: bool
|
|
||||||
sys_mode: str
|
|
||||||
sys_status: str
|
|
||||||
type: python
|
|
||||||
config_info: []
|
|
||||||
description: 扣电工站
|
|
||||||
handles: []
|
|
||||||
icon: koudian.webp
|
|
||||||
init_param_schema:
|
|
||||||
config:
|
|
||||||
properties:
|
|
||||||
address:
|
|
||||||
default: 172.16.28.102
|
|
||||||
type: string
|
|
||||||
config:
|
|
||||||
type: object
|
|
||||||
debug_mode:
|
|
||||||
default: false
|
|
||||||
type: boolean
|
|
||||||
deck:
|
|
||||||
type: string
|
|
||||||
port:
|
|
||||||
default: '502'
|
|
||||||
type: string
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
data:
|
|
||||||
properties:
|
|
||||||
data_assembly_coin_cell_num:
|
|
||||||
type: integer
|
|
||||||
data_assembly_pressure:
|
|
||||||
type: integer
|
|
||||||
data_assembly_time:
|
|
||||||
type: number
|
|
||||||
data_axis_x_pos:
|
|
||||||
type: number
|
|
||||||
data_axis_y_pos:
|
|
||||||
type: number
|
|
||||||
data_axis_z_pos:
|
|
||||||
type: number
|
|
||||||
data_coin_cell_code:
|
|
||||||
type: string
|
|
||||||
data_coin_num:
|
|
||||||
type: integer
|
|
||||||
data_electrolyte_code:
|
|
||||||
type: string
|
|
||||||
data_electrolyte_volume:
|
|
||||||
type: integer
|
|
||||||
data_glove_box_o2_content:
|
|
||||||
type: number
|
|
||||||
data_glove_box_pressure:
|
|
||||||
type: number
|
|
||||||
data_glove_box_water_content:
|
|
||||||
type: number
|
|
||||||
data_open_circuit_voltage:
|
|
||||||
type: number
|
|
||||||
data_pole_weight:
|
|
||||||
type: number
|
|
||||||
request_rec_msg_status:
|
|
||||||
type: boolean
|
|
||||||
request_send_msg_status:
|
|
||||||
type: boolean
|
|
||||||
sys_mode:
|
|
||||||
type: string
|
|
||||||
sys_status:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- sys_status
|
|
||||||
- sys_mode
|
|
||||||
- request_rec_msg_status
|
|
||||||
- request_send_msg_status
|
|
||||||
- data_assembly_coin_cell_num
|
|
||||||
- data_assembly_time
|
|
||||||
- data_open_circuit_voltage
|
|
||||||
- data_axis_x_pos
|
|
||||||
- data_axis_y_pos
|
|
||||||
- data_axis_z_pos
|
|
||||||
- data_pole_weight
|
|
||||||
- data_assembly_pressure
|
|
||||||
- data_electrolyte_volume
|
|
||||||
- data_coin_num
|
|
||||||
- data_coin_cell_code
|
|
||||||
- data_electrolyte_code
|
|
||||||
- data_glove_box_pressure
|
|
||||||
- data_glove_box_o2_content
|
|
||||||
- data_glove_box_water_content
|
|
||||||
type: object
|
|
||||||
registry_type: device
|
|
||||||
version: 1.0.0
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
neware_battery_test_system:
|
neware_battery_test_system:
|
||||||
category:
|
category:
|
||||||
- neware_battery_test_system
|
- neware_battery_test_system
|
||||||
- neware
|
|
||||||
- battery_test
|
|
||||||
class:
|
class:
|
||||||
action_value_mappings:
|
action_value_mappings:
|
||||||
auto-post_init:
|
auto-post_init:
|
||||||
@@ -72,38 +70,6 @@ neware_battery_test_system:
|
|||||||
title: test_connection参数
|
title: test_connection参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
debug_resource_names:
|
|
||||||
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
|
|
||||||
export_status_json:
|
export_status_json:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
@@ -253,9 +219,7 @@ neware_battery_test_system:
|
|||||||
goal_default:
|
goal_default:
|
||||||
string: ''
|
string: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
result:
|
result: {}
|
||||||
return_info: return_info
|
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
@@ -288,56 +252,6 @@ neware_battery_test_system:
|
|||||||
title: StrSingleInput
|
title: StrSingleInput
|
||||||
type: object
|
type: object
|
||||||
type: StrSingleInput
|
type: StrSingleInput
|
||||||
submit_from_csv:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
csv_path: string
|
|
||||||
output_dir: string
|
|
||||||
goal_default:
|
|
||||||
csv_path: ''
|
|
||||||
output_dir: .
|
|
||||||
handles: {}
|
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
submitted_count: submitted_count
|
|
||||||
success: success
|
|
||||||
schema:
|
|
||||||
description: 从CSV文件批量提交Neware测试任务
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
csv_path:
|
|
||||||
description: 输入CSV文件的绝对路径
|
|
||||||
type: string
|
|
||||||
output_dir:
|
|
||||||
description: 输出目录(用于存储XML和备份文件),默认当前目录
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- csv_path
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
properties:
|
|
||||||
return_info:
|
|
||||||
description: 执行结果详细信息
|
|
||||||
type: string
|
|
||||||
submitted_count:
|
|
||||||
description: 成功提交的任务数量
|
|
||||||
type: integer
|
|
||||||
success:
|
|
||||||
description: 是否成功
|
|
||||||
type: boolean
|
|
||||||
total_count:
|
|
||||||
description: CSV文件中的总行数
|
|
||||||
type: integer
|
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
test_connection_action:
|
test_connection_action:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -370,7 +284,7 @@ neware_battery_test_system:
|
|||||||
- goal
|
- goal
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
module: unilabos.devices.neware_battery_test_system.neware_battery_test_system:NewareBatteryTestSystem
|
module: unilabos.devices.battery.neware_battery_test_system:NewareBatteryTestSystem
|
||||||
status_types:
|
status_types:
|
||||||
channel_status: dict
|
channel_status: dict
|
||||||
connection_info: dict
|
connection_info: dict
|
||||||
@@ -380,7 +294,7 @@ neware_battery_test_system:
|
|||||||
total_channels: int
|
total_channels: int
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
description: 新威电池测试系统驱动,提供720个通道的电池测试状态监控、物料管理和CSV批量提交功能。支持TCP通信实现远程控制,包含完整的物料管理系统(2盘电池状态映射),以及从CSV文件批量提交测试任务的能力。
|
description: 新威电池测试系统驱动,支持720个通道的电池测试状态监控和数据导出。通过TCP通信实现远程控制,包含完整的物料管理系统,支持2盘电池的状态映射和监控。
|
||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema:
|
init_param_schema:
|
||||||
@@ -396,13 +310,13 @@ neware_battery_test_system:
|
|||||||
port:
|
port:
|
||||||
type: integer
|
type: integer
|
||||||
size_x:
|
size_x:
|
||||||
default: 50
|
default: 500.0
|
||||||
type: number
|
type: number
|
||||||
size_y:
|
size_y:
|
||||||
default: 50
|
default: 500.0
|
||||||
type: number
|
type: number
|
||||||
size_z:
|
size_z:
|
||||||
default: 20
|
default: 2000.0
|
||||||
type: number
|
type: number
|
||||||
timeout:
|
timeout:
|
||||||
type: integer
|
type: integer
|
||||||
|
|||||||
@@ -834,3 +834,174 @@ linear_motion.toyo_xyz.sim:
|
|||||||
mesh: toyo_xyz
|
mesh: toyo_xyz
|
||||||
type: device
|
type: device
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
motor.iCL42:
|
||||||
|
category:
|
||||||
|
- robot_linear_motion
|
||||||
|
class:
|
||||||
|
action_value_mappings:
|
||||||
|
auto-execute_run_motor:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
mode: null
|
||||||
|
position: null
|
||||||
|
velocity: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 步进电机执行运动函数。直接执行电机运动命令,包括位置设定、速度控制和路径规划。该函数处理底层的电机控制协议,消除警告信息,设置运动参数并启动电机运行。适用于需要直接控制电机运动的应用场景。
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
mode:
|
||||||
|
type: string
|
||||||
|
position:
|
||||||
|
type: number
|
||||||
|
velocity:
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- mode
|
||||||
|
- position
|
||||||
|
- velocity
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: execute_run_motor参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-init_device:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: iCL42电机设备初始化函数。建立与iCL42步进电机驱动器的串口通信连接,配置通信参数包括波特率、数据位、校验位等。该函数是电机使用前的必要步骤,确保驱动器处于可控状态并准备接收运动指令。
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: init_device参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-run_motor:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
mode: null
|
||||||
|
position: null
|
||||||
|
velocity: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 步进电机运动控制函数。根据指定的运动模式、目标位置和速度参数控制电机运动。支持多种运动模式和精确的位置控制,自动处理运动轨迹规划和执行。该函数提供异步执行和状态反馈,确保运动的准确性和可靠性。
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
mode:
|
||||||
|
type: string
|
||||||
|
position:
|
||||||
|
type: number
|
||||||
|
velocity:
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- mode
|
||||||
|
- position
|
||||||
|
- velocity
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: run_motor参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
execute_command_from_outer:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
command: command
|
||||||
|
goal_default:
|
||||||
|
command: ''
|
||||||
|
handles: {}
|
||||||
|
result:
|
||||||
|
success: success
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback:
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
|
title: SendCmd_Feedback
|
||||||
|
type: object
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
command:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- command
|
||||||
|
title: SendCmd_Goal
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
type: string
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
|
title: SendCmd_Result
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: SendCmd
|
||||||
|
type: object
|
||||||
|
type: SendCmd
|
||||||
|
module: unilabos.devices.motor.iCL42:iCL42Driver
|
||||||
|
status_types:
|
||||||
|
is_executing_run: bool
|
||||||
|
motor_position: int
|
||||||
|
success: bool
|
||||||
|
type: python
|
||||||
|
config_info: []
|
||||||
|
description: iCL42步进电机驱动器,用于实验室设备的精密线性运动控制。该设备通过串口通信控制iCL42型步进电机驱动器,支持多种运动模式和精确的位置、速度控制。具备位置反馈、运行状态监控和故障检测功能。适用于自动进样器、样品传送、精密定位平台等需要准确线性运动控制的实验室自动化设备。
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema:
|
||||||
|
config:
|
||||||
|
properties:
|
||||||
|
device_address:
|
||||||
|
default: 1
|
||||||
|
type: integer
|
||||||
|
device_com:
|
||||||
|
default: COM9
|
||||||
|
type: string
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
data:
|
||||||
|
properties:
|
||||||
|
is_executing_run:
|
||||||
|
type: boolean
|
||||||
|
motor_position:
|
||||||
|
type: integer
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- motor_position
|
||||||
|
- is_executing_run
|
||||||
|
- success
|
||||||
|
type: object
|
||||||
|
version: 1.0.0
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
YB_20ml_fenyeping:
|
|
||||||
category:
|
|
||||||
- yb3
|
|
||||||
- YB_bottle
|
|
||||||
class:
|
|
||||||
module: unilabos.resources.bioyond.YB_bottles:YB_20ml_fenyeping
|
|
||||||
type: pylabrobot
|
|
||||||
description: YB_20ml_fenyeping
|
|
||||||
handles: []
|
|
||||||
icon: ''
|
|
||||||
init_param_schema: {}
|
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
|
||||||
YB_5ml_fenyeping:
|
|
||||||
category:
|
|
||||||
- yb3
|
|
||||||
- YB_bottle
|
|
||||||
class:
|
|
||||||
module: unilabos.resources.bioyond.YB_bottles:YB_5ml_fenyeping
|
|
||||||
type: pylabrobot
|
|
||||||
description: YB_5ml_fenyeping
|
|
||||||
handles: []
|
|
||||||
icon: ''
|
|
||||||
init_param_schema: {}
|
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
|
||||||
YB_jia_yang_tou_da:
|
|
||||||
category:
|
|
||||||
- yb3
|
|
||||||
- YB_bottle
|
|
||||||
class:
|
|
||||||
module: unilabos.resources.bioyond.YB_bottles:YB_jia_yang_tou_da
|
|
||||||
type: pylabrobot
|
|
||||||
description: YB_jia_yang_tou_da
|
|
||||||
handles: []
|
|
||||||
icon: ''
|
|
||||||
init_param_schema: {}
|
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
|
||||||
YB_pei_ye_da_Bottle:
|
|
||||||
category:
|
|
||||||
- yb3
|
|
||||||
- YB_bottle
|
|
||||||
class:
|
|
||||||
module: unilabos.resources.bioyond.YB_bottles:YB_pei_ye_da_Bottle
|
|
||||||
type: pylabrobot
|
|
||||||
description: YB_pei_ye_da_Bottle
|
|
||||||
handles: []
|
|
||||||
icon: ''
|
|
||||||
init_param_schema: {}
|
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
|
||||||
YB_pei_ye_xiao_Bottle:
|
|
||||||
category:
|
|
||||||
- yb3
|
|
||||||
- YB_bottle
|
|
||||||
class:
|
|
||||||
module: unilabos.resources.bioyond.YB_bottles:YB_pei_ye_xiao_Bottle
|
|
||||||
type: pylabrobot
|
|
||||||
description: YB_pei_ye_xiao_Bottle
|
|
||||||
handles: []
|
|
||||||
icon: ''
|
|
||||||
init_param_schema: {}
|
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
|
||||||
YB_qiang_tou:
|
|
||||||
category:
|
|
||||||
- yb3
|
|
||||||
- YB_bottle
|
|
||||||
class:
|
|
||||||
module: unilabos.resources.bioyond.YB_bottles:YB_qiang_tou
|
|
||||||
type: pylabrobot
|
|
||||||
description: YB_qiang_tou
|
|
||||||
handles: []
|
|
||||||
icon: ''
|
|
||||||
init_param_schema: {}
|
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
|
||||||
YB_ye_Bottle:
|
|
||||||
category:
|
|
||||||
- yb3
|
|
||||||
- YB_bottle_carriers
|
|
||||||
- YB_bottle
|
|
||||||
class:
|
|
||||||
module: unilabos.resources.bioyond.YB_bottles:YB_ye_Bottle
|
|
||||||
type: pylabrobot
|
|
||||||
description: YB_ye_Bottle
|
|
||||||
handles: []
|
|
||||||
icon: ''
|
|
||||||
init_param_schema: {}
|
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
YB_100ml_yeti:
|
|
||||||
category:
|
|
||||||
- yb3
|
|
||||||
- YB_bottle_carriers
|
|
||||||
class:
|
|
||||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_100ml_yeti
|
|
||||||
type: pylabrobot
|
|
||||||
description: YB_100ml_yeti
|
|
||||||
handles: []
|
|
||||||
icon: ''
|
|
||||||
init_param_schema: {}
|
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
|
||||||
YB_20ml_fenyepingban:
|
|
||||||
category:
|
|
||||||
- yb3
|
|
||||||
- YB_bottle_carriers
|
|
||||||
class:
|
|
||||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_20ml_fenyepingban
|
|
||||||
type: pylabrobot
|
|
||||||
description: YB_20ml_fenyepingban
|
|
||||||
handles: []
|
|
||||||
icon: ''
|
|
||||||
init_param_schema: {}
|
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
|
||||||
YB_5ml_fenyepingban:
|
|
||||||
category:
|
|
||||||
- yb3
|
|
||||||
- YB_bottle_carriers
|
|
||||||
class:
|
|
||||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_5ml_fenyepingban
|
|
||||||
type: pylabrobot
|
|
||||||
description: YB_5ml_fenyepingban
|
|
||||||
handles: []
|
|
||||||
icon: ''
|
|
||||||
init_param_schema: {}
|
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
|
||||||
YB_6StockCarrier:
|
|
||||||
category:
|
|
||||||
- yb3
|
|
||||||
- YB_bottle_carriers
|
|
||||||
class:
|
|
||||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6StockCarrier
|
|
||||||
type: pylabrobot
|
|
||||||
description: YB_6StockCarrier
|
|
||||||
handles: []
|
|
||||||
icon: ''
|
|
||||||
init_param_schema: {}
|
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
|
||||||
YB_6VialCarrier:
|
|
||||||
category:
|
|
||||||
- yb3
|
|
||||||
- YB_bottle_carriers
|
|
||||||
class:
|
|
||||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6VialCarrier
|
|
||||||
type: pylabrobot
|
|
||||||
description: YB_6VialCarrier
|
|
||||||
handles: []
|
|
||||||
icon: ''
|
|
||||||
init_param_schema: {}
|
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
|
||||||
YB_gao_nian_ye_Bottle:
|
|
||||||
category:
|
|
||||||
- yb3
|
|
||||||
- YB_bottle_carriers
|
|
||||||
class:
|
|
||||||
module: unilabos.resources.bioyond.YB_bottles:YB_gao_nian_ye_Bottle
|
|
||||||
type: pylabrobot
|
|
||||||
description: YB_gao_nian_ye_Bottle
|
|
||||||
handles: []
|
|
||||||
icon: ''
|
|
||||||
init_param_schema: {}
|
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
|
||||||
YB_gaonianye:
|
|
||||||
category:
|
|
||||||
- yb3
|
|
||||||
- YB_bottle_carriers
|
|
||||||
class:
|
|
||||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_gaonianye
|
|
||||||
type: pylabrobot
|
|
||||||
description: YB_gaonianye
|
|
||||||
handles: []
|
|
||||||
icon: ''
|
|
||||||
init_param_schema: {}
|
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
|
||||||
YB_jia_yang_tou_da_Carrier:
|
|
||||||
category:
|
|
||||||
- yb3
|
|
||||||
- YB_bottle_carriers
|
|
||||||
class:
|
|
||||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_jia_yang_tou_da_Carrier
|
|
||||||
type: pylabrobot
|
|
||||||
description: YB_jia_yang_tou_da_Carrier
|
|
||||||
handles: []
|
|
||||||
icon: ''
|
|
||||||
init_param_schema: {}
|
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
|
||||||
YB_peiyepingdaban:
|
|
||||||
category:
|
|
||||||
- yb3
|
|
||||||
- YB_bottle_carriers
|
|
||||||
class:
|
|
||||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_peiyepingdaban
|
|
||||||
type: pylabrobot
|
|
||||||
description: YB_peiyepingdaban
|
|
||||||
handles: []
|
|
||||||
icon: ''
|
|
||||||
init_param_schema: {}
|
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
|
||||||
YB_peiyepingxiaoban:
|
|
||||||
category:
|
|
||||||
- yb3
|
|
||||||
- YB_bottle_carriers
|
|
||||||
class:
|
|
||||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_peiyepingxiaoban
|
|
||||||
type: pylabrobot
|
|
||||||
description: YB_peiyepingxiaoban
|
|
||||||
handles: []
|
|
||||||
icon: ''
|
|
||||||
init_param_schema: {}
|
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
|
||||||
YB_qiang_tou_he:
|
|
||||||
category:
|
|
||||||
- yb3
|
|
||||||
- YB_bottle_carriers
|
|
||||||
class:
|
|
||||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_qiang_tou_he
|
|
||||||
type: pylabrobot
|
|
||||||
description: YB_qiang_tou_he
|
|
||||||
handles: []
|
|
||||||
icon: ''
|
|
||||||
init_param_schema: {}
|
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
|
||||||
YB_shi_pei_qi_kuai:
|
|
||||||
category:
|
|
||||||
- yb3
|
|
||||||
- YB_bottle_carriers
|
|
||||||
class:
|
|
||||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_shi_pei_qi_kuai
|
|
||||||
type: pylabrobot
|
|
||||||
description: YB_shi_pei_qi_kuai
|
|
||||||
handles: []
|
|
||||||
icon: ''
|
|
||||||
init_param_schema: {}
|
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
|
||||||
YB_ye:
|
|
||||||
category:
|
|
||||||
- yb3
|
|
||||||
- YB_bottle_carriers
|
|
||||||
class:
|
|
||||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_ye
|
|
||||||
type: pylabrobot
|
|
||||||
description: YB_ye_Bottle_Carrier
|
|
||||||
handles: []
|
|
||||||
icon: ''
|
|
||||||
init_param_schema: {}
|
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
|
||||||
YB_ye_100ml_Bottle:
|
|
||||||
category:
|
|
||||||
- yb3
|
|
||||||
- YB_bottle_carriers
|
|
||||||
class:
|
|
||||||
module: unilabos.resources.bioyond.YB_bottles:YB_ye_100ml_Bottle
|
|
||||||
type: pylabrobot
|
|
||||||
description: YB_ye_100ml_Bottle
|
|
||||||
handles: []
|
|
||||||
icon: ''
|
|
||||||
init_param_schema: {}
|
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
"""Battery-related resource classes for coin cell assembly"""
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
"""
|
|
||||||
瓶架类定义 - 用于纽扣电池组装工作站
|
|
||||||
Bottle Carrier Resource Classes
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
from pylabrobot.resources import ResourceHolder
|
|
||||||
from pylabrobot.resources.utils import create_ordered_items_2d
|
|
||||||
from unilabos.resources.itemized_carrier import ItemizedCarrier
|
|
||||||
|
|
||||||
|
|
||||||
def YIHUA_Electrolyte_12VialCarrier(name: str) -> ItemizedCarrier:
|
|
||||||
"""依华电解液12瓶架 - 3x4布局
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: 瓶架名称
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ItemizedCarrier: 包含12个瓶位的瓶架
|
|
||||||
"""
|
|
||||||
sites = create_ordered_items_2d(
|
|
||||||
klass=ResourceHolder,
|
|
||||||
num_items_x=4,
|
|
||||||
num_items_y=3,
|
|
||||||
dx=10.0,
|
|
||||||
dy=10.0,
|
|
||||||
dz=5.0,
|
|
||||||
item_dx=70.0,
|
|
||||||
item_dy=26.67,
|
|
||||||
size_x=60.0,
|
|
||||||
size_y=20.0,
|
|
||||||
size_z=70.0,
|
|
||||||
)
|
|
||||||
|
|
||||||
return ItemizedCarrier(
|
|
||||||
name=name,
|
|
||||||
size_x=300.0,
|
|
||||||
size_y=100.0,
|
|
||||||
size_z=80.0,
|
|
||||||
num_items_x=4,
|
|
||||||
num_items_y=3,
|
|
||||||
sites=sites,
|
|
||||||
category="bottle_carrier",
|
|
||||||
)
|
|
||||||
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
"""
|
|
||||||
电极片类定义
|
|
||||||
Electrode Sheet Resource Classes
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
from typing import Any, Dict, Optional
|
|
||||||
from pylabrobot.resources.resource import Resource
|
|
||||||
|
|
||||||
|
|
||||||
class ElectrodeSheet(Resource):
|
|
||||||
"""电极片类 - 用于纽扣电池组装"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
size_x: float = 12.0,
|
|
||||||
size_y: float = 12.0,
|
|
||||||
size_z: float = 0.1,
|
|
||||||
category: str = "electrode_sheet",
|
|
||||||
electrode_type: str = "anode", # "anode" 负极, "cathode" 正极, "separator" 隔膜
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
"""初始化电极片
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: 电极片名称
|
|
||||||
size_x: X方向尺寸 (mm)
|
|
||||||
size_y: Y方向尺寸 (mm)
|
|
||||||
size_z: Z方向尺寸/厚度 (mm)
|
|
||||||
category: 类别
|
|
||||||
electrode_type: 电极类型
|
|
||||||
"""
|
|
||||||
super().__init__(
|
|
||||||
name=name,
|
|
||||||
size_x=size_x,
|
|
||||||
size_y=size_y,
|
|
||||||
size_z=size_z,
|
|
||||||
category=category,
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
self._electrode_type = electrode_type
|
|
||||||
self._unilabos_state: Dict[str, Any] = {
|
|
||||||
"electrode_type": electrode_type,
|
|
||||||
"material": "",
|
|
||||||
"thickness": size_z,
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def electrode_type(self) -> str:
|
|
||||||
"""获取电极类型"""
|
|
||||||
return self._electrode_type
|
|
||||||
|
|
||||||
def load_state(self, state: Dict[str, Any]) -> None:
|
|
||||||
"""加载状态"""
|
|
||||||
super().load_state(state)
|
|
||||||
if isinstance(state, dict):
|
|
||||||
self._unilabos_state.update(state)
|
|
||||||
|
|
||||||
def serialize_state(self) -> Dict[str, Any]:
|
|
||||||
"""序列化状态"""
|
|
||||||
data = super().serialize_state()
|
|
||||||
data.update(self._unilabos_state)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
"""
|
|
||||||
弹夹架类定义 - 用于纽扣电池组装工作站
|
|
||||||
Magazine Holder Resource Classes
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
from typing import List, Optional
|
|
||||||
from pylabrobot.resources.coordinate import Coordinate
|
|
||||||
from pylabrobot.resources import ResourceHolder
|
|
||||||
from pylabrobot.resources.utils import create_ordered_items_2d
|
|
||||||
from unilabos.resources.itemized_carrier import ItemizedCarrier
|
|
||||||
|
|
||||||
|
|
||||||
def MagazineHolder_4_Cathode(name: str) -> ItemizedCarrier:
|
|
||||||
"""正极&铝箔弹夹 - 4个洞位 (2x2布局)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: 弹夹名称
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ItemizedCarrier: 包含4个槽位的弹夹架
|
|
||||||
"""
|
|
||||||
sites = create_ordered_items_2d(
|
|
||||||
klass=ResourceHolder,
|
|
||||||
num_items_x=2,
|
|
||||||
num_items_y=2,
|
|
||||||
dx=10.0,
|
|
||||||
dy=10.0,
|
|
||||||
dz=0.0,
|
|
||||||
item_dx=50.0,
|
|
||||||
item_dy=30.0,
|
|
||||||
size_x=40.0,
|
|
||||||
size_y=25.0,
|
|
||||||
size_z=40.0,
|
|
||||||
)
|
|
||||||
|
|
||||||
return ItemizedCarrier(
|
|
||||||
name=name,
|
|
||||||
size_x=120.0,
|
|
||||||
size_y=80.0,
|
|
||||||
size_z=50.0,
|
|
||||||
num_items_x=2,
|
|
||||||
num_items_y=2,
|
|
||||||
sites=sites,
|
|
||||||
category="magazine_holder",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def MagazineHolder_6_Cathode(name: str) -> ItemizedCarrier:
|
|
||||||
"""正极壳&平垫片弹夹 - 6个洞位 (2x3布局)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: 弹夹名称
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ItemizedCarrier: 包含6个槽位的弹夹架
|
|
||||||
"""
|
|
||||||
sites = create_ordered_items_2d(
|
|
||||||
klass=ResourceHolder,
|
|
||||||
num_items_x=3,
|
|
||||||
num_items_y=2,
|
|
||||||
dx=10.0,
|
|
||||||
dy=10.0,
|
|
||||||
dz=0.0,
|
|
||||||
item_dx=40.0,
|
|
||||||
item_dy=30.0,
|
|
||||||
size_x=35.0,
|
|
||||||
size_y=25.0,
|
|
||||||
size_z=40.0,
|
|
||||||
)
|
|
||||||
|
|
||||||
return ItemizedCarrier(
|
|
||||||
name=name,
|
|
||||||
size_x=150.0,
|
|
||||||
size_y=80.0,
|
|
||||||
size_z=50.0,
|
|
||||||
num_items_x=3,
|
|
||||||
num_items_y=2,
|
|
||||||
sites=sites,
|
|
||||||
category="magazine_holder",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def MagazineHolder_6_Anode(name: str) -> ItemizedCarrier:
|
|
||||||
"""负极壳&弹垫片弹夹 - 6个洞位 (2x3布局)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: 弹夹名称
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ItemizedCarrier: 包含6个槽位的弹夹架
|
|
||||||
"""
|
|
||||||
sites = create_ordered_items_2d(
|
|
||||||
klass=ResourceHolder,
|
|
||||||
num_items_x=3,
|
|
||||||
num_items_y=2,
|
|
||||||
dx=10.0,
|
|
||||||
dy=10.0,
|
|
||||||
dz=0.0,
|
|
||||||
item_dx=40.0,
|
|
||||||
item_dy=30.0,
|
|
||||||
size_x=35.0,
|
|
||||||
size_y=25.0,
|
|
||||||
size_z=40.0,
|
|
||||||
)
|
|
||||||
|
|
||||||
return ItemizedCarrier(
|
|
||||||
name=name,
|
|
||||||
size_x=150.0,
|
|
||||||
size_y=80.0,
|
|
||||||
size_z=50.0,
|
|
||||||
num_items_x=3,
|
|
||||||
num_items_y=2,
|
|
||||||
sites=sites,
|
|
||||||
category="magazine_holder",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def MagazineHolder_6_Battery(name: str) -> ItemizedCarrier:
|
|
||||||
"""成品弹夹 - 6个洞位 (3x2布局)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: 弹夹名称
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ItemizedCarrier: 包含6个槽位的弹夹架
|
|
||||||
"""
|
|
||||||
sites = create_ordered_items_2d(
|
|
||||||
klass=ResourceHolder,
|
|
||||||
num_items_x=3,
|
|
||||||
num_items_y=2,
|
|
||||||
dx=10.0,
|
|
||||||
dy=10.0,
|
|
||||||
dz=0.0,
|
|
||||||
item_dx=33.0,
|
|
||||||
item_dy=40.0,
|
|
||||||
size_x=30.0,
|
|
||||||
size_y=35.0,
|
|
||||||
size_z=40.0,
|
|
||||||
)
|
|
||||||
|
|
||||||
return ItemizedCarrier(
|
|
||||||
name=name,
|
|
||||||
size_x=120.0,
|
|
||||||
size_y=100.0,
|
|
||||||
size_z=50.0,
|
|
||||||
num_items_x=3,
|
|
||||||
num_items_y=2,
|
|
||||||
sites=sites,
|
|
||||||
category="magazine_holder",
|
|
||||||
)
|
|
||||||
|
|
||||||
@@ -1,653 +0,0 @@
|
|||||||
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.YB_bottles import (
|
|
||||||
YB_jia_yang_tou_da,
|
|
||||||
YB_ye_Bottle,
|
|
||||||
YB_ye_100ml_Bottle,
|
|
||||||
YB_gao_nian_ye_Bottle,
|
|
||||||
YB_5ml_fenyeping,
|
|
||||||
YB_20ml_fenyeping,
|
|
||||||
YB_pei_ye_xiao_Bottle,
|
|
||||||
YB_pei_ye_da_Bottle,
|
|
||||||
YB_qiang_tou,
|
|
||||||
)
|
|
||||||
# 命名约定:试剂瓶-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="Electrolyte_6VialCarrier",
|
|
||||||
)
|
|
||||||
carrier.num_items_x = 3
|
|
||||||
carrier.num_items_y = 2
|
|
||||||
carrier.num_items_z = 1
|
|
||||||
# for i in range(6):
|
|
||||||
# carrier[i] = YB_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="Electrolyte_1BottleCarrier",
|
|
||||||
)
|
|
||||||
carrier.num_items_x = 1
|
|
||||||
carrier.num_items_y = 1
|
|
||||||
carrier.num_items_z = 1
|
|
||||||
# carrier[0] = YB_Solution_Beaker(f"{name}_beaker_1")
|
|
||||||
return carrier
|
|
||||||
|
|
||||||
|
|
||||||
def YB_6StockCarrier(name: str) -> BottleCarrier:
|
|
||||||
"""6瓶载架 - 2x3布局"""
|
|
||||||
|
|
||||||
# 载架尺寸 (mm)
|
|
||||||
carrier_size_x = 127.8
|
|
||||||
carrier_size_y = 85.5
|
|
||||||
carrier_size_z = 50.0
|
|
||||||
|
|
||||||
# 瓶位尺寸
|
|
||||||
bottle_diameter = 20.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="6StockCarrier",
|
|
||||||
)
|
|
||||||
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] = YB_Solid_Stock(f"{name}_vial_{ordering[i]}")
|
|
||||||
return carrier
|
|
||||||
|
|
||||||
|
|
||||||
def YB_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="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(3):
|
|
||||||
# carrier[i] = YB_Solid_Vial(f"{name}_solidvial_{ordering[i]}")
|
|
||||||
# for i in range(3, 6):
|
|
||||||
# carrier[i] = YB_Liquid_Vial(f"{name}_liquidvial_{ordering[i]}")
|
|
||||||
return carrier
|
|
||||||
|
|
||||||
# 1瓶载架 - 单个中央位置
|
|
||||||
def YB_ye(name: str) -> BottleCarrier:
|
|
||||||
|
|
||||||
# 载架尺寸 (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="YB_ye",
|
|
||||||
)
|
|
||||||
carrier.num_items_x = 1
|
|
||||||
carrier.num_items_y = 1
|
|
||||||
carrier.num_items_z = 1
|
|
||||||
carrier[0] = YB_ye_Bottle(f"{name}_flask_1")
|
|
||||||
return carrier
|
|
||||||
|
|
||||||
|
|
||||||
# 高粘液瓶载架 - 单个中央位置
|
|
||||||
def YB_gaonianye(name: str) -> BottleCarrier:
|
|
||||||
|
|
||||||
# 载架尺寸 (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="YB_gaonianye",
|
|
||||||
)
|
|
||||||
carrier.num_items_x = 1
|
|
||||||
carrier.num_items_y = 1
|
|
||||||
carrier.num_items_z = 1
|
|
||||||
carrier[0] = YB_gao_nian_ye_Bottle(f"{name}_flask_1")
|
|
||||||
return carrier
|
|
||||||
|
|
||||||
|
|
||||||
# 100ml液体瓶载架 - 单个中央位置
|
|
||||||
def YB_100ml_yeti(name: str) -> BottleCarrier:
|
|
||||||
|
|
||||||
# 载架尺寸 (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="YB_100ml_yeti",
|
|
||||||
)
|
|
||||||
carrier.num_items_x = 1
|
|
||||||
carrier.num_items_y = 1
|
|
||||||
carrier.num_items_z = 1
|
|
||||||
carrier[0] = YB_ye_100ml_Bottle(f"{name}_flask_1")
|
|
||||||
return carrier
|
|
||||||
|
|
||||||
# 5ml分液瓶板 - 4x2布局,8个位置
|
|
||||||
def YB_5ml_fenyepingban(name: str) -> BottleCarrier:
|
|
||||||
|
|
||||||
|
|
||||||
# 载架尺寸 (mm)
|
|
||||||
carrier_size_x = 127.8
|
|
||||||
carrier_size_y = 85.5
|
|
||||||
carrier_size_z = 50.0
|
|
||||||
|
|
||||||
# 瓶位尺寸
|
|
||||||
bottle_diameter = 15.0
|
|
||||||
bottle_spacing_x = 42.0 # X方向间距
|
|
||||||
bottle_spacing_y = 35.0 # Y方向间距
|
|
||||||
|
|
||||||
# 计算起始位置 (居中排列)
|
|
||||||
start_x = (carrier_size_x - (4 - 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=4,
|
|
||||||
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="YB_5ml_fenyepingban",
|
|
||||||
)
|
|
||||||
carrier.num_items_x = 4
|
|
||||||
carrier.num_items_y = 2
|
|
||||||
carrier.num_items_z = 1
|
|
||||||
ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"]
|
|
||||||
for i in range(8):
|
|
||||||
carrier[i] = YB_5ml_fenyeping(f"{name}_vial_{ordering[i]}")
|
|
||||||
return carrier
|
|
||||||
|
|
||||||
# 20ml分液瓶板 - 4x2布局,8个位置
|
|
||||||
def YB_20ml_fenyepingban(name: str) -> BottleCarrier:
|
|
||||||
|
|
||||||
|
|
||||||
# 载架尺寸 (mm)
|
|
||||||
carrier_size_x = 127.8
|
|
||||||
carrier_size_y = 85.5
|
|
||||||
carrier_size_z = 70.0
|
|
||||||
|
|
||||||
# 瓶位尺寸
|
|
||||||
bottle_diameter = 20.0
|
|
||||||
bottle_spacing_x = 42.0 # X方向间距
|
|
||||||
bottle_spacing_y = 35.0 # Y方向间距
|
|
||||||
|
|
||||||
# 计算起始位置 (居中排列)
|
|
||||||
start_x = (carrier_size_x - (4 - 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=4,
|
|
||||||
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="YB_20ml_fenyepingban",
|
|
||||||
)
|
|
||||||
carrier.num_items_x = 4
|
|
||||||
carrier.num_items_y = 2
|
|
||||||
carrier.num_items_z = 1
|
|
||||||
ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"]
|
|
||||||
for i in range(8):
|
|
||||||
carrier[i] = YB_20ml_fenyeping(f"{name}_vial_{ordering[i]}")
|
|
||||||
return carrier
|
|
||||||
|
|
||||||
# 配液瓶(小)板 - 4x2布局,8个位置
|
|
||||||
def YB_peiyepingxiaoban(name: str) -> BottleCarrier:
|
|
||||||
|
|
||||||
|
|
||||||
# 载架尺寸 (mm)
|
|
||||||
carrier_size_x = 127.8
|
|
||||||
carrier_size_y = 85.5
|
|
||||||
carrier_size_z = 65.0
|
|
||||||
|
|
||||||
# 瓶位尺寸
|
|
||||||
bottle_diameter = 35.0
|
|
||||||
bottle_spacing_x = 42.0 # X方向间距
|
|
||||||
bottle_spacing_y = 35.0 # Y方向间距
|
|
||||||
|
|
||||||
# 计算起始位置 (居中排列)
|
|
||||||
start_x = (carrier_size_x - (4 - 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=4,
|
|
||||||
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="YB_peiyepingxiaoban",
|
|
||||||
)
|
|
||||||
carrier.num_items_x = 4
|
|
||||||
carrier.num_items_y = 2
|
|
||||||
carrier.num_items_z = 1
|
|
||||||
ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"]
|
|
||||||
for i in range(8):
|
|
||||||
carrier[i] = YB_pei_ye_xiao_Bottle(f"{name}_bottle_{ordering[i]}")
|
|
||||||
return carrier
|
|
||||||
|
|
||||||
|
|
||||||
# 配液瓶(大)板 - 2x2布局,4个位置
|
|
||||||
def YB_peiyepingdaban(name: str) -> BottleCarrier:
|
|
||||||
|
|
||||||
# 载架尺寸 (mm)
|
|
||||||
carrier_size_x = 127.8
|
|
||||||
carrier_size_y = 85.5
|
|
||||||
carrier_size_z = 95.0
|
|
||||||
|
|
||||||
# 瓶位尺寸
|
|
||||||
bottle_diameter = 55.0
|
|
||||||
bottle_spacing_x = 60.0 # X方向间距
|
|
||||||
bottle_spacing_y = 60.0 # Y方向间距
|
|
||||||
|
|
||||||
# 计算起始位置 (居中排列)
|
|
||||||
start_x = (carrier_size_x - (2 - 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=2,
|
|
||||||
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="YB_peiyepingdaban",
|
|
||||||
)
|
|
||||||
carrier.num_items_x = 2
|
|
||||||
carrier.num_items_y = 2
|
|
||||||
carrier.num_items_z = 1
|
|
||||||
ordering = ["A1", "A2", "B1", "B2"]
|
|
||||||
for i in range(4):
|
|
||||||
carrier[i] = YB_pei_ye_da_Bottle(f"{name}_bottle_{ordering[i]}")
|
|
||||||
return carrier
|
|
||||||
|
|
||||||
# 加样头(大)板 - 1x1布局,1个位置
|
|
||||||
def YB_jia_yang_tou_da_Carrier(name: str) -> BottleCarrier:
|
|
||||||
|
|
||||||
# 载架尺寸 (mm)
|
|
||||||
carrier_size_x = 127.8
|
|
||||||
carrier_size_y = 85.5
|
|
||||||
carrier_size_z = 95.0
|
|
||||||
|
|
||||||
# 瓶位尺寸
|
|
||||||
bottle_diameter = 35.0
|
|
||||||
bottle_spacing_x = 42.0 # X方向间距
|
|
||||||
bottle_spacing_y = 35.0 # Y方向间距
|
|
||||||
|
|
||||||
# 计算起始位置 (居中排列)
|
|
||||||
start_x = (carrier_size_x - (1 - 1) * bottle_spacing_x - bottle_diameter) / 2
|
|
||||||
start_y = (carrier_size_y - (1 - 1) * bottle_spacing_y - bottle_diameter) / 2
|
|
||||||
|
|
||||||
sites = create_ordered_items_2d(
|
|
||||||
klass=ResourceHolder,
|
|
||||||
num_items_x=1,
|
|
||||||
num_items_y=1,
|
|
||||||
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="YB_jia_yang_tou_da_Carrier",
|
|
||||||
)
|
|
||||||
carrier.num_items_x = 1
|
|
||||||
carrier.num_items_y = 1
|
|
||||||
carrier.num_items_z = 1
|
|
||||||
carrier[0] = YB_jia_yang_tou_da(f"{name}_head_1")
|
|
||||||
return carrier
|
|
||||||
|
|
||||||
|
|
||||||
def YB_shi_pei_qi_kuai(name: str) -> BottleCarrier:
|
|
||||||
"""适配器块 - 单个中央位置"""
|
|
||||||
|
|
||||||
# 载架尺寸 (mm)
|
|
||||||
carrier_size_x = 127.8
|
|
||||||
carrier_size_y = 85.5
|
|
||||||
carrier_size_z = 30.0
|
|
||||||
|
|
||||||
# 适配器尺寸
|
|
||||||
adapter_diameter = 80.0
|
|
||||||
|
|
||||||
# 计算中央位置
|
|
||||||
center_x = (carrier_size_x - adapter_diameter) / 2
|
|
||||||
center_y = (carrier_size_y - adapter_diameter) / 2
|
|
||||||
center_z = 0.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=adapter_diameter,
|
|
||||||
resource_size_y=adapter_diameter,
|
|
||||||
name_prefix=name,
|
|
||||||
),
|
|
||||||
model="YB_shi_pei_qi_kuai",
|
|
||||||
)
|
|
||||||
carrier.num_items_x = 1
|
|
||||||
carrier.num_items_y = 1
|
|
||||||
carrier.num_items_z = 1
|
|
||||||
# 适配器块本身不包含瓶子,只是一个支撑结构
|
|
||||||
return carrier
|
|
||||||
|
|
||||||
|
|
||||||
def YB_qiang_tou_he(name: str) -> BottleCarrier:
|
|
||||||
"""枪头盒 - 8x12布局,96个位置"""
|
|
||||||
|
|
||||||
# 载架尺寸 (mm)
|
|
||||||
carrier_size_x = 127.8
|
|
||||||
carrier_size_y = 85.5
|
|
||||||
carrier_size_z = 55.0
|
|
||||||
|
|
||||||
# 枪头尺寸
|
|
||||||
tip_diameter = 10.0
|
|
||||||
tip_spacing_x = 9.0 # X方向间距
|
|
||||||
tip_spacing_y = 9.0 # Y方向间距
|
|
||||||
|
|
||||||
# 计算起始位置 (居中排列)
|
|
||||||
start_x = (carrier_size_x - (12 - 1) * tip_spacing_x - tip_diameter) / 2
|
|
||||||
start_y = (carrier_size_y - (8 - 1) * tip_spacing_y - tip_diameter) / 2
|
|
||||||
|
|
||||||
sites = create_ordered_items_2d(
|
|
||||||
klass=ResourceHolder,
|
|
||||||
num_items_x=12,
|
|
||||||
num_items_y=8,
|
|
||||||
dx=start_x,
|
|
||||||
dy=start_y,
|
|
||||||
dz=5.0,
|
|
||||||
item_dx=tip_spacing_x,
|
|
||||||
item_dy=tip_spacing_y,
|
|
||||||
size_x=tip_diameter,
|
|
||||||
size_y=tip_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="YB_qiang_tou_he",
|
|
||||||
)
|
|
||||||
carrier.num_items_x = 12
|
|
||||||
carrier.num_items_y = 8
|
|
||||||
carrier.num_items_z = 1
|
|
||||||
# 创建96个枪头
|
|
||||||
for i in range(96):
|
|
||||||
row = chr(65 + i // 12) # A-H
|
|
||||||
col = (i % 12) + 1 # 1-12
|
|
||||||
carrier[i] = YB_qiang_tou(f"{name}_tip_{row}{col}")
|
|
||||||
return carrier
|
|
||||||
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
|
|
||||||
# 工厂函数
|
|
||||||
"""加样头(大)"""
|
|
||||||
def YB_jia_yang_tou_da(
|
|
||||||
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="YB_jia_yang_tou_da",
|
|
||||||
)
|
|
||||||
|
|
||||||
"""液1x1"""
|
|
||||||
def YB_ye_Bottle(
|
|
||||||
name: str,
|
|
||||||
diameter: float = 40.0,
|
|
||||||
height: float = 70.0,
|
|
||||||
max_volume: float = 50000.0, # 50mL
|
|
||||||
barcode: str = None,
|
|
||||||
) -> Bottle:
|
|
||||||
"""创建液体瓶"""
|
|
||||||
return Bottle(
|
|
||||||
name=name,
|
|
||||||
diameter=diameter,
|
|
||||||
height=height,
|
|
||||||
max_volume=max_volume,
|
|
||||||
barcode=barcode,
|
|
||||||
model="YB_ye_Bottle",
|
|
||||||
)
|
|
||||||
|
|
||||||
"""100ml液体"""
|
|
||||||
def YB_ye_100ml_Bottle(
|
|
||||||
name: str,
|
|
||||||
diameter: float = 50.0,
|
|
||||||
height: float = 90.0,
|
|
||||||
max_volume: float = 100000.0, # 100mL
|
|
||||||
barcode: str = None,
|
|
||||||
) -> Bottle:
|
|
||||||
"""创建100ml液体瓶"""
|
|
||||||
return Bottle(
|
|
||||||
name=name,
|
|
||||||
diameter=diameter,
|
|
||||||
height=height,
|
|
||||||
max_volume=max_volume,
|
|
||||||
barcode=barcode,
|
|
||||||
model="YB_100ml_yeti",
|
|
||||||
)
|
|
||||||
|
|
||||||
"""高粘液"""
|
|
||||||
def YB_gao_nian_ye_Bottle(
|
|
||||||
name: str,
|
|
||||||
diameter: float = 40.0,
|
|
||||||
height: float = 70.0,
|
|
||||||
max_volume: float = 50000.0, # 50mL
|
|
||||||
barcode: str = None,
|
|
||||||
) -> Bottle:
|
|
||||||
"""创建高粘液瓶"""
|
|
||||||
return Bottle(
|
|
||||||
name=name,
|
|
||||||
diameter=diameter,
|
|
||||||
height=height,
|
|
||||||
max_volume=max_volume,
|
|
||||||
barcode=barcode,
|
|
||||||
model="High_Viscosity_Liquid",
|
|
||||||
)
|
|
||||||
|
|
||||||
"""5ml分液瓶"""
|
|
||||||
def YB_5ml_fenyeping(
|
|
||||||
name: str,
|
|
||||||
diameter: float = 20.0,
|
|
||||||
height: float = 50.0,
|
|
||||||
max_volume: float = 5000.0, # 5mL
|
|
||||||
barcode: str = None,
|
|
||||||
) -> Bottle:
|
|
||||||
"""创建5ml分液瓶"""
|
|
||||||
return Bottle(
|
|
||||||
name=name,
|
|
||||||
diameter=diameter,
|
|
||||||
height=height,
|
|
||||||
max_volume=max_volume,
|
|
||||||
barcode=barcode,
|
|
||||||
model="YB_5ml_fenyeping",
|
|
||||||
)
|
|
||||||
|
|
||||||
"""20ml分液瓶"""
|
|
||||||
def YB_20ml_fenyeping(
|
|
||||||
name: str,
|
|
||||||
diameter: float = 30.0,
|
|
||||||
height: float = 65.0,
|
|
||||||
max_volume: float = 20000.0, # 20mL
|
|
||||||
barcode: str = None,
|
|
||||||
) -> Bottle:
|
|
||||||
"""创建20ml分液瓶"""
|
|
||||||
return Bottle(
|
|
||||||
name=name,
|
|
||||||
diameter=diameter,
|
|
||||||
height=height,
|
|
||||||
max_volume=max_volume,
|
|
||||||
barcode=barcode,
|
|
||||||
model="YB_20ml_fenyeping",
|
|
||||||
)
|
|
||||||
|
|
||||||
"""配液瓶(小)"""
|
|
||||||
def YB_pei_ye_xiao_Bottle(
|
|
||||||
name: str,
|
|
||||||
diameter: float = 35.0,
|
|
||||||
height: float = 60.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="YB_pei_ye_xiao_Bottle",
|
|
||||||
)
|
|
||||||
|
|
||||||
"""配液瓶(大)"""
|
|
||||||
def YB_pei_ye_da_Bottle(
|
|
||||||
name: str,
|
|
||||||
diameter: float = 55.0,
|
|
||||||
height: float = 100.0,
|
|
||||||
max_volume: float = 150000.0, # 150mL
|
|
||||||
barcode: str = None,
|
|
||||||
) -> Bottle:
|
|
||||||
"""创建配液瓶(大)"""
|
|
||||||
return Bottle(
|
|
||||||
name=name,
|
|
||||||
diameter=diameter,
|
|
||||||
height=height,
|
|
||||||
max_volume=max_volume,
|
|
||||||
barcode=barcode,
|
|
||||||
model="YB_pei_ye_da_Bottle",
|
|
||||||
)
|
|
||||||
|
|
||||||
"""枪头"""
|
|
||||||
def YB_qiang_tou(
|
|
||||||
name: str,
|
|
||||||
diameter: float = 10.0,
|
|
||||||
height: float = 50.0,
|
|
||||||
max_volume: float = 1000.0, # 1mL
|
|
||||||
barcode: str = None,
|
|
||||||
) -> Bottle:
|
|
||||||
"""创建枪头"""
|
|
||||||
return Bottle(
|
|
||||||
name=name,
|
|
||||||
diameter=diameter,
|
|
||||||
height=height,
|
|
||||||
max_volume=max_volume,
|
|
||||||
barcode=barcode,
|
|
||||||
model="YB_qiang_tou",
|
|
||||||
)
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
from unilabos.resources.warehouse import WareHouse, YB_warehouse_factory
|
|
||||||
|
|
||||||
|
|
||||||
def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
|
|
||||||
"""创建BioYond 4x1x4仓库"""
|
|
||||||
return YB_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 YB_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
|
|
||||||
)
|
|
||||||
# 定义benyond的堆栈
|
|
||||||
def bioyond_warehouse_1x2x2(name: str) -> WareHouse:
|
|
||||||
"""创建BioYond 4x1x4仓库"""
|
|
||||||
return YB_warehouse_factory(
|
|
||||||
name=name,
|
|
||||||
num_items_x=2,
|
|
||||||
num_items_y=2,
|
|
||||||
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="YB_warehouse",
|
|
||||||
)
|
|
||||||
|
|
||||||
def bioyond_warehouse_2x2x1(name: str) -> WareHouse:
|
|
||||||
"""创建BioYond 2x2x1仓库(自动堆栈)"""
|
|
||||||
return YB_warehouse_factory(
|
|
||||||
name=name,
|
|
||||||
num_items_x=2,
|
|
||||||
num_items_y=2,
|
|
||||||
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="YB_warehouse",
|
|
||||||
)
|
|
||||||
|
|
||||||
def bioyond_warehouse_10x1x1(name: str) -> WareHouse:
|
|
||||||
"""创建BioYond 4x1x4仓库"""
|
|
||||||
return YB_warehouse_factory(
|
|
||||||
name=name,
|
|
||||||
num_items_x=10,
|
|
||||||
num_items_y=1,
|
|
||||||
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",
|
|
||||||
)
|
|
||||||
def bioyond_warehouse_1x3x3(name: str) -> WareHouse:
|
|
||||||
"""创建BioYond 4x1x4仓库"""
|
|
||||||
return YB_warehouse_factory(
|
|
||||||
name=name,
|
|
||||||
num_items_x=1,
|
|
||||||
num_items_y=3,
|
|
||||||
num_items_z=3,
|
|
||||||
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_2x1x3(name: str) -> WareHouse:
|
|
||||||
"""创建BioYond 4x1x4仓库"""
|
|
||||||
return YB_warehouse_factory(
|
|
||||||
name=name,
|
|
||||||
num_items_x=2,
|
|
||||||
num_items_y=1,
|
|
||||||
num_items_z=3,
|
|
||||||
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_3x3x1(name: str) -> WareHouse:
|
|
||||||
"""创建BioYond 4x1x4仓库"""
|
|
||||||
return YB_warehouse_factory(
|
|
||||||
name=name,
|
|
||||||
num_items_x=3,
|
|
||||||
num_items_y=3,
|
|
||||||
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",
|
|
||||||
)
|
|
||||||
def bioyond_warehouse_5x1x1(name: str) -> WareHouse:
|
|
||||||
"""创建BioYond 4x1x4仓库"""
|
|
||||||
return YB_warehouse_factory(
|
|
||||||
name=name,
|
|
||||||
num_items_x=5,
|
|
||||||
num_items_y=1,
|
|
||||||
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",
|
|
||||||
)
|
|
||||||
def bioyond_warehouse_3x3x1_2(name: str) -> WareHouse:
|
|
||||||
"""创建BioYond 4x1x4仓库"""
|
|
||||||
return YB_warehouse_factory(
|
|
||||||
name=name,
|
|
||||||
num_items_x=3,
|
|
||||||
num_items_y=3,
|
|
||||||
num_items_z=1,
|
|
||||||
dx=12.0,
|
|
||||||
dy=12.0,
|
|
||||||
dz=12.0,
|
|
||||||
item_dx=137.0,
|
|
||||||
item_dy=96.0,
|
|
||||||
item_dz=120.0,
|
|
||||||
category="warehouse",
|
|
||||||
)
|
|
||||||
|
|
||||||
def bioyond_warehouse_liquid_and_lid_handling(name: str) -> WareHouse:
|
|
||||||
"""创建BioYond开关盖加液模块台面"""
|
|
||||||
return YB_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
|
|
||||||
)
|
|
||||||
|
|
||||||
def bioyond_warehouse_3x5x1(name: str) -> WareHouse:
|
|
||||||
"""创建BioYond 3x5x1仓库(手动堆栈)"""
|
|
||||||
return YB_warehouse_factory(
|
|
||||||
name=name,
|
|
||||||
num_items_x=3,
|
|
||||||
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",
|
|
||||||
)
|
|
||||||
|
|
||||||
def bioyond_warehouse_20x1x1(name: str) -> WareHouse:
|
|
||||||
"""创建BioYond 20x1x1仓库(粉末加样头堆栈)"""
|
|
||||||
return YB_warehouse_factory(
|
|
||||||
name=name,
|
|
||||||
num_items_x=20,
|
|
||||||
num_items_y=1,
|
|
||||||
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",
|
|
||||||
)
|
|
||||||
311
unilabos/resources/bioyond/warehouses.py
Normal file
311
unilabos/resources/bioyond/warehouses.py
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
from unilabos.resources.warehouse import WareHouse, warehouse_factory
|
||||||
|
|
||||||
|
# ================ 反应站相关堆栈 ================
|
||||||
|
|
||||||
|
def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 4x4x1仓库 (左侧堆栈: A01~D04)
|
||||||
|
|
||||||
|
使用行优先排序,前端展示为:
|
||||||
|
A01 | A02 | A03 | A04
|
||||||
|
B01 | B02 | B03 | B04
|
||||||
|
C01 | C02 | C03 | C04
|
||||||
|
D01 | D02 | D03 | D04
|
||||||
|
"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=4, # 4列
|
||||||
|
num_items_y=4, # 4行
|
||||||
|
num_items_z=1,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=147.0,
|
||||||
|
item_dy=106.0,
|
||||||
|
item_dz=130.0,
|
||||||
|
category="warehouse",
|
||||||
|
col_offset=0, # 从01开始: A01, A02, A03, A04
|
||||||
|
layout="row-major", # ⭐ 改为行优先排序
|
||||||
|
)
|
||||||
|
|
||||||
|
def bioyond_warehouse_1x4x4_right(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 4x4x1仓库 (右侧堆栈: A05~D08)"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=4,
|
||||||
|
num_items_y=4,
|
||||||
|
num_items_z=1,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=147.0,
|
||||||
|
item_dy=106.0,
|
||||||
|
item_dz=130.0,
|
||||||
|
category="warehouse",
|
||||||
|
col_offset=4, # 从05开始: A05, A06, A07, A08
|
||||||
|
layout="row-major", # ⭐ 改为行优先排序
|
||||||
|
)
|
||||||
|
|
||||||
|
def bioyond_warehouse_density_vial(name: str) -> WareHouse:
|
||||||
|
"""创建测量小瓶仓库(测密度) A01~B03"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=3, # 3列(01-03)
|
||||||
|
num_items_y=2, # 2行(A-B)
|
||||||
|
num_items_z=1, # 1层
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=40.0,
|
||||||
|
item_dy=40.0,
|
||||||
|
item_dz=50.0,
|
||||||
|
# 用更小的 resource_size 来表现 "小点的孔位"
|
||||||
|
resource_size_x=30.0,
|
||||||
|
resource_size_y=30.0,
|
||||||
|
resource_size_z=12.0,
|
||||||
|
category="warehouse",
|
||||||
|
col_offset=0,
|
||||||
|
layout="row-major",
|
||||||
|
)
|
||||||
|
|
||||||
|
def bioyond_warehouse_reagent_storage(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond站内试剂存放堆栈(A01~A02, 1行×2列)"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=2, # 2列(01-02)
|
||||||
|
num_items_y=1, # 1行(A)
|
||||||
|
num_items_z=1, # 1层
|
||||||
|
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_tipbox_storage(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond站内Tip盒堆栈(A01~B03),用于存放枪头盒"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=3, # 3列(01-03)
|
||||||
|
num_items_y=2, # 2行(A-B)
|
||||||
|
num_items_z=1, # 1层
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
col_offset=0,
|
||||||
|
layout="row-major",
|
||||||
|
)
|
||||||
|
|
||||||
|
def bioyond_warehouse_liquid_preparation(name: str) -> WareHouse:
|
||||||
|
"""已弃用,创建BioYond移液站内10%分装液体准备仓库(A01~B04)"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=4, # 4列(01-04)
|
||||||
|
num_items_y=2, # 2行(A-B)
|
||||||
|
num_items_z=1, # 1层
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
col_offset=0,
|
||||||
|
layout="row-major",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ================ 配液站相关堆栈 ================
|
||||||
|
|
||||||
|
def bioyond_warehouse_reagent_stack(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 试剂堆栈 2x4x1 (2行×4列: A01-A04, B01-B04)
|
||||||
|
|
||||||
|
使用行优先排序,前端展示为:
|
||||||
|
A01 | A02 | A03 | A04
|
||||||
|
B01 | B02 | B03 | B04
|
||||||
|
"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=4, # 4列 (01-04)
|
||||||
|
num_items_y=2, # 2行 (A-B)
|
||||||
|
num_items_z=1, # 1层
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=147.0,
|
||||||
|
item_dy=106.0,
|
||||||
|
item_dz=130.0,
|
||||||
|
category="warehouse",
|
||||||
|
col_offset=0, # 从01开始
|
||||||
|
layout="row-major", # ⭐ 使用行优先排序: A01,A02,A03,A04, B01,B02,B03,B04
|
||||||
|
)
|
||||||
|
|
||||||
|
# 定义bioyond的堆栈
|
||||||
|
|
||||||
|
# =================== Other ===================
|
||||||
|
|
||||||
|
def bioyond_warehouse_1x4x2(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 4x2x1仓库"""
|
||||||
|
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_1x2x2(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 1x2x2仓库"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=1,
|
||||||
|
num_items_y=2,
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
|
||||||
|
def bioyond_warehouse_10x1x1(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 10x1x1仓库"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=10,
|
||||||
|
num_items_y=1,
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
|
||||||
|
def bioyond_warehouse_1x3x3(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 1x3x3仓库"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=1,
|
||||||
|
num_items_y=3,
|
||||||
|
num_items_z=3,
|
||||||
|
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_2x1x3(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 2x1x3仓库"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=2,
|
||||||
|
num_items_y=1,
|
||||||
|
num_items_z=3,
|
||||||
|
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_3x3x1(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 3x3x1仓库"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=3,
|
||||||
|
num_items_y=3,
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
|
||||||
|
def bioyond_warehouse_5x1x1(name: str) -> WareHouse:
|
||||||
|
"""已弃用:创建BioYond 5x1x1仓库"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=5,
|
||||||
|
num_items_y=1,
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
|
||||||
|
def bioyond_warehouse_3x3x1_2(name: str) -> WareHouse:
|
||||||
|
"""已弃用:创建BioYond 3x3x1仓库"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=3,
|
||||||
|
num_items_y=3,
|
||||||
|
num_items_z=1,
|
||||||
|
dx=12.0,
|
||||||
|
dy=12.0,
|
||||||
|
dz=12.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
def bioyond_warehouse_1x8x4(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 8x4x1反应站堆栈(A01~D08)"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=8, # 8列(01-08)
|
||||||
|
num_items_y=4, # 4行(A-D)
|
||||||
|
num_items_z=1, # 1层
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=147.0,
|
||||||
|
item_dy=106.0,
|
||||||
|
item_dz=130.0,
|
||||||
|
category="warehouse",
|
||||||
|
)
|
||||||
@@ -29,7 +29,7 @@ class Bottle(Well):
|
|||||||
size_x: float = 0.0,
|
size_x: float = 0.0,
|
||||||
size_y: float = 0.0,
|
size_y: float = 0.0,
|
||||||
size_z: float = 0.0,
|
size_z: float = 0.0,
|
||||||
barcode: Optional[str] = None,
|
barcode: Optional[str] = "",
|
||||||
category: str = "container",
|
category: str = "container",
|
||||||
model: Optional[str] = None,
|
model: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
|||||||
@@ -664,7 +664,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
if bCreate:
|
if bCreate:
|
||||||
self.lab_logger().trace(f"Status created: {device_id}.{property_name} = {msg.data}")
|
self.lab_logger().trace(f"Status created: {device_id}.{property_name} = {msg.data}")
|
||||||
else:
|
else:
|
||||||
self.lab_logger().trace(f"Status updated: {device_id}.{property_name} = {msg.data}")
|
self.lab_logger().debug(f"Status updated: {device_id}.{property_name} = {msg.data}")
|
||||||
|
|
||||||
def send_goal(
|
def send_goal(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -404,6 +404,7 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
|
|||||||
|
|
||||||
return result_future.result
|
return result_future.result
|
||||||
|
|
||||||
|
"""还没有改过的部分"""
|
||||||
|
|
||||||
def _setup_hardware_proxy(
|
def _setup_hardware_proxy(
|
||||||
self, device: ROS2DeviceNode, communication_device: ROS2DeviceNode, read_method, write_method
|
self, device: ROS2DeviceNode, communication_device: ROS2DeviceNode, read_method, write_method
|
||||||
|
|||||||
34
unilabos/test/experiments/chinwe.json
Normal file
34
unilabos/test/experiments/chinwe.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "ChinWeStation",
|
||||||
|
"name": "分液工作站",
|
||||||
|
"children": [],
|
||||||
|
"parent": null,
|
||||||
|
"type": "device",
|
||||||
|
"class": "separator.chinwe",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"port": "192.168.31.13:8899",
|
||||||
|
"baudrate": 9600,
|
||||||
|
"pump_ids": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"motor_ids": [
|
||||||
|
4,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"sensor_id": 6,
|
||||||
|
"sensor_threshold": 300
|
||||||
|
},
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": []
|
||||||
|
}
|
||||||
@@ -24,9 +24,9 @@
|
|||||||
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
|
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
|
||||||
},
|
},
|
||||||
"material_type_mappings": {
|
"material_type_mappings": {
|
||||||
"烧杯": "YB_1FlaskCarrier",
|
"烧杯": "BIOYOND_PolymerStation_1FlaskCarrier",
|
||||||
"试剂瓶": "YB_1BottleCarrier",
|
"试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier",
|
||||||
"样品板": "YB_6VialCarrier"
|
"样品板": "BIOYOND_PolymerStation_6VialCarrier"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"deck": {
|
"deck": {
|
||||||
|
|||||||
@@ -192,18 +192,6 @@ def configure_logger(loglevel=None, working_dir=None):
|
|||||||
# 添加处理器到根日志记录器
|
# 添加处理器到根日志记录器
|
||||||
root_logger.addHandler(console_handler)
|
root_logger.addHandler(console_handler)
|
||||||
|
|
||||||
# 降低第三方库的日志级别,避免过多输出
|
|
||||||
# pymodbus 库的日志太详细,设置为 WARNING
|
|
||||||
logging.getLogger('pymodbus').setLevel(logging.WARNING)
|
|
||||||
logging.getLogger('pymodbus.logging').setLevel(logging.WARNING)
|
|
||||||
logging.getLogger('pymodbus.logging.base').setLevel(logging.WARNING)
|
|
||||||
logging.getLogger('pymodbus.logging.decoders').setLevel(logging.WARNING)
|
|
||||||
|
|
||||||
# websockets 库的日志输出较多,设置为 WARNING
|
|
||||||
logging.getLogger('websockets').setLevel(logging.WARNING)
|
|
||||||
logging.getLogger('websockets.client').setLevel(logging.WARNING)
|
|
||||||
logging.getLogger('websockets.server').setLevel(logging.WARNING)
|
|
||||||
|
|
||||||
# 如果指定了工作目录,添加文件处理器
|
# 如果指定了工作目录,添加文件处理器
|
||||||
if working_dir is not None:
|
if working_dir is not None:
|
||||||
logs_dir = os.path.join(working_dir, "logs")
|
logs_dir = os.path.join(working_dir, "logs")
|
||||||
|
|||||||
Reference in New Issue
Block a user