Compare commits

...

25 Commits

Author SHA1 Message Date
Xuwznln
c8d16c7024 update todo 2025-10-11 13:53:17 +08:00
Junhan Chang
25d46dc9d5 pass the tests 2025-10-11 07:20:34 +08:00
Junhan Chang
88c4d1a9d1 modify bioyond/plr converter, bioyond resource registry, and tests 2025-10-11 04:59:59 +08:00
Xuwznln
81fd8291c5 update todo 2025-10-11 03:38:59 +08:00
Xuwznln
3a11eb90d4 feat: 允许返回非本节点物料,后面可以通过decoration进行区分,就不进行warning了 2025-10-11 03:38:14 +08:00
Xuwznln
387866b9c9 修复同步任务报错不显示的bug 2025-10-11 03:14:12 +08:00
Xuwznln
7f40f141f6 移动内部action以兼容host node 2025-10-11 03:11:17 +08:00
Xuwznln
6fc7ed1b88 过滤本地动作 2025-10-11 03:06:37 +08:00
Xuwznln
93f0e08d75 fix host_node test_resource error 2025-10-11 03:04:15 +08:00
Xuwznln
4b43734b55 fix host_node test_resource error 2025-10-11 02:57:14 +08:00
Xuwznln
174b1914d4 fix host_node error 2025-10-11 02:54:15 +08:00
Xuwznln
704e13f030 新增test_resource动作 2025-10-11 02:53:18 +08:00
Xuwznln
0c42d60cf2 更新transfer_resource_to_another参数,支持spot入参 2025-10-11 02:41:37 +08:00
Xuwznln
df33e1a214 修复transfer_resource_to_another生成 2025-10-11 01:12:56 +08:00
Xuwznln
1f49924966 修复资源添加 2025-10-11 00:58:56 +08:00
Xuwznln
609b6006e8 支持选择器注册表自动生成
支持转运物料
2025-10-11 00:57:22 +08:00
Xuwznln
67c01271b7 add update remove 2025-10-10 20:15:16 +08:00
Xuwznln
a1783f489e Merge remote-tracking branch 'origin/workstation_dev_YB2' into dev
# Conflicts:
#	unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py
#	unilabos/devices/workstation/bioyond_studio/station.py
#	unilabos/resources/graphio.py
2025-10-10 15:38:45 +08:00
Xuwznln
a8f6527de9 修复to_plr_resources 2025-10-10 15:30:26 +08:00
Xuwznln
5610c28b67 更新物料接口 2025-10-10 07:13:59 +08:00
Junhan Chang
cfc1ee6e79 Workstation templates: Resources and its CRUD, and workstation tasks (#95)
* coin_cell_station draft

* refactor: rename "station_resource" to "deck"

* add standardized BIOYOND resources: bottle_carrier, bottle

* refactor and add BIOYOND resources tests

* add BIOYOND deck assignment and pass all tests

* fix: update resource with correct structure; remove deprecated liquid_handler set_group action

* feat: 将新威电池测试系统驱动与配置文件并入 workstation_dev_YB2 (#92)

* feat: 新威电池测试系统驱动与注册文件

* feat: bring neware driver & battery.json into workstation_dev_YB2

* add bioyond studio draft

* bioyond station with communication init and resource sync

* fix bioyond station and registry

* create/update resources with POST/PUT for big amount/ small amount data

* refactor: add itemized_carrier instead of carrier consists of ResourceHolder

* create warehouse by factory func

* update bioyond launch json

* add child_size for itemized_carrier

* fix bioyond resource io

---------

Co-authored-by: h840473807 <47357934+h840473807@users.noreply.github.com>
Co-authored-by: Xie Qiming <97236197+Andy6M@users.noreply.github.com>
2025-09-30 17:23:13 +08:00
Junhan Chang
709eb0d91c Merge branch 'dev' of https://github.com/dptech-corp/Uni-Lab-OS into dev 2025-09-27 00:10:59 +08:00
Junhan Chang
14b7d52825 create/update resources with POST/PUT for big amount/ small amount data 2025-09-26 23:25:50 +08:00
LccLink
c6c2da69ba frontend_docs 2025-09-26 23:20:22 +08:00
Junhan Chang
622e579063 fix: update resource with correct structure; remove deprecated liquid_handler set_group action 2025-09-26 20:24:15 +08:00
59 changed files with 7258 additions and 637 deletions

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@ configs/
temp/ temp/
output/ output/
unilabos_data/ unilabos_data/
pyrightconfig.json
## Python ## Python
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files

View File

@@ -111,8 +111,8 @@ new_device: # 设备名,要唯一
1.`auto-` 开头的动作:从你 Python 类的方法自动生成 1.`auto-` 开头的动作:从你 Python 类的方法自动生成
2. 通用的驱动动作: 2. 通用的驱动动作:
- `_execute_driver_command`:同步执行驱动命令 - `_execute_driver_command`:同步执行驱动命令(仅本地可用)
- `_execute_driver_command_async`:异步执行驱动命令 - `_execute_driver_command_async`:异步执行驱动命令(仅本地可用)
### 如果要手动定义动作 ### 如果要手动定义动作

View File

@@ -24,6 +24,8 @@ class WSConfig:
max_reconnect_attempts = 999 # 最大重连次数 max_reconnect_attempts = 999 # 最大重连次数
ping_interval = 30 # ping间隔 ping_interval = 30 # ping间隔
``` ```
您可以进入实验室点击左下角的头像在实验室详情中获取所在实验室的ak sk
![copy_aksk.gif](image/copy_aksk.gif)
### 完整配置示例 ### 完整配置示例

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

View File

@@ -245,3 +245,78 @@ unilab --ak your_ak --sk your_sk --port 8080 --disable_browser
- 检查图谱文件格式是否正确 - 检查图谱文件格式是否正确
- 验证设备连接和端点配置 - 验证设备连接和端点配置
- 确保注册表路径正确 - 确保注册表路径正确
## 页面操作
### 1. 启动成功
当您启动成功后,可以看到物料列表,节点模版和组态图如图展示
![material.png](image/material.png)
### 2. 根据需求创建设备和物料
我们可以做一个简单的案例
* 在容器1中加入水
* 通过传输泵将容器1中的水转移到容器2中
#### 2.1 添加所需的设备和物料
仪器设备work_station中的workstation 数量x1
仪器设备virtual_device中的virtual_transfer_pump 数量x1
物料耗材container中的container 数量x2
#### 2.2 将设备和物料根据父子关系进行关联
当我们添加设备时,仪器耗材模块的物料列表也会实时更新
我们需要将设备和物料拖拽到workstation中并在画布上将它们连接起来就像真实的设备操作一样
![links.png](image/links.png)
### 3. 创建工作流
进入工作流模块 → 点击"我创建的" → 新建工作流
![new.png](image/new.png)
#### 3.1 新增工作流节点
我们可以进入指定工作流,在空白处右键
* 选择Laboratory→host_node中的creat_resource
* 选择Laboratory→workstation中的PumpTransferProtocol
![creatworkfollow.gif](image/creatworkfollow.gif)
#### 3.2 配置节点参数
根据案例,工作流包含两个步骤:
1. 使用creat_resource在容器中创建水
2. 通过泵传输协议将水传输到另一个容器
我们点击creat_resource卡片上的编辑按钮来配置参数⭐
class_name container
device_id workstation
liquid_input_slot 0或-1均可
liquid_type : water
liquid_volume 根据需求填写即可默认单位ml这里举例50
parent workstation
res_id containe
关联设备名称(原unilabos_device_id) 这里就填写host_node
**配置完成后点击底部保存按钮**
我们点击PumpTransferProtocol卡片上的编辑按钮来配置参数⭐
event transfer_liquid
from_vessel water
to_vessel container1
volume 根据需求填写即可默认单位ml这里举例50
关联设备名称(原unilabos_device_id) 这里就填写workstation
**配置完成后点击底部保存按钮**
#### 3.3 运行工作流
1. 连接两个节点卡片
2. 点击底部保存按钮
3. 点击运行按钮执行工作流
![linksandrun.png](image/linksandrun.png)
### 运行监控
* 运行状态和消息实时显示在底部控制台
* 如有报错,可点击查看详细信息
### 结果验证
工作流完成后,返回仪器耗材模块:
* 点击 container1卡片查看详情
* 确认其中包含参数指定的水和容量

View File

@@ -21,7 +21,7 @@
"timeout": 10.0, "timeout": 10.0,
"axis": "Left", "axis": "Left",
"channel_num": 8, "channel_num": 8,
"setup": true, "setup": false,
"debug": true, "debug": true,
"simulator": true, "simulator": true,
"matrix_id": "71593" "matrix_id": "71593"

View File

@@ -0,0 +1,181 @@
[
{
"id": "3a1c62c4-c3d2-b803-b72d-7f1153ffef3b",
"typeName": "试剂瓶",
"code": "0004-00050",
"barCode": "",
"name": "NMP",
"quantity": 287.16699029126215,
"lockQuantity": 285.16699029126215,
"unit": "毫升",
"status": 1,
"isUse": false,
"locations": [
{
"id": "3a14198c-c2d0-efce-0939-69ca5a7dfd39",
"whid": "3a14198c-c2cc-0290-e086-44a428fba248",
"whName": "试剂堆栈",
"code": "0001-0008",
"x": 2,
"y": 4,
"z": 1,
"quantity": 0
}
],
"detail": []
},
{
"id": "3a1cdefe-0e03-1bc1-1296-dae1905c4108",
"typeName": "试剂瓶",
"code": "0004-00052",
"barCode": "",
"name": "NMP",
"quantity": 386.8990291262136,
"lockQuantity": 45.89902912621359,
"unit": "毫升",
"status": 1,
"isUse": false,
"locations": [
{
"id": "3a14198c-c2d0-f3e7-871a-e470d144296f",
"whid": "3a14198c-c2cc-0290-e086-44a428fba248",
"whName": "试剂堆栈",
"code": "0001-0005",
"x": 2,
"y": 1,
"z": 1,
"quantity": 0
}
],
"detail": []
},
{
"id": "3a1cdefe-0e03-68a4-bcb3-02fc6ba72d1b",
"typeName": "试剂瓶",
"code": "0004-00053",
"barCode": "",
"name": "NMP",
"quantity": 400.0,
"lockQuantity": 0.0,
"unit": "",
"status": 1,
"isUse": false,
"locations": [
{
"id": "3a14198c-c2d0-2070-efc8-44e245f10c6f",
"whid": "3a14198c-c2cc-0290-e086-44a428fba248",
"whName": "试剂堆栈",
"code": "0001-0006",
"x": 2,
"y": 2,
"z": 1,
"quantity": 0
}
],
"detail": []
},
{
"id": "3a1cdefe-d5e0-d850-5439-4499f20f07fe",
"typeName": "分装板",
"code": "0007-00185",
"barCode": "",
"name": "1010",
"quantity": 1.0,
"lockQuantity": 2.0,
"unit": "块",
"status": 1,
"isUse": false,
"locations": [
{
"id": "3a14198e-6929-46fe-841e-03dd753f1e4a",
"whid": "3a14198e-6928-121f-7ca6-88ad3ae7e6a0",
"whName": "粉末堆栈",
"code": "0002-0009",
"x": 3,
"y": 1,
"z": 1,
"quantity": 0
}
],
"detail": [
{
"id": "3a1cdefe-d5e0-28a4-f5d0-f7e2436c575f",
"detailMaterialId": "3a1cdefe-d5e0-94ae-f770-27847e73ad38",
"code": null,
"name": "90%分装小瓶",
"quantity": "1",
"lockQuantity": "1",
"unit": "个",
"x": 2,
"y": 3,
"z": 1,
"associateId": null
},
{
"id": "3a1cdefe-d5e0-3ed6-3607-133df89baf5b",
"detailMaterialId": "3a1cdefe-d5e0-f2fa-66bf-94c565d852fb",
"code": null,
"name": "10%分装小瓶",
"quantity": "1",
"lockQuantity": "1",
"unit": "个",
"x": 1,
"y": 3,
"z": 1,
"associateId": null
},
{
"id": "3a1cdefe-d5e0-72b6-e015-be7b93cf09eb",
"detailMaterialId": "3a1cdefe-d5e0-81cf-7dad-2e51cab9ffd6",
"code": null,
"name": "90%分装小瓶",
"quantity": "1",
"lockQuantity": "1",
"unit": "个",
"x": 2,
"y": 1,
"z": 1,
"associateId": null
},
{
"id": "3a1cdefe-d5e0-81d3-ad30-48134afc9ce7",
"detailMaterialId": "3a1cdefe-d5e0-3fa1-cc72-fda6276ae38d",
"code": null,
"name": "10%分装小瓶",
"quantity": "1",
"lockQuantity": "1",
"unit": "个",
"x": 1,
"y": 1,
"z": 1,
"associateId": null
},
{
"id": "3a1cdefe-d5e0-dbdf-d966-9a8926fe1e06",
"detailMaterialId": "3a1cdefe-d5e0-c632-c7da-02d385b18628",
"code": null,
"name": "10%分装小瓶",
"quantity": "1",
"lockQuantity": "1",
"unit": "个",
"x": 1,
"y": 2,
"z": 1,
"associateId": null
},
{
"id": "3a1cdefe-d5e0-f099-b260-e3089a2d08c3",
"detailMaterialId": "3a1cdefe-d5e0-561f-73b6-f8501f814dbb",
"code": null,
"name": "90%分装小瓶",
"quantity": "1",
"lockQuantity": "1",
"unit": "个",
"x": 2,
"y": 2,
"z": 1,
"associateId": null
}
]
}
]

View File

@@ -0,0 +1,216 @@
[
{
"id": "3a1cde21-a4f4-4f95-6221-eaafc2ae6a8d",
"typeName": "样品瓶",
"code": "0002-00407",
"barCode": "",
"name": "ODA",
"quantity": 25.0,
"lockQuantity": 2.0,
"unit": "克",
"status": 1,
"isUse": false,
"locations": [],
"detail": []
},
{
"id": "3a1cde21-a4f4-7887-9258-e8f8ab7c8a7a",
"typeName": "样品板",
"code": "0008-00160",
"barCode": "",
"name": "1010sample",
"quantity": 1.0,
"lockQuantity": 27.69187,
"unit": "块",
"status": 1,
"isUse": false,
"locations": [
{
"id": "3a14198e-6929-4379-affa-9a2935c17f99",
"whid": "3a14198e-6928-121f-7ca6-88ad3ae7e6a0",
"whName": "粉末堆栈",
"code": "0002-0002",
"x": 1,
"y": 2,
"z": 1,
"quantity": 0
}
],
"detail": [
{
"id": "3a1cde21-a4f4-0339-f2b6-8e680ad7e8c7",
"detailMaterialId": "3a1cde21-a4f4-ab37-f7a2-ecc3bc083e7c",
"code": null,
"name": "MPDA",
"quantity": "10.505",
"lockQuantity": "-0.0174",
"unit": "克",
"x": 2,
"y": 1,
"z": 1,
"associateId": null
},
{
"id": "3a1cde21-a4f4-a21a-23cf-bb7857b41947",
"detailMaterialId": "3a1cde21-a4f4-99c7-55e7-c80c7320e300",
"code": null,
"name": "ODA",
"quantity": "1.795",
"lockQuantity": "2.0093",
"unit": "克",
"x": 1,
"y": 1,
"z": 1,
"associateId": null
},
{
"id": "3a1cde21-a4f4-af1b-ba0b-2874836800e9",
"detailMaterialId": "3a1cde21-a4f4-4f95-6221-eaafc2ae6a8d",
"code": null,
"name": "ODA",
"quantity": "25",
"lockQuantity": "2",
"unit": "克",
"x": 1,
"y": 2,
"z": 1,
"associateId": null
}
]
},
{
"id": "3a1cde21-a4f4-99c7-55e7-c80c7320e300",
"typeName": "样品瓶",
"code": "0002-00406",
"barCode": "",
"name": "ODA",
"quantity": 1.795,
"lockQuantity": 2.00927,
"unit": "克",
"status": 1,
"isUse": false,
"locations": [],
"detail": []
},
{
"id": "3a1cde21-a4f4-ab37-f7a2-ecc3bc083e7c",
"typeName": "样品瓶",
"code": "0002-00408",
"barCode": "",
"name": "MPDA",
"quantity": 10.505,
"lockQuantity": -0.0174,
"unit": "克",
"status": 1,
"isUse": false,
"locations": [],
"detail": []
},
{
"id": "3a1cdeff-c92a-08f6-c822-732ab734154c",
"typeName": "样品板",
"code": "0008-00161",
"barCode": "",
"name": "1010sample2",
"quantity": 1.0,
"lockQuantity": 3.0,
"unit": "块",
"status": 1,
"isUse": false,
"locations": [
{
"id": "3a14198e-6929-31f0-8a22-0f98f72260df",
"whid": "3a14198e-6928-121f-7ca6-88ad3ae7e6a0",
"whName": "粉末堆栈",
"code": "0002-0001",
"x": 1,
"y": 1,
"z": 1,
"quantity": 0
}
],
"detail": [
{
"id": "3a1cdeff-c92b-3ace-9623-0bcdef6fa07d",
"detailMaterialId": "3a1cdeff-c92b-d084-2a96-5d62746d9321",
"code": null,
"name": "BTDA1",
"quantity": "0.362",
"lockQuantity": "14.494",
"unit": "克",
"x": 1,
"y": 1,
"z": 1,
"associateId": null
},
{
"id": "3a1cdeff-c92b-856e-f481-792b91b6dbde",
"detailMaterialId": "3a1cdeff-c92b-30f2-f907-8f5e2fe0586b",
"code": null,
"name": "BTDA3",
"quantity": "1.935",
"lockQuantity": "13.067",
"unit": "克",
"x": 1,
"y": 2,
"z": 1,
"associateId": null
},
{
"id": "3a1cdeff-c92b-d144-c5e5-ab9d94e21187",
"detailMaterialId": "3a1cdeff-c92b-519f-a70f-0bb71af537a7",
"code": null,
"name": "BTDA2",
"quantity": "1.903",
"lockQuantity": "13.035",
"unit": "克",
"x": 2,
"y": 1,
"z": 1,
"associateId": null
}
]
},
{
"id": "3a1cdeff-c92b-30f2-f907-8f5e2fe0586b",
"typeName": "样品瓶",
"code": "0002-00411",
"barCode": "",
"name": "BTDA3",
"quantity": 1.935,
"lockQuantity": 13.067,
"unit": "克",
"status": 1,
"isUse": false,
"locations": [],
"detail": []
},
{
"id": "3a1cdeff-c92b-519f-a70f-0bb71af537a7",
"typeName": "样品瓶",
"code": "0002-00410",
"barCode": "",
"name": "BTDA2",
"quantity": 1.903,
"lockQuantity": 13.035,
"unit": "克",
"status": 1,
"isUse": false,
"locations": [],
"detail": []
},
{
"id": "3a1cdeff-c92b-d084-2a96-5d62746d9321",
"typeName": "样品瓶",
"code": "0002-00409",
"barCode": "",
"name": "BTDA1",
"quantity": 0.362,
"lockQuantity": 14.494,
"unit": "克",
"status": 1,
"isUse": false,
"locations": [],
"detail": []
}
]

View File

@@ -1,5 +1,4 @@
{ [
"data": [
{ {
"id": "3a1c67a9-aed7-b94d-9e24-bfdf10c8baa9", "id": "3a1c67a9-aed7-b94d-9e24-bfdf10c8baa9",
"typeName": "烧杯", "typeName": "烧杯",
@@ -191,8 +190,4 @@
} }
] ]
} }
], ]
"code": 1,
"message": "",
"timestamp": 1758560573511
}

View File

@@ -2,6 +2,7 @@ import pytest
import json import json
import os import os
from pylabrobot.resources import Resource as ResourcePLR
from unilabos.resources.graphio import resource_bioyond_to_plr from unilabos.resources.graphio import resource_bioyond_to_plr
from unilabos.registry.registry import lab_registry from unilabos.registry.registry import lab_registry
@@ -13,23 +14,63 @@ lab_registry.setup()
type_mapping = { type_mapping = {
"烧杯": "BIOYOND_PolymerStation_1FlaskCarrier", "烧杯": "BIOYOND_PolymerStation_1FlaskCarrier",
"试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier", "试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier",
"样品板": "BIOYOND_PolymerStation_6VialCarrier", "样品板": "BIOYOND_PolymerStation_6StockCarrier",
"分装板": "BIOYOND_PolymerStation_6VialCarrier",
"样品瓶": "BIOYOND_PolymerStation_Solid_Stock",
"90%分装小瓶": "BIOYOND_PolymerStation_Solid_Vial",
"10%分装小瓶": "BIOYOND_PolymerStation_Liquid_Vial",
} }
type_uuid_mapping = {
"烧杯": "",
"试剂瓶": "",
"样品板": "",
"分装板": "3a14196e-5dfe-6e21-0c79-fe2036d052c4",
"样品瓶": "3a14196a-cf7d-8aea-48d8-b9662c7dba94",
"90%分装小瓶": "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea",
"10%分装小瓶": "3a14196c-76be-2279-4e22-7310d69aed68",
}
@pytest.fixture @pytest.fixture
def bioyond_materials() -> list[dict]: def bioyond_materials_reaction() -> list[dict]:
print("加载 BioYond 物料数据...") print("加载 BioYond 物料数据...")
print(os.getcwd()) print(os.getcwd())
with open("bioyond_materials.json", "r", encoding="utf-8") as f: with open("bioyond_materials_reaction.json", "r", encoding="utf-8") as f:
data = json.load(f)["data"] data = json.load(f)
print(f"加载了 {len(data)} 条物料数据") print(f"加载了 {len(data)} 条物料数据")
return data return data
def test_bioyond_to_plr(bioyond_materials) -> list[dict]: @pytest.fixture
def bioyond_materials_liquidhandling_1() -> list[dict]:
print("加载 BioYond 物料数据...")
print(os.getcwd())
with open("bioyond_materials_liquidhandling_1.json", "r", encoding="utf-8") as f:
data = json.load(f)
print(f"加载了 {len(data)} 条物料数据")
return data
@pytest.fixture
def bioyond_materials_liquidhandling_2() -> list[dict]:
print("加载 BioYond 物料数据...")
print(os.getcwd())
with open("bioyond_materials_liquidhandling_2.json", "r", encoding="utf-8") as f:
data = json.load(f)
print(f"加载了 {len(data)} 条物料数据")
return data
@pytest.mark.parametrize("materials_fixture", [
"bioyond_materials_reaction",
"bioyond_materials_liquidhandling_1",
])
def test_bioyond_to_plr(materials_fixture, request) -> list[dict]:
materials = request.getfixturevalue(materials_fixture)
deck = BIOYOND_PolymerReactionStation_Deck("test_deck") deck = BIOYOND_PolymerReactionStation_Deck("test_deck")
print("将 BioYond 物料数据转换为 PLR 格式...") output = resource_bioyond_to_plr(materials, type_mapping=type_mapping, deck=deck)
output = resource_bioyond_to_plr(bioyond_materials, type_mapping=type_mapping, deck=deck)
print(deck.summary()) print(deck.summary())
print([resource.serialize() for resource in output]) print([resource.serialize() for resource in output])
print([resource.serialize_all_state() for resource in output]) print([resource.serialize_all_state() for resource in output])
json.dump(deck.serialize(), open("test.json", "w", encoding="utf-8"), indent=4)

View File

