mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2026-02-06 15:05:13 +00:00
Compare commits
25 Commits
workstatio
...
c8d16c7024
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8d16c7024 | ||
|
|
25d46dc9d5 | ||
|
|
88c4d1a9d1 | ||
|
|
81fd8291c5 | ||
|
|
3a11eb90d4 | ||
|
|
387866b9c9 | ||
|
|
7f40f141f6 | ||
|
|
6fc7ed1b88 | ||
|
|
93f0e08d75 | ||
|
|
4b43734b55 | ||
|
|
174b1914d4 | ||
|
|
704e13f030 | ||
|
|
0c42d60cf2 | ||
|
|
df33e1a214 | ||
|
|
1f49924966 | ||
|
|
609b6006e8 | ||
|
|
67c01271b7 | ||
|
|
a1783f489e | ||
|
|
a8f6527de9 | ||
|
|
5610c28b67 | ||
|
|
cfc1ee6e79 | ||
|
|
709eb0d91c | ||
|
|
14b7d52825 | ||
|
|
c6c2da69ba | ||
|
|
622e579063 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
@@ -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`:异步执行驱动命令(仅本地可用)
|
||||||
|
|
||||||
### 如果要手动定义动作
|
### 如果要手动定义动作
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ class WSConfig:
|
|||||||
max_reconnect_attempts = 999 # 最大重连次数
|
max_reconnect_attempts = 999 # 最大重连次数
|
||||||
ping_interval = 30 # ping间隔(秒)
|
ping_interval = 30 # ping间隔(秒)
|
||||||
```
|
```
|
||||||
|
您可以进入实验室,点击左下角的头像在实验室详情中获取所在实验室的ak sk
|
||||||
|

|
||||||
|
|
||||||
### 完整配置示例
|
### 完整配置示例
|
||||||
|
|
||||||
|
|||||||
BIN
docs/user_guide/image/copy_aksk.gif
Normal file
BIN
docs/user_guide/image/copy_aksk.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 526 KiB |
BIN
docs/user_guide/image/creatworkfollow.gif
Normal file
BIN
docs/user_guide/image/creatworkfollow.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 327 KiB |
BIN
docs/user_guide/image/links.png
Normal file
BIN
docs/user_guide/image/links.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 275 KiB |
BIN
docs/user_guide/image/linksandrun.png
Normal file
BIN
docs/user_guide/image/linksandrun.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 186 KiB |
BIN
docs/user_guide/image/material.png
Normal file
BIN
docs/user_guide/image/material.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 581 KiB |
BIN
docs/user_guide/image/new.png
Normal file
BIN
docs/user_guide/image/new.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
@@ -245,3 +245,78 @@ unilab --ak your_ak --sk your_sk --port 8080 --disable_browser
|
|||||||
- 检查图谱文件格式是否正确
|
- 检查图谱文件格式是否正确
|
||||||
- 验证设备连接和端点配置
|
- 验证设备连接和端点配置
|
||||||
- 确保注册表路径正确
|
- 确保注册表路径正确
|
||||||
|
|
||||||
|
## 页面操作
|
||||||
|
|
||||||
|
### 1. 启动成功
|
||||||
|
当您启动成功后,可以看到物料列表,节点模版和组态图如图展示
|
||||||
|

|
||||||
|
|
||||||
|
### 2. 根据需求创建设备和物料
|
||||||
|
我们可以做一个简单的案例
|
||||||
|
* 在容器1中加入水
|
||||||
|
* 通过传输泵将容器1中的水转移到容器2中
|
||||||
|
#### 2.1 添加所需的设备和物料
|
||||||
|
仪器设备work_station中的workstation 数量x1
|
||||||
|
仪器设备virtual_device中的virtual_transfer_pump 数量x1
|
||||||
|
物料耗材container中的container 数量x2
|
||||||
|
|
||||||
|
#### 2.2 将设备和物料根据父子关系进行关联
|
||||||
|
当我们添加设备时,仪器耗材模块的物料列表也会实时更新
|
||||||
|
我们需要将设备和物料拖拽到workstation中并在画布上将它们连接起来,就像真实的设备操作一样
|
||||||
|

|
||||||
|
|
||||||
|
### 3. 创建工作流
|
||||||
|
进入工作流模块 → 点击"我创建的" → 新建工作流
|
||||||
|

|
||||||
|
|
||||||
|
#### 3.1 新增工作流节点
|
||||||
|
我们可以进入指定工作流,在空白处右键
|
||||||
|
* 选择Laboratory→host_node中的creat_resource
|
||||||
|
* 选择Laboratory→workstation中的PumpTransferProtocol
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### 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. 点击运行按钮执行工作流
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 运行监控
|
||||||
|
* 运行状态和消息实时显示在底部控制台
|
||||||
|
* 如有报错,可点击查看详细信息
|
||||||
|
|
||||||
|
### 结果验证
|
||||||
|
工作流完成后,返回仪器耗材模块:
|
||||||
|
* 点击 container1卡片查看详情
|
||||||
|
* 确认其中包含参数指定的水和容量
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
181
test/resources/bioyond_materials_liquidhandling_1.json
Normal file
181
test/resources/bioyond_materials_liquidhandling_1.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
216
test/resources/bioyond_materials_liquidhandling_2.json
Normal file
216
test/resources/bioyond_materials_liquidhandling_2.json
Normal 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": []
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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=[],
|
||||||
|
|||||||
@@ -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)
|
||||||
if args_dict["graph"] is None:
|
graph: nx.Graph
|
||||||
|
resource_tree_set: ResourceTreeSet
|
||||||
|
resource_links: List[Dict[str, Any]]
|
||||||
request_startup_json = http_client.request_startup_json()
|
request_startup_json = http_client.request_startup_json()
|
||||||
|
if args_dict["graph"] is None:
|
||||||
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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
129
unilabos/devices/workstation/bioyond_studio/config.py
Normal file
129
unilabos/devices/workstation/bioyond_studio/config.py
Normal 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": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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:
|
||||||
"""配置站点类型和功能模块
|
"""配置站点类型和功能模块
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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频率执行,确保视频流的连续性和实时性。
|
||||||
|
|||||||
@@ -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通信连接,配置通信超时参数。该函数是设备使用前的必要步骤,建立成功后可进行状态查询、方法获取、任务启动等操作。连接失败时会抛出异常。
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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: ''
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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的通信协议,将命令转换为十六进制数据包并处理响应解析。
|
||||||
|
|||||||
@@ -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: ''
|
||||||
|
|||||||
@@ -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编辑函数。用于测试和演示资源管理功能,模拟修改夹爪资源的标识信息。该函数接收工作流名称、参数和资源对象,模拟真实的资源更新过程并返回修改后的资源信息。适用于系统测试和开发调试场景。
|
||||||
|
|||||||
@@ -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: 步进电机运动控制函数。根据指定的运动模式、目标位置和速度参数控制电机运动。支持多种运动模式和精确的位置控制,自动处理运动轨迹规划和执行。该函数提供异步执行和状态反馈,确保运动的准确性和可靠性。
|
||||||
|
|||||||
@@ -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分钟的响应等待时间。
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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: ''
|
||||||
|
|||||||
9
unilabos/registry/placeholder_type.py
Normal file
9
unilabos/registry/placeholder_type.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from pylabrobot.resources import Resource
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceSlot(Resource):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceSlot(str):
|
||||||
|
pass
|
||||||
@@ -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结构存在
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
24
unilabos/registry/resources/bioyond/bottles.yaml
Normal file
24
unilabos/registry/resources/bioyond/bottles.yaml
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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:
|
||||||
|
nodes[id2idx[parent]]["children"] = children
|
||||||
for child in children:
|
for child in children:
|
||||||
data["nodes"][id2idx[child]]["parent"] = parent
|
if child in id2idx:
|
||||||
return data
|
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()]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
"machine_name": BasicConfig.machine_name,
|
||||||
"type": "slave",
|
"type": "slave",
|
||||||
"devices_config": devices_config_copy,
|
"devices_config": devices_config.dump(),
|
||||||
"registry_config": lab_registry.obtain_registry_device_info()
|
"registry_config": lab_registry.obtain_registry_device_info(),
|
||||||
}, ensure_ascii=False, cls=TypeEncoder)
|
},
|
||||||
|
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()
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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:
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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",
|
||||||
|
"uuid": str(uuid.uuid4()),
|
||||||
|
"parent_uuid": "",
|
||||||
"name": "host_node",
|
"name": "host_node",
|
||||||
"parent": None,
|
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "host_node",
|
"class": "host_node",
|
||||||
"position": {"x": 0, "y": 0, "z": 0},
|
|
||||||
"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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -111,8 +111,7 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
|
|||||||
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()}")
|
||||||
@@ -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
@@ -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)
|
||||||
|
# 立即设置UUID,state已经在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
|
||||||
|
|||||||
Reference in New Issue
Block a user