@@ -1,14 +1,15 @@
import threading import threading
from unilabos.ros.nodes.resource_tracker import ResourceTreeSet
from unilabos.utils import logger from unilabos.utils import logger
# 根据选择的 backend 启动相应的功能 # 根据选择的 backend 启动相应的功能
def start_backend( def start_backend(
backend: str, backend: str,
devices_config: dict = {}, devices_config: ResourceTreeSet,
resources_config: list = [], resources_config: ResourceTreeSet,
resources_edge_config: list = [], resources_edge_config: list[dict] = [],
graph=None, graph=None,
controllers_config: dict = {}, controllers_config: dict = {},
bridges=[], bridges=[],

View File

@@ -6,10 +6,12 @@ import signal
import sys import sys
import threading import threading
import time import time
from copy import deepcopy from typing import Dict, Any, List
import networkx as nx
import yaml import yaml
from unilabos.ros.nodes.resource_tracker import ResourceTreeSet, ResourceDict
# 首先添加项目根目录到路径 # 首先添加项目根目录到路径
current_dir = os.path.dirname(os.path.abspath(__file__)) current_dir = os.path.dirname(os.path.abspath(__file__))
@@ -43,7 +45,7 @@ def convert_argv_dashes_to_underscores(args: argparse.ArgumentParser):
for i, arg in enumerate(sys.argv): for i, arg in enumerate(sys.argv):
for option_string in option_strings: for option_string in option_strings:
if arg.startswith(option_string): if arg.startswith(option_string):
new_arg = arg[:2] + arg[2 : len(option_string)].replace("-", "_") + arg[len(option_string) :] new_arg = arg[:2] + arg[2:len(option_string)].replace("-", "_") + arg[len(option_string):]
sys.argv[i] = new_arg sys.argv[i] = new_arg
break break
@@ -225,6 +227,15 @@ def main():
else: else:
HTTPConfig.remote_addr = args_dict.get("addr", "") HTTPConfig.remote_addr = args_dict.get("addr", "")
# 设置BasicConfig参数
if args_dict.get("ak", ""):
BasicConfig.ak = args_dict.get("ak", "")
print_status("传入了ak参数优先采用传入参数", "info")
if args_dict.get("sk", ""):
BasicConfig.sk = args_dict.get("sk", "")
print_status("传入了sk参数优先采用传入参数", "info")
# 使用远程资源启动
if args_dict["use_remote_resource"]: if args_dict["use_remote_resource"]:
print_status("使用远程资源启动", "info") print_status("使用远程资源启动", "info")
from unilabos.app.web import http_client from unilabos.app.web import http_client
@@ -236,13 +247,6 @@ def main():
else: else:
print_status("远程资源不存在,本地将进行首次上报!", "info") print_status("远程资源不存在,本地将进行首次上报!", "info")
# 设置BasicConfig参数
if args_dict.get("ak", ""):
BasicConfig.ak = args_dict.get("ak", "")
print_status("传入了ak参数优先采用传入参数", "info")
if args_dict.get("sk", ""):
BasicConfig.sk = args_dict.get("sk", "")
print_status("传入了sk参数优先采用传入参数", "info")
BasicConfig.working_dir = working_dir BasicConfig.working_dir = working_dir
BasicConfig.is_host_mode = not args_dict.get("is_slave", False) BasicConfig.is_host_mode = not args_dict.get("is_slave", False)
BasicConfig.slave_no_host = args_dict.get("slave_no_host", False) BasicConfig.slave_no_host = args_dict.get("slave_no_host", False)
@@ -257,8 +261,6 @@ def main():
read_node_link_json, read_node_link_json,
read_graphml, read_graphml,
dict_from_graph, dict_from_graph,
dict_to_nested_dict,
initialize_resources,
) )
from unilabos.app.communication import get_communication_client from unilabos.app.communication import get_communication_client
from unilabos.registry.registry import build_registry from unilabos.registry.registry import build_registry
@@ -278,8 +280,11 @@ def main():
if not BasicConfig.ak or not BasicConfig.sk: if not BasicConfig.ak or not BasicConfig.sk:
print_status("后续运行必须拥有一个实验室,请前往 https://uni-lab.bohrium.com 注册实验室!", "warning") print_status("后续运行必须拥有一个实验室,请前往 https://uni-lab.bohrium.com 注册实验室!", "warning")
os._exit(1) os._exit(1)
graph: nx.Graph
resource_tree_set: ResourceTreeSet
resource_links: List[Dict[str, Any]]
request_startup_json = http_client.request_startup_json()
if args_dict["graph"] is None: if args_dict["graph"] is None:
request_startup_json = http_client.request_startup_json()
if not request_startup_json: if not request_startup_json:
print_status( print_status(
"未指定设备加载文件路径尝试从HTTP获取失败请检查网络或者使用-g参数指定设备加载文件路径", "error" "未指定设备加载文件路径尝试从HTTP获取失败请检查网络或者使用-g参数指定设备加载文件路径", "error"
@@ -287,57 +292,60 @@ def main():
os._exit(1) os._exit(1)
else: else:
print_status("联网获取设备加载文件成功", "info") print_status("联网获取设备加载文件成功", "info")
graph, data = read_node_link_json(request_startup_json) graph, resource_tree_set, resource_links = read_node_link_json(request_startup_json)
else: else:
file_path = args_dict["graph"] file_path = args_dict["graph"]
if file_path.endswith(".json"): if file_path.endswith(".json"):
graph, data = read_node_link_json(file_path) graph, resource_tree_set, resource_links = read_node_link_json(file_path)
else: else:
graph, data = read_graphml(file_path) graph, resource_tree_set, resource_links = read_graphml(file_path)
import unilabos.resources.graphio as graph_res import unilabos.resources.graphio as graph_res
graph_res.physical_setup_graph = graph graph_res.physical_setup_graph = graph
resource_edge_info = modify_to_backend_format(data["links"]) resource_edge_info = modify_to_backend_format(resource_links)
materials = lab_registry.obtain_registry_resource_info() materials = lab_registry.obtain_registry_resource_info()
materials.extend(lab_registry.obtain_registry_device_info()) materials.extend(lab_registry.obtain_registry_device_info())
materials = {k["id"]: k for k in materials} materials = {k["id"]: k for k in materials}
nodes = {k["id"]: k for k in data["nodes"]} # 从 ResourceTreeSet 中获取节点信息
nodes = {node.res_content.id: node.res_content for node in resource_tree_set.all_nodes}
edge_info = len(resource_edge_info) edge_info = len(resource_edge_info)
for ind, i in enumerate(resource_edge_info[::-1]): for ind, i in enumerate(resource_edge_info[::-1]):
source_node = nodes[i["source"]] source_node: ResourceDict = nodes[i["source"]]
target_node = nodes[i["target"]] target_node: ResourceDict = nodes[i["target"]]
source_handle = i["sourceHandle"] source_handle = i["sourceHandle"]
target_handle = i["targetHandle"] target_handle = i["targetHandle"]
source_handler_keys = [ source_handler_keys = [
h["handler_key"] for h in materials[source_node["class"]]["handles"] if h["io_type"] == "source" h["handler_key"] for h in materials[source_node.klass]["handles"] if h["io_type"] == "source"
] ]
target_handler_keys = [ target_handler_keys = [
h["handler_key"] for h in materials[target_node["class"]]["handles"] if h["io_type"] == "target" h["handler_key"] for h in materials[target_node.klass]["handles"] if h["io_type"] == "target"
] ]
if source_handle not in source_handler_keys: if source_handle not in source_handler_keys:
print_status( print_status(
f"节点 {source_node['id']} 的source端点 {source_handle} 不存在,请检查,支持的端点 {source_handler_keys}", f"节点 {source_node.id} 的source端点 {source_handle} 不存在,请检查,支持的端点 {source_handler_keys}",
"error", "error",
) )
resource_edge_info.pop(edge_info - ind - 1) resource_edge_info.pop(edge_info - ind - 1)
continue continue
if target_handle not in target_handler_keys: if target_handle not in target_handler_keys:
print_status( print_status(
f"节点 {target_node['id']} 的target端点 {target_handle} 不存在,请检查,支持的端点 {target_handler_keys}", f"节点 {target_node.id} 的target端点 {target_handle} 不存在,请检查,支持的端点 {target_handler_keys}",
"error", "error",
) )
resource_edge_info.pop(edge_info - ind - 1) resource_edge_info.pop(edge_info - ind - 1)
continue continue
devices_and_resources = dict_from_graph(graph_res.physical_setup_graph) # 如果从远端获取了物料信息,则与本地物料进行同步
# args_dict["resources_config"] = initialize_resources(list(deepcopy(devices_and_resources).values())) if request_startup_json and "nodes" in request_startup_json:
args_dict["resources_config"] = list(devices_and_resources.values()) print_status("开始同步远端物料到本地...", "info")
args_dict["devices_config"] = dict_to_nested_dict(deepcopy(devices_and_resources), devices_only=False) remote_tree_set = ResourceTreeSet.from_raw_list(request_startup_json["nodes"])
args_dict["graph"] = graph_res.physical_setup_graph resource_tree_set.merge_remote_resources(remote_tree_set)
print_status("远端物料同步完成", "info")
print_status(f"{len(args_dict['resources_config'])} Resources loaded:", "info") # 使用 ResourceTreeSet 代替 list
for i in args_dict["resources_config"]: args_dict["resources_config"] = resource_tree_set
print_status(f"DeviceId: {i['id']}, Class: {i['class']}", "info") args_dict["devices_config"] = resource_tree_set
args_dict["graph"] = graph_res.physical_setup_graph
if BasicConfig.upload_registry: if BasicConfig.upload_registry:
# 设备注册到服务端 - 需要 ak 和 sk # 设备注册到服务端 - 需要 ak 和 sk
@@ -351,9 +359,7 @@ def main():
else: else:
print_status("未提供 ak 和 sk跳过设备注册", "info") print_status("未提供 ak 和 sk跳过设备注册", "info")
else: else:
print_status( print_status("本次启动注册表不报送云端,如果您需要联网调试,请在启动命令增加--upload_registry", "warning")
"本次启动注册表不报送云端,如果您需要联网调试,请在启动命令增加--upload_registry", "warning"
)
if args_dict["controllers"] is not None: if args_dict["controllers"] is not None:
args_dict["controllers_config"] = yaml.safe_load(open(args_dict["controllers"], encoding="utf-8")) args_dict["controllers_config"] = yaml.safe_load(open(args_dict["controllers"], encoding="utf-8"))
@@ -383,13 +389,16 @@ def main():
# web visiualize 2D # web visiualize 2D
if args_dict["visual"] != "disable": if args_dict["visual"] != "disable":
enable_rviz = args_dict["visual"] == "rviz" enable_rviz = args_dict["visual"] == "rviz"
devices_and_resources = dict_from_graph(graph_res.physical_setup_graph)
if devices_and_resources is not None: if devices_and_resources is not None:
from unilabos.device_mesh.resource_visalization import ( from unilabos.device_mesh.resource_visalization import (
ResourceVisualization, ResourceVisualization,
) # 此处开启后logger会变更为INFO有需要请调整 ) # 此处开启后logger会变更为INFO有需要请调整
resource_visualization = ResourceVisualization( resource_visualization = ResourceVisualization(
devices_and_resources, args_dict["resources_config"], enable_rviz=enable_rviz devices_and_resources,
[n.res_content for n in args_dict["resources_config"].all_nodes], # type: ignore # FIXME
enable_rviz=enable_rviz,
) )
args_dict["resources_mesh_config"] = resource_visualization.resource_model args_dict["resources_mesh_config"] = resource_visualization.resource_model
start_backend(**args_dict) start_backend(**args_dict)

View File

@@ -1,11 +1,6 @@
import argparse
import json import json
import time import time
from unilabos.config.config import BasicConfig
from unilabos.registry.registry import build_registry
from unilabos.app.main import load_config_from_file
from unilabos.utils.log import logger from unilabos.utils.log import logger
from unilabos.utils.type_check import TypeEncoder from unilabos.utils.type_check import TypeEncoder

View File

@@ -9,6 +9,7 @@ import os
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
import requests import requests
from unilabos.ros.nodes.resource_tracker import ResourceTreeSet
from unilabos.utils.log import info from unilabos.utils.log import info
from unilabos.config.config import HTTPConfig, BasicConfig from unilabos.config.config import HTTPConfig, BasicConfig
from unilabos.utils import logger from unilabos.utils import logger
@@ -46,7 +47,7 @@ class HTTPClient:
Response: API响应对象 Response: API响应对象
""" """
response = requests.post( response = requests.post(
f"{self.remote_addr}/lab/material/edge", f"{self.remote_addr}/edge/material/edge",
json={ json={
"edges": resources, "edges": resources,
}, },
@@ -61,6 +62,83 @@ class HTTPClient:
logger.error(f"添加物料关系失败: {response.status_code}, {response.text}") logger.error(f"添加物料关系失败: {response.status_code}, {response.text}")
return response return response
def resource_tree_add(self, resources: ResourceTreeSet, mount_uuid: str, first_add: bool) -> Dict[str, str]:
"""
添加资源
Args:
resources: 要添加的资源树集合ResourceTreeSet
mount_uuid: 要挂载的资源的uuid
first_add: 是否为首次添加资源可以是host也可以是slave来的
Returns:
Dict[str, str]: 旧UUID到新UUID的映射关系 {old_uuid: new_uuid}
"""
# 从序列化数据中提取所有节点的UUID保存旧UUID
old_uuids = {n.res_content.uuid: n for n in resources.all_nodes}
if not self.initialized or first_add:
self.initialized = True
info(f"首次添加资源,当前远程地址: {self.remote_addr}")
response = requests.post(
f"{self.remote_addr}/edge/material",
json={"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid},
headers={"Authorization": f"Lab {self.auth}"},
timeout=100,
)
else:
response = requests.put(
f"{self.remote_addr}/edge/material",
json={"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid},
headers={"Authorization": f"Lab {self.auth}"},
timeout=100,
)
# 处理响应构建UUID映射
uuid_mapping = {}
if response.status_code == 200:
res = response.json()
if "code" in res and res["code"] != 0:
logger.error(f"添加物料失败: {response.text}")
else:
data = res["data"]
for i in data:
uuid_mapping[i["uuid"]] = i["cloud_uuid"]
else:
logger.error(f"添加物料失败: {response.text}")
for u, n in old_uuids.items():
if u in uuid_mapping:
n.res_content.uuid = uuid_mapping[u]
for c in n.children:
c.res_content.parent_uuid = n.res_content.uuid
else:
logger.warning(f"资源UUID未更新: {u}")
return uuid_mapping
def resource_tree_get(self, uuid_list: List[str], with_children: bool) -> List[Dict[str, Any]]:
"""
添加资源
Args:
uuid_list: List[str]
Returns:
Dict[str, str]: 旧UUID到新UUID的映射关系 {old_uuid: new_uuid}
"""
response = requests.post(
f"{self.remote_addr}/edge/material/query",
json={"uuids": uuid_list, "with_children": with_children},
headers={"Authorization": f"Lab {self.auth}"},
timeout=100,
)
if response.status_code == 200:
res = response.json()
if "code" in res and res["code"] != 0:
logger.error(f"查询物料失败: {response.text}")
else:
data = res["data"]["nodes"]
return data
else:
logger.error(f"查询物料失败: {response.text}")
return []
def resource_add(self, resources: List[Dict[str, Any]]) -> requests.Response: def resource_add(self, resources: List[Dict[str, Any]]) -> requests.Response:
""" """
添加资源 添加资源
@@ -220,7 +298,7 @@ class HTTPClient:
Response: API响应对象 Response: API响应对象
""" """
response = requests.get( response = requests.get(
f"{self.remote_addr}/lab/resource/graph_info/", f"{self.remote_addr}/edge/material/download",
headers={"Authorization": f"Lab {self.auth}"}, headers={"Authorization": f"Lab {self.auth}"},
timeout=(3, 30), timeout=(3, 30),
) )

View File

@@ -19,9 +19,12 @@ import websockets
import ssl as ssl_module import ssl as ssl_module
from queue import Queue, Empty from queue import Queue, Empty
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional, Dict, Any, Callable, List, Set from typing import Optional, Dict, Any, List
from urllib.parse import urlparse from urllib.parse import urlparse
from enum import Enum from enum import Enum
from jedi.inference.gradual.typing import TypedDict
from unilabos.app.model import JobAddReq from unilabos.app.model import JobAddReq
from unilabos.ros.nodes.presets.host_node import HostNode from unilabos.ros.nodes.presets.host_node import HostNode
from unilabos.utils.type_check import serialize_result_info from unilabos.utils.type_check import serialize_result_info
@@ -96,6 +99,14 @@ class WebSocketMessage:
timestamp: float = field(default_factory=time.time) timestamp: float = field(default_factory=time.time)
class WSResourceChatData(TypedDict):
uuid: str
device_uuid: str
device_id: str
device_old_uuid: str
device_old_id: str
class DeviceActionManager: class DeviceActionManager:
"""设备动作管理器 - 管理每个device_action_key的任务队列""" """设备动作管理器 - 管理每个device_action_key的任务队列"""
@@ -543,7 +554,7 @@ class MessageProcessor:
async def _process_message(self, data: Dict[str, Any]): async def _process_message(self, data: Dict[str, Any]):
"""处理收到的消息""" """处理收到的消息"""
message_type = data.get("action", "") message_type = data.get("action", "")
message_data = data.get("data", {}) message_data = data.get("data")
logger.debug(f"[MessageProcessor] Processing message: {message_type}") logger.debug(f"[MessageProcessor] Processing message: {message_type}")
@@ -556,8 +567,12 @@ class MessageProcessor:
await self._handle_job_start(message_data) await self._handle_job_start(message_data)
elif message_type == "cancel_action" or message_type == "cancel_task": elif message_type == "cancel_action" or message_type == "cancel_task":
await self._handle_cancel_action(message_data) await self._handle_cancel_action(message_data)
elif message_type == "": elif message_type == "add_material":
return await self._handle_resource_tree_update(message_data, "add")
elif message_type == "update_material":
await self._handle_resource_tree_update(message_data, "update")
elif message_type == "remove_material":
await self._handle_resource_tree_update(message_data, "remove")
else: else:
logger.debug(f"[MessageProcessor] Unknown message type: {message_type}") logger.debug(f"[MessageProcessor] Unknown message type: {message_type}")
@@ -574,6 +589,7 @@ class MessageProcessor:
async def _handle_query_action_state(self, data: Dict[str, Any]): async def _handle_query_action_state(self, data: Dict[str, Any]):
"""处理query_action_state消息""" """处理query_action_state消息"""
device_id = data.get("device_id", "") device_id = data.get("device_id", "")
device_uuid = data.get("device_uuid", "")
action_name = data.get("action_name", "") action_name = data.get("action_name", "")
task_id = data.get("task_id", "") task_id = data.get("task_id", "")
job_id = data.get("job_id", "") job_id = data.get("job_id", "")
@@ -760,6 +776,92 @@ class MessageProcessor:
else: else:
logger.warning("[MessageProcessor] Cancel request missing both task_id and job_id") logger.warning("[MessageProcessor] Cancel request missing both task_id and job_id")
async def _handle_resource_tree_update(self, resource_uuid_list: List[WSResourceChatData], action: str):
"""处理资源树更新消息add_material/update_material/remove_material"""
if not resource_uuid_list:
return
# 按device_id和action分组
# device_action_groups: {(device_id, action): [uuid_list]}
device_action_groups = {}
for item in resource_uuid_list:
device_id = item["device_id"]
if not device_id:
device_id = "host_node"
# 特殊处理update action: 检查是否设备迁移
if action == "update":
device_old_id = item.get("device_old_id", "")
if not device_old_id:
device_old_id = "host_node"
# 设备迁移device_id != device_old_id
if device_id != device_old_id:
# 给旧设备发送remove
key_remove = (device_old_id, "remove")
if key_remove not in device_action_groups:
device_action_groups[key_remove] = []
device_action_groups[key_remove].append(item["uuid"])
# 给新设备发送add
key_add = (device_id, "add")
if key_add not in device_action_groups:
device_action_groups[key_add] = []
device_action_groups[key_add].append(item["uuid"])
logger.info(
f"[MessageProcessor] Resource migrated: {item['uuid'][:8]} from {device_old_id} to {device_id}"
)
else:
# 正常update
key = (device_id, "update")
if key not in device_action_groups:
device_action_groups[key] = []
device_action_groups[key].append(item["uuid"])
else:
# add或remove action直接分组
key = (device_id, action)
if key not in device_action_groups:
device_action_groups[key] = []
device_action_groups[key].append(item["uuid"])
logger.info(f"触发物料更新 {action} 分组数量: {len(device_action_groups)}, 总数量: {len(resource_uuid_list)}")
# 为每个(device_id, action)创建独立的更新线程
for (device_id, actual_action), items in device_action_groups.items():
logger.info(f"设备 {device_id} 物料更新 {actual_action} 数量: {len(items)}")
def _notify_resource_tree(dev_id, act, item_list):
try:
host_node = HostNode.get_instance(timeout=5)
if not host_node:
logger.error(f"[MessageProcessor] HostNode instance not available for {act}")
return
success = host_node.notify_resource_tree_update(dev_id, act, item_list)
if success:
logger.info(
f"[MessageProcessor] Resource tree {act} completed for device {dev_id}, "
f"items: {len(item_list)}"
)
else:
logger.warning(f"[MessageProcessor] Resource tree {act} failed for device {dev_id}")
except Exception as e:
logger.error(f"[MessageProcessor] Error in resource tree {act} for device {dev_id}: {str(e)}")
logger.error(traceback.format_exc())
# 在新线程中执行通知
thread = threading.Thread(
target=_notify_resource_tree,
args=(device_id, actual_action, items),
daemon=True,
name=f"ResourceTreeUpdate-{actual_action}-{device_id}",
)
thread.start()
async def _send_action_state_response( async def _send_action_state_response(
self, device_id: str, action_name: str, task_id: str, job_id: str, typ: str, free: bool, need_more: int self, device_id: str, action_name: str, task_id: str, job_id: str, typ: str, free: bool, need_more: int
): ):
@@ -1008,6 +1110,8 @@ class WebSocketClient(BaseCommunicationClient):
# 构建WebSocket URL # 构建WebSocket URL
self.websocket_url = self._build_websocket_url() self.websocket_url = self._build_websocket_url()
if not self.websocket_url:
self.websocket_url = "" # 默认空字符串避免None
# 两个核心线程 # 两个核心线程
self.message_processor = MessageProcessor(self.websocket_url, self.send_queue, self.device_manager) self.message_processor = MessageProcessor(self.websocket_url, self.send_queue, self.device_manager)

View File

@@ -0,0 +1,129 @@
# config.py
"""
配置文件 - 包含所有配置信息和映射关系
"""
# API配置
API_CONFIG = {
"api_key": "",
"api_host": ""
}
# 站点类型配置
STATION_TYPES = {
"REACTION": "reaction_station", # 仅反应站
"DISPENSING": "dispensing_station", # 仅配液站
"HYBRID": "hybrid_station" # 混合模式
}
# 默认站点配置
DEFAULT_STATION_CONFIG = {
"station_type": STATION_TYPES["REACTION"], # 默认反应站模式
"enable_reaction_station": True, # 是否启用反应站功能
"enable_dispensing_station": False, # 是否启用配液站功能
"station_name": "BioyondReactionStation", # 站点名称
"description": "Bioyond反应工作站" # 站点描述
}
# 工作流映射配置
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": "",
}
# 工作流名称到DisplaySectionName的映射
WORKFLOW_TO_SECTION_MAP = {
'reactor_taken_in': '反应器放入',
'liquid_feeding_beaker': '液体投料-烧杯',
'Liquid_feeding_vials(non-titration)': '液体投料-小瓶(非滴定)',
'Liquid_feeding_solvents': '液体投料-溶剂',
'Solid_feeding_vials': '固体投料-小瓶',
'Liquid_feeding(titration)': '液体投料-滴定',
'reactor_taken_out': '反应器取出'
}
# 库位映射配置
LOCATION_MAPPING = {
'A01': '',
'A02': '',
'A03': '',
'A04': '',
'A05': '',
'A06': '',
'A07': '',
'A08': '',
'B01': '',
'B02': '',
'B03': '',
'B04': '',
'B05': '',
'B06': '',
'B07': '',
'B08': '',
'C01': '',
'C02': '',
'C03': '',
'C04': '',
'C05': '',
'C06': '',
'C07': '',
'C08': '',
'D01': '',
'D02': '',
'D03': '',
'D04': '',
'D05': '',
'D06': '',
'D07': '',
'D08': '',
}
# 物料类型配置
MATERIAL_TYPE_IDS = {
"样品板": "",
"样品": "",
"烧杯": ""
}
MATERIAL_TYPE_MAPPINGS = {
"烧杯": "BIOYOND_PolymerStation_1FlaskCarrier",
"试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier",
"样品板": "BIOYOND_PolymerStation_6VialCarrier",
}
# 步骤参数配置各工作流的步骤UUID
WORKFLOW_STEP_IDS = {
"reactor_taken_in": {
"config": ""
},
"liquid_feeding_beaker": {
"liquid": "",
"observe": ""
},
"liquid_feeding_vials_non_titration": {
"liquid": "",
"observe": ""
},
"liquid_feeding_solvents": {
"liquid": "",
"observe": ""
},
"solid_feeding_vials": {
"feeding": "",
"observe": ""
},
"liquid_feeding_titration": {
"liquid": "",
"observe": ""
},
"drip_back": {
"liquid": "",
"observe": ""
}
}

View File

@@ -10,12 +10,14 @@ import json
from unilabos.devices.workstation.workstation_base import WorkstationBase, ResourceSynchronizer from unilabos.devices.workstation.workstation_base import WorkstationBase, ResourceSynchronizer
from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondV1RPC from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondV1RPC
from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot
from unilabos.resources.warehouse import WareHouse from unilabos.resources.warehouse import WareHouse
from unilabos.utils.log import logger from unilabos.utils.log import logger
from unilabos.resources.graphio import resource_bioyond_to_plr from unilabos.resources.graphio import resource_bioyond_to_plr
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 pylabrobot.resources.resource import Resource as ResourcePLR
from unilabos.devices.workstation.bioyond_studio.config import ( from unilabos.devices.workstation.bioyond_studio.config import (
API_CONFIG, WORKFLOW_MAPPINGS, MATERIAL_TYPE_MAPPINGS, API_CONFIG, WORKFLOW_MAPPINGS, MATERIAL_TYPE_MAPPINGS,
@@ -153,6 +155,14 @@ class BioyondWorkstation(WorkstationBase):
"resources": [self.deck] "resources": [self.deck]
}) })
def transfer_resource_to_another(self, resource: List[ResourceSlot], mount_resource: List[ResourceSlot], sites: List[str], mount_device_id: DeviceSlot):
ROS2DeviceNode.run_async_func(self._ros_node.transfer_resource_to_another, True, **{
"plr_resources": resource,
"target_device_id": mount_device_id,
"target_resources": mount_resource,
"sites": sites,
})
def _configure_station_type(self, station_config: Optional[Dict[str, Any]] = None) -> None: def _configure_station_type(self, station_config: Optional[Dict[str, Any]] = None) -> None:
"""配置站点类型和功能模块 """配置站点类型和功能模块

View File

@@ -61,7 +61,6 @@ class ElectrodeSheet(Resource):
info=None info=None
) )
# TODO: 这个还要不要给self._unilabos_state赋值的
def load_state(self, state: Dict[str, Any]) -> None: def load_state(self, state: Dict[str, Any]) -> None:
"""格式不变""" """格式不变"""
super().load_state(state) super().load_state(state)
@@ -665,7 +664,6 @@ class BatteryPressSlot(Resource):
reassign: bool = True, reassign: bool = True,
): ):
"""放置极片""" """放置极片"""
# TODO: 让高京看下槽位只有一个电池时是否这么写。
if self.has_battery(): if self.has_battery():
raise ValueError(f"槽位已含有一个电池,无法再放置其他电池") raise ValueError(f"槽位已含有一个电池,无法再放置其他电池")
super().assign_child_resource(resource, location, reassign) super().assign_child_resource(resource, location, reassign)
@@ -674,7 +672,6 @@ class BatteryPressSlot(Resource):
def get_battery_info(self, index: int) -> Battery: def get_battery_info(self, index: int) -> Battery:
return self.children[0] return self.children[0]
# TODO:这个移液枪架子看一下从哪继承
class TipBox64State(TypedDict): class TipBox64State(TypedDict):
"""电池状态字典""" """电池状态字典"""
tip_diameter: float = 5.0 tip_diameter: float = 5.0

View File

@@ -1012,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():

View File

@@ -10,6 +10,7 @@ serial:
request: null request: null
response: null response: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: handle_serial_request的参数schema description: handle_serial_request的参数schema
@@ -36,6 +37,7 @@ serial:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: read_data的参数schema description: read_data的参数schema
@@ -57,6 +59,7 @@ serial:
goal_default: goal_default:
command: null command: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: send_command的参数schema description: send_command的参数schema

View File

@@ -8,6 +8,7 @@ camera.USB:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 用于安全地关闭摄像头设备释放摄像头资源停止视频采集和发布服务。调用此函数将清理OpenCV摄像头连接并销毁ROS2节点。 description: 用于安全地关闭摄像头设备释放摄像头资源停止视频采集和发布服务。调用此函数将清理OpenCV摄像头连接并销毁ROS2节点。
@@ -28,6 +29,7 @@ camera.USB:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 定时器回调函数的参数schema。此函数负责定期采集摄像头视频帧将OpenCV格式的图像转换为ROS Image消息格式并发布到指定的视频话题。默认以10Hz频率执行确保视频流的连续性和实时性。 description: 定时器回调函数的参数schema。此函数负责定期采集摄像头视频帧将OpenCV格式的图像转换为ROS Image消息格式并发布到指定的视频话题。默认以10Hz频率执行确保视频流的连续性和实时性。

View File

@@ -8,6 +8,7 @@ hplc.agilent:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 检查安捷伦HPLC设备状态的函数。用于监控设备的运行状态、连接状态、错误信息等关键指标。该函数定期查询设备状态确保系统稳定运行及时发现和报告设备异常。适用于自动化流程中的设备监控、故障诊断、系统维护等场景。 description: 检查安捷伦HPLC设备状态的函数。用于监控设备的运行状态、连接状态、错误信息等关键指标。该函数定期查询设备状态确保系统稳定运行及时发现和报告设备异常。适用于自动化流程中的设备监控、故障诊断、系统维护等场景。
@@ -29,6 +30,7 @@ hplc.agilent:
goal_default: goal_default:
file_path: null file_path: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 从文本文件中提取分析数据的函数。用于解析安捷伦HPLC生成的结果文件提取峰面积、保留时间、浓度等关键分析数据。支持多种文件格式的自动识别和数据结构化处理为后续数据分析和报告生成提供标准化的数据格式。适用于批量数据处理、结果验证、质量控制等分析工作流程。 description: 从文本文件中提取分析数据的函数。用于解析安捷伦HPLC生成的结果文件提取峰面积、保留时间、浓度等关键分析数据。支持多种文件格式的自动识别和数据结构化处理为后续数据分析和报告生成提供标准化的数据格式。适用于批量数据处理、结果验证、质量控制等分析工作流程。
@@ -55,6 +57,7 @@ hplc.agilent:
resource: null resource: null
wf_name: null wf_name: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 启动安捷伦HPLC分析序列的函数。用于执行预定义的分析方法序列包括样品进样、色谱分离、检测等完整的分析流程。支持参数配置、资源分配、工作流程管理等功能实现全自动的样品分析。适用于批量样品处理、标准化分析、质量检测等需要连续自动分析的应用场景。 description: 启动安捷伦HPLC分析序列的函数。用于执行预定义的分析方法序列包括样品进样、色谱分离、检测等完整的分析流程。支持参数配置、资源分配、工作流程管理等功能实现全自动的样品分析。适用于批量样品处理、标准化分析、质量检测等需要连续自动分析的应用场景。
@@ -83,6 +86,7 @@ hplc.agilent:
goal_default: goal_default:
device_name: null device_name: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 尝试关闭HPLC子设备的函数。用于安全地关闭泵、检测器、进样器等各个子模块确保设备正常断开连接并保护硬件安全。该函数提供错误处理和状态确认机制避免强制关闭可能造成的设备损坏。适用于设备维护、系统重启、紧急停机等需要安全关闭设备的场景。 description: 尝试关闭HPLC子设备的函数。用于安全地关闭泵、检测器、进样器等各个子模块确保设备正常断开连接并保护硬件安全。该函数提供错误处理和状态确认机制避免强制关闭可能造成的设备损坏。适用于设备维护、系统重启、紧急停机等需要安全关闭设备的场景。
@@ -106,6 +110,7 @@ hplc.agilent:
goal_default: goal_default:
device_name: null device_name: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 尝试打开HPLC子设备的函数。用于初始化和连接泵、检测器、进样器等各个子模块建立设备通信并进行自检。该函数提供连接验证和错误恢复机制确保子设备正常启动并准备就绪。适用于设备初始化、系统启动、设备重连等需要建立设备连接的场景。 description: 尝试打开HPLC子设备的函数。用于初始化和连接泵、检测器、进样器等各个子模块建立设备通信并进行自检。该函数提供连接验证和错误恢复机制确保子设备正常启动并准备就绪。适用于设备初始化、系统启动、设备重连等需要建立设备连接的场景。
@@ -263,6 +268,7 @@ hplc.agilent-zhida:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: HPLC设备连接关闭函数。安全地断开与智达HPLC设备的TCP socket连接释放网络资源。该函数确保连接的正确关闭避免网络资源泄露。通常在设备使用完毕或系统关闭时调用。 description: HPLC设备连接关闭函数。安全地断开与智达HPLC设备的TCP socket连接释放网络资源。该函数确保连接的正确关闭避免网络资源泄露。通常在设备使用完毕或系统关闭时调用。
@@ -283,6 +289,7 @@ hplc.agilent-zhida:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: HPLC设备连接建立函数。与智达HPLC设备建立TCP socket通信连接配置通信超时参数。该函数是设备使用前的必要步骤建立成功后可进行状态查询、方法获取、任务启动等操作。连接失败时会抛出异常。 description: HPLC设备连接建立函数。与智达HPLC设备建立TCP socket通信连接配置通信超时参数。该函数是设备使用前的必要步骤建立成功后可进行状态查询、方法获取、任务启动等操作。连接失败时会抛出异常。

View File

@@ -9,6 +9,7 @@ raman.home_made:
goal_default: goal_default:
int_time: null int_time: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 设置CCD检测器积分时间的函数。用于配置拉曼光谱仪的信号采集时间控制光谱数据的质量和信噪比。较长的积分时间可获得更高的信号强度和更好的光谱质量但会增加测量时间。该函数允许根据样品特性和测量要求动态调整检测参数优化测量效果。 description: 设置CCD检测器积分时间的函数。用于配置拉曼光谱仪的信号采集时间控制光谱数据的质量和信噪比。较长的积分时间可获得更高的信号强度和更好的光谱质量但会增加测量时间。该函数允许根据样品特性和测量要求动态调整检测参数优化测量效果。
@@ -33,6 +34,7 @@ raman.home_made:
goal_default: goal_default:
output_voltage_laser: null output_voltage_laser: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 设置激光器输出功率的函数。用于控制拉曼光谱仪激光器的功率输出,调节激光强度以适应不同样品的测量需求。适当的激光功率能够获得良好的拉曼信号同时避免样品损伤。该函数支持精确的功率控制,确保测量结果的稳定性和重现性。 description: 设置激光器输出功率的函数。用于控制拉曼光谱仪激光器的功率输出,调节激光强度以适应不同样品的测量需求。适当的激光功率能够获得良好的拉曼信号同时避免样品损伤。该函数支持精确的功率控制,确保测量结果的稳定性和重现性。
@@ -58,6 +60,7 @@ raman.home_made:
int_time: null int_time: null
laser_power: null laser_power: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 执行无背景扣除的拉曼光谱测量函数。用于直接采集样品的拉曼光谱信号,不进行背景校正处理。该函数配置积分时间和激光功率参数,获取原始光谱数据用于后续的数据处理分析。适用于对光谱数据质量要求较高或需要自定义背景处理流程的测量场景。 description: 执行无背景扣除的拉曼光谱测量函数。用于直接采集样品的拉曼光谱信号,不进行背景校正处理。该函数配置积分时间和激光功率参数,获取原始光谱数据用于后续的数据处理分析。适用于对光谱数据质量要求较高或需要自定义背景处理流程的测量场景。
@@ -88,6 +91,7 @@ raman.home_made:
laser_power: null laser_power: null
sample_name: null sample_name: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 执行多次平均的无背景拉曼光谱测量函数。通过多次测量取平均值来提高光谱数据的信噪比和测量精度,减少随机噪声影响。该函数支持自定义平均次数、积分时间、激光功率等参数,并可为样品指定名称便于数据管理。适用于对测量精度要求较高的定量分析和研究应用。 description: 执行多次平均的无背景拉曼光谱测量函数。通过多次测量取平均值来提高光谱数据的信噪比和测量精度,减少随机噪声影响。该函数支持自定义平均次数、积分时间、激光功率等参数,并可为样品指定名称便于数据管理。适用于对测量精度要求较高的定量分析和研究应用。

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@ gas_source.mock:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: is_closed的参数schema description: is_closed的参数schema
@@ -28,6 +29,7 @@ gas_source.mock:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: is_open的参数schema description: is_open的参数schema
@@ -188,6 +190,7 @@ vacuum_pump.mock:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: is_closed的参数schema description: is_closed的参数schema
@@ -208,6 +211,7 @@ vacuum_pump.mock:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: is_open的参数schema description: is_open的参数schema

View File

@@ -564,6 +564,7 @@ liquid_handler:
protocol_type: null protocol_type: null
protocol_version: null protocol_version: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 创建实验协议函数。用于建立新的液体处理实验协议,定义协议名称、描述、版本、作者、日期等基本信息。该函数支持协议模板化管理,便于实验流程的标准化和重复性。适用于实验设计、方法开发、标准操作程序建立等需要协议管理的应用场景。 description: 创建实验协议函数。用于建立新的液体处理实验协议,定义协议名称、描述、版本、作者、日期等基本信息。该函数支持协议模板化管理,便于实验流程的标准化和重复性。适用于实验设计、方法开发、标准操作程序建立等需要协议管理的应用场景。
@@ -607,6 +608,7 @@ liquid_handler:
msg: null msg: null
seconds: 0 seconds: 0
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 自定义延时函数。在实验流程中插入可配置的等待时间,用于满足特定的反应时间、孵育时间或设备稳定时间要求。支持自定义延时消息和秒数设置,提供流程控制和时间管理功能。适用于酶反应等待、温度平衡、样品孵育等需要时间控制的实验步骤。 description: 自定义延时函数。在实验流程中插入可配置的等待时间,用于满足特定的反应时间、孵育时间或设备稳定时间要求。支持自定义延时消息和秒数设置,提供流程控制和时间管理功能。适用于酶反应等待、温度平衡、样品孵育等需要时间控制的实验步骤。
@@ -633,6 +635,7 @@ liquid_handler:
goal_default: goal_default:
tip_racks: null tip_racks: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 吸头迭代函数。用于自动管理和切换吸头架中的吸头,实现批量实验中的吸头自动分配和追踪。该函数监控吸头使用状态,自动切换到下一个可用吸头位置,确保实验流程的连续性。适用于高通量实验、批量处理、自动化流水线等需要大量吸头管理的应用场景。 description: 吸头迭代函数。用于自动管理和切换吸头架中的吸头,实现批量实验中的吸头自动分配和追踪。该函数监控吸头使用状态,自动切换到下一个可用吸头位置,确保实验流程的连续性。适用于高通量实验、批量处理、自动化流水线等需要大量吸头管理的应用场景。
@@ -659,6 +662,7 @@ liquid_handler:
volumes: null volumes: null
wells: null wells: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: '' description: ''
@@ -689,6 +693,7 @@ liquid_handler:
goal_default: goal_default:
tip_racks: null tip_racks: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 吸头架设置函数。用于配置和初始化液体处理系统的吸头架信息,包括吸头架位置、类型、容量等参数。该函数建立吸头资源管理系统,为后续的吸头选择和使用提供基础配置。适用于系统初始化、吸头架更换、实验配置等需要吸头资源管理的操作场景。 description: 吸头架设置函数。用于配置和初始化液体处理系统的吸头架信息,包括吸头架位置、类型、容量等参数。该函数建立吸头资源管理系统,为后续的吸头选择和使用提供基础配置。适用于系统初始化、吸头架更换、实验配置等需要吸头资源管理的操作场景。
@@ -713,6 +718,7 @@ liquid_handler:
goal_default: goal_default:
targets: null targets: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 吸头碰触函数。控制移液器吸头轻触容器边缘或底部,用于去除吸头外壁附着的液滴,提高移液精度和减少污染。该函数支持多目标位置操作,可配置碰触参数和位置偏移。适用于精密移液、减少液体残留、防止交叉污染等需要提高移液质量的实验操作。 description: 吸头碰触函数。控制移液器吸头轻触容器边缘或底部,用于去除吸头外壁附着的液滴,提高移液精度和减少污染。该函数支持多目标位置操作,可配置碰触参数和位置偏移。适用于精密移液、减少液体残留、防止交叉污染等需要提高移液质量的实验操作。
@@ -739,6 +745,7 @@ liquid_handler:
target_group_name: null target_group_name: null
unit_volume: null unit_volume: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: '' description: ''
@@ -4495,6 +4502,7 @@ liquid_handler.biomek:
resources: null resources: null
slot_on_deck: null slot_on_deck: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: create_resource的参数schema description: create_resource的参数schema
@@ -4554,6 +4562,7 @@ liquid_handler.biomek:
parent: null parent: null
slot_on_deck: null slot_on_deck: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: instrument_setup_biomek的参数schema description: instrument_setup_biomek的参数schema
@@ -6042,6 +6051,7 @@ liquid_handler.prcxi:
protocol_type: '' protocol_type: ''
protocol_version: '' protocol_version: ''
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: create_protocol的参数schema description: create_protocol的参数schema
@@ -6087,6 +6097,7 @@ liquid_handler.prcxi:
msg: null msg: null
seconds: 0 seconds: 0
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: custom_delay的参数schema description: custom_delay的参数schema
@@ -6113,6 +6124,7 @@ liquid_handler.prcxi:
goal_default: goal_default:
tip_racks: null tip_racks: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: iter_tips的参数schema description: iter_tips的参数schema
@@ -6139,6 +6151,7 @@ liquid_handler.prcxi:
dis_to_top: 0 dis_to_top: 0
well: null well: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: move_to的参数schema description: move_to的参数schema
@@ -6168,6 +6181,7 @@ liquid_handler.prcxi:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: run_protocol的参数schema description: run_protocol的参数schema
@@ -6183,12 +6197,50 @@ liquid_handler.prcxi:
title: run_protocol参数 title: run_protocol参数
type: object type: object
type: UniLabJsonCommandAsync type: UniLabJsonCommandAsync
auto-set_group:
feedback: {}
goal: {}
goal_default:
group_name: null
volumes: null
wells: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
group_name:
type: string
volumes:
items:
type: number
type: array
wells:
items:
type: object
type: array
required:
- group_name
- wells
- volumes
type: object
result: {}
required:
- goal
title: set_group参数
type: object
type: UniLabJsonCommand
auto-touch_tip: auto-touch_tip:
feedback: {} feedback: {}
goal: {} goal: {}
goal_default: goal_default:
targets: null targets: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: touch_tip的参数schema description: touch_tip的参数schema
@@ -6207,6 +6259,39 @@ liquid_handler.prcxi:
title: touch_tip参数 title: touch_tip参数
type: object type: object
type: UniLabJsonCommandAsync type: UniLabJsonCommandAsync
auto-transfer_group:
feedback: {}
goal: {}
goal_default:
source_group_name: null
target_group_name: null
unit_volume: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
source_group_name:
type: string
target_group_name:
type: string
unit_volume:
type: number
required:
- source_group_name
- target_group_name
- unit_volume
type: object
result: {}
required:
- goal
title: transfer_group参数
type: object
type: UniLabJsonCommandAsync
discard_tips: discard_tips:
feedback: {} feedback: {}
goal: goal:

View File

@@ -9,6 +9,7 @@ neware_battery_test_system:
goal_default: goal_default:
ros_node: null ros_node: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: '' description: ''
@@ -32,6 +33,7 @@ neware_battery_test_system:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: '' description: ''
@@ -52,6 +54,7 @@ neware_battery_test_system:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: '' description: ''

View File

@@ -9,6 +9,7 @@ rotavap.one:
goal_default: goal_default:
cmd: null cmd: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: cmd_write的参数schema description: cmd_write的参数schema
@@ -32,6 +33,7 @@ rotavap.one:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: main_loop的参数schema description: main_loop的参数schema
@@ -53,6 +55,7 @@ rotavap.one:
goal_default: goal_default:
time: null time: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: set_pump_time的参数schema description: set_pump_time的参数schema
@@ -77,6 +80,7 @@ rotavap.one:
goal_default: goal_default:
time: null time: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: set_rotate_time的参数schema description: set_rotate_time的参数schema
@@ -172,6 +176,7 @@ separator.homemade:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: read_sensor_loop的参数schema description: read_sensor_loop的参数schema
@@ -194,6 +199,7 @@ separator.homemade:
condition: null condition: null
value: null value: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: valve_open的参数schema description: valve_open的参数schema
@@ -221,6 +227,7 @@ separator.homemade:
goal_default: goal_default:
data: null data: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: write的参数schema description: write的参数schema

View File

@@ -8,6 +8,7 @@ solenoid_valve:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: close的参数schema description: close的参数schema
@@ -28,6 +29,7 @@ solenoid_valve:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: is_closed的参数schema description: is_closed的参数schema
@@ -48,6 +50,7 @@ solenoid_valve:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: is_open的参数schema description: is_open的参数schema
@@ -68,6 +71,7 @@ solenoid_valve:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: '' description: ''
@@ -88,6 +92,7 @@ solenoid_valve:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: read_data的参数schema description: read_data的参数schema
@@ -109,6 +114,7 @@ solenoid_valve:
goal_default: goal_default:
command: null command: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: send_command的参数schema description: send_command的参数schema
@@ -205,6 +211,7 @@ solenoid_valve.mock:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: is_closed的参数schema description: is_closed的参数schema
@@ -225,6 +232,7 @@ solenoid_valve.mock:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: is_open的参数schema description: is_open的参数schema
@@ -246,6 +254,7 @@ solenoid_valve.mock:
goal_default: goal_default:
position: null position: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: set_valve_position的参数schema description: set_valve_position的参数schema
@@ -376,6 +385,7 @@ syringe_pump_with_valve.runze.SY03B-T06:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: close的参数schema description: close的参数schema
@@ -396,6 +406,7 @@ syringe_pump_with_valve.runze.SY03B-T06:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: initialize的参数schema description: initialize的参数schema
@@ -417,6 +428,7 @@ syringe_pump_with_valve.runze.SY03B-T06:
goal_default: goal_default:
volume: null volume: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: pull_plunger的参数schema description: pull_plunger的参数schema
@@ -441,6 +453,7 @@ syringe_pump_with_valve.runze.SY03B-T06:
goal_default: goal_default:
volume: null volume: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: push_plunger的参数schema description: push_plunger的参数schema
@@ -464,6 +477,7 @@ syringe_pump_with_valve.runze.SY03B-T06:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: query_aux_input_status_1的参数schema description: query_aux_input_status_1的参数schema
@@ -484,6 +498,7 @@ syringe_pump_with_valve.runze.SY03B-T06:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: query_aux_input_status_2的参数schema description: query_aux_input_status_2的参数schema
@@ -504,6 +519,7 @@ syringe_pump_with_valve.runze.SY03B-T06:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: query_backlash_position的参数schema description: query_backlash_position的参数schema
@@ -524,6 +540,7 @@ syringe_pump_with_valve.runze.SY03B-T06:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: query_command_buffer_status的参数schema description: query_command_buffer_status的参数schema
@@ -544,6 +561,7 @@ syringe_pump_with_valve.runze.SY03B-T06:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: query_software_version的参数schema description: query_software_version的参数schema
@@ -565,6 +583,7 @@ syringe_pump_with_valve.runze.SY03B-T06:
goal_default: goal_default:
full_command: null full_command: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: send_command的参数schema description: send_command的参数schema
@@ -589,6 +608,7 @@ syringe_pump_with_valve.runze.SY03B-T06:
goal_default: goal_default:
baudrate: null baudrate: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: set_baudrate的参数schema description: set_baudrate的参数schema
@@ -613,6 +633,7 @@ syringe_pump_with_valve.runze.SY03B-T06:
goal_default: goal_default:
velocity: null velocity: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: set_max_velocity的参数schema description: set_max_velocity的参数schema
@@ -638,6 +659,7 @@ syringe_pump_with_valve.runze.SY03B-T06:
max_velocity: null max_velocity: null
position: null position: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: set_position的参数schema description: set_position的参数schema
@@ -664,6 +686,7 @@ syringe_pump_with_valve.runze.SY03B-T06:
goal_default: goal_default:
position: null position: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: set_valve_position的参数schema description: set_valve_position的参数schema
@@ -688,6 +711,7 @@ syringe_pump_with_valve.runze.SY03B-T06:
goal_default: goal_default:
velocity: null velocity: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: set_velocity_grade的参数schema description: set_velocity_grade的参数schema
@@ -711,6 +735,7 @@ syringe_pump_with_valve.runze.SY03B-T06:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: stop_operation的参数schema description: stop_operation的参数schema
@@ -731,6 +756,7 @@ syringe_pump_with_valve.runze.SY03B-T06:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: wait_error的参数schema description: wait_error的参数schema
@@ -880,6 +906,7 @@ syringe_pump_with_valve.runze.SY03B-T08:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: close的参数schema description: close的参数schema
@@ -900,6 +927,7 @@ syringe_pump_with_valve.runze.SY03B-T08:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: initialize的参数schema description: initialize的参数schema
@@ -921,6 +949,7 @@ syringe_pump_with_valve.runze.SY03B-T08:
goal_default: goal_default:
volume: null volume: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: pull_plunger的参数schema description: pull_plunger的参数schema
@@ -945,6 +974,7 @@ syringe_pump_with_valve.runze.SY03B-T08:
goal_default: goal_default:
volume: null volume: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: push_plunger的参数schema description: push_plunger的参数schema
@@ -968,6 +998,7 @@ syringe_pump_with_valve.runze.SY03B-T08:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: query_aux_input_status_1的参数schema description: query_aux_input_status_1的参数schema
@@ -988,6 +1019,7 @@ syringe_pump_with_valve.runze.SY03B-T08:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: query_aux_input_status_2的参数schema description: query_aux_input_status_2的参数schema
@@ -1008,6 +1040,7 @@ syringe_pump_with_valve.runze.SY03B-T08:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: query_backlash_position的参数schema description: query_backlash_position的参数schema
@@ -1028,6 +1061,7 @@ syringe_pump_with_valve.runze.SY03B-T08:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: query_command_buffer_status的参数schema description: query_command_buffer_status的参数schema
@@ -1048,6 +1082,7 @@ syringe_pump_with_valve.runze.SY03B-T08:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: query_software_version的参数schema description: query_software_version的参数schema
@@ -1069,6 +1104,7 @@ syringe_pump_with_valve.runze.SY03B-T08:
goal_default: goal_default:
full_command: null full_command: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: send_command的参数schema description: send_command的参数schema
@@ -1093,6 +1129,7 @@ syringe_pump_with_valve.runze.SY03B-T08:
goal_default: goal_default:
baudrate: null baudrate: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: set_baudrate的参数schema description: set_baudrate的参数schema
@@ -1117,6 +1154,7 @@ syringe_pump_with_valve.runze.SY03B-T08:
goal_default: goal_default:
velocity: null velocity: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: set_max_velocity的参数schema description: set_max_velocity的参数schema
@@ -1142,6 +1180,7 @@ syringe_pump_with_valve.runze.SY03B-T08:
max_velocity: null max_velocity: null
position: null position: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: set_position的参数schema description: set_position的参数schema
@@ -1168,6 +1207,7 @@ syringe_pump_with_valve.runze.SY03B-T08:
goal_default: goal_default:
position: null position: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: set_valve_position的参数schema description: set_valve_position的参数schema
@@ -1192,6 +1232,7 @@ syringe_pump_with_valve.runze.SY03B-T08:
goal_default: goal_default:
velocity: null velocity: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: set_velocity_grade的参数schema description: set_velocity_grade的参数schema
@@ -1215,6 +1256,7 @@ syringe_pump_with_valve.runze.SY03B-T08:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: stop_operation的参数schema description: stop_operation的参数schema
@@ -1235,6 +1277,7 @@ syringe_pump_with_valve.runze.SY03B-T08:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: wait_error的参数schema description: wait_error的参数schema

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,7 @@ agv.SEER:
ex_data: '' ex_data: ''
obj: receive_socket obj: receive_socket
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: AGV底层通信命令发送函数。通过TCP socket连接向AGV发送底层控制命令支持pose位置、status状态、nav导航等命令类型。用于获取AGV当前位置坐标、运行状态或发送导航指令。该函数封装了AGV的通信协议将命令转换为十六进制数据包并处理响应解析。 description: AGV底层通信命令发送函数。通过TCP socket连接向AGV发送底层控制命令支持pose位置、status状态、nav导航等命令类型。用于获取AGV当前位置坐标、运行状态或发送导航指令。该函数封装了AGV的通信协议将命令转换为十六进制数据包并处理响应解析。

View File

@@ -8,6 +8,7 @@ robotic_arm.SCARA_with_slider.virtual:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: check_tf_update_actions的参数schema description: check_tf_update_actions的参数schema
@@ -33,6 +34,7 @@ robotic_arm.SCARA_with_slider.virtual:
retry: 10 retry: 10
speed: 1 speed: 1
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: moveit_joint_task的参数schema description: moveit_joint_task的参数schema
@@ -78,6 +80,7 @@ robotic_arm.SCARA_with_slider.virtual:
speed: 1 speed: 1
target_link: null target_link: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: moveit_task的参数schema description: moveit_task的参数schema
@@ -125,6 +128,7 @@ robotic_arm.SCARA_with_slider.virtual:
goal_default: goal_default:
ros_node: null ros_node: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: post_init的参数schema description: post_init的参数schema
@@ -150,6 +154,7 @@ robotic_arm.SCARA_with_slider.virtual:
parent_link: null parent_link: null
resource: null resource: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: resource_manager的参数schema description: resource_manager的参数schema
@@ -176,6 +181,7 @@ robotic_arm.SCARA_with_slider.virtual:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: wait_for_resource_action的参数schema description: wait_for_resource_action的参数schema
@@ -360,6 +366,7 @@ robotic_arm.UR:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 机械臂初始化函数。执行UR机械臂的完整初始化流程包括上电、释放制动器、解除保护停止状态等。该函数确保机械臂从安全停止状态恢复到可操作状态是机械臂使用前的必要步骤。初始化完成后机械臂将处于就绪状态可以接收后续的运动指令。 description: 机械臂初始化函数。执行UR机械臂的完整初始化流程包括上电、释放制动器、解除保护停止状态等。该函数确保机械臂从安全停止状态恢复到可操作状态是机械臂使用前的必要步骤。初始化完成后机械臂将处于就绪状态可以接收后续的运动指令。
@@ -381,6 +388,7 @@ robotic_arm.UR:
goal_default: goal_default:
data: null data: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 从JSON字符串加载位置数据函数。接收包含机械臂位置信息的JSON格式字符串解析并存储位置数据供后续运动任务使用。位置数据通常包含多个预定义的工作位置坐标用于实现精确的多点运动控制。适用于动态配置机械臂工作位置的场景。 description: 从JSON字符串加载位置数据函数。接收包含机械臂位置信息的JSON格式字符串解析并存储位置数据供后续运动任务使用。位置数据通常包含多个预定义的工作位置坐标用于实现精确的多点运动控制。适用于动态配置机械臂工作位置的场景。
@@ -405,6 +413,7 @@ robotic_arm.UR:
goal_default: goal_default:
file: null file: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 从文件加载位置数据函数。读取指定的JSON文件并加载其中的机械臂位置信息。该函数支持从外部配置文件中获取预设的工作位置便于位置数据的管理和重用。适用于需要从固定配置文件中读取复杂位置序列的应用场景。 description: 从文件加载位置数据函数。读取指定的JSON文件并加载其中的机械臂位置信息。该函数支持从外部配置文件中获取预设的工作位置便于位置数据的管理和重用。适用于需要从固定配置文件中读取复杂位置序列的应用场景。
@@ -428,6 +437,7 @@ robotic_arm.UR:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 重新加载位置数据函数。重新读取并解析之前设置的位置文件,更新内存中的位置数据。该函数用于在位置文件被修改后刷新机械臂的位置配置,无需重新初始化整个系统。适用于动态更新机械臂工作位置的场景。 description: 重新加载位置数据函数。重新读取并解析之前设置的位置文件,更新内存中的位置数据。该函数用于在位置文件被修改后刷新机械臂的位置配置,无需重新初始化整个系统。适用于动态更新机械臂工作位置的场景。
@@ -536,6 +546,7 @@ robotic_arm.elite:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: '' description: ''
@@ -556,6 +567,7 @@ robotic_arm.elite:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: '' description: ''
@@ -579,6 +591,7 @@ robotic_arm.elite:
start_addr: null start_addr: null
unit_id: null unit_id: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: '' description: ''
@@ -609,6 +622,7 @@ robotic_arm.elite:
goal_default: goal_default:
job_id: null job_id: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: '' description: ''
@@ -635,6 +649,7 @@ robotic_arm.elite:
unit_id: null unit_id: null
value: null value: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: '' description: ''
@@ -665,6 +680,7 @@ robotic_arm.elite:
goal_default: goal_default:
response: null response: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: '' description: ''
@@ -689,6 +705,7 @@ robotic_arm.elite:
goal_default: goal_default:
command: null command: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: '' description: ''

View File

@@ -8,6 +8,7 @@ gripper.misumi_rz:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: data_loop的参数schema description: data_loop的参数schema
@@ -28,6 +29,7 @@ gripper.misumi_rz:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: data_reader的参数schema description: data_reader的参数schema
@@ -51,6 +53,7 @@ gripper.misumi_rz:
pos: null pos: null
speed: null speed: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 夹爪抓取运动控制函数。控制夹爪的开合运动,支持位置、速度、力矩的精确设定。位置参数控制夹爪开合程度,速度参数控制运动快慢,力矩参数控制夹持强度。该函数提供安全的力控制,避免损坏被抓取物体,适用于各种形状和材质的物品抓取。 description: 夹爪抓取运动控制函数。控制夹爪的开合运动,支持位置、速度、力矩的精确设定。位置参数控制夹爪开合程度,速度参数控制运动快慢,力矩参数控制夹持强度。该函数提供安全的力控制,避免损坏被抓取物体,适用于各种形状和材质的物品抓取。
@@ -80,6 +83,7 @@ gripper.misumi_rz:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 夹爪初始化函数。执行Misumi RZ夹爪的完整初始化流程包括Modbus通信建立、电机参数配置、传感器校准等。该函数确保夹爪系统从安全状态恢复到可操作状态是夹爪使用前的必要步骤。初始化完成后夹爪将处于就绪状态可接收抓取和旋转指令。 description: 夹爪初始化函数。执行Misumi RZ夹爪的完整初始化流程包括Modbus通信建立、电机参数配置、传感器校准等。该函数确保夹爪系统从安全状态恢复到可操作状态是夹爪使用前的必要步骤。初始化完成后夹爪将处于就绪状态可接收抓取和旋转指令。
@@ -101,6 +105,7 @@ gripper.misumi_rz:
goal_default: goal_default:
data: null data: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: modbus_crc的参数schema description: modbus_crc的参数schema
@@ -130,6 +135,7 @@ gripper.misumi_rz:
spin_pos: null spin_pos: null
spin_v: null spin_v: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: move_and_rotate的参数schema description: move_and_rotate的参数schema
@@ -169,6 +175,7 @@ gripper.misumi_rz:
goal_default: goal_default:
cmd: null cmd: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 节点夹爪移动任务函数。接收逗号分隔的命令字符串,解析位置、速度、力矩参数并执行夹爪抓取动作。该函数等待运动完成并返回执行结果,提供同步的运动控制接口。适用于需要可靠完成确认的精密抓取操作。 description: 节点夹爪移动任务函数。接收逗号分隔的命令字符串,解析位置、速度、力矩参数并执行夹爪抓取动作。该函数等待运动完成并返回执行结果,提供同步的运动控制接口。适用于需要可靠完成确认的精密抓取操作。
@@ -193,6 +200,7 @@ gripper.misumi_rz:
goal_default: goal_default:
cmd: null cmd: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 节点旋转移动任务函数。接收逗号分隔的命令字符串,解析角度、速度、力矩参数并执行夹爪旋转动作。该函数等待旋转完成并返回执行结果,提供同步的旋转控制接口。适用于需要精确角度定位和完成确认的旋转操作。 description: 节点旋转移动任务函数。接收逗号分隔的命令字符串,解析角度、速度、力矩参数并执行夹爪旋转动作。该函数等待旋转完成并返回执行结果,提供同步的旋转控制接口。适用于需要精确角度定位和完成确认的旋转操作。
@@ -219,6 +227,7 @@ gripper.misumi_rz:
data_len: null data_len: null
id: null id: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: read_address的参数schema description: read_address的参数schema
@@ -251,6 +260,7 @@ gripper.misumi_rz:
pos: null pos: null
speed: null speed: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 夹爪绝对位置旋转控制函数。控制夹爪主轴旋转到指定的绝对角度位置支持360度连续旋转。位置参数指定目标角度速度参数控制旋转速率力矩参数设定旋转阻力限制。该函数提供高精度的角度定位适用于需要精确方向控制的操作场景。 description: 夹爪绝对位置旋转控制函数。控制夹爪主轴旋转到指定的绝对角度位置支持360度连续旋转。位置参数指定目标角度速度参数控制旋转速率力矩参数设定旋转阻力限制。该函数提供高精度的角度定位适用于需要精确方向控制的操作场景。
@@ -284,6 +294,7 @@ gripper.misumi_rz:
fun: null fun: null
id: null id: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: send_cmd的参数schema description: send_cmd的参数schema
@@ -316,6 +327,7 @@ gripper.misumi_rz:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: wait_for_gripper的参数schema description: wait_for_gripper的参数schema
@@ -336,6 +348,7 @@ gripper.misumi_rz:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: wait_for_gripper_init的参数schema description: wait_for_gripper_init的参数schema
@@ -356,6 +369,7 @@ gripper.misumi_rz:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: wait_for_rotate的参数schema description: wait_for_rotate的参数schema
@@ -462,6 +476,7 @@ gripper.mock:
Gripper1: {} Gripper1: {}
wf_name: gripper_run wf_name: gripper_run
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 模拟夹爪资源ID编辑函数。用于测试和演示资源管理功能模拟修改夹爪资源的标识信息。该函数接收工作流名称、参数和资源对象模拟真实的资源更新过程并返回修改后的资源信息。适用于系统测试和开发调试场景。 description: 模拟夹爪资源ID编辑函数。用于测试和演示资源管理功能模拟修改夹爪资源的标识信息。该函数接收工作流名称、参数和资源对象模拟真实的资源更新过程并返回修改后的资源信息。适用于系统测试和开发调试场景。

View File

@@ -8,6 +8,7 @@ linear_motion.grbl:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: CNC设备初始化函数。执行Grbl CNC的完整初始化流程包括归零操作、轴校准和状态复位。该函数将所有轴移动到原点位置(0,0,0),确保设备处于已知的参考状态。初始化完成后设备进入空闲状态,可接收后续的运动指令。 description: CNC设备初始化函数。执行Grbl CNC的完整初始化流程包括归零操作、轴校准和状态复位。该函数将所有轴移动到原点位置(0,0,0),确保设备处于已知的参考状态。初始化完成后设备进入空闲状态,可接收后续的运动指令。
@@ -29,6 +30,7 @@ linear_motion.grbl:
goal_default: goal_default:
position: null position: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: CNC绝对位置设定函数。控制CNC设备移动到指定的三维坐标位置(x,y,z)。该函数支持安全限位检查,防止超出设备工作范围。移动过程中会监控设备状态,确保安全到达目标位置。适用于精确定位和轨迹控制操作。 description: CNC绝对位置设定函数。控制CNC设备移动到指定的三维坐标位置(x,y,z)。该函数支持安全限位检查,防止超出设备工作范围。移动过程中会监控设备状态,确保安全到达目标位置。适用于精确定位和轨迹控制操作。
@@ -52,6 +54,7 @@ linear_motion.grbl:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: CNC操作停止函数。立即停止当前正在执行的所有CNC运动包括轴移动和主轴旋转。该函数用于紧急停止或任务中断确保设备和工件的安全。停止后设备将保持当前位置等待新的指令。 description: CNC操作停止函数。立即停止当前正在执行的所有CNC运动包括轴移动和主轴旋转。该函数用于紧急停止或任务中断确保设备和工件的安全。停止后设备将保持当前位置等待新的指令。
@@ -72,6 +75,7 @@ linear_motion.grbl:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: wait_error的参数schema description: wait_error的参数schema
@@ -482,6 +486,7 @@ linear_motion.toyo_xyz.sim:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: check_tf_update_actions的参数schema description: check_tf_update_actions的参数schema
@@ -507,6 +512,7 @@ linear_motion.toyo_xyz.sim:
retry: 10 retry: 10
speed: 1 speed: 1
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: moveit_joint_task的参数schema description: moveit_joint_task的参数schema
@@ -552,6 +558,7 @@ linear_motion.toyo_xyz.sim:
speed: 1 speed: 1
target_link: null target_link: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: moveit_task的参数schema description: moveit_task的参数schema
@@ -599,6 +606,7 @@ linear_motion.toyo_xyz.sim:
goal_default: goal_default:
ros_node: null ros_node: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: post_init的参数schema description: post_init的参数schema
@@ -624,6 +632,7 @@ linear_motion.toyo_xyz.sim:
parent_link: null parent_link: null
resource: null resource: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: resource_manager的参数schema description: resource_manager的参数schema
@@ -650,6 +659,7 @@ linear_motion.toyo_xyz.sim:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: wait_for_resource_action的参数schema description: wait_for_resource_action的参数schema
@@ -837,6 +847,7 @@ motor.iCL42:
position: null position: null
velocity: null velocity: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 步进电机执行运动函数。直接执行电机运动命令,包括位置设定、速度控制和路径规划。该函数处理底层的电机控制协议,消除警告信息,设置运动参数并启动电机运行。适用于需要直接控制电机运动的应用场景。 description: 步进电机执行运动函数。直接执行电机运动命令,包括位置设定、速度控制和路径规划。该函数处理底层的电机控制协议,消除警告信息,设置运动参数并启动电机运行。适用于需要直接控制电机运动的应用场景。
@@ -866,6 +877,7 @@ motor.iCL42:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: iCL42电机设备初始化函数。建立与iCL42步进电机驱动器的串口通信连接配置通信参数包括波特率、数据位、校验位等。该函数是电机使用前的必要步骤确保驱动器处于可控状态并准备接收运动指令。 description: iCL42电机设备初始化函数。建立与iCL42步进电机驱动器的串口通信连接配置通信参数包括波特率、数据位、校验位等。该函数是电机使用前的必要步骤确保驱动器处于可控状态并准备接收运动指令。
@@ -889,6 +901,7 @@ motor.iCL42:
position: null position: null
velocity: null velocity: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 步进电机运动控制函数。根据指定的运动模式、目标位置和速度参数控制电机运动。支持多种运动模式和精确的位置控制,自动处理运动轨迹规划和执行。该函数提供异步执行和状态反馈,确保运动的准确性和可靠性。 description: 步进电机运动控制函数。根据指定的运动模式、目标位置和速度参数控制电机运动。支持多种运动模式和精确的位置控制,自动处理运动轨迹规划和执行。该函数提供异步执行和状态反馈,确保运动的准确性和可靠性。

View File

@@ -65,6 +65,7 @@ solid_dispenser.laiyu:
goal_default: goal_default:
data: null data: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: Modbus CRC-16校验码计算函数。计算Modbus RTU通信协议所需的CRC-16校验码确保数据传输的完整性和可靠性。该函数实现标准的CRC-16算法用于构造完整的Modbus指令帧。 description: Modbus CRC-16校验码计算函数。计算Modbus RTU通信协议所需的CRC-16校验码确保数据传输的完整性和可靠性。该函数实现标准的CRC-16算法用于构造完整的Modbus指令帧。
@@ -89,6 +90,7 @@ solid_dispenser.laiyu:
goal_default: goal_default:
command: null command: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: Modbus指令发送函数。构造完整的Modbus RTU指令帧包含CRC校验发送给分装设备并等待响应。该函数处理底层通信协议确保指令的正确传输和响应接收支持最长3分钟的响应等待时间。 description: Modbus指令发送函数。构造完整的Modbus RTU指令帧包含CRC校验发送给分装设备并等待响应。该函数处理底层通信协议确保指令的正确传输和响应接收支持最长3分钟的响应等待时间。

View File

@@ -12,6 +12,7 @@ chiller:
register_address: null register_address: null
value: null value: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: build_modbus_frame的参数schema description: build_modbus_frame的参数schema
@@ -46,6 +47,7 @@ chiller:
decimal_points: 1 decimal_points: 1
temperature: null temperature: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: convert_temperature_to_modbus_value的参数schema description: convert_temperature_to_modbus_value的参数schema
@@ -73,6 +75,7 @@ chiller:
goal_default: goal_default:
data: null data: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: modbus_crc的参数schema description: modbus_crc的参数schema
@@ -96,6 +99,7 @@ chiller:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: stop的参数schema description: stop的参数schema
@@ -188,6 +192,7 @@ heaterstirrer.dalong:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: close的参数schema description: close的参数schema
@@ -209,6 +214,7 @@ heaterstirrer.dalong:
goal_default: goal_default:
speed: null speed: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: set_stir_speed的参数schema description: set_stir_speed的参数schema
@@ -234,6 +240,7 @@ heaterstirrer.dalong:
temp: null temp: null
type: warning type: warning
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: set_temp_inner的参数schema description: set_temp_inner的参数schema
@@ -580,6 +587,7 @@ tempsensor:
register_address: null register_address: null
register_count: null register_count: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: build_modbus_request的参数schema description: build_modbus_request的参数schema
@@ -613,6 +621,7 @@ tempsensor:
goal_default: goal_default:
data: null data: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: calculate_crc的参数schema description: calculate_crc的参数schema
@@ -637,6 +646,7 @@ tempsensor:
goal_default: goal_default:
response: null response: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: read_modbus_response的参数schema description: read_modbus_response的参数schema
@@ -661,6 +671,7 @@ tempsensor:
goal_default: goal_default:
command: null command: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: send_prototype_command的参数schema description: send_prototype_command的参数schema

View File

@@ -8,6 +8,7 @@ virtual_centrifuge:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: cleanup的参数schema description: cleanup的参数schema
@@ -28,6 +29,7 @@ virtual_centrifuge:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: initialize的参数schema description: initialize的参数schema
@@ -296,6 +298,7 @@ virtual_column:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: cleanup的参数schema description: cleanup的参数schema
@@ -316,6 +319,7 @@ virtual_column:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: initialize的参数schema description: initialize的参数schema
@@ -691,6 +695,7 @@ virtual_filter:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: cleanup的参数schema description: cleanup的参数schema
@@ -711,6 +716,7 @@ virtual_filter:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: initialize的参数schema description: initialize的参数schema
@@ -1089,6 +1095,7 @@ virtual_gas_source:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: cleanup的参数schema description: cleanup的参数schema
@@ -1109,6 +1116,7 @@ virtual_gas_source:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: initialize的参数schema description: initialize的参数schema
@@ -1129,6 +1137,7 @@ virtual_gas_source:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: is_closed的参数schema description: is_closed的参数schema
@@ -1149,6 +1158,7 @@ virtual_gas_source:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: is_open的参数schema description: is_open的参数schema
@@ -1311,6 +1321,7 @@ virtual_heatchill:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: cleanup的参数schema description: cleanup的参数schema
@@ -1331,6 +1342,7 @@ virtual_heatchill:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: initialize的参数schema description: initialize的参数schema
@@ -1880,6 +1892,7 @@ virtual_multiway_valve:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: close的参数schema description: close的参数schema
@@ -1901,6 +1914,7 @@ virtual_multiway_valve:
goal_default: goal_default:
port_number: null port_number: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: is_at_port的参数schema description: is_at_port的参数schema
@@ -1925,6 +1939,7 @@ virtual_multiway_valve:
goal_default: goal_default:
position: null position: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: is_at_position的参数schema description: is_at_position的参数schema
@@ -1948,6 +1963,7 @@ virtual_multiway_valve:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: is_at_pump_position的参数schema description: is_at_pump_position的参数schema
@@ -1968,6 +1984,7 @@ virtual_multiway_valve:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: '' description: ''
@@ -1988,6 +2005,7 @@ virtual_multiway_valve:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: reset的参数schema description: reset的参数schema
@@ -2009,6 +2027,7 @@ virtual_multiway_valve:
goal_default: goal_default:
port_number: null port_number: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: set_to_port的参数schema description: set_to_port的参数schema
@@ -2032,6 +2051,7 @@ virtual_multiway_valve:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: set_to_pump_position的参数schema description: set_to_pump_position的参数schema
@@ -2053,6 +2073,7 @@ virtual_multiway_valve:
goal_default: goal_default:
port_number: null port_number: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: switch_between_pump_and_port的参数schema description: switch_between_pump_and_port的参数schema
@@ -2300,6 +2321,7 @@ virtual_rotavap:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: cleanup的参数schema description: cleanup的参数schema
@@ -2320,6 +2342,7 @@ virtual_rotavap:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: initialize的参数schema description: initialize的参数schema
@@ -2630,6 +2653,7 @@ virtual_separator:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: cleanup的参数schema description: cleanup的参数schema
@@ -2650,6 +2674,7 @@ virtual_separator:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: initialize的参数schema description: initialize的参数schema
@@ -3517,6 +3542,7 @@ virtual_solenoid_valve:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: cleanup的参数schema description: cleanup的参数schema
@@ -3537,6 +3563,7 @@ virtual_solenoid_valve:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: initialize的参数schema description: initialize的参数schema
@@ -3557,6 +3584,7 @@ virtual_solenoid_valve:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: is_closed的参数schema description: is_closed的参数schema
@@ -3577,6 +3605,7 @@ virtual_solenoid_valve:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: reset的参数schema description: reset的参数schema
@@ -3597,6 +3626,7 @@ virtual_solenoid_valve:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: toggle的参数schema description: toggle的参数schema
@@ -4035,6 +4065,7 @@ virtual_solid_dispenser:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: cleanup的参数schema description: cleanup的参数schema
@@ -4056,6 +4087,7 @@ virtual_solid_dispenser:
goal_default: goal_default:
reagent_name: null reagent_name: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: '' description: ''
@@ -4079,6 +4111,7 @@ virtual_solid_dispenser:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: initialize的参数schema description: initialize的参数schema
@@ -4100,6 +4133,7 @@ virtual_solid_dispenser:
goal_default: goal_default:
mass_str: null mass_str: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: '' description: ''
@@ -4124,6 +4158,7 @@ virtual_solid_dispenser:
goal_default: goal_default:
mol_str: null mol_str: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: '' description: ''
@@ -4206,6 +4241,7 @@ virtual_stirrer:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: cleanup的参数schema description: cleanup的参数schema
@@ -4226,6 +4262,7 @@ virtual_stirrer:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: initialize的参数schema description: initialize的参数schema
@@ -4777,6 +4814,7 @@ virtual_transfer_pump:
velocity: null velocity: null
volume: null volume: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: aspirate的参数schema description: aspirate的参数schema
@@ -4802,6 +4840,7 @@ virtual_transfer_pump:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: cleanup的参数schema description: cleanup的参数schema
@@ -4824,6 +4863,7 @@ virtual_transfer_pump:
velocity: null velocity: null
volume: null volume: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: dispense的参数schema description: dispense的参数schema
@@ -4850,6 +4890,7 @@ virtual_transfer_pump:
goal_default: goal_default:
velocity: null velocity: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: empty_syringe的参数schema description: empty_syringe的参数schema
@@ -4873,6 +4914,7 @@ virtual_transfer_pump:
goal_default: goal_default:
velocity: null velocity: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: fill_syringe的参数schema description: fill_syringe的参数schema
@@ -4895,6 +4937,7 @@ virtual_transfer_pump:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: initialize的参数schema description: initialize的参数schema
@@ -4915,6 +4958,7 @@ virtual_transfer_pump:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: is_empty的参数schema description: is_empty的参数schema
@@ -4935,6 +4979,7 @@ virtual_transfer_pump:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: is_full的参数schema description: is_full的参数schema
@@ -4957,6 +5002,7 @@ virtual_transfer_pump:
velocity: null velocity: null
volume: null volume: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: pull_plunger的参数schema description: pull_plunger的参数schema
@@ -4984,6 +5030,7 @@ virtual_transfer_pump:
velocity: null velocity: null
volume: null volume: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: push_plunger的参数schema description: push_plunger的参数schema
@@ -5010,6 +5057,7 @@ virtual_transfer_pump:
goal_default: goal_default:
velocity: null velocity: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: set_max_velocity的参数schema description: set_max_velocity的参数schema
@@ -5033,6 +5081,7 @@ virtual_transfer_pump:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: stop_operation的参数schema description: stop_operation的参数schema
@@ -5277,6 +5326,7 @@ virtual_vacuum_pump:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: cleanup的参数schema description: cleanup的参数schema
@@ -5297,6 +5347,7 @@ virtual_vacuum_pump:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: initialize的参数schema description: initialize的参数schema
@@ -5317,6 +5368,7 @@ virtual_vacuum_pump:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: is_closed的参数schema description: is_closed的参数schema
@@ -5337,6 +5389,7 @@ virtual_vacuum_pump:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: is_open的参数schema description: is_open的参数schema

File diff suppressed because it is too large Load Diff

View File

@@ -40,6 +40,7 @@ zhida_gcms:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 安全关闭与智达 GCMS 设备的 TCP 连接,释放网络资源。 description: 安全关闭与智达 GCMS 设备的 TCP 连接,释放网络资源。
@@ -60,6 +61,7 @@ zhida_gcms:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 与智达 GCMS 设备建立 TCP 连接,配置超时参数。 description: 与智达 GCMS 设备建立 TCP 连接,配置超时参数。
@@ -81,6 +83,7 @@ zhida_gcms:
goal_default: goal_default:
ros_node: null ros_node: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: '' description: ''

View File

@@ -0,0 +1,9 @@
from pylabrobot.resources import Resource
class ResourceSlot(Resource):
pass
class DeviceSlot(str):
pass

View File

@@ -8,10 +8,16 @@ from pathlib import Path
from typing import Any, Dict, List, Union, Tuple from typing import Any, Dict, List, Union, Tuple
import yaml import yaml
from unilabos_msgs.msg import Resource
from unilabos.config.config import BasicConfig from unilabos.config.config import BasicConfig
from unilabos.resources.graphio import resource_plr_to_ulab, tree_to_list from unilabos.resources.graphio import resource_plr_to_ulab, tree_to_list
from unilabos.ros.msgs.message_converter import msg_converter_manager, ros_action_to_json_schema, String from unilabos.ros.msgs.message_converter import (
msg_converter_manager,
ros_action_to_json_schema,
String,
ros_message_to_json_schema,
)
from unilabos.utils import logger from unilabos.utils import logger
from unilabos.utils.decorator import singleton from unilabos.utils.decorator import singleton
from unilabos.utils.import_manager import get_enhanced_class_info, get_class from unilabos.utils.import_manager import get_enhanced_class_info, get_class
@@ -19,6 +25,7 @@ from unilabos.utils.type_check import NoAliasDumper
DEFAULT_PATHS = [Path(__file__).absolute().parent] DEFAULT_PATHS = [Path(__file__).absolute().parent]
class ROSMsgNotFound(Exception): class ROSMsgNotFound(Exception):
pass pass
@@ -48,6 +55,7 @@ class Registry:
"ResourceCreateFromOuterEasy", "host_node", f"动作 create_resource" "ResourceCreateFromOuterEasy", "host_node", f"动作 create_resource"
) )
self.EmptyIn = self._replace_type_with_class("EmptyIn", "host_node", f"") self.EmptyIn = self._replace_type_with_class("EmptyIn", "host_node", f"")
self.StrSingleInput = self._replace_type_with_class("StrSingleInput", "host_node", f"")
self.device_type_registry = {} self.device_type_registry = {}
self.device_module_to_registry = {} self.device_module_to_registry = {}
self.resource_type_registry = {} self.resource_type_registry = {}
@@ -123,7 +131,6 @@ class Registry:
} }
] ]
}, },
# todo: support nested keys, switch to non ros message schema
"placeholder_keys": { "placeholder_keys": {
"res_id": "unilabos_resources", # 将当前实验室的全部物料id作为下拉框可选择 "res_id": "unilabos_resources", # 将当前实验室的全部物料id作为下拉框可选择
"device_id": "unilabos_devices", # 将当前实验室的全部设备id作为下拉框可选择 "device_id": "unilabos_devices", # 将当前实验室的全部设备id作为下拉框可选择
@@ -134,13 +141,53 @@ class Registry:
"type": self.EmptyIn, "type": self.EmptyIn,
"goal": {}, "goal": {},
"feedback": {}, "feedback": {},
"result": {"latency_ms": "latency_ms", "time_diff_ms": "time_diff_ms"}, "result": {},
"schema": ros_action_to_json_schema( "schema": ros_action_to_json_schema(
self.EmptyIn, "用于测试延迟的动作,返回延迟时间和时间差。" self.EmptyIn, "用于测试延迟的动作,返回延迟时间和时间差。"
), ),
"goal_default": {}, "goal_default": {},
"handles": {}, "handles": {},
}, },
"auto-test_resource": {
"type": "UniLabJsonCommand",
"goal": {},
"feedback": {},
"result": {},
"schema": {
"description": "",
"properties": {
"feedback": {},
"goal": {
"properties": {
"resource": ros_message_to_json_schema(Resource, "resource"),
"resources": {
"items": {
"properties": ros_message_to_json_schema(
Resource, "resources"
),
"type": "object",
},
"type": "array",
},
"device": {"type": "string"},
"devices": {"items": {"type": "string"}, "type": "array"},
},
"type": "object",
},
"result": {},
},
"title": "test_resource",
"type": "object",
},
"placeholder_keys": {
"device": "unilabos_devices",
"devices": "unilabos_devices",
"resource": "unilabos_resources",
"resources": "unilabos_resources",
},
"goal_default": {},
"handles": {},
},
}, },
}, },
"version": "1.0.0", "version": "1.0.0",
@@ -154,6 +201,8 @@ class Registry:
} }
} }
) )
# 为host_node添加内置的驱动命令动作
self._add_builtin_actions(self.device_type_registry["host_node"], "host_node")
logger.trace(f"[UniLab Registry] ----------Setup----------") logger.trace(f"[UniLab Registry] ----------Setup----------")
self.registry_paths = [Path(path).absolute() for path in self.registry_paths] self.registry_paths = [Path(path).absolute() for path in self.registry_paths]
for i, path in enumerate(self.registry_paths): for i, path in enumerate(self.registry_paths):
@@ -426,7 +475,17 @@ class Registry:
param_type = arg_info.get("type", "") param_type = arg_info.get("type", "")
param_default = arg_info.get("default") param_default = arg_info.get("default")
param_required = arg_info.get("required", True) param_required = arg_info.get("required", True)
schema["properties"][param_name] = self._generate_schema_from_info(param_name, param_type, param_default) if param_type == "unilabos.registry.placeholder_type:ResourceSlot":
schema["properties"][param_name] = ros_message_to_json_schema(Resource, param_name)
elif param_type == ("list", "unilabos.registry.placeholder_type:ResourceSlot"):
schema["properties"][param_name] = {
"items": ros_message_to_json_schema(Resource, param_name),
"type": "array",
}
else:
schema["properties"][param_name] = self._generate_schema_from_info(
param_name, param_type, param_default
)
if param_required: if param_required:
schema["required"].append(param_name) schema["required"].append(param_name)
@@ -438,6 +497,43 @@ class Registry:
"required": ["goal"], "required": ["goal"],
} }
def _add_builtin_actions(self, device_config: Dict[str, Any], device_id: str):
"""
为设备配置添加内置的执行驱动命令动作
Args:
device_config: 设备配置字典
device_id: 设备ID
"""
from unilabos.app.web.utils.action_utils import get_yaml_from_goal_type
if "class" not in device_config:
return
if "action_value_mappings" not in device_config["class"]:
device_config["class"]["action_value_mappings"] = {}
for additional_action in ["_execute_driver_command", "_execute_driver_command_async"]:
device_config["class"]["action_value_mappings"][additional_action] = {
"type": self._replace_type_with_class("StrSingleInput", device_id, f"动作 {additional_action}"),
"goal": {"string": "string"},
"feedback": {},
"result": {},
"schema": ros_action_to_json_schema(
self._replace_type_with_class("StrSingleInput", device_id, f"动作 {additional_action}")
),
"goal_default": yaml.safe_load(
io.StringIO(
get_yaml_from_goal_type(
self._replace_type_with_class(
"StrSingleInput", device_id, f"动作 {additional_action}"
).Goal
)
)
),
"handles": {},
}
def load_device_types(self, path: os.PathLike, complete_registry: bool): def load_device_types(self, path: os.PathLike, complete_registry: bool):
# return # return
abs_path = Path(path).absolute() abs_path = Path(path).absolute()
@@ -499,7 +595,9 @@ class Registry:
status_type = "String" # 替换成ROS的String便于显示 status_type = "String" # 替换成ROS的String便于显示
device_config["class"]["status_types"][status_name] = status_type device_config["class"]["status_types"][status_name] = status_type
try: try:
target_type = self._replace_type_with_class(status_type, device_id, f"状态 {status_name}") target_type = self._replace_type_with_class(
status_type, device_id, f"状态 {status_name}"
)
except ROSMsgNotFound: except ROSMsgNotFound:
continue continue
if target_type in [ if target_type in [
@@ -536,13 +634,29 @@ class Registry:
"schema": self._generate_unilab_json_command_schema(v["args"], k), "schema": self._generate_unilab_json_command_schema(v["args"], k),
"goal_default": {i["name"]: i["default"] for i in v["args"]}, "goal_default": {i["name"]: i["default"] for i in v["args"]},
"handles": [], "handles": [],
"placeholder_keys": {
i["name"]: (
"unilabos_resources"
if i["type"] == "unilabos.registry.placeholder_type:ResourceSlot"
or i["type"]
== ("list", "unilabos.registry.placeholder_type:ResourceSlot")
else "unilabos_devices"
)
for i in v["args"]
if i.get("type", "")
in [
"unilabos.registry.placeholder_type:ResourceSlot",
"unilabos.registry.placeholder_type:DeviceSlot",
("list", "unilabos.registry.placeholder_type:ResourceSlot"),
("list", "unilabos.registry.placeholder_type:DeviceSlot"),
]
},
} }
# 不生成已配置action的动作 # 不生成已配置action的动作
for k, v in enhanced_info["action_methods"].items() for k, v in enhanced_info["action_methods"].items()
if k not in device_config["class"]["action_value_mappings"] if k not in device_config["class"]["action_value_mappings"]
} }
) )
# 恢复原有的description信息auto开头的不修改 # 恢复原有的description信息auto开头的不修改
for action_name, description in old_descriptions.items(): for action_name, description in old_descriptions.items():
if action_name in device_config["class"]["action_value_mappings"]: # 有一些会被删除 if action_name in device_config["class"]["action_value_mappings"]: # 有一些会被删除
@@ -595,30 +709,8 @@ class Registry:
device_config["class"]["status_types"][status_name] = status_str_type_mapping[status_type] device_config["class"]["status_types"][status_name] = status_str_type_mapping[status_type]
for action_name, action_config in device_config["class"]["action_value_mappings"].items(): for action_name, action_config in device_config["class"]["action_value_mappings"].items():
action_config["type"] = action_str_type_mapping[action_config["type"]] action_config["type"] = action_str_type_mapping[action_config["type"]]
for additional_action in ["_execute_driver_command", "_execute_driver_command_async"]: # 添加内置的驱动命令动作
device_config["class"]["action_value_mappings"][additional_action] = { self._add_builtin_actions(device_config, device_id)
"type": self._replace_type_with_class(
"StrSingleInput", device_id, f"动作 {additional_action}"
),
"goal": {"string": "string"},
"feedback": {},
"result": {},
"schema": ros_action_to_json_schema(
self._replace_type_with_class(
"StrSingleInput", device_id, f"动作 {additional_action}"
)
),
"goal_default": yaml.safe_load(
io.StringIO(
get_yaml_from_goal_type(
self._replace_type_with_class(
"StrSingleInput", device_id, f"动作 {additional_action}"
).Goal
)
)
),
"handles": {},
}
device_config["file_path"] = str(file.absolute()).replace("\\", "/") device_config["file_path"] = str(file.absolute()).replace("\\", "/")
device_config["registry_type"] = "device" device_config["registry_type"] = "device"
logger.trace( # type: ignore logger.trace( # type: ignore
@@ -642,7 +734,16 @@ class Registry:
device_info_copy = copy.deepcopy(device_info) device_info_copy = copy.deepcopy(device_info)
if "class" in device_info_copy and "action_value_mappings" in device_info_copy["class"]: if "class" in device_info_copy and "action_value_mappings" in device_info_copy["class"]:
action_mappings = device_info_copy["class"]["action_value_mappings"] action_mappings = device_info_copy["class"]["action_value_mappings"]
for action_name, action_config in action_mappings.items(): # 过滤掉内置的驱动命令动作
builtin_actions = ["_execute_driver_command", "_execute_driver_command_async"]
filtered_action_mappings = {
action_name: action_config
for action_name, action_config in action_mappings.items()
if action_name not in builtin_actions
}
device_info_copy["class"]["action_value_mappings"] = filtered_action_mappings
for action_name, action_config in filtered_action_mappings.items():
if "schema" in action_config and action_config["schema"]: if "schema" in action_config and action_config["schema"]:
schema = action_config["schema"] schema = action_config["schema"]
# 确保schema结构存在 # 确保schema结构存在

View File

@@ -34,3 +34,16 @@ BIOYOND_PolymerStation_6VialCarrier:
init_param_schema: {} init_param_schema: {}
registry_type: resource registry_type: resource
version: 1.0.0 version: 1.0.0
BIOYOND_PolymerStation_6StockCarrier:
category:
- bottle_carriers
class:
module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_6StockCarrier
type: pylabrobot
description: BIOYOND_PolymerStation_6StockCarrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -0,0 +1,24 @@
BIOYOND_PolymerStation_Solid_Stock:
class:
module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Solid_Stock
type: pylabrobot
BIOYOND_PolymerStation_Solid_Vial:
class:
module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Solid_Vial
type: pylabrobot
BIOYOND_PolymerStation_Liquid_Vial:
class:
module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Liquid_Vial
type: pylabrobot
BIOYOND_PolymerStation_Solution_Beaker:
class:
module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Solution_Beaker
type: pylabrobot
BIOYOND_PolymerStation_Reagent_Bottle:
class:
module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Reagent_Bottle
type: pylabrobot

View File

@@ -1,15 +1,3 @@
BIOYOND_PolymerReactionStation_Deck:
category:
- deck
class:
module: unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck
type: pylabrobot
description: BIOYOND PolymerReactionStation Deck
handles: []
icon: '反应站.webp'
init_param_schema: {}
registry_type: resource
version: 1.0.0
BIOYOND_PolymerPreparationStation_Deck: BIOYOND_PolymerPreparationStation_Deck:
category: category:
- deck - deck
@@ -18,7 +6,19 @@ BIOYOND_PolymerPreparationStation_Deck:
type: pylabrobot type: pylabrobot
description: BIOYOND PolymerPreparationStation Deck description: BIOYOND PolymerPreparationStation Deck
handles: [] handles: []
icon: '配液站.webp' icon: 配液站.webp
init_param_schema: {}
registry_type: resource
version: 1.0.0
BIOYOND_PolymerReactionStation_Deck:
category:
- deck
class:
module: unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck
type: pylabrobot
description: BIOYOND PolymerReactionStation Deck
handles: []
icon: 反应站.webp
init_param_schema: {} init_param_schema: {}
registry_type: resource registry_type: resource
version: 1.0.0 version: 1.0.0

View File

@@ -1,7 +1,13 @@
from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder, create_ordered_items_2d from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder, create_ordered_items_2d
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
from unilabos.resources.bioyond.bottles import BIOYOND_PolymerStation_Solid_Vial, BIOYOND_PolymerStation_Solution_Beaker, BIOYOND_PolymerStation_Reagent_Bottle from unilabos.resources.bioyond.bottles import (
BIOYOND_PolymerStation_Solid_Stock,
BIOYOND_PolymerStation_Solid_Vial,
BIOYOND_PolymerStation_Liquid_Vial,
BIOYOND_PolymerStation_Solution_Beaker,
BIOYOND_PolymerStation_Reagent_Bottle
)
# 命名约定:试剂瓶-Bottle烧杯-Beaker烧瓶-Flask小瓶-Vial # 命名约定:试剂瓶-Bottle烧杯-Beaker烧瓶-Flask小瓶-Vial
@@ -92,6 +98,57 @@ def BIOYOND_Electrolyte_1BottleCarrier(name: str) -> BottleCarrier:
return carrier return carrier
def BIOYOND_PolymerStation_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="BIOYOND_PolymerStation_6VialCarrier",
)
carrier.num_items_x = 3
carrier.num_items_y = 2
carrier.num_items_z = 1
ordering = ["A1", "A2", "A3", "B1", "B2", "B3"] # 自定义顺序
for i in range(6):
carrier[i] = BIOYOND_PolymerStation_Solid_Stock(f"{name}_vial_{ordering[i]}")
return carrier
def BIOYOND_PolymerStation_6VialCarrier(name: str) -> BottleCarrier: def BIOYOND_PolymerStation_6VialCarrier(name: str) -> BottleCarrier:
"""6瓶载架 - 2x3布局""" """6瓶载架 - 2x3布局"""
@@ -138,8 +195,10 @@ def BIOYOND_PolymerStation_6VialCarrier(name: str) -> BottleCarrier:
carrier.num_items_y = 2 carrier.num_items_y = 2
carrier.num_items_z = 1 carrier.num_items_z = 1
ordering = ["A1", "A2", "A3", "B1", "B2", "B3"] # 自定义顺序 ordering = ["A1", "A2", "A3", "B1", "B2", "B3"] # 自定义顺序
for i in range(6): for i in range(3):
carrier[i] = BIOYOND_PolymerStation_Solid_Vial(f"{name}_vial_{ordering[i]}") carrier[i] = BIOYOND_PolymerStation_Solid_Vial(f"{name}_solidvial_{ordering[i]}")
for i in range(3, 6):
carrier[i] = BIOYOND_PolymerStation_Liquid_Vial(f"{name}_liquidvial_{ordering[i]}")
return carrier return carrier

View File

@@ -2,12 +2,30 @@ from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
# 工厂函数 # 工厂函数
def BIOYOND_PolymerStation_Solid_Vial( def BIOYOND_PolymerStation_Solid_Stock(
name: str, name: str,
diameter: float = 20.0, diameter: float = 20.0,
height: float = 100.0, height: float = 100.0,
max_volume: float = 30000.0, # 30mL max_volume: float = 30000.0, # 30mL
barcode: str = None, barcode: str = None,
) -> Bottle:
"""创建粉末瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="BIOYOND_PolymerStation_Solid_Stock",
)
def BIOYOND_PolymerStation_Solid_Vial(
name: str,
diameter: float = 25.0,
height: float = 60.0,
max_volume: float = 30000.0, # 30mL
barcode: str = None,
) -> Bottle: ) -> Bottle:
"""创建粉末瓶""" """创建粉末瓶"""
return Bottle( return Bottle(
@@ -20,6 +38,24 @@ def BIOYOND_PolymerStation_Solid_Vial(
) )
def BIOYOND_PolymerStation_Liquid_Vial(
name: str,
diameter: float = 25.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="BIOYOND_PolymerStation_Liquid_Vial",
)
def BIOYOND_PolymerStation_Solution_Beaker( def BIOYOND_PolymerStation_Solution_Beaker(
name: str, name: str,
diameter: float = 60.0, diameter: float = 60.0,

View File

@@ -1,89 +1,125 @@
import importlib import importlib
import inspect import inspect
import json import json
from typing import Union, Any, Dict import traceback
import numpy as np from typing import Union, Any, Dict, List
import networkx as nx import networkx as nx
from pylabrobot.resources import ResourceHolder from pylabrobot.resources import ResourceHolder
from unilabos_msgs.msg import Resource from unilabos_msgs.msg import Resource
from unilabos.resources.container import RegularContainer from unilabos.resources.container import RegularContainer
from unilabos.ros.msgs.message_converter import convert_to_ros_msg from unilabos.ros.msgs.message_converter import convert_to_ros_msg
from unilabos.ros.nodes.resource_tracker import (
ResourceDictInstance,
ResourceTreeSet,
)
from unilabos.utils.banner_print import print_status
try: try:
from pylabrobot.resources.resource import Resource as ResourcePLR from pylabrobot.resources.resource import Resource as ResourcePLR
except ImportError: except ImportError:
pass pass
from typing import Union, get_origin from typing import get_origin
physical_setup_graph: nx.Graph = None physical_setup_graph: nx.Graph = None
def canonicalize_nodes_data(data: dict, parent_relation: dict = {}) -> dict: def canonicalize_nodes_data(
for node in data.get("nodes", []): nodes: List[Dict[str, Any]], parent_relation: Dict[str, List[str]] = {}
) -> ResourceTreeSet:
"""
标准化节点数据,使用 ResourceInstanceDictFlatten 进行规范化并创建 ResourceTreeSet
Args:
nodes: 原始节点列表
parent_relation: 父子关系映射 {parent_id: [child_id1, child_id2, ...]}
Returns:
ResourceTreeSet: 标准化后的资源树集合
"""
print_status(f"{len(nodes)} Resources loaded:", "info")
# 第一步基本预处理处理graphml的label字段
for node in nodes:
if node.get("label") is not None: if node.get("label") is not None:
id = node.pop("label") node_id = node.pop("label")
node["id"] = node["name"] = id node["id"] = node["name"] = node_id
if "id" not in node:
node["id"] = node.get("name", "NaN")
if "name" not in node:
node["name"] = node["id"]
if node.get("position") is None:
node["position"] = {
"x": node.pop("x", 0.0),
"y": node.pop("y", 0.0),
"z": node.pop("z", 0.0),
}
if node.get("config") is None:
node["config"] = {}
node["data"] = {}
for k in list(node.keys()):
if k not in [
"id",
"name",
"class",
"type",
"position",
"children",
"parent",
"config",
"data",
]:
if k in ["chemical", "current_volume"]:
if node["data"].get("liquids") is None:
node["data"]["liquids"] = [{}]
if k == "chemical":
node["data"]["liquids"][0]["liquid_name"] = node.pop(k)
elif k == "current_volume":
node["data"]["liquids"][0]["liquid_volume"] = node.pop(k)
elif k == "max_volume":
node["data"]["max_volume"] = node.pop(k)
elif k == "url":
node.pop(k)
else:
node["config"][k] = node.pop(k)
if "class" not in node:
node["class"] = None
if "type" not in node:
node["type"] = (
"container"
if node["class"] is None
else "device" if node["class"] not in ["container", "plate"] else node["class"]
)
if "children" not in node:
node["children"] = []
id2idx = {node_data["id"]: idx for idx, node_data in enumerate(data["nodes"])} # 第二步处理parent_relation
id2idx = {node["id"]: idx for idx, node in enumerate(nodes)}
for parent, children in parent_relation.items(): for parent, children in parent_relation.items():
data["nodes"][id2idx[parent]]["children"] = children if parent in id2idx:
for child in children: nodes[id2idx[parent]]["children"] = children
data["nodes"][id2idx[child]]["parent"] = parent for child in children:
return data if child in id2idx:
nodes[id2idx[child]]["parent"] = parent
# 第三步:使用 ResourceInstanceDictFlatten 标准化每个节点
standardized_instances = []
known_nodes: Dict[str, ResourceDictInstance] = {} # {node_id: ResourceDictInstance}
uuid_to_instance: Dict[str, ResourceDictInstance] = {} # {uuid: ResourceDictInstance}
for node in nodes:
try:
print_status(f"DeviceId: {node['id']}, Class: {node['class']}", "info")
# 使用标准化方法
resource_instance = ResourceDictInstance.get_resource_instance_from_dict(node)
known_nodes[node["id"]] = resource_instance
uuid_to_instance[resource_instance.res_content.uuid] = resource_instance
standardized_instances.append(resource_instance)
except Exception as e:
print_status(f"Failed to standardize node {node.get('id', 'unknown')}:\n{traceback.format_exc()}", "error")
continue
# 第四步:建立 parent 和 children 关系
for node in nodes:
node_id = node["id"]
if node_id not in known_nodes:
continue
current_instance = known_nodes[node_id]
# 优先使用 parent_uuid 进行匹配,如果不存在则使用 parent
parent_uuid = node.get("parent_uuid")
parent_id = node.get("parent")
parent_instance = None
# 优先用 parent_uuid 匹配
if parent_uuid and parent_uuid in uuid_to_instance:
parent_instance = uuid_to_instance[parent_uuid]
# 否则用 parent_id 匹配
elif parent_id and parent_id in known_nodes:
parent_instance = known_nodes[parent_id]
# 设置 parent 引用
if parent_instance:
current_instance.res_content.parent = parent_instance.res_content
# 将当前节点添加到父节点的 children 列表
parent_instance.children.append(current_instance)
# 第五步:创建 ResourceTreeSet
resource_tree_set = ResourceTreeSet.from_nested_list(standardized_instances)
return resource_tree_set
def canonicalize_links_ports(data: dict) -> dict: def canonicalize_links_ports(links: List[Dict[str, Any]], resource_tree_set: ResourceTreeSet) -> List[Dict[str, Any]]:
"""
标准化边/连接的端口信息
Args:
links: 原始连接列表
resource_tree_set: 资源树集合用于获取节点的UUID信息
Returns:
标准化后的连接列表
"""
# 构建 id 到 uuid 的映射
id_to_uuid: Dict[str, str] = {}
for node in resource_tree_set.all_nodes:
id_to_uuid[node.res_content.id] = node.res_content.uuid
# 第一遍处理将字符串类型的port转换为字典格式 # 第一遍处理将字符串类型的port转换为字典格式
for link in data.get("links", []): for link in links:
port = link.get("port") port = link.get("port")
if link.get("type", "physical") == "physical": if link.get("type", "physical") == "physical":
link["type"] = "fluid" link["type"] = "fluid"
@@ -107,11 +143,11 @@ def canonicalize_links_ports(data: dict) -> dict:
link["port"] = {link["source"]: None, link["target"]: None} link["port"] = {link["source"]: None, link["target"]: None}
# 构建边字典,键为(source节点, target节点)值为对应的port信息 # 构建边字典,键为(source节点, target节点)值为对应的port信息
edges = {(link["source"], link["target"]): link["port"] for link in data.get("links", [])} edges = {(link["source"], link["target"]): link["port"] for link in links}
# 第二遍处理填充反向边的dest信息 # 第二遍处理填充反向边的dest信息
delete_reverses = [] delete_reverses = []
for i, link in enumerate(data.get("links", [])): for i, link in enumerate(links):
s, t = link["source"], link["target"] s, t = link["source"], link["target"]
current_port = link["port"] current_port = link["port"]
if current_port.get(t) is None: if current_port.get(t) is None:
@@ -127,9 +163,22 @@ def canonicalize_links_ports(data: dict) -> dict:
# 若不存在反向边,初始化为空结构 # 若不存在反向边,初始化为空结构
current_port[t] = current_port[s] current_port[t] = current_port[s]
# 删除已被使用反向端口信息的反向边 # 删除已被使用反向端口信息的反向边
data["links"] = [link for i, link in enumerate(data.get("links", [])) if i not in delete_reverses] standardized_links = [link for i, link in enumerate(links) if i not in delete_reverses]
return data # 第三遍处理:为每个 link 添加 source_uuid 和 target_uuid
for link in standardized_links:
source_id = link.get("source")
target_id = link.get("target")
# 添加 source_uuid
if source_id and source_id in id_to_uuid:
link["source_uuid"] = id_to_uuid[source_id]
# 添加 target_uuid
if target_id and target_id in id_to_uuid:
link["target_uuid"] = id_to_uuid[target_id]
return standardized_links
def handle_communications(G: nx.Graph): def handle_communications(G: nx.Graph):
@@ -151,18 +200,43 @@ def handle_communications(G: nx.Graph):
G.nodes[device]["config"]["io_device_port"] = int(edata["port"][device_comm]) G.nodes[device]["config"]["io_device_port"] = int(edata["port"][device_comm])
def read_node_link_json(json_info: Union[str, Dict[str, Any]]) -> tuple[nx.Graph, dict]: def read_node_link_json(
json_info: Union[str, Dict[str, Any]],
) -> tuple[nx.Graph, ResourceTreeSet, List[Dict[str, Any]]]:
"""
读取节点-边的JSON数据并构建图
Args:
json_info: JSON文件路径或字典数据
Returns:
tuple[nx.Graph, ResourceTreeSet, List[Dict[str, Any]]]:
返回NetworkX图对象、资源树集合和标准化后的连接列表
"""
global physical_setup_graph global physical_setup_graph
if isinstance(json_info, str): if isinstance(json_info, str):
data = json.load(open(json_info, encoding="utf-8")) data = json.load(open(json_info, encoding="utf-8"))
else: else:
data = json_info data = json_info
data = canonicalize_nodes_data(data)
data = canonicalize_links_ports(data)
physical_setup_graph = nx.node_link_graph(data, multigraph=False) # edges="links" 3.6 warning # 标准化节点数据并创建 ResourceTreeSet
nodes = data.get("nodes", [])
resource_tree_set = canonicalize_nodes_data(nodes)
# 标准化边数据
links = data.get("links", [])
standardized_links = canonicalize_links_ports(links, resource_tree_set)
# 构建 NetworkX 图(需要转换回 dict 格式)
# 从 ResourceTreeSet 获取所有节点
graph_data = {
"nodes": [node.res_content.model_dump(by_alias=True) for node in resource_tree_set.all_nodes],
"links": standardized_links,
}
physical_setup_graph = nx.node_link_graph(graph_data, edges="links", multigraph=False)
handle_communications(physical_setup_graph) handle_communications(physical_setup_graph)
return physical_setup_graph, data
return physical_setup_graph, resource_tree_set, standardized_links
def modify_to_backend_format(data: list[dict[str, Any]]) -> list[dict[str, Any]]: def modify_to_backend_format(data: list[dict[str, Any]]) -> list[dict[str, Any]]:
@@ -185,7 +259,17 @@ def modify_to_backend_format(data: list[dict[str, Any]]) -> list[dict[str, Any]]
return data return data
def read_graphml(graphml_file): def read_graphml(graphml_file: str) -> tuple[nx.Graph, ResourceTreeSet, List[Dict[str, Any]]]:
"""
读取GraphML文件并构建图
Args:
graphml_file: GraphML文件路径
Returns:
tuple[nx.Graph, ResourceTreeSet, List[Dict[str, Any]]]:
返回NetworkX图对象、资源树集合和标准化后的连接列表
"""
global physical_setup_graph global physical_setup_graph
G = nx.read_graphml(graphml_file) G = nx.read_graphml(graphml_file)
@@ -202,12 +286,25 @@ def read_graphml(graphml_file):
G2 = nx.relabel_nodes(G, mapping) G2 = nx.relabel_nodes(G, mapping)
data = nx.node_link_data(G2) data = nx.node_link_data(G2)
data = canonicalize_nodes_data(data, parent_relation=parent_relation)
data = canonicalize_links_ports(data)
physical_setup_graph = nx.node_link_graph(data, edges="links", multigraph=False) # edges="links" 3.6 warning # 标准化节点数据并创建 ResourceTreeSet
nodes = data.get("nodes", [])
resource_tree_set = canonicalize_nodes_data(nodes, parent_relation=parent_relation)
# 标准化边数据
links = data.get("links", [])
standardized_links = canonicalize_links_ports(links, resource_tree_set)
# 构建 NetworkX 图(需要转换回 dict 格式)
# 从 ResourceTreeSet 获取所有节点
graph_data = {
"nodes": [node.res_content.model_dump(by_alias=True) for node in resource_tree_set.all_nodes],
"links": standardized_links,
}
physical_setup_graph = nx.node_link_graph(graph_data, link="links", multigraph=False)
handle_communications(physical_setup_graph) handle_communications(physical_setup_graph)
return physical_setup_graph, data
return physical_setup_graph, resource_tree_set, standardized_links
def dict_from_graph(graph: nx.Graph) -> dict: def dict_from_graph(graph: nx.Graph) -> dict:
@@ -229,11 +326,7 @@ def dict_to_tree(nodes: dict, devices_only: bool = False) -> list[dict]:
is_root[child_id] = False is_root[child_id] = False
# 找到根节点并返回 # 找到根节点并返回
root_nodes = [ root_nodes = [node for node in nodes_list if is_root.get(node["id"], False) or len(nodes_list) == 1]
node
for node in nodes_list
if is_root.get(node["id"], False) or len(nodes_list) == 1
]
# 如果存在多个根节点,返回所有根节点 # 如果存在多个根节点,返回所有根节点
return root_nodes return root_nodes
@@ -258,11 +351,7 @@ def dict_to_nested_dict(nodes: dict, devices_only: bool = False) -> dict:
node["config"]["children"] = node["children"] node["config"]["children"] = node["children"]
# 找到根节点并返回 # 找到根节点并返回
root_nodes = { root_nodes = {node["id"]: node for node in nodes_list if is_root.get(node["id"], False) or len(nodes_list) == 1}
node["id"]: node
for node in nodes_list
if is_root.get(node["id"], False) or len(nodes_list) == 1
}
# 如果存在多个根节点,返回所有根节点 # 如果存在多个根节点,返回所有根节点
return root_nodes return root_nodes
@@ -337,6 +426,7 @@ def nested_dict_to_list(nested_dict: dict) -> list[dict]: # FIXME 是tree
return result return result
def convert_resources_to_type( def convert_resources_to_type(
resources_list: list[dict], resource_type: Union[type, list[type]], *, plr_model: bool = False resources_list: list[dict], resource_type: Union[type, list[type]], *, plr_model: bool = False
) -> Union[list[dict], dict, None, "ResourcePLR"]: ) -> Union[list[dict], dict, None, "ResourcePLR"]:
@@ -369,7 +459,9 @@ def convert_resources_to_type(
return None return None
def convert_resources_from_type(resources_list, resource_type: Union[type, list[type]], *, is_plr: bool = False) -> Union[list[dict], dict, None, "ResourcePLR"]: def convert_resources_from_type(
resources_list, resource_type: Union[type, list[type]], *, is_plr: bool = False
) -> Union[list[dict], dict, None, "ResourcePLR"]:
""" """
Convert resources from a given type (PyLabRobot or NestedDict) to flattened list of dictionaries. Convert resources from a given type (PyLabRobot or NestedDict) to flattened list of dictionaries.
@@ -432,6 +524,7 @@ def resource_ulab_to_plr(resource: dict, plr_model=False) -> "ResourcePLR":
d = resource_ulab_to_plr_inner(resource) d = resource_ulab_to_plr_inner(resource)
"""无法通过Resource进行反序列化例如TipSpot必须内部序列化好直接用TipSpot序列化会多参数导致出错""" """无法通过Resource进行反序列化例如TipSpot必须内部序列化好直接用TipSpot序列化会多参数导致出错"""
from pylabrobot.utils.object_parsing import find_subclass from pylabrobot.utils.object_parsing import find_subclass
sub_cls = find_subclass(d["type"], ResourcePLR) sub_cls = find_subclass(d["type"], ResourcePLR)
spect = inspect.signature(sub_cls) spect = inspect.signature(sub_cls)
if "category" not in spect.parameters: if "category" not in spect.parameters:
@@ -456,6 +549,7 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None, w
else: else:
print("转换pylabrobot的时候出现未知类型", source) print("转换pylabrobot的时候出现未知类型", source)
return "container" return "container"
def resource_plr_to_ulab_inner(d: dict, all_states: dict, child=True) -> dict: def resource_plr_to_ulab_inner(d: dict, all_states: dict, child=True) -> dict:
r = { r = {
"id": d["name"], "id": d["name"],
@@ -474,6 +568,7 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None, w
"data": all_states[d["name"]], "data": all_states[d["name"]],
} }
return r return r
d = resource_plr.serialize() d = resource_plr.serialize()
all_states = resource_plr.serialize_all_state() all_states = resource_plr.serialize_all_state()
r = resource_plr_to_ulab_inner(d, all_states, with_children) r = resource_plr_to_ulab_inner(d, all_states, with_children)
@@ -496,21 +591,41 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict =
plr_materials = [] plr_materials = []
for material in bioyond_materials: for material in bioyond_materials:
className = type_mapping.get(material.get("typeName"), "RegularContainer") if type_mapping else "RegularContainer" className = (
type_mapping.get(material.get("typeName"), "RegularContainer") if type_mapping else "RegularContainer"
)
plr_material: ResourcePLR = initialize_resource({"name": material["name"], "class": className}, resource_type=ResourcePLR) plr_material: ResourcePLR = initialize_resource(
{"name": material["name"], "class": className}, resource_type=ResourcePLR
)
plr_material.code = material.get("code", "") and material.get("barCode", "") or "" plr_material.code = material.get("code", "") and material.get("barCode", "") or ""
# 处理子物料detail # 处理子物料detail
if material.get("detail") and len(material["detail"]) > 0: if material.get("detail") and len(material["detail"]) > 0:
child_ids = [] child_ids = []
for detail in material["detail"]: for detail in material["detail"]:
number = (detail.get("z", 0) - 1) * plr_material.num_items_x * plr_material.num_items_y + \ number = (
(detail.get("x", 0) - 1) * plr_material.num_items_x + \ (detail.get("z", 0) - 1) * plr_material.num_items_x * plr_material.num_items_y
(detail.get("y", 0) - 1) + (detail.get("x", 0) - 1) * plr_material.num_items_x
+ (detail.get("y", 0) - 1)
)
bottle = plr_material[number] bottle = plr_material[number]
if detail["name"] in type_mapping:
# plr_material.unassign_child_resource(bottle)
plr_material.sites[number] = None
plr_material[number] = initialize_resource(
{"name": f'{detail["name"]}_{number}', "class": type_mapping[detail["name"]]}, resource_type=ResourcePLR
)
else:
bottle.tracker.liquids = [
(detail["name"], float(detail.get("quantity", 0)) if detail.get("quantity") else 0)
]
bottle.code = detail.get("code", "") bottle.code = detail.get("code", "")
bottle.tracker.liquids = [(detail["name"], float(detail.get("quantity", 0)) if detail.get("quantity") else 0)] else:
bottle = plr_material[0] if plr_material.capacity > 0 else plr_material
bottle.tracker.liquids = [
(material["name"], float(material.get("quantity", 0)) if material.get("quantity") else 0)
]
plr_materials.append(plr_material) plr_materials.append(plr_material)
@@ -518,9 +633,11 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict =
for loc in material.get("locations", []): for loc in material.get("locations", []):
if hasattr(deck, "warehouses") and loc.get("whName") in deck.warehouses: if hasattr(deck, "warehouses") and loc.get("whName") in deck.warehouses:
warehouse = deck.warehouses[loc["whName"]] warehouse = deck.warehouses[loc["whName"]]
idx = (loc.get("y", 0) - 1) * warehouse.num_items_x * warehouse.num_items_y + \ idx = (
(loc.get("x", 0) - 1) * warehouse.num_items_x + \ (loc.get("y", 0) - 1) * warehouse.num_items_x * warehouse.num_items_y
(loc.get("z", 0) - 1) + (loc.get("x", 0) - 1) * warehouse.num_items_x
+ (loc.get("z", 0) - 1)
)
if 0 <= idx < warehouse.capacity: if 0 <= idx < warehouse.capacity:
if warehouse[idx] is None or isinstance(warehouse[idx], ResourceHolder): if warehouse[idx] is None or isinstance(warehouse[idx], ResourceHolder):
warehouse[idx] = plr_material warehouse[idx] = plr_material
@@ -528,6 +645,36 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict =
return plr_materials return plr_materials
def resource_plr_to_bioyond(plr_materials: list[ResourcePLR], type_mapping: dict = {}, warehouse_mapping: dict = {}) -> list[dict]:
bioyond_materials = []
for plr_material in plr_materials:
material = {
"name": plr_material.name,
"typeName": plr_material.__class__.__name__,
"code": plr_material.code,
"quantity": 0,
"detail": [],
"locations": [],
}
if hasattr(plr_material, "capacity") and plr_material.capacity > 1:
for idx in range(plr_material.capacity):
bottle = plr_material[idx]
detail = {
"x": (idx // (plr_material.num_items_x * plr_material.num_items_y)) + 1,
"y": ((idx % (plr_material.num_items_x * plr_material.num_items_y)) // plr_material.num_items_x) + 1,
"z": (idx % plr_material.num_items_x) + 1,
"code": bottle.code if hasattr(bottle, "code") else "",
"quantity": sum(qty for _, qty in bottle.tracker.liquids) if hasattr(bottle, "tracker") else 0,
}
material["detail"].append(detail)
material["quantity"] = 1.0
else:
bottle = plr_material[0] if plr_material.capacity > 0 else plr_material
material["quantity"] = sum(qty for _, qty in bottle.tracker.liquids) if hasattr(plr_material, "tracker") else 0
bioyond_materials.append(material)
return bioyond_materials
def initialize_resource(resource_config: dict, resource_type: Any = None) -> Union[list[dict], ResourcePLR]: def initialize_resource(resource_config: dict, resource_type: Any = None) -> Union[list[dict], ResourcePLR]:
"""Initializes a resource based on its configuration. """Initializes a resource based on its configuration.
@@ -541,6 +688,7 @@ def initialize_resource(resource_config: dict, resource_type: Any = None) -> Uni
None None
""" """
from unilabos.registry.registry import lab_registry from unilabos.registry.registry import lab_registry
resource_class_config = resource_config.get("class", None) resource_class_config = resource_config.get("class", None)
if resource_class_config is None: if resource_class_config is None:
return [resource_config] return [resource_config]
@@ -570,7 +718,9 @@ def initialize_resource(resource_config: dict, resource_type: Any = None) -> Uni
r = resource_plr r = resource_plr
elif resource_class_config["type"] == "unilabos": elif resource_class_config["type"] == "unilabos":
res_instance: RegularContainer = RESOURCE(id=resource_config["name"]) res_instance: RegularContainer = RESOURCE(id=resource_config["name"])
res_instance.ulr_resource = convert_to_ros_msg(Resource, {k:v for k,v in resource_config.items() if k != "class"}) res_instance.ulr_resource = convert_to_ros_msg(
Resource, {k: v for k, v in resource_config.items() if k != "class"}
)
r = [res_instance.get_ulr_resource_as_dict()] r = [res_instance.get_ulr_resource_as_dict()]
elif isinstance(RESOURCE, dict): elif isinstance(RESOURCE, dict):
r = [RESOURCE.copy()] r = [RESOURCE.copy()]

View File

@@ -5,15 +5,19 @@ Automated Liquid Handling Station Resource Classes - Simplified Version
from __future__ import annotations from __future__ import annotations
from typing import Dict, Optional from typing import Dict, List, Optional, TypeVar, Union, Sequence, Tuple
import pylabrobot
from pylabrobot.resources.coordinate import Coordinate
from pylabrobot.resources.container import Container
from pylabrobot.resources.resource_holder import ResourceHolder
from pylabrobot.resources import Resource as ResourcePLR from pylabrobot.resources import Resource as ResourcePLR
from pylabrobot.resources import Well, ResourceHolder
from pylabrobot.resources.coordinate import Coordinate
class Bottle(Container): LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
class Bottle(Well):
"""瓶子类 - 简化版,不追踪瓶盖""" """瓶子类 - 简化版,不追踪瓶盖"""
def __init__( def __init__(
@@ -37,6 +41,8 @@ class Bottle(Container):
max_volume=max_volume, max_volume=max_volume,
category=category, category=category,
model=model, model=model,
bottom_type="flat",
cross_section_type="circle"
) )
self.diameter = diameter self.diameter = diameter
self.height = height self.height = height
@@ -50,13 +56,6 @@ class Bottle(Container):
"barcode": self.barcode, "barcode": self.barcode,
} }
from string import ascii_uppercase as LETTERS
from typing import Dict, List, Optional, Type, TypeVar, Union, Sequence, Tuple
import pylabrobot
from pylabrobot.resources.resource_holder import ResourceHolder
T = TypeVar("T", bound=ResourceHolder) T = TypeVar("T", bound=ResourceHolder)
S = TypeVar("S", bound=ResourceHolder) S = TypeVar("S", bound=ResourceHolder)

View File

@@ -1,25 +1,22 @@
import copy import copy
import json import json
import os
import threading import threading
import time import time
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
import rclpy import rclpy
from unilabos_msgs.srv._serial_command import SerialCommand_Response
from unilabos.ros.nodes.presets.resource_mesh_manager import ResourceMeshManager from unilabos.ros.nodes.presets.resource_mesh_manager import ResourceMeshManager
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker, ResourceTreeSet
from unilabos.devices.ros_dev.liquid_handler_joint_publisher import LiquidHandlerJointPublisher from unilabos.devices.ros_dev.liquid_handler_joint_publisher import LiquidHandlerJointPublisher
from unilabos_msgs.msg import Resource # type: ignore from unilabos_msgs.srv import SerialCommand # type: ignore
from unilabos_msgs.srv import ResourceAdd, SerialCommand # type: ignore
from rclpy.executors import MultiThreadedExecutor from rclpy.executors import MultiThreadedExecutor
from rclpy.node import Node from rclpy.node import Node
from rclpy.timer import Timer from rclpy.timer import Timer
from unilabos.registry.registry import lab_registry from unilabos.registry.registry import lab_registry
from unilabos.ros.initialize_device import initialize_device_from_dict from unilabos.ros.initialize_device import initialize_device_from_dict
from unilabos.ros.msgs.message_converter import (
convert_to_ros_msg,
)
from unilabos.ros.nodes.presets.host_node import HostNode from unilabos.ros.nodes.presets.host_node import HostNode
from unilabos.utils import logger from unilabos.utils import logger
from unilabos.config.config import BasicConfig from unilabos.config.config import BasicConfig
@@ -43,9 +40,9 @@ def exit() -> None:
def main( def main(
devices_config: Dict[str, Any] = {}, devices_config: ResourceTreeSet,
resources_config: list=[], resources_config: ResourceTreeSet,
resources_edge_config: list=[], resources_edge_config: list[dict] = [],
graph: Optional[Dict[str, Any]] = None, graph: Optional[Dict[str, Any]] = None,
controllers_config: Dict[str, Any] = {}, controllers_config: Dict[str, Any] = {},
bridges: List[Any] = [], bridges: List[Any] = [],
@@ -73,18 +70,22 @@ def main(
if visual != "disable": if visual != "disable":
from unilabos.ros.nodes.presets.joint_republisher import JointRepublisher from unilabos.ros.nodes.presets.joint_republisher import JointRepublisher
# 将 ResourceTreeSet 转换为 list 用于 visual 组件
resources_list = (
[node.res_content.model_dump(by_alias=True) for node in resources_config.all_nodes]
if resources_config
else []
)
resource_mesh_manager = ResourceMeshManager( resource_mesh_manager = ResourceMeshManager(
resources_mesh_config, resources_mesh_config,
resources_config, resources_list,
resource_tracker = host_node.resource_tracker, resource_tracker=host_node.resource_tracker,
device_id = 'resource_mesh_manager', device_id="resource_mesh_manager",
) )
joint_republisher = JointRepublisher( joint_republisher = JointRepublisher("joint_republisher", host_node.resource_tracker)
'joint_republisher', lh_joint_pub = LiquidHandlerJointPublisher(
host_node.resource_tracker resources_config=resources_list, resource_tracker=host_node.resource_tracker
) )
lh_joint_pub = LiquidHandlerJointPublisher(resources_config=resources_config,
resource_tracker=host_node.resource_tracker)
executor.add_node(resource_mesh_manager) executor.add_node(resource_mesh_manager)
executor.add_node(joint_republisher) executor.add_node(joint_republisher)
executor.add_node(lh_joint_pub) executor.add_node(lh_joint_pub)
@@ -97,9 +98,9 @@ def main(
def slave( def slave(
devices_config: Dict[str, Any] = {}, devices_config: ResourceTreeSet,
resources_config=[], resources_config: ResourceTreeSet,
resources_edge_config=[], resources_edge_config: list = [],
graph: Optional[Dict[str, Any]] = None, graph: Optional[Dict[str, Any]] = None,
controllers_config: Dict[str, Any] = {}, controllers_config: Dict[str, Any] = {},
bridges: List[Any] = [], bridges: List[Any] = [],
@@ -113,11 +114,12 @@ def slave(
executor = rclpy.__executor executor = rclpy.__executor
if not executor: if not executor:
executor = rclpy.__executor = MultiThreadedExecutor() executor = rclpy.__executor = MultiThreadedExecutor()
devices_config_copy = copy.deepcopy(devices_config) devices_instances = {}
for device_id, device_config in devices_config.items(): for device_config in devices_config.root_nodes:
d = initialize_device_from_dict(device_id, device_config) device_id = device_config.res_content.id
if d is None: if device_config.res_content.type != "device":
continue d = initialize_device_from_dict(device_id, device_config.get_nested_dict())
devices_instances[device_id] = d
# 默认初始化 # 默认初始化
# if d is not None and isinstance(d, Node): # if d is not None and isinstance(d, Node):
# executor.add_node(d) # executor.add_node(d)
@@ -129,20 +131,17 @@ def slave(
if visual != "disable": if visual != "disable":
from unilabos.ros.nodes.presets.joint_republisher import JointRepublisher from unilabos.ros.nodes.presets.joint_republisher import JointRepublisher
resource_mesh_manager = ResourceMeshManager( resource_mesh_manager = ResourceMeshManager(
resources_mesh_config, resources_mesh_config,
resources_config, resources_config, # type: ignore FIXME
resource_tracker= DeviceNodeResourceTracker(), resource_tracker=DeviceNodeResourceTracker(),
device_id = 'resource_mesh_manager', device_id="resource_mesh_manager",
)
joint_republisher = JointRepublisher(
'joint_republisher',
DeviceNodeResourceTracker()
) )
joint_republisher = JointRepublisher("joint_republisher", DeviceNodeResourceTracker())
executor.add_node(resource_mesh_manager) executor.add_node(resource_mesh_manager)
executor.add_node(joint_republisher) executor.add_node(joint_republisher)
thread = threading.Thread(target=executor.spin, daemon=True, name="slave_executor_thread") thread = threading.Thread(target=executor.spin, daemon=True, name="slave_executor_thread")
thread.start() thread.start()
@@ -151,25 +150,61 @@ def slave(
sclient.wait_for_service() sclient.wait_for_service()
request = SerialCommand.Request() request = SerialCommand.Request()
request.command = json.dumps({ request.command = json.dumps(
"machine_name": BasicConfig.machine_name, {
"type": "slave", "machine_name": BasicConfig.machine_name,
"devices_config": devices_config_copy, "type": "slave",
"registry_config": lab_registry.obtain_registry_device_info() "devices_config": devices_config.dump(),
}, ensure_ascii=False, cls=TypeEncoder) "registry_config": lab_registry.obtain_registry_device_info(),
},
ensure_ascii=False,
cls=TypeEncoder,
)
response = sclient.call_async(request).result() response = sclient.call_async(request).result()
logger.info(f"Slave node info updated.") logger.info(f"Slave node info updated.")
rclient = n.create_client(ResourceAdd, "/resources/add") # 使用新的 c2s_update_resource_tree 服务
rclient = n.create_client(SerialCommand, "/c2s_update_resource_tree")
rclient.wait_for_service() rclient.wait_for_service()
request = ResourceAdd.Request() # 序列化 ResourceTreeSet 为 JSON
request.resources = [convert_to_ros_msg(Resource, resource) for resource in resources_config] if resources_config:
response = rclient.call_async(request).result() request = SerialCommand.Request()
logger.info(f"Slave resource added.") request.command = json.dumps(
{
"data": {
"data": resources_config.dump(),
"mount_uuid": "",
"first_add": True,
},
"action": "add",
},
ensure_ascii=False,
)
tree_response: SerialCommand_Response = rclient.call_async(request).result()
uuid_mapping = json.loads(tree_response.response)
for node in resources_config.root_nodes:
if node.res_content.type == "device":
for sub_node in node.children:
# 只有二级子设备
if sub_node.res_content.type != "device":
device_tracker = devices_instances[node.res_content.id].resource_tracker
resource_instance = device_tracker.figure_resource(
{"uuid": sub_node.res_content.uuid})
device_tracker.loop_update_uuid(resource_instance, uuid_mapping)
else:
logger.error("Slave模式不允许新增非设备节点下的物料")
continue
if tree_response:
logger.info(f"Slave resource tree added. Response: {tree_response.response}")
else:
logger.warning("Slave resource tree add response is None")
else:
logger.info("No resources to add.")
while True: while True:
time.sleep(1) time.sleep(1)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -1,11 +1,12 @@
import copy import copy
import inspect
import io import io
import json import json
import threading import threading
import time import time
import traceback import traceback
import uuid import uuid
from typing import get_type_hints, TypeVar, Generic, Dict, Any, Type, TypedDict, Optional, List, Union from typing import get_type_hints, TypeVar, Generic, Dict, Any, Type, TypedDict, Optional, List, TYPE_CHECKING
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
import asyncio import asyncio
@@ -24,8 +25,6 @@ from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialComma
from unilabos.resources.container import RegularContainer from unilabos.resources.container import RegularContainer
from unilabos.resources.graphio import ( from unilabos.resources.graphio import (
convert_resources_to_type,
convert_resources_from_type,
resource_ulab_to_plr, resource_ulab_to_plr,
initialize_resources, initialize_resources,
dict_to_tree, dict_to_tree,
@@ -35,7 +34,6 @@ from unilabos.resources.graphio import (
from unilabos.resources.plr_additional_res_reg import register from unilabos.resources.plr_additional_res_reg import register
from unilabos.ros.msgs.message_converter import ( from unilabos.ros.msgs.message_converter import (
convert_to_ros_msg, convert_to_ros_msg,
convert_from_ros_msg,
convert_from_ros_msg_with_mapping, convert_from_ros_msg_with_mapping,
convert_to_ros_msg_with_mapping, convert_to_ros_msg_with_mapping,
) )
@@ -49,12 +47,19 @@ from unilabos_msgs.srv import (
) # type: ignore ) # type: ignore
from unilabos_msgs.msg import Resource # type: ignore from unilabos_msgs.msg import Resource # type: ignore
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker from unilabos.ros.nodes.resource_tracker import (
DeviceNodeResourceTracker,
ResourceTreeSet,
)
from unilabos.ros.x.rclpyx import get_event_loop from unilabos.ros.x.rclpyx import get_event_loop
from unilabos.ros.utils.driver_creator import WorkstationNodeCreator, PyLabRobotCreator, DeviceClassCreator from unilabos.ros.utils.driver_creator import WorkstationNodeCreator, PyLabRobotCreator, DeviceClassCreator
from unilabos.utils.async_util import run_async_func from unilabos.utils.async_util import run_async_func
from unilabos.utils.import_manager import default_manager
from unilabos.utils.log import info, debug, warning, error, critical, logger, trace from unilabos.utils.log import info, debug, warning, error, critical, logger, trace
from unilabos.utils.type_check import get_type_class, TypeEncoder, serialize_result_info, get_result_info_str from unilabos.utils.type_check import get_type_class, TypeEncoder, get_result_info_str
if TYPE_CHECKING:
from pylabrobot.resources import Resource as ResourcePLR
T = TypeVar("T") T = TypeVar("T")
@@ -178,7 +183,9 @@ class PropertyPublisher:
try: try:
self.publisher_ = node.create_publisher(msg_type, f"{name}", 10) self.publisher_ = node.create_publisher(msg_type, f"{name}", 10)
except AttributeError as ex: except AttributeError as ex:
self.node.lab_logger().error(f"创建发布者 {name} 失败,可能由于注册表有误,类型: {msg_type},错误: {ex}\n{traceback.format_exc()}") self.node.lab_logger().error(
f"创建发布者 {name} 失败,可能由于注册表有误,类型: {msg_type},错误: {ex}\n{traceback.format_exc()}"
)
self.timer = node.create_timer(self.timer_period, self.publish_property) self.timer = node.create_timer(self.timer_period, self.publish_property)
self.__loop = get_event_loop() self.__loop = get_event_loop()
str_msg_type = str(msg_type)[8:-2] str_msg_type = str(msg_type)[8:-2]
@@ -187,48 +194,48 @@ class PropertyPublisher:
def get_property(self): def get_property(self):
if asyncio.iscoroutinefunction(self.get_method): if asyncio.iscoroutinefunction(self.get_method):
# 如果是异步函数,运行事件循环并等待结果 # 如果是异步函数,运行事件循环并等待结果
self.node.lab_logger().trace(f"PropertyPublisher.get_property】获取异步属性: {self.name}") self.node.lab_logger().trace(f"【.get_property】获取异步属性: {self.name}")
loop = self.__loop loop = self.__loop
if loop: if loop:
future = asyncio.run_coroutine_threadsafe(self.get_method(), loop) future = asyncio.run_coroutine_threadsafe(self.get_method(), loop)
self._value = future.result() self._value = future.result()
return self._value return self._value
else: else:
self.node.lab_logger().error(f"PropertyPublisher.get_property】事件循环未初始化") self.node.lab_logger().error(f"【.get_property】事件循环未初始化")
return None return None
else: else:
# 如果是同步函数,直接调用并返回结果 # 如果是同步函数,直接调用并返回结果
self.node.lab_logger().trace(f"PropertyPublisher.get_property】获取同步属性: {self.name}") self.node.lab_logger().trace(f"【.get_property】获取同步属性: {self.name}")
self._value = self.get_method() self._value = self.get_method()
return self._value return self._value
async def get_property_async(self): async def get_property_async(self):
try: try:
# 获取异步属性值 # 获取异步属性值
self.node.lab_logger().trace(f"PropertyPublisher.get_property_async】异步获取属性: {self.name}") self.node.lab_logger().trace(f"【.get_property_async】异步获取属性: {self.name}")
self._value = await self.get_method() self._value = await self.get_method()
except Exception as e: except Exception as e:
self.node.lab_logger().error(f"PropertyPublisher.get_property_async】获取异步属性出错: {str(e)}") self.node.lab_logger().error(f"【.get_property_async】获取异步属性出错: {str(e)}")
def publish_property(self): def publish_property(self):
try: try:
self.node.lab_logger().trace(f"PropertyPublisher.publish_property】开始发布属性: {self.name}") self.node.lab_logger().trace(f"【.publish_property】开始发布属性: {self.name}")
value = self.get_property() value = self.get_property()
if self.print_publish: if self.print_publish:
self.node.lab_logger().trace(f"PropertyPublisher.publish_property】发布 {self.msg_type}: {value}") self.node.lab_logger().trace(f"【.publish_property】发布 {self.msg_type}: {value}")
if value is not None: if value is not None:
msg = convert_to_ros_msg(self.msg_type, value) msg = convert_to_ros_msg(self.msg_type, value)
self.publisher_.publish(msg) self.publisher_.publish(msg)
self.node.lab_logger().trace(f"PropertyPublisher.publish_property】属性 {self.name} 发布成功") self.node.lab_logger().trace(f"【.publish_property】属性 {self.name} 发布成功")
except Exception as e: except Exception as e:
self.node.lab_logger().error(f"【PropertyPublisher.publish_property】发布属性 {self.publisher_.topic} 出错: {str(e)}\n{traceback.format_exc()}") self.node.lab_logger().error(
f"【.publish_property】发布属性 {self.publisher_.topic} 出错: {str(e)}\n{traceback.format_exc()}"
)
def change_frequency(self, period): def change_frequency(self, period):
# 动态改变定时器频率 # 动态改变定时器频率
self.timer_period = period self.timer_period = period
self.node.get_logger().info( self.node.get_logger().info(f"【.change_frequency】修改 {self.name} 定时器周期为: {self.timer_period}")
f"【PropertyPublisher.change_frequency】修改 {self.name} 定时器周期为: {self.timer_period}"
)
# 重置定时器 # 重置定时器
self.timer.cancel() self.timer.cancel()
@@ -249,9 +256,10 @@ class BaseROS2DeviceNode(Node, Generic[T]):
node_name: str node_name: str
namespace: str namespace: str
# TODO 要删除,添加时间相关的属性,避免动态添加属性的警告 # 内部共享变量
time_spent = 0.0 _time_spent = 0.0
time_remaining = 0.0 _time_remaining = 0.0
# 是否创建Action
create_action_server = True create_action_server = True
def __init__( def __init__(
@@ -262,7 +270,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
action_value_mappings: Dict[str, Any], action_value_mappings: Dict[str, Any],
hardware_interface: Dict[str, Any], hardware_interface: Dict[str, Any],
print_publish=True, print_publish=True,
resource_tracker: Optional["DeviceNodeResourceTracker"] = None, resource_tracker: "DeviceNodeResourceTracker" = None, # type: ignore
): ):
""" """
初始化ROS2设备节点 初始化ROS2设备节点
@@ -313,7 +321,9 @@ class BaseROS2DeviceNode(Node, Generic[T]):
# 创建动作服务 # 创建动作服务
if self.create_action_server: if self.create_action_server:
for action_name, action_value_mapping in self._action_value_mappings.items(): for action_name, action_value_mapping in self._action_value_mappings.items():
if action_name.startswith("auto-") or str(action_value_mapping.get("type", "")).startswith("UniLabJsonCommand"): if action_name.startswith("auto-") or str(action_value_mapping.get("type", "")).startswith(
"UniLabJsonCommand"
):
continue continue
self.create_ros_action_server(action_name, action_value_mapping) self.create_ros_action_server(action_name, action_value_mapping)
@@ -325,13 +335,14 @@ class BaseROS2DeviceNode(Node, Generic[T]):
# 创建资源管理客户端 # 创建资源管理客户端
self._resource_clients: Dict[str, Client] = { self._resource_clients: Dict[str, Client] = {
"resource_add": self.create_client(ResourceAdd, "/resources/add"), "resource_add": self.create_client(ResourceAdd, "/resources/add"),
"resource_get": self.create_client(ResourceGet, "/resources/get"), "resource_get": self.create_client(SerialCommand, "/resources/get"),
"resource_delete": self.create_client(ResourceDelete, "/resources/delete"), "resource_delete": self.create_client(ResourceDelete, "/resources/delete"),
"resource_update": self.create_client(ResourceUpdate, "/resources/update"), "resource_update": self.create_client(ResourceUpdate, "/resources/update"),
"resource_list": self.create_client(ResourceList, "/resources/list"), "resource_list": self.create_client(ResourceList, "/resources/list"),
"c2s_update_resource_tree": self.create_client(SerialCommand, "/c2s_update_resource_tree"),
} }
def query_host_name_cb(req, res): def re_register_device(req, res):
self.register_device() self.register_device()
self.lab_logger().info("Host要求重新注册当前节点") self.lab_logger().info("Host要求重新注册当前节点")
res.response = "" res.response = ""
@@ -380,12 +391,16 @@ class BaseROS2DeviceNode(Node, Generic[T]):
if len(LIQUID_INPUT_SLOT) and LIQUID_INPUT_SLOT[0] == -1: if len(LIQUID_INPUT_SLOT) and LIQUID_INPUT_SLOT[0] == -1:
container_instance = request.resources[0] container_instance = request.resources[0]
container_query_dict: dict = resources container_query_dict: dict = resources
found_resources = self.resource_tracker.figure_resource({"id": container_query_dict["name"]}, try_mode=True) found_resources = self.resource_tracker.figure_resource(
{"id": container_query_dict["name"]}, try_mode=True
)
if not len(found_resources): if not len(found_resources):
self.resource_tracker.add_resource(container_instance) self.resource_tracker.add_resource(container_instance)
logger.info(f"添加物料{container_query_dict['name']}到资源跟踪器") logger.info(f"添加物料{container_query_dict['name']}到资源跟踪器")
else: else:
assert len(found_resources) == 1, f"找到多个同名物料: {container_query_dict['name']}, 请检查物料系统" assert (
len(found_resources) == 1
), f"找到多个同名物料: {container_query_dict['name']}, 请检查物料系统"
resource = found_resources[0] resource = found_resources[0]
if isinstance(resource, Resource): if isinstance(resource, Resource):
regular_container = RegularContainer(resource.id) regular_container = RegularContainer(resource.id)
@@ -399,12 +414,14 @@ class BaseROS2DeviceNode(Node, Generic[T]):
request.resources[0].name = resource["name"] request.resources[0].name = resource["name"]
logger.info(f"更新物料{container_query_dict['name']}的数据{resource['data']} dict") logger.info(f"更新物料{container_query_dict['name']}的数据{resource['data']} dict")
else: else:
logger.info(f"更新物料{container_query_dict['name']}出现不支持的数据类型{type(resource)} {resource}") logger.info(
f"更新物料{container_query_dict['name']}出现不支持的数据类型{type(resource)} {resource}"
)
response: ResourceAdd.Response = await rclient.call_async(request) response: ResourceAdd.Response = await rclient.call_async(request)
# 应该先add_resource了 # 应该先add_resource了
final_response = { final_response = {
"created_resources": [ROS2MessageInstance(i).get_python_dict() for i in request.resources], "created_resources": [ROS2MessageInstance(i).get_python_dict() for i in request.resources],
"liquid_input_resources": [] "liquid_input_resources": [],
} }
res.response = json.dumps(final_response) res.response = json.dumps(final_response)
# 如果driver自己就有assign的方法那就使用driver自己的assign方法 # 如果driver自己就有assign的方法那就使用driver自己的assign方法
@@ -423,12 +440,16 @@ class BaseROS2DeviceNode(Node, Generic[T]):
) )
res.response = get_result_info_str("", True, ret) res.response = get_result_info_str("", True, ret)
except Exception as e: except Exception as e:
self.lab_logger().error(f"运行设备的create_resource出错{create_resource_func}\n{traceback.format_exc()}") self.lab_logger().error(
f"运行设备的create_resource出错{create_resource_func}\n{traceback.format_exc()}"
)
res.response = get_result_info_str(traceback.format_exc(), False, {}) res.response = get_result_info_str(traceback.format_exc(), False, {})
return res return res
# 接下来该根据bind_parent_id进行assign了目前只有plr可以进行assign不然没有办法输入到物料系统中 # 接下来该根据bind_parent_id进行assign了目前只有plr可以进行assign不然没有办法输入到物料系统中
if bind_parent_id != self.node_name: if bind_parent_id != self.node_name:
resource = self.resource_tracker.figure_resource({"name": bind_parent_id}) # 拿到父节点进行具体assign等操作 resource = self.resource_tracker.figure_resource(
{"name": bind_parent_id}
) # 拿到父节点进行具体assign等操作
# request.resources = [convert_to_ros_msg(Resource, resources)] # request.resources = [convert_to_ros_msg(Resource, resources)]
try: try:
@@ -452,9 +473,15 @@ class BaseROS2DeviceNode(Node, Generic[T]):
empty_liquid_info_in[liquid_input_slot] = (liquid_type, liquid_volume) empty_liquid_info_in[liquid_input_slot] = (liquid_type, liquid_volume)
plr_instance.set_well_liquids(empty_liquid_info_in) plr_instance.set_well_liquids(empty_liquid_info_in)
input_wells_ulr = [ input_wells_ulr = [
convert_to_ros_msg(Resource, resource_plr_to_ulab(plr_instance.get_well(LIQUID_INPUT_SLOT), with_children=False)) for r in LIQUID_INPUT_SLOT convert_to_ros_msg(
Resource,
resource_plr_to_ulab(plr_instance.get_well(LIQUID_INPUT_SLOT), with_children=False),
)
for r in LIQUID_INPUT_SLOT
]
final_response["liquid_input_resources"] = [
ROS2MessageInstance(i).get_python_dict() for i in input_wells_ulr
] ]
final_response["liquid_input_resources"] = [ROS2MessageInstance(i).get_python_dict() for i in input_wells_ulr]
res.response = json.dumps(final_response) res.response = json.dumps(final_response)
if isinstance(resource, OTDeck) and "slot" in other_calling_param: if isinstance(resource, OTDeck) and "slot" in other_calling_param:
other_calling_param["slot"] = int(other_calling_param["slot"]) other_calling_param["slot"] = int(other_calling_param["slot"])
@@ -499,16 +526,22 @@ class BaseROS2DeviceNode(Node, Generic[T]):
# noinspection PyTypeChecker # noinspection PyTypeChecker
self._service_server: Dict[str, Service] = { self._service_server: Dict[str, Service] = {
"query_host_name": self.create_service( "re_register_device": self.create_service(
SerialCommand, SerialCommand,
f"/srv{self.namespace}/query_host_name", f"/srv{self.namespace}/re_register_device",
query_host_name_cb, re_register_device,
callback_group=self.callback_group, callback_group=self.callback_group,
), ),
"append_resource": self.create_service( "append_resource": self.create_service(
SerialCommand, SerialCommand,
f"/srv{self.namespace}/append_resource", f"/srv{self.namespace}/append_resource",
append_resource, append_resource, # type: ignore
callback_group=self.callback_group,
),
"s2c_resource_tree": self.create_service(
SerialCommand,
f"/srv{self.namespace}/s2c_resource_tree",
self.s2c_resource_tree, # type: ignore
callback_group=self.callback_group, callback_group=self.callback_group,
), ),
} }
@@ -518,17 +551,208 @@ class BaseROS2DeviceNode(Node, Generic[T]):
rclpy.get_global_executor().add_node(self) rclpy.get_global_executor().add_node(self)
self.lab_logger().debug(f"ROS节点初始化完成") self.lab_logger().debug(f"ROS节点初始化完成")
async def update_resource(self, resources: List[Any]): async def update_resource(self, resources: List["ResourcePLR"]):
r = ResourceUpdate.Request() r = SerialCommand.Request()
unique_resources = [] tree_set = ResourceTreeSet.from_plr_resources(resources)
for resource in resources: # resource是list[ResourcePLR] r.command = json.dumps({"data": {"data": tree_set.dump()}, "action": "update"})
# 目前更新资源只支持传入plr的对象后面要更新convert_resources_from_type函数 response: SerialCommand_Response = await self._resource_clients["c2s_update_resource_tree"].call_async(r) # type: ignore
converted_list = convert_resources_from_type([resource], resource_type=[object], is_plr=True) try:
unique_resources.extend([convert_to_ros_msg(Resource, converted) for converted in converted_list]) uuid_maps = json.loads(response.response)
r.resources = unique_resources self.resource_tracker.loop_update_uuid(resources, uuid_maps)
response = await self._resource_clients["resource_update"].call_async(r) except Exception as e:
self.lab_logger().error(f"更新资源uuid失败: {e}")
self.lab_logger().error(traceback.format_exc())
self.lab_logger().debug(f"资源更新结果: {response}") self.lab_logger().debug(f"资源更新结果: {response}")
async def s2c_resource_tree(self, req: SerialCommand_Request, res: SerialCommand_Response):
"""
处理资源树更新请求
支持三种操作:
- add: 添加新资源到资源树
- update: 更新现有资源
- remove: 从资源树中移除资源
"""
try:
data = json.loads(req.command)
results = []
for i in data:
action = i.get("action") # remove, add, update
resources_uuid: List[str] = i.get("data") # 资源数据
additional_add_params = i.get("additional_add_params", {}) # 额外参数
self.lab_logger().info(
f"[Resource Tree Update] Processing {action} operation, " f"resources count: {len(resources_uuid)}"
)
tree_set = None
if action in ["add", "update"]:
response: SerialCommand.Response = await self._resource_clients[
"c2s_update_resource_tree"
].call_async(
SerialCommand.Request(
command=json.dumps(
{"data": {"data": resources_uuid, "with_children": False}, "action": "get"}
)
)
) # type: ignore
raw_nodes = json.loads(response.response)
tree_set = ResourceTreeSet.from_raw_list(raw_nodes)
try:
if action == "add":
# 添加资源到资源跟踪器
plr_resources = tree_set.to_plr_resources()
for plr_resource, tree in zip(plr_resources, tree_set.trees):
self.resource_tracker.add_resource(plr_resource)
parent_uuid = tree.root_node.res_content.parent_uuid
if parent_uuid:
parent_resource: ResourcePLR = self.resource_tracker.uuid_to_resources.get(parent_uuid)
if parent_resource is None:
self.lab_logger().warning(
f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_uuid}不存在"
)
else:
try:
# 特殊兼容所有plr的物料的assign方法和create_resource append_resource后期同步
additional_params = {}
site = additional_add_params.get("site", None)
spec = inspect.signature(parent_resource.assign_child_resource)
if "spot" in spec.parameters:
additional_params["spot"] = site
parent_resource.assign_child_resource(
plr_resource, location=None, **additional_params
)
except Exception as e:
self.lab_logger().warning(
f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_resource}[{parent_uuid}]失败!\n{traceback.format_exc()}"
)
func = getattr(self.driver_instance, "resource_tree_add", None)
if callable(func):
func(plr_resources)
results.append({"success": True, "action": "add"})
elif action == "update":
# 更新资源
plr_resources = tree_set.to_plr_resources()
for plr_resource, tree in zip(plr_resources, tree_set.trees):
states = plr_resource.serialize_all_state()
original_instance: ResourcePLR = self.resource_tracker.figure_resource(
{"uuid": tree.root_node.res_content.uuid}, try_mode=False
)
original_instance.load_all_state(states)
self.lab_logger().info(
f"更新了资源属性 {plr_resource}[{tree.root_node.res_content.uuid}] 及其子节点 {len(original_instance.get_all_children())}"
)
func = getattr(self.driver_instance, "resource_tree_update", None)
if callable(func):
func(plr_resources)
results.append({"success": True, "action": "update"})
elif action == "remove":
# 移除资源
plr_resources: List[ResourcePLR] = [
self.resource_tracker.uuid_to_resources[i] for i in resources_uuid
]
func = getattr(self.driver_instance, "resource_tree_remove", None)
if callable(func):
func(plr_resources)
for plr_resource in plr_resources:
plr_resource.parent.unassign_child_resource(plr_resource)
self.resource_tracker.remove_resource(plr_resource)
results.append({"success": True, "action": "remove"})
except Exception as e:
error_msg = f"Error processing {action} operation: {str(e)}"
self.lab_logger().error(f"[Resource Tree Update] {error_msg}")
self.lab_logger().error(traceback.format_exc())
results.append({"success": False, "action": action, "error": error_msg})
# 返回处理结果
result_json = {"results": results, "total": len(data)}
res.response = json.dumps(result_json, ensure_ascii=False)
self.lab_logger().info(f"[Resource Tree Update] Completed processing {len(data)} operations")
except json.JSONDecodeError as e:
error_msg = f"Invalid JSON format: {str(e)}"
self.lab_logger().error(f"[Resource Tree Update] {error_msg}")
res.response = json.dumps({"success": False, "error": error_msg}, ensure_ascii=False)
except Exception as e:
error_msg = f"Unexpected error: {str(e)}"
self.lab_logger().error(f"[Resource Tree Update] {error_msg}")
self.lab_logger().error(traceback.format_exc())
res.response = json.dumps({"success": False, "error": error_msg}, ensure_ascii=False)
return res
async def transfer_resource_to_another(
self,
plr_resources: List["ResourcePLR"],
target_device_id: str,
target_resources: List["ResourcePLR"],
sites: List[str],
):
# 准备工作
uids = []
target_uids = []
for plr_resource in plr_resources:
uid = getattr(plr_resource, "unilabos_uuid", None)
if uid is None:
raise ValueError(f"来源物料{plr_resource}没有unilabos_uuid属性无法转运")
uids.append(uid)
for target_resource in target_resources:
uid = getattr(target_resource, "unilabos_uuid", None)
if uid is None:
raise ValueError(f"目标物料{target_resource}没有unilabos_uuid属性无法转运")
target_uids.append(uid)
srv_address = f"/srv{target_device_id}/s2c_resource_tree"
sclient = self.create_client(SerialCommand, srv_address)
# 等待服务可用(设置超时)
if not sclient.wait_for_service(timeout_sec=5.0):
self.lab_logger().error(f"[{self.device_id} Node-Resource] Service {srv_address} not available")
raise ValueError(f"[{self.device_id} Node-Resource] Service {srv_address} not available")
# 先从当前节点移除资源
await self.s2c_resource_tree(
SerialCommand_Request(
command=json.dumps([{"action": "remove", "data": uids}], ensure_ascii=False) # 只移除父节点
),
SerialCommand_Response(),
)
# 通知云端转运资源
for plr_resource, target_uid, site in zip(plr_resources, target_uids, sites):
tree_set = ResourceTreeSet.from_plr_resources([plr_resource])
for root_node in tree_set.root_nodes:
root_node.res_content.parent = None
root_node.res_content.parent_uuid = target_uid
r = SerialCommand.Request()
r.command = json.dumps({"data": {"data": tree_set.dump()}, "action": "update"}) # 和Update Resource一致
response: SerialCommand_Response = await self._resource_clients["c2s_update_resource_tree"].call_async(r) # type: ignore
self.lab_logger().info(f"资源云端转运到{target_device_id}结果: {response.response}")
# 创建请求
request = SerialCommand.Request()
request.command = json.dumps(
[
{
"action": "add",
"data": tree_set.all_nodes_uuid, # 只添加父节点,子节点会自动添加
"additional_add_params": {"site": site},
}
],
ensure_ascii=False,
)
future = sclient.call_async(request)
timeout = 30.0
start_time = time.time()
while not future.done():
if time.time() - start_time > timeout:
self.lab_logger().error(
f"[{self.device_id} Node-Resource] Timeout waiting for response from {target_device_id}"
)
return False
time.sleep(0.05)
self.lab_logger().info(f"资源本地增加到{target_device_id}结果: {response.response}")
return None
def register_device(self): def register_device(self):
"""向注册表中注册设备信息""" """向注册表中注册设备信息"""
topics_info = self._property_publishers.copy() topics_info = self._property_publishers.copy()
@@ -657,7 +881,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
execution_success = False execution_success = False
action_return_value = None action_return_value = None
##### self.lab_logger().info(f"执行动作: {action_name}") ##### self.lab_logger().info(f"执行动作: {action_name}")
goal = goal_handle.request goal = goal_handle.request
# 从目标消息中提取参数, 并调用对应的方法 # 从目标消息中提取参数, 并调用对应的方法
@@ -672,7 +896,9 @@ class BaseROS2DeviceNode(Node, Generic[T]):
self.lab_logger().info(f"执行序列动作后续步骤: {action}") self.lab_logger().info(f"执行序列动作后续步骤: {action}")
self.get_real_function(self.driver_instance, action)[0]() self.get_real_function(self.driver_instance, action)[0]()
action_paramtypes = self.get_real_function(self.driver_instance, action_value_mapping["sequence"][0])[1] action_paramtypes = self.get_real_function(self.driver_instance, action_value_mapping["sequence"][0])[
1
]
else: else:
ACTION, action_paramtypes = self.get_real_function(self.driver_instance, action_name) ACTION, action_paramtypes = self.get_real_function(self.driver_instance, action_name)
@@ -684,43 +910,34 @@ class BaseROS2DeviceNode(Node, Generic[T]):
for k, v in goal.get_fields_and_field_types().items(): for k, v in goal.get_fields_and_field_types().items():
if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]: if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
self.lab_logger().info(f"{action_name} 查询资源状态: Key: {k} Type: {v}") self.lab_logger().info(f"{action_name} 查询资源状态: Key: {k} Type: {v}")
current_resources: Union[List[Resource], List[List[Resource]]] = []
# TODO: resource后面需要分组
only_one_resource = False
try: try:
if isinstance(action_kwargs[k], list) and len(action_kwargs[k]) > 1: # 统一处理单个或多个资源
for i in action_kwargs[k]: is_sequence = v != "unilabos_msgs/Resource"
r = ResourceGet.Request() resource_inputs = action_kwargs[k] if is_sequence else [action_kwargs[k]]
r.id = i["id"] # splash optional
r.with_children = True # 批量查询资源
response = await self._resource_clients["resource_get"].call_async(r) queried_resources = []
current_resources.append(response.resources) for resource_data in resource_inputs:
else: r = SerialCommand.Request()
only_one_resource = True r.command = json.dumps({"id": resource_data["id"], "with_children": True})
r = ResourceGet.Request() # 发送请求并等待响应
r.id = ( response: SerialCommand_Response = await self._resource_clients[
action_kwargs[k]["id"] "resource_get"
if v == "unilabos_msgs/Resource" ].call_async(r)
else action_kwargs[k][0]["id"] raw_data = json.loads(response.response)
)
r.with_children = True # 转换为 PLR 资源
response = await self._resource_clients["resource_get"].call_async(r) tree_set = ResourceTreeSet.from_raw_list(raw_data)
current_resources.extend(response.resources) plr_resource = tree_set.to_plr_resources()[0]
except Exception: queried_resources.append(plr_resource)
logger.error(f"资源查询失败,默认使用本地资源")
# 删除对response.resources的检查因为它总是存在 self.lab_logger().debug(f"资源查询结果: 共 {len(queried_resources)} 个资源")
type_hint = action_paramtypes[k]
final_type = get_type_class(type_hint) # 通过资源跟踪器获取本地实例
if only_one_resource: final_resources = queried_resources if is_sequence else queried_resources[0]
resources_list: List[Dict[str, Any]] = [convert_from_ros_msg(rs) for rs in current_resources] # type: ignore action_kwargs[k] = self.resource_tracker.figure_resource(final_resources, try_mode=False)
self.lab_logger().debug(f"资源查询结果: {len(resources_list)} 个资源")
final_resource = convert_resources_to_type(resources_list, final_type)
# 判断 ACTION 是否需要特殊的物料类型如 pylabrobot.resources.Resource并做转换
else:
resources_list: List[List[Dict[str, Any]]] = [[convert_from_ros_msg(rs) for rs in sub_res_list] for sub_res_list in current_resources] # type: ignore
final_resource = [convert_resources_to_type(sub_res_list, final_type)[0] for sub_res_list in resources_list]
try:
action_kwargs[k] = self.resource_tracker.figure_resource(final_resource, try_mode=False)
except Exception as e: except Exception as e:
self.lab_logger().error(f"{action_name} 物料实例获取失败: {e}\n{traceback.format_exc()}") self.lab_logger().error(f"{action_name} 物料实例获取失败: {e}\n{traceback.format_exc()}")
error_skip = True error_skip = True
@@ -745,8 +962,9 @@ class BaseROS2DeviceNode(Node, Generic[T]):
execution_success = True execution_success = True
except Exception as e: except Exception as e:
execution_error = traceback.format_exc() execution_error = traceback.format_exc()
error(f"异步任务 {ACTION.__name__} 报错了\n{traceback.format_exc()}\n原始输入:{action_kwargs}") error(
error(traceback.format_exc()) f"异步任务 {ACTION.__name__} 报错了\n{traceback.format_exc()}\n原始输入:{action_kwargs}"
)
future.add_done_callback(_handle_future_exception) future.add_done_callback(_handle_future_exception)
except Exception as e: except Exception as e:
@@ -754,7 +972,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
execution_success = False execution_success = False
self.lab_logger().error(f"创建异步任务失败: {traceback.format_exc()}") self.lab_logger().error(f"创建异步任务失败: {traceback.format_exc()}")
else: else:
##### self.lab_logger().info(f"同步执行动作 {ACTION}") ##### self.lab_logger().info(f"同步执行动作 {ACTION}")
future = self._executor.submit(ACTION, **action_kwargs) future = self._executor.submit(ACTION, **action_kwargs)
def _handle_future_exception(fut): def _handle_future_exception(fut):
@@ -763,7 +981,10 @@ class BaseROS2DeviceNode(Node, Generic[T]):
action_return_value = fut.result() action_return_value = fut.result()
execution_success = True execution_success = True
except Exception as e: except Exception as e:
error(f"同步任务 {ACTION.__name__} 报错了\n{traceback.format_exc()}\n原始输入:{action_kwargs}") execution_error = traceback.format_exc()
error(
f"同步任务 {ACTION.__name__} 报错了\n{traceback.format_exc()}\n原始输入:{action_kwargs}"
)
future.add_done_callback(_handle_future_exception) future.add_done_callback(_handle_future_exception)
@@ -778,8 +999,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
goal_handle.canceled() goal_handle.canceled()
return action_type.Result() return action_type.Result()
self.time_spent = time.time() - time_start self._time_spent = time.time() - time_start
self.time_remaining = time_overall - self.time_spent self._time_remaining = time_overall - self._time_spent
# 发布反馈 # 发布反馈
feedback_values = {} feedback_values = {}
@@ -807,7 +1028,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
self.lab_logger().info(f"动作 {action_name} 已取消") self.lab_logger().info(f"动作 {action_name} 已取消")
return action_type.Result() return action_type.Result()
##### self.lab_logger().info(f"动作执行完成: {action_name}") # self.lab_logger().info(f"动作执行完成: {action_name}")
del future del future
# 向Host更新物料当前状态 # 向Host更新物料当前状态
@@ -816,27 +1037,25 @@ class BaseROS2DeviceNode(Node, Generic[T]):
if v not in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]: if v not in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
continue continue
self.lab_logger().info(f"更新资源状态: {k}") self.lab_logger().info(f"更新资源状态: {k}")
r = ResourceUpdate.Request()
# 仅当action_kwargs[k]不为None时尝试转换 # 仅当action_kwargs[k]不为None时尝试转换
akv = action_kwargs[k] # 已经是完成转换的物料了只需要转换成ros msg Resource了 akv = action_kwargs[k] # 已经是完成转换的物料了
apv = action_paramtypes[k] apv = action_paramtypes[k]
final_type = get_type_class(apv) final_type = get_type_class(apv)
if final_type is None: if final_type is None:
continue continue
try: try:
# 去重:使用 seen 集合获取唯一的资源对象
seen = set() seen = set()
unique_resources = [] unique_resources = []
for rs in akv: for rs in akv: # todo: 这里目前只支持plr的类型
res = self.resource_tracker.parent_resource(rs) # 获取 resource 对象 res = self.resource_tracker.parent_resource(rs) # 获取 resource 对象
if id(res) not in seen: if id(res) not in seen:
seen.add(id(res)) seen.add(id(res))
converted_list = convert_resources_from_type([res], final_type) unique_resources.append(res)
unique_resources.extend([convert_to_ros_msg(Resource, converted) for converted in converted_list])
r.resources = unique_resources # 使用新的资源树接口
if unique_resources:
response = await self._resource_clients["resource_update"].call_async(r) await self.update_resource(unique_resources)
self.lab_logger().debug(f"资源更新结果: {response}")
except Exception as e: except Exception as e:
self.lab_logger().error(f"资源更新失败: {e}") self.lab_logger().error(f"资源更新失败: {e}")
self.lab_logger().error(traceback.format_exc()) self.lab_logger().error(traceback.format_exc())
@@ -860,13 +1079,217 @@ class BaseROS2DeviceNode(Node, Generic[T]):
if attr_name in ["success", "reached_goal"]: if attr_name in ["success", "reached_goal"]:
setattr(result_msg, attr_name, True) setattr(result_msg, attr_name, True)
elif attr_name == "return_info": elif attr_name == "return_info":
setattr(result_msg, attr_name, get_result_info_str(execution_error, execution_success, action_return_value)) setattr(
result_msg,
attr_name,
get_result_info_str(execution_error, execution_success, action_return_value),
)
##### self.lab_logger().info(f"动作 {action_name} 完成并返回结果") ##### self.lab_logger().info(f"动作 {action_name} 完成并返回结果")
return result_msg return result_msg
return execute_callback return execute_callback
def _execute_driver_command(self, string: str):
try:
target = json.loads(string)
except Exception as ex:
try:
target = yaml.safe_load(io.StringIO(string))
except Exception as ex2:
raise JsonCommandInitError(
f"执行动作时JSON/YAML解析失败: \n{ex}\n{ex2}\n原内容: {string}\n{traceback.format_exc()}"
)
try:
function_name = target["function_name"]
function_args = target["function_args"]
assert isinstance(function_args, dict), "执行动作时JSON必须为dict类型\n原JSON: {string}"
function = getattr(self.driver_instance, function_name)
assert callable(
function
), f"执行动作时JSON中的function_name对应的函数不可调用: {function_name}\n原JSON: {string}"
# 处理 ResourceSlot 类型参数
args_list = default_manager._analyze_method_signature(function)["args"]
for arg in args_list:
arg_name = arg["name"]
arg_type = arg["type"]
# 跳过不在 function_args 中的参数
if arg_name not in function_args:
continue
# 处理单个 ResourceSlot
if arg_type == "unilabos.registry.placeholder_type:ResourceSlot":
resource_data = function_args[arg_name]
if isinstance(resource_data, dict) and "id" in resource_data:
try:
converted_resource = self._convert_resource_sync(resource_data)
function_args[arg_name] = converted_resource
except Exception as e:
self.lab_logger().error(
f"转换ResourceSlot参数 {arg_name} 失败: {e}\n{traceback.format_exc()}"
)
raise JsonCommandInitError(f"ResourceSlot参数转换失败: {arg_name}")
# 处理 ResourceSlot 列表
elif isinstance(arg_type, tuple) and len(arg_type) == 2:
resource_slot_type = "unilabos.registry.placeholder_type:ResourceSlot"
if arg_type[0] == "list" and arg_type[1] == resource_slot_type:
resource_list = function_args[arg_name]
if isinstance(resource_list, list):
try:
converted_resources = []
for resource_data in resource_list:
if isinstance(resource_data, dict) and "id" in resource_data:
converted_resource = self._convert_resource_sync(resource_data)
converted_resources.append(converted_resource)
function_args[arg_name] = converted_resources
except Exception as e:
self.lab_logger().error(
f"转换ResourceSlot列表参数 {arg_name} 失败: {e}\n{traceback.format_exc()}"
)
raise JsonCommandInitError(f"ResourceSlot列表参数转换失败: {arg_name}")
return function(**function_args)
except KeyError as ex:
raise JsonCommandInitError(
f"执行动作时JSON缺少function_name或function_args: {ex}\n原JSON: {string}\n{traceback.format_exc()}"
)
def _convert_resource_sync(self, resource_data: Dict[str, Any]):
"""同步转换资源数据为实例"""
# 创建资源查询请求
r = SerialCommand.Request()
r.command = json.dumps({"id": resource_data["id"], "with_children": True})
# 同步调用资源查询服务
future = self._resource_clients["resource_get"].call_async(r)
# 等待结果使用while循环每次sleep 0.5秒最多等待5秒
timeout = 30.0
elapsed = 0.0
while not future.done() and elapsed < timeout:
time.sleep(0.05)
elapsed += 0.05
if not future.done():
raise Exception(f"资源查询超时: {resource_data['id']}")
response = future.result()
if response is None:
raise Exception(f"资源查询返回空结果: {resource_data['id']}")
current_resources = json.loads(response.response)
# 转换为 PLR 资源
tree_set = ResourceTreeSet.from_raw_list(current_resources)
plr_resource = tree_set.to_plr_resources()[0]
# 通过资源跟踪器获取本地实例
res = self.resource_tracker.figure_resource(plr_resource, try_mode=True)
if len(res) == 0:
self.lab_logger().warning(f"资源转换未能索引到实例: {resource_data},返回新建实例")
return plr_resource
elif len(res) == 1:
return res[0]
else:
raise ValueError(f"资源转换得到多个实例: {res}")
async def _execute_driver_command_async(self, string: str):
try:
target = json.loads(string)
except Exception as ex:
try:
target = yaml.safe_load(io.StringIO(string))
except Exception as ex2:
raise JsonCommandInitError(
f"执行动作时JSON/YAML解析失败: \n{ex}\n{ex2}\n原内容: {string}\n{traceback.format_exc()}"
)
try:
function_name = target["function_name"]
function_args = target["function_args"]
assert isinstance(function_args, dict), "执行动作时JSON必须为dict类型\n原JSON: {string}"
function = getattr(self.driver_instance, function_name)
assert callable(
function
), f"执行动作时JSON中的function_name对应的函数不可调用: {function_name}\n原JSON: {string}"
assert asyncio.iscoroutinefunction(
function
), f"执行动作时JSON中的function并非异步: {function_name}\n原JSON: {string}"
# 处理 ResourceSlot 类型参数
args_list = default_manager._analyze_method_signature(function)["args"]
for arg in args_list:
arg_name = arg["name"]
arg_type = arg["type"]
# 跳过不在 function_args 中的参数
if arg_name not in function_args:
continue
# 处理单个 ResourceSlot
if arg_type == "unilabos.registry.placeholder_type:ResourceSlot":
resource_data = function_args[arg_name]
if isinstance(resource_data, dict) and "id" in resource_data:
try:
converted_resource = await self._convert_resource_async(resource_data)
function_args[arg_name] = converted_resource
except Exception as e:
self.lab_logger().error(
f"转换ResourceSlot参数 {arg_name} 失败: {e}\n{traceback.format_exc()}"
)
raise JsonCommandInitError(f"ResourceSlot参数转换失败: {arg_name}")
# 处理 ResourceSlot 列表
elif isinstance(arg_type, tuple) and len(arg_type) == 2:
resource_slot_type = "unilabos.registry.placeholder_type:ResourceSlot"
if arg_type[0] == "list" and arg_type[1] == resource_slot_type:
resource_list = function_args[arg_name]
if isinstance(resource_list, list):
try:
converted_resources = []
for resource_data in resource_list:
if isinstance(resource_data, dict) and "id" in resource_data:
converted_resource = await self._convert_resource_async(resource_data)
converted_resources.append(converted_resource)
function_args[arg_name] = converted_resources
except Exception as e:
self.lab_logger().error(
f"转换ResourceSlot列表参数 {arg_name} 失败: {e}\n{traceback.format_exc()}"
)
raise JsonCommandInitError(f"ResourceSlot列表参数转换失败: {arg_name}")
return await function(**function_args)
except KeyError as ex:
raise JsonCommandInitError(
f"执行动作时JSON缺少function_name或function_args: {ex}\n原JSON: {string}\n{traceback.format_exc()}"
)
async def _convert_resource_async(self, resource_data: Dict[str, Any]):
"""异步转换资源数据为实例"""
# 创建资源查询请求
r = SerialCommand.Request()
r.command = json.dumps({"id": resource_data["id"], "with_children": True})
# 异步调用资源查询服务
response: SerialCommand_Response = await self._resource_clients["resource_get"].call_async(r)
current_resources = json.loads(response.response)
# 转换为 PLR 资源
tree_set = ResourceTreeSet.from_raw_list(current_resources)
plr_resource = tree_set.to_plr_resources()[0]
# 通过资源跟踪器获取本地实例
res = self.resource_tracker.figure_resource(plr_resource, try_mode=True)
if len(res) == 0:
# todo: 后续通过decoration来区分减少warning
self.lab_logger().warning(f"资源转换未能索引到实例: {resource_data},返回新建实例")
return plr_resource
elif len(res) == 1:
return res[0]
else:
raise ValueError(f"资源转换得到多个实例: {res}")
# 异步上下文管理方法 # 异步上下文管理方法
async def __aenter__(self): async def __aenter__(self):
"""进入异步上下文""" """进入异步上下文"""
@@ -887,9 +1310,11 @@ class BaseROS2DeviceNode(Node, Generic[T]):
class DeviceInitError(Exception): class DeviceInitError(Exception):
pass pass
class JsonCommandInitError(Exception): class JsonCommandInitError(Exception):
pass pass
class ROS2DeviceNode: class ROS2DeviceNode:
""" """
ROS2设备节点类 ROS2设备节点类
@@ -969,7 +1394,6 @@ class ROS2DeviceNode:
or driver_class.__name__ == "PRCXI9300Handler" or driver_class.__name__ == "PRCXI9300Handler"
) )
# TODO: 要在创建之前预先请求服务器是否有当前id的物料放到resource_tracker中让pylabrobot进行创建
# 创建设备类实例 # 创建设备类实例
if use_pylabrobot_creator: if use_pylabrobot_creator:
# 先对pylabrobot的子资源进行加载不然subclass无法认出 # 先对pylabrobot的子资源进行加载不然subclass无法认出
@@ -980,11 +1404,18 @@ class ROS2DeviceNode:
) )
else: else:
from unilabos.devices.workstation.workstation_base import WorkstationBase from unilabos.devices.workstation.workstation_base import WorkstationBase
if issubclass(self._driver_class, WorkstationBase): # 是WorkstationNode的子节点就要调用WorkstationNodeCreator
if issubclass(
self._driver_class, WorkstationBase
): # 是WorkstationNode的子节点就要调用WorkstationNodeCreator
self.driver_is_workstation = True self.driver_is_workstation = True
self._driver_creator = WorkstationNodeCreator(driver_class, children=children, resource_tracker=self.resource_tracker) self._driver_creator = WorkstationNodeCreator(
driver_class, children=children, resource_tracker=self.resource_tracker
)
else: else:
self._driver_creator = DeviceClassCreator(driver_class, children=children, resource_tracker=self.resource_tracker) self._driver_creator = DeviceClassCreator(
driver_class, children=children, resource_tracker=self.resource_tracker
)
if driver_is_ros: if driver_is_ros:
driver_params["device_id"] = device_id driver_params["device_id"] = device_id
@@ -999,6 +1430,7 @@ class ROS2DeviceNode:
self._ros_node = self._driver_instance # type: ignore self._ros_node = self._driver_instance # type: ignore
elif self.driver_is_workstation: elif self.driver_is_workstation:
from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
self._ros_node = ROS2WorkstationNode( self._ros_node = ROS2WorkstationNode(
protocol_type=driver_params["protocol_type"], protocol_type=driver_params["protocol_type"],
children=children, children=children,
@@ -1023,51 +1455,14 @@ class ROS2DeviceNode:
self._ros_node: BaseROS2DeviceNode self._ros_node: BaseROS2DeviceNode
self._ros_node.lab_logger().info(f"初始化完成 {self._ros_node.uuid} {self.driver_is_ros}") self._ros_node.lab_logger().info(f"初始化完成 {self._ros_node.uuid} {self.driver_is_ros}")
self.driver_instance._ros_node = self._ros_node # type: ignore self.driver_instance._ros_node = self._ros_node # type: ignore
self.driver_instance._execute_driver_command = self._execute_driver_command # type: ignore self.driver_instance._execute_driver_command = self._ros_node._execute_driver_command # type: ignore
self.driver_instance._execute_driver_command_async = self._execute_driver_command_async # type: ignore self.driver_instance._execute_driver_command_async = self._ros_node._execute_driver_command_async # type: ignore
if hasattr(self.driver_instance, "post_init"): if hasattr(self.driver_instance, "post_init"):
try: try:
self.driver_instance.post_init(self._ros_node) # type: ignore self.driver_instance.post_init(self._ros_node) # type: ignore
except Exception as e: except Exception as e:
self._ros_node.lab_logger().error(f"设备后初始化失败: {e}") self._ros_node.lab_logger().error(f"设备后初始化失败: {e}")
def _execute_driver_command(self, string: str):
try:
target = json.loads(string)
except Exception as ex:
try:
target = yaml.safe_load(io.StringIO(string))
except Exception as ex2:
raise JsonCommandInitError(f"执行动作时JSON/YAML解析失败: \n{ex}\n{ex2}\n原内容: {string}\n{traceback.format_exc()}")
try:
function_name = target["function_name"]
function_args = target["function_args"]
assert isinstance(function_args, dict), "执行动作时JSON必须为dict类型\n原JSON: {string}"
function = getattr(self.driver_instance, function_name)
assert callable(function), f"执行动作时JSON中的function_name对应的函数不可调用: {function_name}\n原JSON: {string}"
return function(**function_args)
except KeyError as ex:
raise JsonCommandInitError(f"执行动作时JSON缺少function_name或function_args: {ex}\n原JSON: {string}\n{traceback.format_exc()}")
async def _execute_driver_command_async(self, string: str):
try:
target = json.loads(string)
except Exception as ex:
try:
target = yaml.safe_load(io.StringIO(string))
except Exception as ex2:
raise JsonCommandInitError(f"执行动作时JSON/YAML解析失败: \n{ex}\n{ex2}\n原内容: {string}\n{traceback.format_exc()}")
try:
function_name = target["function_name"]
function_args = target["function_args"]
assert isinstance(function_args, dict), "执行动作时JSON必须为dict类型\n原JSON: {string}"
function = getattr(self.driver_instance, function_name)
assert callable(function), f"执行动作时JSON中的function_name对应的函数不可调用: {function_name}\n原JSON: {string}"
assert asyncio.iscoroutinefunction(function), f"执行动作时JSON中的function并非异步: {function_name}\n原JSON: {string}"
return await function(**function_args)
except KeyError as ex:
raise JsonCommandInitError(f"执行动作时JSON缺少function_name或function_args: {ex}\n原JSON: {string}\n{traceback.format_exc()}")
def _start_loop(self): def _start_loop(self):
def run_event_loop(): def run_event_loop():
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()

View File

@@ -1,5 +1,4 @@
import collections import collections
import copy
from dataclasses import dataclass, field from dataclasses import dataclass, field
import json import json
import threading import threading
@@ -13,16 +12,15 @@ from geometry_msgs.msg import Point
from rclpy.action import ActionClient, get_action_server_names_and_types_by_node from rclpy.action import ActionClient, get_action_server_names_and_types_by_node
from rclpy.callback_groups import ReentrantCallbackGroup from rclpy.callback_groups import ReentrantCallbackGroup
from rclpy.service import Service from rclpy.service import Service
from rosidl_runtime_py import set_message_fields
from unilabos_msgs.msg import Resource # type: ignore from unilabos_msgs.msg import Resource # type: ignore
from unilabos_msgs.srv import ( from unilabos_msgs.srv import (
ResourceAdd, ResourceAdd,
ResourceGet,
ResourceDelete, ResourceDelete,
ResourceUpdate, ResourceUpdate,
ResourceList, ResourceList,
SerialCommand, SerialCommand,
) # type: ignore ) # type: ignore
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
from unique_identifier_msgs.msg import UUID from unique_identifier_msgs.msg import UUID
from unilabos.registry.registry import lab_registry from unilabos.registry.registry import lab_registry
@@ -38,11 +36,17 @@ from unilabos.ros.msgs.message_converter import (
) )
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, ROS2DeviceNode, DeviceNodeResourceTracker from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, ROS2DeviceNode, DeviceNodeResourceTracker
from unilabos.ros.nodes.presets.controller_node import ControllerNode from unilabos.ros.nodes.presets.controller_node import ControllerNode
from unilabos.ros.nodes.resource_tracker import (
ResourceDictInstance,
ResourceTreeSet,
ResourceTreeInstance,
)
from unilabos.utils.exception import DeviceClassInvalid from unilabos.utils.exception import DeviceClassInvalid
from unilabos.utils.type_check import serialize_result_info from unilabos.utils.type_check import serialize_result_info
from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot
if TYPE_CHECKING: if TYPE_CHECKING:
from unilabos.app.ws_client import QueueItem from unilabos.app.ws_client import QueueItem, WSResourceChatData
@dataclass @dataclass
@@ -62,6 +66,7 @@ class HostNode(BaseROS2DeviceNode):
_device_action_status: ClassVar[collections.defaultdict[str, DeviceActionStatus]] = collections.defaultdict( _device_action_status: ClassVar[collections.defaultdict[str, DeviceActionStatus]] = collections.defaultdict(
DeviceActionStatus DeviceActionStatus
) )
_resource_tracker: ClassVar[DeviceNodeResourceTracker] = DeviceNodeResourceTracker() # 资源管理器实例
@classmethod @classmethod
def get_instance(cls, timeout=None) -> Optional["HostNode"]: def get_instance(cls, timeout=None) -> Optional["HostNode"]:
@@ -72,8 +77,8 @@ class HostNode(BaseROS2DeviceNode):
def __init__( def __init__(
self, self,
device_id: str, device_id: str,
devices_config: Dict[str, Any], devices_config: ResourceTreeSet,
resources_config: list, resources_config: ResourceTreeSet,
resources_edge_config: list[dict], resources_edge_config: list[dict],
physical_setup_graph: Optional[Dict[str, Any]] = None, physical_setup_graph: Optional[Dict[str, Any]] = None,
controllers_config: Optional[Dict[str, Any]] = None, controllers_config: Optional[Dict[str, Any]] = None,
@@ -103,7 +108,7 @@ class HostNode(BaseROS2DeviceNode):
action_value_mappings=lab_registry.device_type_registry["host_node"]["class"]["action_value_mappings"], action_value_mappings=lab_registry.device_type_registry["host_node"]["class"]["action_value_mappings"],
hardware_interface={}, hardware_interface={},
print_publish=False, print_publish=False,
resource_tracker=DeviceNodeResourceTracker(), # host node并不是通过initialize 包一层传进来的 resource_tracker=self._resource_tracker, # host node并不是通过initialize 包一层传进来的
) )
# 设置单例实例 # 设置单例实例
@@ -112,7 +117,7 @@ class HostNode(BaseROS2DeviceNode):
# 初始化配置 # 初始化配置
self.server_latest_timestamp = 0.0 # self.server_latest_timestamp = 0.0 #
self.devices_config = devices_config self.devices_config = devices_config
self.resources_config = resources_config self.resources_config = resources_config # 直接保存 ResourceTreeSet
self.resources_edge_config = resources_edge_config self.resources_edge_config = resources_edge_config
self.physical_setup_graph = physical_setup_graph self.physical_setup_graph = physical_setup_graph
if controllers_config is None: if controllers_config is None:
@@ -147,6 +152,24 @@ class HostNode(BaseROS2DeviceNode):
"/devices/host_node/test_latency", "/devices/host_node/test_latency",
callback_group=self.callback_group, callback_group=self.callback_group,
), ),
"/devices/host_node/test_resource": ActionClient(
self,
lab_registry.EmptyIn,
"/devices/host_node/test_resource",
callback_group=self.callback_group,
),
"/devices/host_node/_execute_driver_command": ActionClient(
self,
lab_registry.StrSingleInput,
"/devices/host_node/_execute_driver_command",
callback_group=self.callback_group,
),
"/devices/host_node/_execute_driver_command_async": ActionClient(
self,
lab_registry.StrSingleInput,
"/devices/host_node/_execute_driver_command_async",
callback_group=self.callback_group,
),
} # 用来存储多个ActionClient实例 } # 用来存储多个ActionClient实例
self._action_value_mappings: Dict[str, Dict] = ( self._action_value_mappings: Dict[str, Dict] = (
{} {}
@@ -167,11 +190,9 @@ class HostNode(BaseROS2DeviceNode):
self._discover_devices() self._discover_devices()
# 初始化所有本机设备节点,多一次过滤,防止重复初始化 # 初始化所有本机设备节点,多一次过滤,防止重复初始化
for device_id, device_config in devices_config.items(): for device_config in devices_config.root_nodes:
if device_config.get("type", "device") != "device": device_id = device_config.res_content.id
self.lab_logger().debug( if device_config.res_content.type != "device":
f"[Host Node] Skipping type {device_config['type']} {device_id} already existed, skipping."
)
continue continue
if device_id not in self.devices_names: if device_id not in self.devices_names:
self.initialize_device(device_id, device_config) self.initialize_device(device_id, device_config)
@@ -186,58 +207,71 @@ class HostNode(BaseROS2DeviceNode):
].items(): ].items():
controller_config["update_rate"] = update_rate controller_config["update_rate"] = update_rate
self.initialize_controller(controller_id, controller_config) self.initialize_controller(controller_id, controller_config)
resources_config.insert( # 创建 host_node 作为一个单独的 ResourceTree
0,
{ host_node_dict = {
"id": "host_node", "id": "host_node",
"name": "host_node", "uuid": str(uuid.uuid4()),
"parent": None, "parent_uuid": "",
"type": "device", "name": "host_node",
"class": "host_node", "type": "device",
"position": {"x": 0, "y": 0, "z": 0}, "class": "host_node",
"config": {}, "config": {},
"data": {}, "data": {},
"children": [], "children": [],
}, "description": "",
) "schema": {},
resource_with_dirs_name = [] "model": {},
resource_ids_to_instance = {i["id"]: i for i in resources_config} "icon": "",
for res in resources_config: }
temp_res = res
res_paths = [res] # 创建 host_node 的 ResourceTree
while temp_res.get("parent"): host_node_instance = ResourceDictInstance.get_resource_instance_from_dict(host_node_dict)
temp_res = resource_ids_to_instance[temp_res.get("parent")] host_node_tree = ResourceTreeInstance(host_node_instance)
res_paths.append(temp_res) resources_config.trees.insert(0, host_node_tree)
dirs = "/" + "/".join([res["id"] for res in res_paths[::-1]])
new_res = copy.deepcopy(res)
new_res["data"]["unilabos_dirs"] = dirs
resource_with_dirs_name.append(new_res)
try: try:
for bridge in self.bridges: for bridge in self.bridges:
if hasattr(bridge, "resource_add"): if hasattr(bridge, "resource_tree_add") and resources_config:
from unilabos.app.web.client import HTTPClient from unilabos.app.web.client import HTTPClient
client: HTTPClient = bridge client: HTTPClient = bridge
resource_start_time = time.time() resource_start_time = time.time()
resource_add_res = client.resource_add(add_schema(resources_config)) # 传递 ResourceTreeSet 对象,在 client 中转换为字典并获取 UUID 映射
# DEBUG ONLY uuid_mapping = client.resource_tree_add(resources_config, "", True)
# for i in resource_with_dirs_name:
# http_req = self.bridges[-1].resource_get(i["data"]["unilabos_dirs"], True)
# res = self._resource_get_process(http_req)
# print(res)
resource_end_time = time.time() resource_end_time = time.time()
self.lab_logger().info( self.lab_logger().info(
f"[Host Node-Resource] 物料上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms" f"[Host Node-Resource] 物料上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms"
) )
for edge in self.resources_edge_config:
edge["source_uuid"] = uuid_mapping.get(edge["source_uuid"], edge["source_uuid"])
edge["target_uuid"] = uuid_mapping.get(edge["target_uuid"], edge["target_uuid"])
resource_add_res = client.resource_edge_add(self.resources_edge_config) resource_add_res = client.resource_edge_add(self.resources_edge_config)
resource_edge_end_time = time.time() resource_edge_end_time = time.time()
self.lab_logger().info( self.lab_logger().info(
f"[Host Node-Resource] 物料关系上传 {round(resource_edge_end_time - resource_end_time, 5) * 1000} ms" f"[Host Node-Resource] 物料关系上传 {round(resource_edge_end_time - resource_end_time, 5) * 1000} ms"
) )
# resources_config 通过各个设备的 resource_tracker 进行uuid更新利用uuid_mapping
# resources_config 的 root node 是
for tree in resources_config.trees:
node = tree.root_node
if node.res_content.type == "device":
for sub_node in node.children:
# 只有二级子设备
if sub_node.res_content.type != "device":
# slave节点走c2s更新接口拿到add自行update uuid
device_tracker = self.devices_instances[node.res_content.id].resource_tracker
resource_instance = device_tracker.figure_resource(
{"uuid": sub_node.res_content.uuid})
device_tracker.loop_update_uuid(resource_instance, uuid_mapping)
else:
try:
for plr_resource in ResourceTreeSet([tree]).to_plr_resources():
self.resource_tracker.add_resource(plr_resource)
except Exception as ex:
self.lab_logger().warning("[Host Node-Resource] 根节点物料序列化失败!")
except Exception as ex: except Exception as ex:
self.lab_logger().error("[Host Node-Resource] 添加物料出错!") self.lab_logger().error("[Host Node-Resource] 添加物料出错!")
self.lab_logger().error(traceback.format_exc()) self.lab_logger().error(traceback.format_exc())
# 创建定时器,定期发现设备 # 创建定时器,定期发现设备
self._discovery_timer = self.create_timer( self._discovery_timer = self.create_timer(
discovery_interval, self._discovery_devices_callback, callback_group=ReentrantCallbackGroup() discovery_interval, self._discovery_devices_callback, callback_group=ReentrantCallbackGroup()
@@ -286,23 +320,23 @@ class HostNode(BaseROS2DeviceNode):
self.devices_names[edge_device_id] = namespace self.devices_names[edge_device_id] = namespace
self._create_action_clients_for_device(device_id, namespace) self._create_action_clients_for_device(device_id, namespace)
self._online_devices.add(device_key) self._online_devices.add(device_key)
sclient = self.create_client(SerialCommand, f"/srv{namespace}/query_host_name") sclient = self.create_client(SerialCommand, f"/srv{namespace}/re_register_device")
threading.Thread( threading.Thread(
target=self._send_re_register, target=self._send_re_register,
args=(sclient,), args=(sclient,),
daemon=True, daemon=True,
name=f"ROSDevice{self.device_id}_query_host_name_{namespace}", name=f"ROSDevice{self.device_id}_re_register_device_{namespace}",
).start() ).start()
elif device_key not in self._online_devices: elif device_key not in self._online_devices:
# 设备重新上线 # 设备重新上线
self.lab_logger().info(f"[Host Node] Device reconnected: {device_key}") self.lab_logger().info(f"[Host Node] Device reconnected: {device_key}")
self._online_devices.add(device_key) self._online_devices.add(device_key)
sclient = self.create_client(SerialCommand, f"/srv{namespace}/query_host_name") sclient = self.create_client(SerialCommand, f"/srv{namespace}/re_register_device")
threading.Thread( threading.Thread(
target=self._send_re_register, target=self._send_re_register,
args=(sclient,), args=(sclient,),
daemon=True, daemon=True,
name=f"ROSDevice{self.device_id}_query_host_name_{namespace}", name=f"ROSDevice{self.device_id}_re_register_device_{namespace}",
).start() ).start()
# 检测离线设备 # 检测离线设备
@@ -473,16 +507,13 @@ class HostNode(BaseROS2DeviceNode):
for i in response: for i in response:
res = json.loads(i) res = json.loads(i)
new_li.append(res) new_li.append(res)
return { return {"resources": new_li, "liquid_input_resources": new_li}
"resources": new_li,
"liquid_input_resources": new_li
}
except Exception as ex: except Exception as ex:
pass pass
_n = "\n" _n = "\n"
raise ValueError(f"创建资源时失败!\n{_n.join(response)}") raise ValueError(f"创建资源时失败!\n{_n.join(response)}")
def initialize_device(self, device_id: str, device_config: Dict[str, Any]) -> None: def initialize_device(self, device_id: str, device_config: ResourceDictInstance) -> None:
""" """
根据配置初始化设备, 根据配置初始化设备,
@@ -495,9 +526,8 @@ class HostNode(BaseROS2DeviceNode):
""" """
self.lab_logger().info(f"[Host Node] Initializing device: {device_id}") self.lab_logger().info(f"[Host Node] Initializing device: {device_id}")
device_config_copy = copy.deepcopy(device_config)
try: try:
d = initialize_device_from_dict(device_id, device_config_copy) d = initialize_device_from_dict(device_id, device_config.get_nested_dict())
except DeviceClassInvalid as e: except DeviceClassInvalid as e:
self.lab_logger().error(f"[Host Node] Device class invalid: {e}") self.lab_logger().error(f"[Host Node] Device class invalid: {e}")
d = None d = None
@@ -677,9 +707,7 @@ class HostNode(BaseROS2DeviceNode):
feedback_callback=lambda feedback_msg: self.feedback_callback(item, action_id, feedback_msg), feedback_callback=lambda feedback_msg: self.feedback_callback(item, action_id, feedback_msg),
goal_uuid=goal_uuid_obj, goal_uuid=goal_uuid_obj,
) )
future.add_done_callback( future.add_done_callback(lambda future: self.goal_response_callback(item, action_id, future))
lambda future: self.goal_response_callback(item, action_id, future)
)
def goal_response_callback(self, item: "QueueItem", action_id: str, future) -> None: def goal_response_callback(self, item: "QueueItem", action_id: str, future) -> None:
"""目标响应回调""" """目标响应回调"""
@@ -793,7 +821,7 @@ class HostNode(BaseROS2DeviceNode):
ResourceAdd, "/resources/add", self._resource_add_callback, callback_group=ReentrantCallbackGroup() ResourceAdd, "/resources/add", self._resource_add_callback, callback_group=ReentrantCallbackGroup()
), ),
"resource_get": self.create_service( "resource_get": self.create_service(
ResourceGet, "/resources/get", self._resource_get_callback, callback_group=ReentrantCallbackGroup() SerialCommand, "/resources/get", self._resource_get_callback, callback_group=ReentrantCallbackGroup()
), ),
"resource_delete": self.create_service( "resource_delete": self.create_service(
ResourceDelete, ResourceDelete,
@@ -816,8 +844,125 @@ class HostNode(BaseROS2DeviceNode):
self._node_info_update_callback, self._node_info_update_callback,
callback_group=ReentrantCallbackGroup(), callback_group=ReentrantCallbackGroup(),
), ),
"c2s_update_resource_tree": self.create_service(
SerialCommand,
"/c2s_update_resource_tree",
self._resource_tree_update_callback,
callback_group=ReentrantCallbackGroup(),
),
} }
def _resource_tree_action_add_callback(self, data: dict, response: SerialCommand_Response): # OK
resource_tree_set = ResourceTreeSet.load(data["data"])
mount_uuid = data["mount_uuid"]
first_add = data["first_add"]
self.lab_logger().info(
f"[Host Node-Resource] Loaded ResourceTreeSet with {len(resource_tree_set.trees)} trees, "
f"{len(resource_tree_set.all_nodes)} total nodes"
)
# 处理资源添加逻辑
success = False
uuid_mapping = {}
if len(self.bridges) > 0:
from unilabos.app.web.client import HTTPClient
client: HTTPClient = self.bridges[-1]
resource_start_time = time.time()
uuid_mapping = client.resource_tree_add(resource_tree_set, mount_uuid, first_add)
success = True
resource_end_time = time.time()
self.lab_logger().info(
f"[Host Node-Resource] 物料创建上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms"
)
if uuid_mapping:
self.lab_logger().info(f"[Host Node-Resource] UUID映射: {len(uuid_mapping)} 个节点")
if success:
from unilabos.resources.graphio import physical_setup_graph
# 将资源添加到本地图中
for node in resource_tree_set.all_nodes:
resource_dict = node.res_content.model_dump(by_alias=True)
if resource_dict.get("id") not in physical_setup_graph.nodes:
physical_setup_graph.add_node(resource_dict["id"], **resource_dict)
else:
physical_setup_graph.nodes[resource_dict["id"]]["data"].update(resource_dict.get("data", {}))
response.response = json.dumps(uuid_mapping) if success else "FAILED"
self.lab_logger().info(f"[Host Node-Resource] Resource tree add completed, success: {success}")
def _resource_tree_action_get_callback(self, data: dict, response: SerialCommand_Response): # OK
uuid_list: List[str] = data["data"]
with_children: bool = data["with_children"]
from unilabos.app.web.client import http_client
resource_response = http_client.resource_tree_get(uuid_list, with_children)
response.response = json.dumps(resource_response)
def _resource_tree_action_remove_callback(self, data: dict, response: SerialCommand_Response):
"""
子节点通知Host物料树删除
"""
self.lab_logger().info(f"[Host Node-Resource] Resource tree remove request received")
response.response = "OK"
self.lab_logger().info(f"[Host Node-Resource] Resource tree remove completed")
def _resource_tree_action_update_callback(self, data: dict, response: SerialCommand_Response):
"""
子节点通知Host物料树更新
"""
resource_tree_set = ResourceTreeSet.load(data["data"])
self.lab_logger().info(
f"[Host Node-Resource] Loaded ResourceTreeSet with {len(resource_tree_set.trees)} trees, "
f"{len(resource_tree_set.all_nodes)} total nodes"
)
from unilabos.app.web.client import http_client
resource_start_time = time.time()
uuid_mapping = http_client.resource_tree_update(resource_tree_set, "", False)
success = bool(uuid_mapping)
resource_end_time = time.time()
self.lab_logger().info(
f"[Host Node-Resource] 物料更新上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms"
)
if uuid_mapping:
self.lab_logger().info(f"[Host Node-Resource] UUID映射: {len(uuid_mapping)} 个节点")
# 还需要加入到资源图中,暂不实现,考虑资源图新的获取方式
response.response = json.dumps(uuid_mapping)
self.lab_logger().info(f"[Host Node-Resource] Resource tree add completed, success: {success}")
def _resource_tree_update_callback(self, request: SerialCommand_Request, response: SerialCommand_Response):
"""
子节点通知Host物料树更新
接收序列化的 ResourceTreeSet 数据并进行处理
"""
self.lab_logger().info(f"[Host Node-Resource] Resource tree add request received")
try:
# 解析请求数据
data = json.loads(request.command)
action = data["action"]
data = data["data"]
if action == "add":
self._resource_tree_action_add_callback(data, response)
elif action == "get":
self._resource_tree_action_get_callback(data, response)
elif action == "update":
self._resource_tree_action_update_callback(data, response)
elif action == "remove":
self._resource_tree_action_remove_callback(data, response)
else:
self.lab_logger().error(f"[Host Node-Resource] Invalid action: {action}")
response.response = "ERROR"
except Exception as e:
self.lab_logger().error(f"[Host Node-Resource] Error adding resource tree: {e}")
self.lab_logger().error(traceback.format_exc())
response.response = f"ERROR: {str(e)}"
return response
def _node_info_update_callback(self, request, response): def _node_info_update_callback(self, request, response):
""" """
更新节点信息回调 更新节点信息回调
@@ -888,7 +1033,7 @@ class HostNode(BaseROS2DeviceNode):
resources = [convert_to_ros_msg(Resource, resource) for resource in r] resources = [convert_to_ros_msg(Resource, resource) for resource in r]
return resources return resources
def _resource_get_callback(self, request: ResourceGet.Request, response: ResourceGet.Response): def _resource_get_callback(self, request: SerialCommand.Request, response: SerialCommand.Response):
""" """
获取资源回调 获取资源回调
@@ -902,14 +1047,12 @@ class HostNode(BaseROS2DeviceNode):
响应对象,包含查询到的资源 响应对象,包含查询到的资源
""" """
try: try:
http_req = self.bridges[-1].resource_get(request.id, request.with_children) data = json.loads(request.command)
response.resources = self._resource_get_process(http_req) http_req = self.bridges[-1].resource_get(data["id"], data["with_children"])
response.response = json.dumps(http_req["data"])
return response return response
except Exception as e: except Exception as e:
self.lab_logger().error(f"[Host Node-Resource] Error retrieving from bridge: {str(e)}") self.lab_logger().error(f"[Host Node-Resource] Error retrieving from bridge: {str(e)}")
r = [resource for resource in self.resources_config if resource.get("id") == request.id]
self.lab_logger().debug(f"[Host Node-Resource] Retrieved from local: {len(r)} resources")
response.resources = [convert_to_ros_msg(Resource, resource) for resource in r]
return response return response
def _resource_delete_callback(self, request, response): def _resource_delete_callback(self, request, response):
@@ -1094,6 +1237,7 @@ class HostNode(BaseROS2DeviceNode):
else: else:
self.lab_logger().warning("⚠️ 无法获取服务端任务下发时间,跳过任务延迟分析") self.lab_logger().warning("⚠️ 无法获取服务端任务下发时间,跳过任务延迟分析")
raw_delay_ms = -1
corrected_delay_ms = -1 corrected_delay_ms = -1
self.lab_logger().info("=" * 60) self.lab_logger().info("=" * 60)
@@ -1110,6 +1254,12 @@ class HostNode(BaseROS2DeviceNode):
"status": "success", "status": "success",
} }
def test_resource(self, resource: ResourceSlot, resources: List[ResourceSlot], device: DeviceSlot, devices: List[DeviceSlot]):
return {
"resources": ResourceTreeSet.from_plr_resources([resource, *resources]).dump(),
"devices": [device, *devices],
}
def handle_pong_response(self, pong_data: dict): def handle_pong_response(self, pong_data: dict):
""" """
处理pong响应 处理pong响应
@@ -1129,3 +1279,78 @@ class HostNode(BaseROS2DeviceNode):
) )
else: else:
self.lab_logger().warning("⚠️ 收到无效的Pong响应缺少ping_id") self.lab_logger().warning("⚠️ 收到无效的Pong响应缺少ping_id")
def notify_resource_tree_update(
self, device_id: str, action: str, resource_uuid_list: List[str]
) -> bool:
"""
通知设备节点更新资源树
Args:
device_id: 目标设备ID
action: 操作类型 "add", "update", "remove"
resource_uuid_list: 资源UUIDs
Returns:
bool: 操作是否成功
"""
try:
# 检查设备是否存在
if device_id not in self.devices_names:
self.lab_logger().error(f"[Host Node-Resource] Device {device_id} not found in devices_names")
return False
namespace = self.devices_names[device_id]
device_key = f"{namespace}/{device_id}"
# 检查设备是否在线
if device_key not in self._online_devices:
self.lab_logger().error(f"[Host Node-Resource] Device {device_key} is offline")
return False
# 构建服务地址
srv_address = f"/srv{namespace}/s2c_resource_tree"
self.lab_logger().info(f"[Host Node-Resource] Notifying {device_id} for resource tree {action} operation")
# 创建服务客户端
sclient = self.create_client(SerialCommand, srv_address)
# 等待服务可用(设置超时)
if not sclient.wait_for_service(timeout_sec=5.0):
self.lab_logger().error(f"[Host Node-Resource] Service {srv_address} not available")
return False
# 构建请求数据
request_data = [
{
"action": action,
"data": resource_uuid_list,
}
]
# 创建请求
request = SerialCommand.Request()
request.command = json.dumps(request_data, ensure_ascii=False)
# 发送异步请求
future = sclient.call_async(request)
# 等待响应
timeout = 30.0
start_time = time.time()
while not future.done():
if time.time() - start_time > timeout:
self.lab_logger().error(f"[Host Node-Resource] Timeout waiting for response from {device_id}")
return False
time.sleep(0.05)
response = future.result()
self.lab_logger().info(
f"[Host Node-Resource] Resource tree {action} notification completed for {device_id}"
)
return True
except Exception as e:
self.lab_logger().error(f"[Host Node-Resource] Error notifying resource tree update: {str(e)}")
self.lab_logger().error(traceback.format_exc())
return False

View File

@@ -29,9 +29,11 @@ from unilabos.utils.type_check import serialize_result_info, get_result_info_str
if TYPE_CHECKING: if TYPE_CHECKING:
from unilabos.devices.workstation.workstation_base import WorkstationBase from unilabos.devices.workstation.workstation_base import WorkstationBase
class ROS2WorkstationNodeTempError(Exception): class ROS2WorkstationNodeTempError(Exception):
pass pass
class ROS2WorkstationNode(BaseROS2DeviceNode): class ROS2WorkstationNode(BaseROS2DeviceNode):
""" """
ROS2WorkstationNode代表管理ROS2环境中设备通信和动作的协议节点。 ROS2WorkstationNode代表管理ROS2环境中设备通信和动作的协议节点。
@@ -63,10 +65,7 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
driver_instance=driver_instance, driver_instance=driver_instance,
device_id=device_id, device_id=device_id,
status_types=status_types, status_types=status_types,
action_value_mappings={ action_value_mappings={**action_value_mappings, **self.protocol_action_mappings},
**action_value_mappings,
**self.protocol_action_mappings
},
hardware_interface=hardware_interface, hardware_interface=hardware_interface,
print_publish=print_publish, print_publish=print_publish,
resource_tracker=resource_tracker, resource_tracker=resource_tracker,
@@ -89,7 +88,8 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
d = self.initialize_device(device_id, device_config) d = self.initialize_device(device_id, device_config)
except Exception as ex: except Exception as ex:
self.lab_logger().error( self.lab_logger().error(
f"[Protocol Node] Failed to initialize device {device_id}: {ex}\n{traceback.format_exc()}") f"[Protocol Node] Failed to initialize device {device_id}: {ex}\n{traceback.format_exc()}"
)
d = None d = None
if d is None: if d is None:
continue continue
@@ -109,10 +109,9 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
if d: if d:
hardware_interface = d.ros_node_instance._hardware_interface hardware_interface = d.ros_node_instance._hardware_interface
if ( if (
hasattr(d.driver_instance, hardware_interface["name"]) hasattr(d.driver_instance, hardware_interface["name"])
and hasattr(d.driver_instance, hardware_interface["write"]) and hasattr(d.driver_instance, hardware_interface["write"])
and ( and (hardware_interface["read"] is None or hasattr(d.driver_instance, hardware_interface["read"]))
hardware_interface["read"] is None or hasattr(d.driver_instance, hardware_interface["read"]))
): ):
name = getattr(d.driver_instance, hardware_interface["name"]) name = getattr(d.driver_instance, hardware_interface["name"])
@@ -130,7 +129,7 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
f"添加了{write}方法(来源:{name} {communicate_hardware_info['read']})" f"添加了{write}方法(来源:{name} {communicate_hardware_info['read']})"
) )
self.lab_logger().info(f"ROS2ProtocolNode {device_id} initialized with protocols: {self.protocol_names}") self.lab_logger().info(f"ROS2WorkstationNode {device_id} initialized with protocols: {self.protocol_names}")
def _setup_protocol_names(self, protocol_type): def _setup_protocol_names(self, protocol_type):
# 处理协议类型 # 处理协议类型
@@ -160,7 +159,8 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
node.resource_tracker = self.resource_tracker # 站内应当共享资源跟踪器 node.resource_tracker = self.resource_tracker # 站内应当共享资源跟踪器
for action_name, action_mapping in node._action_value_mappings.items(): for action_name, action_mapping in node._action_value_mappings.items():
if action_name.startswith("auto-") or str(action_mapping.get("type", "")).startswith( if action_name.startswith("auto-") or str(action_mapping.get("type", "")).startswith(
"UniLabJsonCommand"): "UniLabJsonCommand"
):
continue continue
action_id = f"/devices/{device_id_abs}/{action_name}" action_id = f"/devices/{device_id_abs}/{action_name}"
if action_id not in self._action_clients: if action_id not in self._action_clients:
@@ -245,8 +245,10 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
logs.append(step) logs.append(step)
elif isinstance(step, list): elif isinstance(step, list):
logs.append(step) logs.append(step)
self.lab_logger().info(f"Goal received: {protocol_kwargs}, running steps: " self.lab_logger().info(
f"{json.dumps(logs, indent=4, ensure_ascii=False)}") f"Goal received: {protocol_kwargs}, running steps: "
f"{json.dumps(logs, indent=4, ensure_ascii=False)}"
)
time_start = time.time() time_start = time.time()
time_overall = 100 time_overall = 100
@@ -268,7 +270,9 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
if not ret_info.get("suc", False): if not ret_info.get("suc", False):
raise RuntimeError(f"Step {i + 1} failed.") raise RuntimeError(f"Step {i + 1} failed.")
except ROS2WorkstationNodeTempError as ex: except ROS2WorkstationNodeTempError as ex:
step_results.append({"step": i + 1, "action": action["action_name"], "result": ex.args[0]}) step_results.append(
{"step": i + 1, "action": action["action_name"], "result": ex.args[0]}
)
elif isinstance(action, list): elif isinstance(action, list):
# 如果是并行动作,同时执行 # 如果是并行动作,同时执行
actions = action actions = action
@@ -307,8 +311,12 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
except Exception as e: except Exception as e:
# 捕获并记录错误信息 # 捕获并记录错误信息
str_step_results = [ str_step_results = [
{k: dict(message_to_ordereddict(v)) if k == "result" and hasattr(v, "SLOT_TYPES") else v for k, v in {
i.items()} for i in step_results] k: dict(message_to_ordereddict(v)) if k == "result" and hasattr(v, "SLOT_TYPES") else v
for k, v in i.items()
}
for i in step_results
]
execution_error = f"{traceback.format_exc()}\n\nStep Result: {pformat(str_step_results)}" execution_error = f"{traceback.format_exc()}\n\nStep Result: {pformat(str_step_results)}"
execution_success = False execution_success = False
self.lab_logger().error(f"协议 {protocol_name} 执行出错: {str(e)} \n{traceback.format_exc()}") self.lab_logger().error(f"协议 {protocol_name} 执行出错: {str(e)} \n{traceback.format_exc()}")
@@ -381,7 +389,7 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
"""还没有改过的部分""" """还没有改过的部分"""
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
): ):
"""为设备设置硬件接口代理""" """为设备设置硬件接口代理"""
# extra_info = [getattr(device.driver_instance, info) for info in communication_device.ros_node_instance._hardware_interface.get("extra_info", [])] # extra_info = [getattr(device.driver_instance, info) for info in communication_device.ros_node_instance._hardware_interface.get("extra_info", [])]
@@ -405,17 +413,3 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
if write_method: if write_method:
# bound_write = MethodType(_write, device.driver_instance) # bound_write = MethodType(_write, device.driver_instance)
setattr(device.driver_instance, write_method, _write) setattr(device.driver_instance, write_method, _write)
async def _update_resources(self, goal, protocol_kwargs):
"""更新资源状态"""
for k, v in goal.get_fields_and_field_types().items():
if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
if protocol_kwargs[k] is not None:
try:
r = ResourceUpdate.Request()
r.resources = [
convert_to_ros_msg(Resource, rs) for rs in nested_dict_to_list(protocol_kwargs[k])
]
await self._resource_clients["resource_update"].call_async(r)
except Exception as e:
self.lab_logger().error(f"更新资源失败: {e}")

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@
这个模块包含用于创建设备类实例的工厂类。 这个模块包含用于创建设备类实例的工厂类。
基础工厂类提供通用的实例创建方法,而特定工厂类提供针对特定设备类的创建方法。 基础工厂类提供通用的实例创建方法,而特定工厂类提供针对特定设备类的创建方法。
""" """
import asyncio import asyncio
import inspect import inspect
import traceback import traceback
@@ -53,7 +54,6 @@ class DeviceClassCreator(Generic[T]):
if c["type"] != "device": if c["type"] != "device":
self.resource_tracker.add_resource(c) self.resource_tracker.add_resource(c)
def create_instance(self, data: Dict[str, Any]) -> T: def create_instance(self, data: Dict[str, Any]) -> T:
""" """
创建设备类实例 创建设备类实例
@@ -118,7 +118,9 @@ class PyLabRobotCreator(DeviceClassCreator[T]):
return nested_dict_to_list(resource), Resource return nested_dict_to_list(resource), Resource
return resource, source_type return resource, source_type
def _process_resource_references(self, data: Any, to_dict=False, states=None, prefix_path="") -> Any: def _process_resource_references(
self, data: Any, to_dict=False, states=None, prefix_path="", name_to_uuid=None
) -> Any:
""" """
递归处理资源引用替换_resource_child_name对应的资源 递归处理资源引用替换_resource_child_name对应的资源
@@ -127,11 +129,13 @@ class PyLabRobotCreator(DeviceClassCreator[T]):
to_dict: 是否返回字典形式的资源 to_dict: 是否返回字典形式的资源
states: 用于保存所有资源状态 states: 用于保存所有资源状态
prefix_path: 当前递归路径 prefix_path: 当前递归路径
name_to_uuid: name到uuid的映射字典
Returns: Returns:
处理后的数据 处理后的数据
""" """
from pylabrobot.resources import Deck, Resource from pylabrobot.resources import Deck, Resource
if states is None: if states is None:
states = {} states = {}
@@ -146,7 +150,7 @@ class PyLabRobotCreator(DeviceClassCreator[T]):
target_type = import_manager.get_class(type_path) target_type = import_manager.get_class(type_path)
contain_model = not issubclass(target_type, Deck) contain_model = not issubclass(target_type, Deck)
resource, target_type = self._process_resource_mapping(resource, target_type) resource, target_type = self._process_resource_mapping(resource, target_type)
resource_instance: Resource = resource_ulab_to_plr(resource, contain_model) resource_instance: Resource = resource_ulab_to_plr(resource, contain_model) # 带state
states[prefix_path] = resource_instance.serialize_all_state() states[prefix_path] = resource_instance.serialize_all_state()
# 使用 prefix_path 作为 key 存储资源状态 # 使用 prefix_path 作为 key 存储资源状态
if to_dict: if to_dict:
@@ -155,6 +159,9 @@ class PyLabRobotCreator(DeviceClassCreator[T]):
return serialized return serialized
else: else:
self.resource_tracker.add_resource(resource_instance) self.resource_tracker.add_resource(resource_instance)
# 立即设置UUIDstate已经在resource_ulab_to_plr中处理过了
if name_to_uuid:
self.resource_tracker.loop_set_uuid(resource_instance, name_to_uuid)
return resource_instance return resource_instance
except Exception as e: except Exception as e:
logger.warning(f"无法导入资源类型 {type_path}: {e}") logger.warning(f"无法导入资源类型 {type_path}: {e}")
@@ -169,12 +176,12 @@ class PyLabRobotCreator(DeviceClassCreator[T]):
result = {} result = {}
for key, value in data.items(): for key, value in data.items():
new_prefix = f"{prefix_path}.{key}" if prefix_path else key new_prefix = f"{prefix_path}.{key}" if prefix_path else key
result[key] = self._process_resource_references(value, to_dict, states, new_prefix) result[key] = self._process_resource_references(value, to_dict, states, new_prefix, name_to_uuid)
return result return result
elif isinstance(data, list): elif isinstance(data, list):
return [ return [
self._process_resource_references(item, to_dict, states, f"{prefix_path}[{i}]") self._process_resource_references(item, to_dict, states, f"{prefix_path}[{i}]", name_to_uuid)
for i, item in enumerate(data) for i, item in enumerate(data)
] ]
@@ -193,22 +200,42 @@ class PyLabRobotCreator(DeviceClassCreator[T]):
""" """
deserialize_error = None deserialize_error = None
stack = None stack = None
# 递归遍历 children 构建 name_to_uuid 映射
def collect_name_to_uuid(children_dict: Dict[str, Any], result: Dict[str, str]):
"""递归遍历嵌套的 children 字典,收集 name 到 uuid 的映射"""
for child in children_dict.values():
if isinstance(child, dict):
result[child["name"]] = child["uuid"]
collect_name_to_uuid(child["children"], result)
name_to_uuid = {}
collect_name_to_uuid(self.children, name_to_uuid)
if self.has_deserialize: if self.has_deserialize:
deserialize_method = getattr(self.device_cls, "deserialize") deserialize_method = getattr(self.device_cls, "deserialize")
spect = inspect.signature(deserialize_method) spect = inspect.signature(deserialize_method)
spec_args = spect.parameters spec_args = spect.parameters
for param_name, param_value in data.copy().items(): for param_name, param_value in data.copy().items():
if isinstance(param_value, dict) and "_resource_child_name" in param_value and "_resource_type" not in param_value: if (
isinstance(param_value, dict)
and "_resource_child_name" in param_value
and "_resource_type" not in param_value
):
arg_value = spec_args[param_name].annotation arg_value = spec_args[param_name].annotation
data[param_name]["_resource_type"] = self.device_cls.__module__ + ":" + arg_value data[param_name]["_resource_type"] = self.device_cls.__module__ + ":" + arg_value
logger.debug(f"自动补充 _resource_type: {data[param_name]['_resource_type']}") logger.debug(f"自动补充 _resource_type: {data[param_name]['_resource_type']}")
# 首先处理资源引用 # 首先处理资源引用
states = {} states = {}
processed_data = self._process_resource_references(data, to_dict=True, states=states) processed_data = self._process_resource_references(
data, to_dict=True, states=states, name_to_uuid=name_to_uuid
)
try: try:
self.device_instance = deserialize_method(**processed_data) from pylabrobot.resources import Resource
self.device_instance: Resource = deserialize_method(**processed_data)
self.resource_tracker.loop_set_uuid(self.device_instance, name_to_uuid)
all_states = self.device_instance.serialize_all_state() all_states = self.device_instance.serialize_all_state()
for k, v in states.items(): for k, v in states.items():
logger.debug(f"PyLabRobot反序列化设置状态{k}") logger.debug(f"PyLabRobot反序列化设置状态{k}")
@@ -217,7 +244,7 @@ class PyLabRobotCreator(DeviceClassCreator[T]):
v[kk] = vv v[kk] = vv
self.device_instance.load_all_state(v) self.device_instance.load_all_state(v)
self.resource_tracker.add_resource(self.device_instance) self.resource_tracker.add_resource(self.device_instance)
self.post_create() self.post_create() # 对应DeviceClassCreator进行调用
return self.device_instance # type: ignore return self.device_instance # type: ignore
except Exception as e: except Exception as e:
# 先静默继续,尝试另外一种创建方法 # 先静默继续,尝试另外一种创建方法
@@ -229,12 +256,16 @@ class PyLabRobotCreator(DeviceClassCreator[T]):
spect = inspect.signature(self.device_cls.__init__) spect = inspect.signature(self.device_cls.__init__)
spec_args = spect.parameters spec_args = spect.parameters
for param_name, param_value in data.copy().items(): for param_name, param_value in data.copy().items():
if isinstance(param_value, dict) and "_resource_child_name" in param_value and "_resource_type" not in param_value: if (
isinstance(param_value, dict)
and "_resource_child_name" in param_value
and "_resource_type" not in param_value
):
arg_value = spec_args[param_name].annotation arg_value = spec_args[param_name].annotation
data[param_name]["_resource_type"] = self.device_cls.__module__ + ":" + arg_value data[param_name]["_resource_type"] = self.device_cls.__module__ + ":" + arg_value
logger.debug(f"自动补充 _resource_type: {data[param_name]['_resource_type']}") logger.debug(f"自动补充 _resource_type: {data[param_name]['_resource_type']}")
processed_data = self._process_resource_references(data, to_dict=False) processed_data = self._process_resource_references(data, to_dict=False, name_to_uuid=name_to_uuid)
self.device_instance = super(PyLabRobotCreator, self).create_instance(processed_data) self.device_instance = super(PyLabRobotCreator, self).create_instance(processed_data) # 补全变量后直接调用调用的自身的attach_resource
except Exception as e: except Exception as e:
logger.error(f"PyLabRobot创建实例失败: {e}") logger.error(f"PyLabRobot创建实例失败: {e}")
logger.error(f"PyLabRobot创建实例堆栈: {traceback.format_exc()}") logger.error(f"PyLabRobot创建实例堆栈: {traceback.format_exc()}")
@@ -247,22 +278,31 @@ class PyLabRobotCreator(DeviceClassCreator[T]):
return self.device_instance return self.device_instance
def post_create(self): def post_create(self):
if hasattr(self.device_instance, "setup") and asyncio.iscoroutinefunction(getattr(self.device_instance, "setup")): if hasattr(self.device_instance, "setup") and asyncio.iscoroutinefunction(
getattr(self.device_instance, "setup")
):
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode from unilabos.ros.nodes.base_device_node import ROS2DeviceNode
def done_cb(*args): def done_cb(*args):
from pylabrobot.resources import set_volume_tracking from pylabrobot.resources import set_volume_tracking
# from pylabrobot.resources import set_tip_tracking # from pylabrobot.resources import set_tip_tracking
set_volume_tracking(enabled=True) set_volume_tracking(enabled=True)
# set_tip_tracking(enabled=True) # 序列化tip_spot has为False # set_tip_tracking(enabled=True) # 序列化tip_spot has为False
logger.debug(f"PyLabRobot设备实例 {self.device_instance} 设置完成") logger.debug(f"PyLabRobot设备实例 {self.device_instance} 设置完成")
from unilabos.config.config import BasicConfig from unilabos.config.config import BasicConfig
if BasicConfig.vis_2d_enable: if BasicConfig.vis_2d_enable:
from pylabrobot.visualizer.visualizer import Visualizer from pylabrobot.visualizer.visualizer import Visualizer
vis = Visualizer(resource=self.device_instance, open_browser=True) vis = Visualizer(resource=self.device_instance, open_browser=True)
def vis_done_cb(*args): def vis_done_cb(*args):
logger.info(f"PyLabRobot设备实例开启了Visualizer {self.device_instance}") logger.info(f"PyLabRobot设备实例开启了Visualizer {self.device_instance}")
ROS2DeviceNode.run_async_func(vis.setup).add_done_callback(vis_done_cb) ROS2DeviceNode.run_async_func(vis.setup).add_done_callback(vis_done_cb)
logger.debug(f"PyLabRobot设备实例提交开启Visualizer {self.device_instance}") logger.debug(f"PyLabRobot设备实例提交开启Visualizer {self.device_instance}")
ROS2DeviceNode.run_async_func(getattr(self.device_instance, "setup")).add_done_callback(done_cb) ROS2DeviceNode.run_async_func(getattr(self.device_instance, "setup")).add_done_callback(done_cb)
@@ -299,6 +339,7 @@ class WorkstationNodeCreator(DeviceClassCreator[T]):
deck_dict = data.get("deck") deck_dict = data.get("deck")
if deck_dict: if deck_dict:
from pylabrobot.resources import Deck, Resource from pylabrobot.resources import Deck, Resource
plrc = PyLabRobotCreator(Deck, self.children, self.resource_tracker) plrc = PyLabRobotCreator(Deck, self.children, self.resource_tracker)
deck = plrc.create_instance(deck_dict) deck = plrc.create_instance(deck_dict)
data["deck"] = deck data["deck"] = deck